Module: ClaudeAgentSDK::Sessions
- Defined in:
- lib/claude_agent_sdk/sessions.rb
Overview
Session browsing functions
Constant Summary collapse
- LITE_READ_BUF_SIZE =
rubocop:disable Metrics/ModuleLength
65_536- MAX_SANITIZED_LENGTH =
200- UUID_RE =
/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i- SKIP_FIRST_PROMPT_PATTERN =
%r{\A(?:<local-command-stdout>|<session-start-hook>|<tick>|<goal>| \[Request\ interrupted\ by\ user[^\]]*\]| \s*<ide_opened_file>[\s\S]*</ide_opened_file>\s*\z| \s*<ide_selection>[\s\S]*</ide_selection>\s*\z)}x- COMMAND_NAME_RE =
%r{<command-name>(.*?)</command-name>}- SANITIZE_RE =
/[^a-zA-Z0-9]/
Class Method Summary collapse
-
.config_dir ⇒ Object
Get the Claude config directory.
- .detect_worktrees(path) ⇒ Object
-
.extract_first_prompt_from_head(head) ⇒ Object
Extract the first meaningful user prompt from the head of a JSONL file.
-
.extract_json_string_field(text, key, last: false) ⇒ Object
Extract a JSON string field value from raw text without full JSON parse.
-
.extract_json_string_value(text, start) ⇒ Object
Extract string value starting at pos (handles escapes).
-
.find_project_dir(path) ⇒ Object
Find the project directory for a given path.
-
.get_session_info(session_id:, directory: nil) ⇒ SDKSessionInfo?
Read metadata for a single session by ID without a full directory scan.
-
.get_session_messages(session_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Get messages from a session transcript.
-
.list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true) ⇒ Array<SDKSessionInfo>
List sessions for a directory (or all sessions).
-
.read_session_lite(file_path, project_path) ⇒ Object
Read a single session file with lite (head/tail) strategy.
-
.read_sessions_from_dir(project_dir, project_path = nil) ⇒ Object
Read all sessions from a project directory.
-
.sanitize_path(name) ⇒ Object
Sanitize a filesystem path to a project directory name.
-
.simple_hash(str) ⇒ Object
Match TypeScript’s simpleHash: signed 32-bit integer, base-36 output.
-
.unescape_json_string(str) ⇒ Object
Unescape a JSON string value.
Class Method Details
.config_dir ⇒ Object
Get the Claude config directory
124 125 126 |
# File 'lib/claude_agent_sdk/sessions.rb', line 124 def config_dir ENV.fetch('CLAUDE_CONFIG_DIR', File.('~/.claude')) end |
.detect_worktrees(path) ⇒ Object
475 476 477 478 479 480 481 482 483 484 485 |
# File 'lib/claude_agent_sdk/sessions.rb', line 475 def detect_worktrees(path) output, _err, status = Open3.capture3('git', '-C', path, 'worktree', 'list', '--porcelain') return [path] unless status.success? paths = output.lines.filter_map do |line| line.strip.delete_prefix('worktree ') if line.start_with?('worktree ') end paths.empty? ? [path] : paths rescue StandardError [path] end |
.extract_first_prompt_from_head(head) ⇒ Object
Extract the first meaningful user prompt from the head of a JSONL file
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/claude_agent_sdk/sessions.rb', line 197 def extract_first_prompt_from_head(head) command_fallback = nil head.each_line do |line| next unless line.include?('"type":"user"') || line.include?('"type": "user"') next if line.include?('"tool_result"') next if line.include?('"isMeta":true') || line.include?('"isMeta": true') next if line.include?('"isCompactSummary":true') || line.include?('"isCompactSummary": true') entry = JSON.parse(line, symbolize_names: false) content = entry.dig('message', 'content') next unless content texts = if content.is_a?(String) [content] elsif content.is_a?(Array) content.filter_map { |block| block['text'] if block.is_a?(Hash) && block['type'] == 'text' } else next end texts.each do |text| text = text.gsub(/\n+/, ' ').strip next if text.empty? if (m = text.match(COMMAND_NAME_RE)) command_fallback ||= m[1] next end next if text.match?(SKIP_FIRST_PROMPT_PATTERN) return text.length > 200 ? "#{text[0, 200]}…" : text end rescue JSON::ParserError next end command_fallback || '' end |
.extract_json_string_field(text, key, last: false) ⇒ Object
Extract a JSON string field value from raw text without full JSON parse
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/claude_agent_sdk/sessions.rb', line 150 def extract_json_string_field(text, key, last: false) search_patterns = ["\"#{key}\":\"", "\"#{key}\": \""] result = nil search_patterns.each do |pattern| pos = 0 loop do idx = text.index(pattern, pos) break unless idx value_start = idx + pattern.length value = extract_json_string_value(text, value_start) if value result = unescape_json_string(value) return result unless last end pos = value_start end end result end |
.extract_json_string_value(text, start) ⇒ Object
Extract string value starting at pos (handles escapes)
174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/claude_agent_sdk/sessions.rb', line 174 def extract_json_string_value(text, start) pos = start while pos < text.length ch = text[pos] if ch == '\\' pos += 2 elsif ch == '"' return text[start...pos] else pos += 1 end end nil end |
.find_project_dir(path) ⇒ Object
Find the project directory for a given path
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/claude_agent_sdk/sessions.rb', line 129 def find_project_dir(path) projects_dir = File.join(config_dir, 'projects') return nil unless File.directory?(projects_dir) sanitized = sanitize_path(path) exact_path = File.join(projects_dir, sanitized) return exact_path if File.directory?(exact_path) # For long paths, scan for prefix match if sanitized.length > MAX_SANITIZED_LENGTH prefix = sanitized[0, MAX_SANITIZED_LENGTH + 1] # includes the trailing '-' Dir.children(projects_dir).each do |child| candidate = File.join(projects_dir, child) return candidate if File.directory?(candidate) && child.start_with?(prefix) end end nil end |
.get_session_info(session_id:, directory: nil) ⇒ SDKSessionInfo?
Read metadata for a single session by ID without a full directory scan.
360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/claude_agent_sdk/sessions.rb', line 360 def get_session_info(session_id:, directory: nil) return nil unless session_id.match?(UUID_RE) file_name = "#{session_id}.jsonl" return get_session_info_for_directory(file_name, directory) if directory # No directory — search all project directories. projects_dir = File.join(config_dir, 'projects') return nil unless File.directory?(projects_dir) Dir.children(projects_dir).each do |child| entry = File.join(projects_dir, child) next unless File.directory?(entry) info = read_session_lite(File.join(entry, file_name), nil) return info if info end nil end |
.get_session_messages(session_id:, directory: nil, limit: nil, offset: 0) ⇒ Array<SessionMessage>
Get messages from a session transcript
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/claude_agent_sdk/sessions.rb', line 386 def (session_id:, directory: nil, limit: nil, offset: 0) return [] unless session_id.match?(UUID_RE) offset ||= 0 file_path = find_session_file(session_id, directory) return [] unless file_path && File.exist?(file_path) entries = parse_jsonl_entries(file_path) chain = build_conversation_chain(entries) = (chain) # Apply offset and limit = [offset..] || [] = .first(limit) if limit end |
.list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true) ⇒ Array<SDKSessionInfo>
List sessions for a directory (or all sessions)
339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
# File 'lib/claude_agent_sdk/sessions.rb', line 339 def list_sessions(directory: nil, limit: nil, offset: 0, include_worktrees: true) offset ||= 0 sessions = if directory list_sessions_for_directory(directory, include_worktrees) else list_all_sessions end # Sort by last_modified descending, then apply offset and limit sessions.sort_by! { |s| -s.last_modified } sessions = sessions[offset..] || [] if offset.positive? sessions = sessions.first(limit) if limit sessions end |
.read_session_lite(file_path, project_path) ⇒ Object
Read a single session file with lite (head/tail) strategy
239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/claude_agent_sdk/sessions.rb', line 239 def read_session_lite(file_path, project_path) stat = File.stat(file_path) return nil if stat.size.zero? # rubocop:disable Style/ZeroLengthPredicate head, tail = read_head_tail(file_path, stat.size) # Check first line for sidechain first_line = head.lines.first || '' return nil if first_line.include?('"isSidechain":true') || first_line.include?('"isSidechain": true') build_session_info(file_path, head, tail, stat, project_path) rescue StandardError nil end |
.read_sessions_from_dir(project_dir, project_path = nil) ⇒ Object
Read all sessions from a project directory
319 320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/claude_agent_sdk/sessions.rb', line 319 def read_sessions_from_dir(project_dir, project_path = nil) return [] unless File.directory?(project_dir) sessions = [] Dir.glob(File.join(project_dir, '*.jsonl')).each do |file_path| stem = File.basename(file_path, '.jsonl') next unless stem.match?(UUID_RE) session = read_session_lite(file_path, project_path) sessions << session if session end sessions end |
.sanitize_path(name) ⇒ Object
Sanitize a filesystem path to a project directory name
116 117 118 119 120 121 |
# File 'lib/claude_agent_sdk/sessions.rb', line 116 def sanitize_path(name) sanitized = name.gsub(SANITIZE_RE, '-') return sanitized if sanitized.length <= MAX_SANITIZED_LENGTH "#{sanitized[0, MAX_SANITIZED_LENGTH]}-#{simple_hash(name)}" end |
.simple_hash(str) ⇒ Object
Match TypeScript’s simpleHash: signed 32-bit integer, base-36 output. JS’s charCodeAt returns UTF-16 code units, so supplementary characters (emoji, CJK extensions) emit two surrogate code units — iterate over UTF-16LE shorts instead of Unicode codepoints to preserve parity.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/claude_agent_sdk/sessions.rb', line 95 def simple_hash(str) h = 0 str.encode('UTF-16LE').unpack('v*').each do |char_code| h = ((h << 5) - h + char_code) & 0xFFFFFFFF h -= 0x100000000 if h >= 0x80000000 end h = h.abs return '0' if h.zero? digits = '0123456789abcdefghijklmnopqrstuvwxyz' out = [] n = h while n.positive? out.unshift(digits[n % 36]) n /= 36 end out.join end |
.unescape_json_string(str) ⇒ Object
Unescape a JSON string value
190 191 192 193 194 |
# File 'lib/claude_agent_sdk/sessions.rb', line 190 def unescape_json_string(str) JSON.parse("\"#{str}\"") rescue JSON::ParserError str end |