claude_agent 0.7.9 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90d9094d4e4428fc7dfc1e452759403a92f2e4badf5d1ac5f2f90e950c511255
4
- data.tar.gz: fd2a8407c794aa672a4b03a774a6573dac3682bc09b8fe666170b90f5bd9f79a
3
+ metadata.gz: d1029f92c20c52b0d86d2942e3bb681fe0b0810ee14e5c848dc3f918a60969bb
4
+ data.tar.gz: 13b11ee08fd011a9787a404b389c0c1290619eeec15abd4d7bde3885235daf61
5
5
  SHA512:
6
- metadata.gz: ce7d705ebe01a38ad1ac86a8caeaabd700831e200e73f134c9adef507b6f7b695effece63ae902849ee408714c3ae576930012dbbefe2b05da403d0de8549317
7
- data.tar.gz: 7f06c5b0b5ed5ad798442672ad8ca969a0fdefc414a4c273e86911df0e171d24d5a378b9a88117bd3c60092a6affb5219d06cc58dd0107245643b571e2236440
6
+ metadata.gz: c7a0c0d980c96d6ebb4875916b15a7a9c49d6e6f657021722b54794951c9cb5cabad0589b4a7120210390aa0a9b27d4238e902bf6d26e141489a667bac8c9efa
7
+ data.tar.gz: 72e41cc15471827325a75f67de90bef61fe0b42ebedd36dd5b88a6c942d5fcba51caafbb83005a6eea560baa2be73b4b38bc765026c58f287e1d802c61e09c57
data/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.10] - 2026-02-26
11
+
12
+ ### Added
13
+ - `Session.find(id, dir:)` — find a past session by UUID, returns `Session` or `nil`
14
+ - `Session.all` — list all past sessions as `Session` objects
15
+ - `Session.where(dir:, limit:)` — query sessions with optional directory and limit filters
16
+ - `Session#messages` — returns a chainable, `Enumerable` `SessionMessageRelation` for reading transcript messages
17
+ - `SessionMessageRelation#where(limit:, offset:)` — paginate messages with immutable chaining
18
+ - `ClaudeAgent.get_session_messages(session_id, dir:, limit:, offset:)` — read session transcripts from disk (TypeScript SDK v0.2.59 parity)
19
+ - `SessionMessage` type — message from a session transcript with `type`, `uuid`, `session_id`, `message`
20
+ - `SessionPaths` module — shared path infrastructure for session discovery (extracted from `ListSessions`)
21
+
22
+ ### Changed
23
+ - Renamed `Session` (V2 multi-turn API) to `V2Session` to free the `Session` name for the finder API
24
+ - `unstable_v2_create_session`, `unstable_v2_resume_session`, and `unstable_v2_prompt` now return `V2Session`
25
+
10
26
  ## [0.7.9] - 2026-02-25
11
27
 
12
28
  ### Added
data/README.md CHANGED
@@ -1180,14 +1180,20 @@ client.disconnect
1180
1180
 
1181
1181
  ## Session Discovery
1182
1182
 
1183
- List past Claude Code sessions from disk without spawning a CLI subprocess:
1183
+ Find and inspect past Claude Code sessions from disk without spawning a CLI subprocess.
1184
+
1185
+ ### Session.find / Session.all
1184
1186
 
1185
1187
  ```ruby
1186
- # All sessions (most recent first)
1187
- sessions = ClaudeAgent.list_sessions
1188
+ # Find a specific session by ID
1189
+ session = ClaudeAgent::Session.find("abc-123-def")
1190
+ session = ClaudeAgent::Session.find("abc-123-def", dir: "/my/project")
1191
+
1192
+ # List all sessions (most recent first)
1193
+ sessions = ClaudeAgent::Session.all
1188
1194
 
1189
- # Scoped to a project directory (includes git worktree siblings)
1190
- sessions = ClaudeAgent.list_sessions(dir: "/path/to/project", limit: 10)
1195
+ # Filter by directory, limit results
1196
+ sessions = ClaudeAgent::Session.where(dir: "/path/to/project", limit: 10)
1191
1197
 
1192
1198
  sessions.each do |s|
1193
1199
  puts "#{s.summary} (#{s.git_branch || 'no branch'})"
@@ -1197,7 +1203,27 @@ sessions.each do |s|
1197
1203
  end
1198
1204
  ```
1199
1205
 
1200
- Each session is a `SessionInfo` with these fields:
1206
+ ### Reading Messages
1207
+
1208
+ `Session#messages` returns a chainable, `Enumerable` relation:
1209
+
1210
+ ```ruby
1211
+ session = ClaudeAgent::Session.find("abc-123-def")
1212
+
1213
+ # All messages
1214
+ session.messages.each { |m| puts "#{m.type}: #{m.uuid}" }
1215
+
1216
+ # Paginated
1217
+ session.messages.where(limit: 10).map(&:uuid)
1218
+ session.messages.where(offset: 5, limit: 10).to_a
1219
+
1220
+ # Enumerable methods work
1221
+ session.messages.first
1222
+ session.messages.count
1223
+ session.messages.select { |m| m.type == "assistant" }
1224
+ ```
1225
+
1226
+ ### Session Fields
1201
1227
 
1202
1228
  | Field | Type | Description |
1203
1229
  |------------------|-----------------|--------------------------------------------------|
@@ -1210,11 +1236,24 @@ Each session is a `SessionInfo` with these fields:
1210
1236
  | `git_branch` | `String\|nil` | Git branch the session was on |
1211
1237
  | `cwd` | `String\|nil` | Working directory of the session |
1212
1238
 
1239
+ ### Functional API
1240
+
1241
+ The lower-level functional API is also available:
1242
+
1243
+ ```ruby
1244
+ # List sessions (returns SessionInfo objects)
1245
+ infos = ClaudeAgent.list_sessions(dir: "/path/to/project", limit: 10)
1246
+
1247
+ # Read messages directly
1248
+ messages = ClaudeAgent.get_session_messages("abc-123-def", limit: 10, offset: 5)
1249
+ ```
1250
+
1251
+ ### Resume a Past Session
1252
+
1213
1253
  Use with `Conversation.resume` to pick up where you left off:
1214
1254
 
1215
1255
  ```ruby
1216
- sessions = ClaudeAgent.list_sessions(dir: Dir.pwd, limit: 5)
1217
- session = sessions.first
1256
+ session = ClaudeAgent::Session.where(dir: Dir.pwd, limit: 5).first
1218
1257
 
1219
1258
  conversation = ClaudeAgent.resume_conversation(session.session_id)
1220
1259
  turn = conversation.say("Continue where we left off")
@@ -1307,23 +1346,26 @@ session = ClaudeAgent.unstable_v2_create_session(options)
1307
1346
 
1308
1347
  ### Return Types
1309
1348
 
1310
- | Type | Purpose |
1311
- |-----------------------|----------------------------------------------------------------------------------|
1312
- | `TurnResult` | Complete agent turn with text, tools, usage, and status accessors |
1313
- | `ToolActivity` | Tool use/result pair with turn index and timing |
1314
- | `CumulativeUsage` | Running totals of tokens, cost, turns, and duration |
1315
- | `PermissionRequest` | Deferred permission promise resolvable from any thread |
1316
- | `PermissionQueue` | Thread-safe queue of pending permission requests |
1317
- | `EventHandler` | Typed event callback registry |
1318
- | `SlashCommand` | Available slash commands (name, description, argument_hint) |
1319
- | `ModelInfo` | Available models (value, display_name, description) |
1320
- | `McpServerStatus` | MCP server status (name, status, server_info) |
1321
- | `AccountInfo` | Account information (email, organization, subscription_type) |
1322
- | `ModelUsage` | Per-model usage stats (input_tokens, output_tokens, cost_usd) |
1323
- | `McpSetServersResult` | Result of set_mcp_servers (added, removed, errors) |
1324
- | `RewindFilesResult` | Result of rewind_files (can_rewind, error, files_changed, insertions, deletions) |
1325
- | `SessionInfo` | Session metadata from `list_sessions` (session_id, summary, git_branch, cwd) |
1326
- | `SDKPermissionDenial` | Permission denial info (tool_name, tool_use_id, tool_input) |
1349
+ | Type | Purpose |
1350
+ |--------------------------|----------------------------------------------------------------------------------|
1351
+ | `TurnResult` | Complete agent turn with text, tools, usage, and status accessors |
1352
+ | `ToolActivity` | Tool use/result pair with turn index and timing |
1353
+ | `CumulativeUsage` | Running totals of tokens, cost, turns, and duration |
1354
+ | `PermissionRequest` | Deferred permission promise resolvable from any thread |
1355
+ | `PermissionQueue` | Thread-safe queue of pending permission requests |
1356
+ | `EventHandler` | Typed event callback registry |
1357
+ | `SlashCommand` | Available slash commands (name, description, argument_hint) |
1358
+ | `ModelInfo` | Available models (value, display_name, description) |
1359
+ | `McpServerStatus` | MCP server status (name, status, server_info) |
1360
+ | `AccountInfo` | Account information (email, organization, subscription_type) |
1361
+ | `ModelUsage` | Per-model usage stats (input_tokens, output_tokens, cost_usd) |
1362
+ | `McpSetServersResult` | Result of set_mcp_servers (added, removed, errors) |
1363
+ | `RewindFilesResult` | Result of rewind_files (can_rewind, error, files_changed, insertions, deletions) |
1364
+ | `Session` | Session finder with `.find`, `.all`, `#messages` (wraps SessionInfo) |
1365
+ | `SessionMessageRelation` | Chainable, Enumerable query object for session messages |
1366
+ | `SessionInfo` | Session metadata from `list_sessions` (session_id, summary, git_branch, cwd) |
1367
+ | `SessionMessage` | Message from a session transcript (type, uuid, session_id, message) |
1368
+ | `SDKPermissionDenial` | Permission denial info (tool_name, tool_use_id, tool_input) |
1327
1369
 
1328
1370
  ## Logging
1329
1371
 
data/SPEC.md CHANGED
@@ -3,11 +3,11 @@
3
3
  This document provides a comprehensive specification of the Claude Agent SDK, comparing feature parity across the official TypeScript and Python SDKs with this Ruby implementation.
4
4
 
5
5
  **Reference Versions:**
6
- - TypeScript SDK: v0.2.56 (npm package)
7
- - Python SDK: v0.1.43 from GitHub (commit 9d758dd)
6
+ - TypeScript SDK: v0.2.61 (npm package)
7
+ - Python SDK: v0.1.44 from GitHub (commit 7297bdc)
8
8
  - Ruby SDK: This repository
9
9
 
10
- **Last Updated:** 2026-02-25
10
+ **Last Updated:** 2026-02-26
11
11
 
12
12
  ---
13
13
 
@@ -524,9 +524,35 @@ Session management and resumption.
524
524
 
525
525
  ### Session Discovery
526
526
 
527
- | Feature | TypeScript | Python | Ruby | Notes |
528
- |------------------|:----------:|:------:|:----:|--------------------------------------------|
529
- | `listSessions()` | ✅ | ❌ | ✅ | List past sessions with metadata (v0.2.53) |
527
+ | Feature | TypeScript | Python | Ruby | Notes |
528
+ |------------------------|:----------:|:------:|:----:|--------------------------------------------------|
529
+ | `listSessions()` | ✅ | ❌ | ✅ | List past sessions with metadata (v0.2.53) |
530
+ | `getSessionMessages()` | ✅ | ❌ | ✅ | Read session transcript messages (v0.2.59) |
531
+
532
+ #### ListSessionsOptions
533
+
534
+ | Field | TypeScript | Python | Ruby | Notes |
535
+ |---------|:----------:|:------:|:----:|-------------------------------------------|
536
+ | `dir` | ✅ | ❌ | ✅ | Project directory (includes worktrees) |
537
+ | `limit` | ✅ | ❌ | ✅ | Maximum number of sessions to return |
538
+
539
+ #### GetSessionMessagesOptions
540
+
541
+ | Field | TypeScript | Python | Ruby | Notes |
542
+ |----------|:----------:|:------:|:----:|----------------------------------------------|
543
+ | `dir` | ✅ | ❌ | ✅ | Project directory to find session in |
544
+ | `limit` | ✅ | ❌ | ✅ | Maximum number of messages to return |
545
+ | `offset` | ✅ | ❌ | ✅ | Number of messages to skip from the start |
546
+
547
+ #### SessionMessage Fields
548
+
549
+ | Field | TypeScript | Python | Ruby | Notes |
550
+ |----------------------|:----------:|:------:|:----:|----------------------------------|
551
+ | `type` | ✅ | ❌ | ✅ | 'user' or 'assistant' |
552
+ | `uuid` | ✅ | ❌ | ✅ | Message UUID |
553
+ | `session_id` | ✅ | ❌ | ✅ | Session ID |
554
+ | `message` | ✅ | ❌ | ✅ | Raw message content |
555
+ | `parent_tool_use_id` | ✅ | ❌ | ✅ | Parent tool use ID (always null) |
530
556
 
531
557
  #### SDKSessionInfo Fields
532
558
 
@@ -649,9 +675,10 @@ Public API surface for SDK clients.
649
675
 
650
676
  ### Standalone Functions
651
677
 
652
- | Feature | TypeScript | Python | Ruby | Notes |
653
- |------------------|:----------------:|:------:|:----:|--------------------------------------------|
654
- | `listSessions()` | `listSessions` | ❌ | ✅ | List past sessions with metadata (v0.2.53) |
678
+ | Feature | TypeScript | Python | Ruby | Notes |
679
+ |------------------------|:----------:|:------:|:----:|--------------------------------------------|
680
+ | `listSessions()` | | ❌ | ✅ | List past sessions with metadata (v0.2.53) |
681
+ | `getSessionMessages()` | ✅ | ❌ | ✅ | Read session transcript (v0.2.59) |
655
682
 
656
683
  ### Query Interface
657
684
 
@@ -727,7 +754,9 @@ Public API surface for SDK clients.
727
754
  - v0.2.51: Added `TaskProgressMessage` for real-time background agent progress reporting
728
755
  - v0.2.52: Added `mcp_authenticate`/`mcp_clear_auth` control requests for MCP server authentication
729
756
  - v0.2.53: Added `listSessions()` for discovering and listing past sessions with `SDKSessionInfo` metadata
730
- - v0.2.54 – v0.2.56: CLI parity updates (no new SDK-facing features)
757
+ - v0.2.54 – v0.2.58: CLI parity updates (no new SDK-facing features)
758
+ - v0.2.59: Added `getSessionMessages()` for reading session transcript history with pagination (limit/offset)
759
+ - v0.2.61: CLI parity update (no new SDK-facing features)
731
760
 
732
761
  ### Python SDK
733
762
  - Full source available with `Transport` abstract class
@@ -741,10 +770,10 @@ Public API surface for SDK clients.
741
770
  - Added `thinking` config and `effort` option in v0.1.36
742
771
  - Handles `rate_limit_event` and unknown message types gracefully (v0.1.40)
743
772
  - Client has `get_server_info()` for accessing the initialization result (v0.1.31+)
744
- - v0.1.42 – v0.1.43: CLI parity updates (no new SDK-facing features)
773
+ - v0.1.42 – v0.1.44: CLI parity updates (no new SDK-facing features; latest commit bumps bundled CLI to v2.1.61)
745
774
 
746
775
  ### Ruby SDK (This Repository)
747
- - Feature parity with TypeScript SDK v0.2.56
776
+ - Feature parity with TypeScript SDK v0.2.61
748
777
  - Ruby-idiomatic patterns (Data.define, snake_case)
749
778
  - Complete control protocol, hook, and V2 Session API support
750
779
  - Dedicated Client class for multi-turn conversations
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module ClaudeAgent
6
+ # Reads a session's conversation transcript from its JSONL file.
7
+ #
8
+ # Given a session UUID, finds the session file on disk, parses all messages,
9
+ # reconstructs the main conversation thread (handling branches and forks),
10
+ # filters to user/assistant messages, and returns SessionMessage objects
11
+ # with optional pagination.
12
+ #
13
+ # Matches the TypeScript SDK's getSessionMessages() (v0.2.59).
14
+ #
15
+ # @example Basic usage
16
+ # messages = ClaudeAgent.get_session_messages("abc-123-def")
17
+ # messages.each { |m| puts "#{m.type}: #{m.message}" }
18
+ #
19
+ # @example With pagination
20
+ # messages = ClaudeAgent.get_session_messages("abc-123-def", limit: 10, offset: 5)
21
+ #
22
+ module GetSessionMessages
23
+ # Valid message types to include when parsing JSONL lines.
24
+ PARSEABLE_TYPES = %w[user assistant progress system attachment].freeze
25
+
26
+ class << self
27
+ # Read session messages with optional pagination.
28
+ #
29
+ # @param session_id [String] UUID of the session to read
30
+ # @param dir [String, nil] Project directory to find the session in
31
+ # @param limit [Integer, nil] Maximum number of messages to return
32
+ # @param offset [Integer, nil] Number of messages to skip from the start
33
+ # @return [Array<SessionMessage>] User/assistant messages, or empty array if not found
34
+ def call(session_id, dir: nil, limit: nil, offset: nil)
35
+ return [] unless SessionPaths::UUID_PATTERN.match?(session_id)
36
+
37
+ content = find_session_file(session_id, dir)
38
+ return [] unless content
39
+
40
+ raw_messages = parse_jsonl(content)
41
+ thread = reconstruct_thread(raw_messages)
42
+ result = thread.select { |msg| include_message?(msg) }.map { |msg| to_session_message(msg) }
43
+
44
+ off = offset || 0
45
+ if limit && limit > 0
46
+ result.slice(off, limit) || []
47
+ elsif off > 0
48
+ result.slice(off..) || []
49
+ else
50
+ result
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Locate and read a session JSONL file from disk.
57
+ #
58
+ # @param session_id [String] Session UUID
59
+ # @param dir [String, nil] Optional project directory to scope search
60
+ # @return [String, nil] File contents or nil
61
+ def find_session_file(session_id, dir)
62
+ filename = "#{session_id}.jsonl"
63
+
64
+ if dir
65
+ resolved = SessionPaths.realpath(dir)
66
+ project_dir = SessionPaths.find_project_dir(resolved)
67
+ if project_dir
68
+ content = read_session_content(project_dir, filename)
69
+ return content if content
70
+ end
71
+
72
+ # Try worktrees
73
+ worktrees = SessionPaths.git_worktrees(resolved)
74
+ worktrees.each do |wt|
75
+ next if wt == resolved
76
+ wt_project_dir = SessionPaths.find_project_dir(wt)
77
+ if wt_project_dir
78
+ content = read_session_content(wt_project_dir, filename)
79
+ return content if content
80
+ end
81
+ end
82
+
83
+ nil
84
+ else
85
+ # Search all project directories
86
+ base = SessionPaths.projects_dir
87
+ return nil unless File.directory?(base)
88
+
89
+ Dir.entries(base).each do |entry|
90
+ next if entry.start_with?(".")
91
+ entry_path = File.join(base, entry)
92
+ next unless File.directory?(entry_path)
93
+ content = read_session_content(entry_path, filename)
94
+ return content if content
95
+ end
96
+
97
+ nil
98
+ end
99
+ end
100
+
101
+ # Read a session file's full contents.
102
+ #
103
+ # @param dir [String] Directory containing the file
104
+ # @param filename [String] JSONL filename
105
+ # @return [String, nil] File contents or nil
106
+ def read_session_content(dir, filename)
107
+ path = File.join(dir, filename)
108
+ File.read(path, encoding: "UTF-8")
109
+ rescue SystemCallError
110
+ nil
111
+ end
112
+
113
+ # Parse JSONL content into raw message hashes.
114
+ # Only includes messages with valid types and a uuid field.
115
+ #
116
+ # @param content [String] JSONL file contents
117
+ # @return [Array<Hash>] Parsed message hashes
118
+ def parse_jsonl(content)
119
+ messages = []
120
+ content.each_line do |line|
121
+ line = line.strip
122
+ next if line.empty?
123
+
124
+ begin
125
+ parsed = JSON.parse(line)
126
+ type = parsed["type"]
127
+ next unless PARSEABLE_TYPES.include?(type) && parsed["uuid"].is_a?(String)
128
+ messages << parsed
129
+ rescue JSON::ParserError
130
+ next
131
+ end
132
+ end
133
+ messages
134
+ end
135
+
136
+ # Reconstruct the main conversation thread from raw messages.
137
+ #
138
+ # Handles branches, forks, and sidechains by:
139
+ # 1. Building UUID -> message and UUID -> index maps
140
+ # 2. Finding leaf messages (messages with no children)
141
+ # 3. Walking up parentUuid chains to find user/assistant ancestors
142
+ # 4. Picking the best leaf (highest file position, prefer main thread)
143
+ # 5. Walking from best leaf to root to build chronological thread
144
+ #
145
+ # @param messages [Array<Hash>] All parsed messages from JSONL
146
+ # @return [Array<Hash>] Chronologically ordered main thread messages
147
+ def reconstruct_thread(messages)
148
+ # Build UUID -> message map
149
+ by_uuid = {}
150
+ messages.each { |msg| by_uuid[msg["uuid"]] = msg }
151
+
152
+ # Build UUID -> file index map (position in file)
153
+ index_map = {}
154
+ messages.each_with_index { |msg, i| index_map[msg["uuid"]] = i }
155
+
156
+ # Find parent UUIDs (messages that have children pointing to them)
157
+ parent_uuids = Set.new
158
+ messages.each do |msg|
159
+ parent_uuids.add(msg["parentUuid"]) if msg["parentUuid"]
160
+ end
161
+
162
+ # Find leaf messages (no children point to them)
163
+ leaves = messages.reject { |msg| parent_uuids.include?(msg["uuid"]) }
164
+
165
+ # For each leaf, walk up parentUuid chain to find nearest user/assistant ancestor
166
+ candidates = []
167
+ leaves.each do |leaf|
168
+ current = leaf
169
+ seen = Set.new
170
+ while current
171
+ break if seen.include?(current["uuid"])
172
+ seen.add(current["uuid"])
173
+ if current["type"] == "user" || current["type"] == "assistant"
174
+ candidates << current
175
+ break
176
+ end
177
+ current = current["parentUuid"] ? by_uuid[current["parentUuid"]] : nil
178
+ end
179
+ end
180
+
181
+ return [] if candidates.empty?
182
+
183
+ # Filter out sidechain/team/meta candidates
184
+ main_candidates = candidates.reject do |msg|
185
+ msg["isSidechain"] || msg["teamName"] || msg["isMeta"]
186
+ end
187
+
188
+ # Pick best: highest file position index
189
+ pick_best = ->(arr) {
190
+ arr.max_by { |msg| index_map[msg["uuid"]] || -1 }
191
+ }
192
+
193
+ best_leaf = main_candidates.empty? ? pick_best.call(candidates) : pick_best.call(main_candidates)
194
+
195
+ # Walk from best leaf back to root, building chronological thread
196
+ thread = []
197
+ visited = Set.new
198
+ current = best_leaf
199
+ while current
200
+ break if visited.include?(current["uuid"])
201
+ visited.add(current["uuid"])
202
+ thread.unshift(current) # prepend for chronological order
203
+ current = current["parentUuid"] ? by_uuid[current["parentUuid"]] : nil
204
+ end
205
+
206
+ thread
207
+ end
208
+
209
+ # Whether a message should be included in the final output.
210
+ #
211
+ # @param msg [Hash] Raw message hash
212
+ # @return [Boolean]
213
+ def include_message?(msg)
214
+ return false unless msg["type"] == "user" || msg["type"] == "assistant"
215
+ return false if msg["isMeta"]
216
+ return false if msg["isSidechain"]
217
+ return false if msg["teamName"]
218
+ true
219
+ end
220
+
221
+ # Transform a raw message hash into a SessionMessage.
222
+ #
223
+ # @param msg [Hash] Raw message hash from JSONL
224
+ # @return [SessionMessage]
225
+ def to_session_message(msg)
226
+ SessionMessage.new(
227
+ type: msg["type"],
228
+ uuid: msg["uuid"],
229
+ session_id: msg["sessionId"],
230
+ message: msg["message"],
231
+ parent_tool_use_id: nil
232
+ )
233
+ end
234
+ end
235
+ end
236
+ end
@@ -17,9 +17,7 @@ module ClaudeAgent
17
17
  #
18
18
  module ListSessions
19
19
  # Constants matching TypeScript SDK
20
- MAX_SLUG_LENGTH = 200
21
20
  BUFFER_SIZE = 65_536 # 64KB
22
- UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
23
21
 
24
22
  # Patterns for filtering non-meaningful user prompts (matches TypeScript MM regex)
25
23
  META_MESSAGE_PATTERN = /\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)/
@@ -44,57 +42,6 @@ module ClaudeAgent
44
42
 
45
43
  private
46
44
 
47
- # --- Directory Encoding ---
48
-
49
- # Encode a project directory path to a slug for the projects directory.
50
- # Matches the TypeScript SDK's q9 function exactly.
51
- #
52
- # @param path [String] Absolute directory path
53
- # @return [String] Encoded slug
54
- def encode_project_dir(path)
55
- slug = path.gsub(/[^a-zA-Z0-9]/, "-")
56
- return slug if slug.length <= MAX_SLUG_LENGTH
57
-
58
- hash = java_string_hash(path)
59
- "#{slug[0, MAX_SLUG_LENGTH]}-#{hash}"
60
- end
61
-
62
- # Java-style string hash (matches TypeScript DM function).
63
- # Computes hash = ((hash << 5) - hash + charCode) as 32-bit signed integer,
64
- # then returns absolute value in base 36.
65
- #
66
- # @param str [String]
67
- # @return [String] Base-36 hash
68
- def java_string_hash(str)
69
- hash = 0
70
- str.each_char do |c|
71
- hash = ((hash << 5) - hash + c.ord) & 0xFFFFFFFF
72
- # Convert to signed 32-bit integer
73
- hash -= 0x100000000 if hash >= 0x80000000
74
- end
75
- hash.abs.to_s(36)
76
- end
77
-
78
- # --- Config Paths ---
79
-
80
- # @return [String] Claude config directory
81
- def config_dir
82
- (ENV["CLAUDE_CONFIG_DIR"] || File.join(Dir.home, ".claude"))
83
- end
84
-
85
- # @return [String] Projects directory within config
86
- def projects_dir
87
- File.join(config_dir, "projects")
88
- end
89
-
90
- # Get the expected project directory path for a given working directory.
91
- #
92
- # @param path [String] Working directory
93
- # @return [String] Full path to project sessions directory
94
- def project_dir_for(path)
95
- File.join(projects_dir, encode_project_dir(path))
96
- end
97
-
98
45
  # --- Session File Reading ---
99
46
 
100
47
  # Read head and tail buffers from a session file.
@@ -329,7 +276,7 @@ module ClaudeAgent
329
276
  next unless entry.end_with?(".jsonl")
330
277
 
331
278
  stem = entry[0...-6] # Remove .jsonl extension
332
- next unless UUID_PATTERN.match?(stem)
279
+ next unless SessionPaths::UUID_PATTERN.match?(stem)
333
280
 
334
281
  full_path = File.join(dir_path, entry)
335
282
  session = parse_session_file(full_path, stem)
@@ -339,63 +286,6 @@ module ClaudeAgent
339
286
  sessions
340
287
  end
341
288
 
342
- # Look up the project directory for a given path, handling hash suffix fallback.
343
- # Matches TypeScript's tQ function.
344
- #
345
- # @param path [String] Working directory
346
- # @return [String, nil] Project directory path or nil
347
- def find_project_dir(path)
348
- expected = project_dir_for(path)
349
- return expected if File.directory?(expected)
350
-
351
- # Try prefix matching for hash-suffixed directories
352
- slug = encode_project_dir(path)
353
- return nil if slug.length <= MAX_SLUG_LENGTH
354
-
355
- prefix = slug[0, MAX_SLUG_LENGTH]
356
- base = projects_dir
357
- return nil unless File.directory?(base)
358
-
359
- Dir.entries(base).each do |entry|
360
- next if entry.start_with?(".")
361
- next unless File.directory?(File.join(base, entry))
362
- return File.join(base, entry) if entry.start_with?("#{prefix}-")
363
- end
364
-
365
- nil
366
- end
367
-
368
- # --- Worktree Support ---
369
-
370
- # Get git worktree paths for a directory.
371
- #
372
- # @param dir [String] Working directory
373
- # @return [Array<String>] Worktree paths
374
- def git_worktrees(dir)
375
- output = nil
376
- IO.popen([ "git", "worktree", "list", "--porcelain" ], chdir: dir, err: File::NULL) do |io|
377
- Timeout.timeout(5) { output = io.read }
378
- end
379
-
380
- return [] unless output
381
-
382
- output.lines
383
- .select { |line| line.start_with?("worktree ") }
384
- .map { |line| line[9..].strip.unicode_normalize(:nfc) }
385
- rescue SystemCallError, Timeout::Error, Errno::ENOENT
386
- []
387
- end
388
-
389
- # Resolve symlinks and normalize a path.
390
- #
391
- # @param path [String]
392
- # @return [String]
393
- def realpath(path)
394
- File.realpath(path).unicode_normalize(:nfc)
395
- rescue SystemCallError
396
- path.unicode_normalize(:nfc)
397
- end
398
-
399
289
  # --- Listing Modes ---
400
290
 
401
291
  # List sessions for a specific directory, including worktree siblings.
@@ -405,28 +295,28 @@ module ClaudeAgent
405
295
  # @param limit [Integer, nil]
406
296
  # @return [Array<SessionInfo>]
407
297
  def list_for_directory(dir, limit)
408
- resolved = realpath(dir)
409
- worktrees = git_worktrees(resolved)
298
+ resolved = SessionPaths.realpath(dir)
299
+ worktrees = SessionPaths.git_worktrees(resolved)
410
300
 
411
301
  # Simple case: not in a worktree (or single worktree)
412
302
  if worktrees.length <= 1
413
- project_dir = find_project_dir(resolved)
303
+ project_dir = SessionPaths.find_project_dir(resolved)
414
304
  return [] unless project_dir
415
305
  return sort_and_limit(scan_project_dir(project_dir), limit)
416
306
  end
417
307
 
418
308
  # Complex case: multiple worktrees - scan all related project directories
419
- base = projects_dir
309
+ base = SessionPaths.projects_dir
420
310
 
421
311
  # Build prefix list from worktree paths (longest first for matching)
422
- prefixes = worktrees.map { |wt| { path: wt, prefix: encode_project_dir(wt) } }
312
+ prefixes = worktrees.map { |wt| { path: wt, prefix: SessionPaths.encode_project_dir(wt) } }
423
313
  prefixes.sort_by! { |p| -p[:prefix].length }
424
314
 
425
315
  all_sessions = []
426
316
  seen_dirs = Set.new
427
317
 
428
318
  # First: sessions from the exact directory
429
- project_dir = find_project_dir(resolved)
319
+ project_dir = SessionPaths.find_project_dir(resolved)
430
320
  if project_dir
431
321
  seen_dirs.add(File.basename(project_dir))
432
322
  all_sessions.concat(scan_project_dir(project_dir))
@@ -442,7 +332,7 @@ module ClaudeAgent
442
332
 
443
333
  prefixes.each do |p|
444
334
  prefix = p[:prefix]
445
- if entry == prefix || (prefix.length >= MAX_SLUG_LENGTH && entry.start_with?("#{prefix[0, MAX_SLUG_LENGTH]}-"))
335
+ if entry == prefix || (prefix.length >= SessionPaths::MAX_SLUG_LENGTH && entry.start_with?("#{prefix[0, SessionPaths::MAX_SLUG_LENGTH]}-"))
446
336
  seen_dirs.add(entry)
447
337
  all_sessions.concat(scan_project_dir(entry_path))
448
338
  break
@@ -460,7 +350,7 @@ module ClaudeAgent
460
350
  # @param limit [Integer, nil]
461
351
  # @return [Array<SessionInfo>]
462
352
  def list_all(limit)
463
- base = projects_dir
353
+ base = SessionPaths.projects_dir
464
354
  return [] unless File.directory?(base)
465
355
 
466
356
  all_sessions = []
@@ -49,7 +49,7 @@ module ClaudeAgent
49
49
  # session.stream.each { |msg| puts msg.inspect }
50
50
  # session.close
51
51
  #
52
- class Session
52
+ class V2Session
53
53
  attr_reader :session_id, :options
54
54
 
55
55
  def initialize(options)
@@ -141,7 +141,7 @@ module ClaudeAgent
141
141
  # session = ClaudeAgent.unstable_v2_create_session(model: "claude-sonnet-4-5-20250929")
142
142
  #
143
143
  def unstable_v2_create_session(options)
144
- Session.new(options)
144
+ V2Session.new(options)
145
145
  end
146
146
 
147
147
  # V2 API - UNSTABLE
@@ -158,7 +158,7 @@ module ClaudeAgent
158
158
  def unstable_v2_resume_session(session_id, options)
159
159
  # For resumption, we need to pass the resume option through
160
160
  # Since SessionOptions doesn't have resume, we handle it in the Client options
161
- session = Session.new(options)
161
+ session = V2Session.new(options)
162
162
  session.instance_variable_set(:@resume_session_id, session_id)
163
163
 
164
164
  # Override build_client_options to include resume
@@ -204,4 +204,72 @@ module ClaudeAgent
204
204
  end
205
205
  end
206
206
  end
207
+
208
+ # Historical session finder with Rails-like API.
209
+ #
210
+ # Wraps SessionInfo with a rich interface for finding sessions
211
+ # and querying their message transcripts.
212
+ #
213
+ # @example Find a session by ID
214
+ # session = ClaudeAgent::Session.find("abc-123")
215
+ # session.summary # => "Fix login bug"
216
+ #
217
+ # @example List all sessions
218
+ # sessions = ClaudeAgent::Session.all(limit: 10)
219
+ #
220
+ # @example Query messages with chainable relation
221
+ # session.messages.where(limit: 5).each { |m| puts m.type }
222
+ #
223
+ class Session
224
+ attr_reader :session_id, :summary, :last_modified, :file_size,
225
+ :custom_title, :first_prompt, :git_branch, :cwd
226
+
227
+ def initialize(session_info)
228
+ @session_id = session_info.session_id
229
+ @summary = session_info.summary
230
+ @last_modified = session_info.last_modified
231
+ @file_size = session_info.file_size
232
+ @custom_title = session_info.custom_title
233
+ @first_prompt = session_info.first_prompt
234
+ @git_branch = session_info.git_branch
235
+ @cwd = session_info.cwd
236
+ @dir = session_info.cwd
237
+ end
238
+
239
+ # Returns a chainable, Enumerable relation for this session's messages.
240
+ #
241
+ # @return [SessionMessageRelation]
242
+ def messages
243
+ SessionMessageRelation.new(session_id, dir: @dir)
244
+ end
245
+
246
+ class << self
247
+ # Find a session by its UUID.
248
+ #
249
+ # @param session_id [String] UUID of the session
250
+ # @param dir [String, nil] Directory to scope the search
251
+ # @return [Session, nil] The session, or nil if not found
252
+ def find(session_id, dir: nil)
253
+ sessions = ListSessions.call(dir: dir)
254
+ info = sessions.find { |s| s.session_id == session_id }
255
+ info ? new(info) : nil
256
+ end
257
+
258
+ # List all sessions.
259
+ #
260
+ # @return [Array<Session>]
261
+ def all
262
+ ListSessions.call.map { |info| new(info) }
263
+ end
264
+
265
+ # Query sessions with optional filters.
266
+ #
267
+ # @param dir [String, nil] Directory to scope sessions to
268
+ # @param limit [Integer, nil] Maximum number of sessions to return
269
+ # @return [Array<Session>]
270
+ def where(dir: nil, limit: nil)
271
+ ListSessions.call(dir: dir, limit: limit).map { |info| new(info) }
272
+ end
273
+ end
274
+ end
207
275
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeAgent
4
+ # Chainable, lazy query object for session transcript messages.
5
+ #
6
+ # Wraps GetSessionMessages with an Enumerable interface and
7
+ # Rails-style `where` chaining for pagination.
8
+ #
9
+ # @example Basic usage
10
+ # relation = SessionMessageRelation.new("abc-123")
11
+ # relation.each { |m| puts m.type }
12
+ #
13
+ # @example Chaining
14
+ # relation.where(limit: 10).map(&:uuid)
15
+ # relation.where(offset: 5, limit: 10).to_a
16
+ #
17
+ class SessionMessageRelation
18
+ include Enumerable
19
+
20
+ def initialize(session_id, dir: nil, limit: nil, offset: nil)
21
+ @session_id = session_id
22
+ @dir = dir
23
+ @limit = limit
24
+ @offset = offset
25
+ end
26
+
27
+ # Return a new relation with updated pagination parameters.
28
+ #
29
+ # @param limit [Integer, nil] Maximum number of messages
30
+ # @param offset [Integer, nil] Number of messages to skip
31
+ # @return [SessionMessageRelation]
32
+ def where(limit: @limit, offset: @offset)
33
+ self.class.new(@session_id, dir: @dir, limit: limit, offset: offset)
34
+ end
35
+
36
+ # Iterate over messages. Required by Enumerable.
37
+ #
38
+ # @yield [SessionMessage] Each message in the result set
39
+ # @return [Enumerator] if no block given
40
+ def each(&block)
41
+ results.each(&block)
42
+ end
43
+
44
+ # Materialize the relation into an array.
45
+ #
46
+ # @return [Array<SessionMessage>]
47
+ def to_a
48
+ results.dup
49
+ end
50
+
51
+ private
52
+
53
+ def results
54
+ @results ||= GetSessionMessages.call(
55
+ @session_id, dir: @dir, limit: @limit, offset: @offset
56
+ )
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module ClaudeAgent
6
+ # Shared path/directory infrastructure for session discovery.
7
+ #
8
+ # Provides methods to locate Claude Code session files on disk,
9
+ # encode project directory paths, and resolve git worktrees.
10
+ # Used by both ListSessions and GetSessionMessages.
11
+ #
12
+ module SessionPaths
13
+ MAX_SLUG_LENGTH = 200
14
+ UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
15
+
16
+ module_function
17
+
18
+ # @return [String] Claude config directory
19
+ def config_dir
20
+ (ENV["CLAUDE_CONFIG_DIR"] || File.join(Dir.home, ".claude"))
21
+ end
22
+
23
+ # @return [String] Projects directory within config
24
+ def projects_dir
25
+ File.join(config_dir, "projects")
26
+ end
27
+
28
+ # Get the expected project directory path for a given working directory.
29
+ #
30
+ # @param path [String] Working directory
31
+ # @return [String] Full path to project sessions directory
32
+ def project_dir_for(path)
33
+ File.join(projects_dir, encode_project_dir(path))
34
+ end
35
+
36
+ # Encode a project directory path to a slug for the projects directory.
37
+ # Matches the TypeScript SDK's q9 function exactly.
38
+ #
39
+ # @param path [String] Absolute directory path
40
+ # @return [String] Encoded slug
41
+ def encode_project_dir(path)
42
+ slug = path.gsub(/[^a-zA-Z0-9]/, "-")
43
+ return slug if slug.length <= MAX_SLUG_LENGTH
44
+
45
+ hash = java_string_hash(path)
46
+ "#{slug[0, MAX_SLUG_LENGTH]}-#{hash}"
47
+ end
48
+
49
+ # Java-style string hash (matches TypeScript DM function).
50
+ # Computes hash = ((hash << 5) - hash + charCode) as 32-bit signed integer,
51
+ # then returns absolute value in base 36.
52
+ #
53
+ # @param str [String]
54
+ # @return [String] Base-36 hash
55
+ def java_string_hash(str)
56
+ hash = 0
57
+ str.each_char do |c|
58
+ hash = ((hash << 5) - hash + c.ord) & 0xFFFFFFFF
59
+ # Convert to signed 32-bit integer
60
+ hash -= 0x100000000 if hash >= 0x80000000
61
+ end
62
+ hash.abs.to_s(36)
63
+ end
64
+
65
+ # Look up the project directory for a given path, handling hash suffix fallback.
66
+ # Matches TypeScript's tQ function.
67
+ #
68
+ # @param path [String] Working directory
69
+ # @return [String, nil] Project directory path or nil
70
+ def find_project_dir(path)
71
+ expected = project_dir_for(path)
72
+ return expected if File.directory?(expected)
73
+
74
+ # Try prefix matching for hash-suffixed directories
75
+ slug = encode_project_dir(path)
76
+ return nil if slug.length <= MAX_SLUG_LENGTH
77
+
78
+ prefix = slug[0, MAX_SLUG_LENGTH]
79
+ base = projects_dir
80
+ return nil unless File.directory?(base)
81
+
82
+ Dir.entries(base).each do |entry|
83
+ next if entry.start_with?(".")
84
+ next unless File.directory?(File.join(base, entry))
85
+ return File.join(base, entry) if entry.start_with?("#{prefix}-")
86
+ end
87
+
88
+ nil
89
+ end
90
+
91
+ # Resolve symlinks and normalize a path.
92
+ #
93
+ # @param path [String]
94
+ # @return [String]
95
+ def realpath(path)
96
+ File.realpath(path).unicode_normalize(:nfc)
97
+ rescue SystemCallError
98
+ path.unicode_normalize(:nfc)
99
+ end
100
+
101
+ # Get git worktree paths for a directory.
102
+ #
103
+ # @param dir [String] Working directory
104
+ # @return [Array<String>] Worktree paths
105
+ def git_worktrees(dir)
106
+ output = nil
107
+ IO.popen([ "git", "worktree", "list", "--porcelain" ], chdir: dir, err: File::NULL) do |io|
108
+ Timeout.timeout(5) { output = io.read }
109
+ end
110
+
111
+ return [] unless output
112
+
113
+ output.lines
114
+ .select { |line| line.start_with?("worktree ") }
115
+ .map { |line| line[9..].strip.unicode_normalize(:nfc) }
116
+ rescue SystemCallError, Timeout::Error, Errno::ENOENT
117
+ []
118
+ end
119
+ end
120
+ end
@@ -213,6 +213,22 @@ module ClaudeAgent
213
213
  end
214
214
  end
215
215
 
216
+ # Message from a session transcript returned by get_session_messages (TypeScript SDK v0.2.59 parity)
217
+ #
218
+ # @example
219
+ # msg = SessionMessage.new(
220
+ # type: "user",
221
+ # uuid: "abc-123",
222
+ # session_id: "def-456",
223
+ # message: { "role" => "user", "content" => [{ "type" => "text", "text" => "Hello" }] }
224
+ # )
225
+ #
226
+ SessionMessage = Data.define(:type, :uuid, :session_id, :message, :parent_tool_use_id) do
227
+ def initialize(type:, uuid:, session_id:, message:, parent_tool_use_id: nil)
228
+ super
229
+ end
230
+ end
231
+
216
232
  AgentDefinition = Data.define(
217
233
  :description,
218
234
  :prompt,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgent
4
- VERSION = "0.7.9"
4
+ VERSION = "0.7.10"
5
5
  end
data/lib/claude_agent.rb CHANGED
@@ -30,8 +30,11 @@ require_relative "claude_agent/tool_activity"
30
30
  require_relative "claude_agent/query"
31
31
  require_relative "claude_agent/client"
32
32
  require_relative "claude_agent/conversation"
33
+ require_relative "claude_agent/session_paths" # Shared session path infrastructure
33
34
  require_relative "claude_agent/list_sessions" # Session discovery (TypeScript SDK v0.2.53 parity)
34
- require_relative "claude_agent/session" # V2 Session API (unstable)
35
+ require_relative "claude_agent/get_session_messages" # Session transcript reading (TypeScript SDK v0.2.59 parity)
36
+ require_relative "claude_agent/session_message_relation" # Chainable message query object
37
+ require_relative "claude_agent/session" # Session finder + V2 Session API (unstable)
35
38
 
36
39
  module ClaudeAgent
37
40
  class << self
@@ -56,6 +59,21 @@ module ClaudeAgent
56
59
  ListSessions.call(dir: dir, limit: limit)
57
60
  end
58
61
 
62
+ # Read messages from a past session's transcript
63
+ #
64
+ # Reads the session JSONL file from disk, reconstructs the main conversation
65
+ # thread, and returns user/assistant messages with optional pagination.
66
+ #
67
+ # @param session_id [String] UUID of the session to read
68
+ # @param dir [String, nil] Project directory to find the session in.
69
+ # When nil, searches all projects.
70
+ # @param limit [Integer, nil] Maximum number of messages to return.
71
+ # @param offset [Integer, nil] Number of messages to skip from the start.
72
+ # @return [Array<SessionMessage>]
73
+ def get_session_messages(session_id, dir: nil, limit: nil, offset: nil)
74
+ GetSessionMessages.call(session_id, dir: dir, limit: limit, offset: offset)
75
+ end
76
+
59
77
  # Resume a previous Conversation by session ID
60
78
  #
61
79
  # @param session_id [String] Session ID to resume
data/sig/claude_agent.rbs CHANGED
@@ -215,6 +215,17 @@ module ClaudeAgent
215
215
  def initialize: (session_id: String, summary: String, last_modified: Integer, file_size: Integer, ?custom_title: String?, ?first_prompt: String?, ?git_branch: String?, ?cwd: String?) -> void
216
216
  end
217
217
 
218
+ # Session transcript message returned by get_session_messages (TypeScript SDK v0.2.59 parity)
219
+ class SessionMessage
220
+ attr_reader type: String
221
+ attr_reader uuid: String
222
+ attr_reader session_id: String
223
+ attr_reader message: untyped
224
+ attr_reader parent_tool_use_id: nil
225
+
226
+ def initialize: (type: String, uuid: String, session_id: String, message: untyped, ?parent_tool_use_id: nil) -> void
227
+ end
228
+
218
229
  # Agent definition for custom subagents (TypeScript SDK parity)
219
230
  class AgentDefinition
220
231
  attr_reader description: String
@@ -1181,16 +1192,37 @@ module ClaudeAgent
1181
1192
 
1182
1193
  # Session discovery
1183
1194
  def self.list_sessions: (?dir: String?, ?limit: Integer?) -> Array[SessionInfo]
1195
+ def self.get_session_messages: (String session_id, ?dir: String?, ?limit: Integer?, ?offset: Integer?) -> Array[SessionMessage]
1196
+
1197
+ # Shared session path infrastructure
1198
+ module SessionPaths
1199
+ MAX_SLUG_LENGTH: Integer
1200
+ UUID_PATTERN: Regexp
1201
+
1202
+ def self.config_dir: () -> String
1203
+ def self.projects_dir: () -> String
1204
+ def self.project_dir_for: (String path) -> String
1205
+ def self.encode_project_dir: (String path) -> String
1206
+ def self.java_string_hash: (String str) -> String
1207
+ def self.find_project_dir: (String path) -> String?
1208
+ def self.realpath: (String path) -> String
1209
+ def self.git_worktrees: (String dir) -> Array[String]
1210
+ end
1184
1211
 
1185
1212
  # ListSessions module
1186
1213
  module ListSessions
1187
- MAX_SLUG_LENGTH: Integer
1188
1214
  BUFFER_SIZE: Integer
1189
- UUID_PATTERN: Regexp
1190
1215
 
1191
1216
  def self.call: (?dir: String?, ?limit: Integer?) -> Array[SessionInfo]
1192
1217
  end
1193
1218
 
1219
+ # GetSessionMessages module (TypeScript SDK v0.2.59 parity)
1220
+ module GetSessionMessages
1221
+ PARSEABLE_TYPES: Array[String]
1222
+
1223
+ def self.call: (String session_id, ?dir: String?, ?limit: Integer?, ?offset: Integer?) -> Array[SessionMessage]
1224
+ end
1225
+
1194
1226
  # Convenience methods
1195
1227
  def self.conversation: (**untyped) -> Conversation
1196
1228
  def self.resume_conversation: (String session_id, **untyped) -> Conversation
@@ -1423,7 +1455,7 @@ module ClaudeAgent
1423
1455
  end
1424
1456
 
1425
1457
  # V2 Session interface for multi-turn conversations
1426
- class Session
1458
+ class V2Session
1427
1459
  attr_reader session_id: String?
1428
1460
  attr_reader options: SessionOptions
1429
1461
 
@@ -1436,7 +1468,37 @@ module ClaudeAgent
1436
1468
  end
1437
1469
 
1438
1470
  # V2 API module-level methods
1439
- def self.unstable_v2_create_session: (Hash[Symbol, untyped] | SessionOptions options) -> Session
1440
- def self.unstable_v2_resume_session: (String session_id, Hash[Symbol, untyped] | SessionOptions options) -> Session
1471
+ def self.unstable_v2_create_session: (Hash[Symbol, untyped] | SessionOptions options) -> V2Session
1472
+ def self.unstable_v2_resume_session: (String session_id, Hash[Symbol, untyped] | SessionOptions options) -> V2Session
1441
1473
  def self.unstable_v2_prompt: (String message, Hash[Symbol, untyped] | SessionOptions options) -> ResultMessage
1474
+
1475
+ # Chainable, lazy query object for session transcript messages
1476
+ class SessionMessageRelation
1477
+ include Enumerable[SessionMessage]
1478
+
1479
+ def initialize: (String session_id, ?dir: String?, ?limit: Integer?, ?offset: Integer?) -> void
1480
+ def where: (?limit: Integer?, ?offset: Integer?) -> SessionMessageRelation
1481
+ def each: () { (SessionMessage) -> void } -> void
1482
+ | () -> Enumerator[SessionMessage, void]
1483
+ def to_a: () -> Array[SessionMessage]
1484
+ end
1485
+
1486
+ # Historical session finder with Rails-like API
1487
+ class Session
1488
+ attr_reader session_id: String
1489
+ attr_reader summary: String
1490
+ attr_reader last_modified: Integer
1491
+ attr_reader file_size: Integer
1492
+ attr_reader custom_title: String?
1493
+ attr_reader first_prompt: String?
1494
+ attr_reader git_branch: String?
1495
+ attr_reader cwd: String?
1496
+
1497
+ def initialize: (SessionInfo session_info) -> void
1498
+ def messages: () -> SessionMessageRelation
1499
+
1500
+ def self.find: (String session_id, ?dir: String?) -> Session?
1501
+ def self.all: () -> Array[Session]
1502
+ def self.where: (?dir: String?, ?limit: Integer?) -> Array[Session]
1503
+ end
1442
1504
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.9
4
+ version: 0.7.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Carr
@@ -59,6 +59,7 @@ files:
59
59
  - lib/claude_agent/cumulative_usage.rb
60
60
  - lib/claude_agent/errors.rb
61
61
  - lib/claude_agent/event_handler.rb
62
+ - lib/claude_agent/get_session_messages.rb
62
63
  - lib/claude_agent/hooks.rb
63
64
  - lib/claude_agent/list_sessions.rb
64
65
  - lib/claude_agent/logging.rb
@@ -73,6 +74,8 @@ files:
73
74
  - lib/claude_agent/query.rb
74
75
  - lib/claude_agent/sandbox_settings.rb
75
76
  - lib/claude_agent/session.rb
77
+ - lib/claude_agent/session_message_relation.rb
78
+ - lib/claude_agent/session_paths.rb
76
79
  - lib/claude_agent/spawn.rb
77
80
  - lib/claude_agent/tool_activity.rb
78
81
  - lib/claude_agent/transport/base.rb