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
94 95 96 |
# File 'lib/claude_agent_sdk/sessions.rb', line 94 def config_dir ENV.fetch('CLAUDE_CONFIG_DIR', File.('~/.claude')) end |
.detect_worktrees(path) ⇒ Object
445 446 447 448 449 450 451 452 453 454 455 |
# File 'lib/claude_agent_sdk/sessions.rb', line 445 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
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/claude_agent_sdk/sessions.rb', line 167 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
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/claude_agent_sdk/sessions.rb', line 120 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)
144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/claude_agent_sdk/sessions.rb', line 144 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
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/claude_agent_sdk/sessions.rb', line 99 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.
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
# File 'lib/claude_agent_sdk/sessions.rb', line 330 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
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/claude_agent_sdk/sessions.rb', line 356 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)
309 310 311 312 313 314 315 316 317 318 319 320 321 322 |
# File 'lib/claude_agent_sdk/sessions.rb', line 309 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
209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/claude_agent_sdk/sessions.rb', line 209 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
289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/claude_agent_sdk/sessions.rb', line 289 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
86 87 88 89 90 91 |
# File 'lib/claude_agent_sdk/sessions.rb', line 86 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.
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/claude_agent_sdk/sessions.rb', line 65 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
160 161 162 163 164 |
# File 'lib/claude_agent_sdk/sessions.rb', line 160 def unescape_json_string(str) JSON.parse("\"#{str}\"") rescue JSON::ParserError str end |