braindump 0.3.0

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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +180 -0
  3. package/dist/adapters/base-adapter.d.ts +13 -0
  4. package/dist/adapters/base-adapter.d.ts.map +1 -0
  5. package/dist/adapters/base-adapter.js +7 -0
  6. package/dist/adapters/base-adapter.js.map +1 -0
  7. package/dist/adapters/claude-code/adapter.d.ts +47 -0
  8. package/dist/adapters/claude-code/adapter.d.ts.map +1 -0
  9. package/dist/adapters/claude-code/adapter.js +382 -0
  10. package/dist/adapters/claude-code/adapter.js.map +1 -0
  11. package/dist/adapters/codex/adapter.d.ts +26 -0
  12. package/dist/adapters/codex/adapter.d.ts.map +1 -0
  13. package/dist/adapters/codex/adapter.js +446 -0
  14. package/dist/adapters/codex/adapter.js.map +1 -0
  15. package/dist/adapters/cursor/adapter.d.ts +35 -0
  16. package/dist/adapters/cursor/adapter.d.ts.map +1 -0
  17. package/dist/adapters/cursor/adapter.js +675 -0
  18. package/dist/adapters/cursor/adapter.js.map +1 -0
  19. package/dist/adapters/index.d.ts +19 -0
  20. package/dist/adapters/index.d.ts.map +1 -0
  21. package/dist/adapters/index.js +65 -0
  22. package/dist/adapters/index.js.map +1 -0
  23. package/dist/cli/index.d.ts +3 -0
  24. package/dist/cli/index.d.ts.map +1 -0
  25. package/dist/cli/index.js +424 -0
  26. package/dist/cli/index.js.map +1 -0
  27. package/dist/cli/utils.d.ts +10 -0
  28. package/dist/cli/utils.d.ts.map +1 -0
  29. package/dist/cli/utils.js +21 -0
  30. package/dist/cli/utils.js.map +1 -0
  31. package/dist/core/compression.d.ts +11 -0
  32. package/dist/core/compression.d.ts.map +1 -0
  33. package/dist/core/compression.js +182 -0
  34. package/dist/core/compression.js.map +1 -0
  35. package/dist/core/conversation-analyzer.d.ts +9 -0
  36. package/dist/core/conversation-analyzer.d.ts.map +1 -0
  37. package/dist/core/conversation-analyzer.js +220 -0
  38. package/dist/core/conversation-analyzer.js.map +1 -0
  39. package/dist/core/project-context.d.ts +7 -0
  40. package/dist/core/project-context.d.ts.map +1 -0
  41. package/dist/core/project-context.js +136 -0
  42. package/dist/core/project-context.js.map +1 -0
  43. package/dist/core/prompt-builder.d.ts +7 -0
  44. package/dist/core/prompt-builder.d.ts.map +1 -0
  45. package/dist/core/prompt-builder.js +88 -0
  46. package/dist/core/prompt-builder.js.map +1 -0
  47. package/dist/core/registry.d.ts +10 -0
  48. package/dist/core/registry.d.ts.map +1 -0
  49. package/dist/core/registry.js +51 -0
  50. package/dist/core/registry.js.map +1 -0
  51. package/dist/core/token-estimator.d.ts +10 -0
  52. package/dist/core/token-estimator.d.ts.map +1 -0
  53. package/dist/core/token-estimator.js +14 -0
  54. package/dist/core/token-estimator.js.map +1 -0
  55. package/dist/core/validation.d.ts +188 -0
  56. package/dist/core/validation.d.ts.map +1 -0
  57. package/dist/core/validation.js +61 -0
  58. package/dist/core/validation.js.map +1 -0
  59. package/dist/core/watcher.d.ts +20 -0
  60. package/dist/core/watcher.d.ts.map +1 -0
  61. package/dist/core/watcher.js +208 -0
  62. package/dist/core/watcher.js.map +1 -0
  63. package/dist/providers/clipboard-provider.d.ts +8 -0
  64. package/dist/providers/clipboard-provider.d.ts.map +1 -0
  65. package/dist/providers/clipboard-provider.js +15 -0
  66. package/dist/providers/clipboard-provider.js.map +1 -0
  67. package/dist/providers/file-provider.d.ts +8 -0
  68. package/dist/providers/file-provider.d.ts.map +1 -0
  69. package/dist/providers/file-provider.js +14 -0
  70. package/dist/providers/file-provider.js.map +1 -0
  71. package/dist/providers/index.d.ts +9 -0
  72. package/dist/providers/index.d.ts.map +1 -0
  73. package/dist/providers/index.js +18 -0
  74. package/dist/providers/index.js.map +1 -0
  75. package/dist/types/index.d.ts +132 -0
  76. package/dist/types/index.d.ts.map +1 -0
  77. package/dist/types/index.js +3 -0
  78. package/dist/types/index.js.map +1 -0
  79. package/package.json +69 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kushal Srivastava
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # AgentRelay
2
+
3
+ A CLI tool that captures your AI coding agent session and generates a portable resume prompt so you can seamlessly continue in a different agent when tokens run out.
4
+
5
+ ## The Problem
6
+
7
+ AI coding agents are context silos. When your session hits a rate limit or runs out of tokens, you lose all that context. AgentRelay captures it and generates a handoff prompt so a new agent can pick up exactly where the last one left off.
8
+
9
+ ## Supported Agents
10
+
11
+ | Agent | Status |
12
+ |-------|--------|
13
+ | Claude Code | Working |
14
+ | Cursor | Working |
15
+ | Codex CLI | Working |
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ # From npm
21
+ npm install -g agentrelay
22
+
23
+ # From source
24
+ git clone https://github.com/Kushalwho/agentrelay.git
25
+ cd agentrelay
26
+ npm install
27
+ npm run build
28
+ npm link
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```bash
34
+ # Detect installed agents
35
+ agentrelay detect
36
+
37
+ # Full handoff — capture, compress, generate resume prompt
38
+ agentrelay handoff
39
+
40
+ # Target a specific agent for the resume format
41
+ agentrelay handoff --target cursor
42
+
43
+ # Preview without writing files
44
+ agentrelay handoff --dry-run
45
+
46
+ # Watch for rate limits (auto-detects agents)
47
+ agentrelay watch
48
+
49
+ # The resume prompt is in .handoff/RESUME.md and on your clipboard
50
+ # Paste it into your target agent and keep working
51
+ ```
52
+
53
+ ## Commands
54
+
55
+ ```
56
+ agentrelay detect Scan for installed agents
57
+ agentrelay list [--source <agent>] List recent sessions
58
+ agentrelay capture [--source <agent>] Capture session to .handoff/session.json
59
+ agentrelay handoff [options] Full pipeline: capture -> compress -> resume
60
+ agentrelay watch [--agents <csv>] Watch sessions for changes and rate limits
61
+ agentrelay resume [--file <path>] Re-generate resume from captured session
62
+ agentrelay info Show agent paths and config
63
+ ```
64
+
65
+ ### Handoff Options
66
+
67
+ ```
68
+ -s, --source <agent> Source agent (claude-code, cursor, codex). Auto-detected if omitted.
69
+ -t, --target <target> Target agent or "file"/"clipboard". Default: file + clipboard.
70
+ --session <id> Specific session ID. Default: most recent session.
71
+ -p, --project <path> Project path. Default: current directory.
72
+ --tokens <n> Token budget override. Default: based on target agent.
73
+ --dry-run Preview what would be captured without writing files.
74
+ --no-clipboard Skip clipboard copy (useful in CI/headless environments).
75
+ -o, --output <path> Custom output path. Directory or file path.
76
+ -v, --verbose Show detailed debug output.
77
+ ```
78
+
79
+ ### Watch Options
80
+
81
+ ```
82
+ --agents <csv> Comma-separated agents to watch (claude-code, cursor, codex).
83
+ --interval <seconds> Polling interval in seconds. Default: 30.
84
+ -p, --project <path> Only watch sessions for this project.
85
+ ```
86
+
87
+ ### Target-Specific Hints
88
+
89
+ When you specify `--target`, the resume prompt includes agent-specific instructions:
90
+
91
+ | Target | Hint |
92
+ |--------|------|
93
+ | `cursor` | "Paste this into Cursor's Composer to continue." |
94
+ | `codex` | "Feed this to Codex CLI with `codex resume` or paste it." |
95
+ | `claude-code` | "Paste this into a new Claude Code session to continue." |
96
+
97
+ ## How It Works
98
+
99
+ ```
100
+ +-----------------+ +--------------+ +-----------------+ +--------------+
101
+ | Agent Session | | Capture | | Compress | | RESUME.md |
102
+ | (JSONL/SQLite) | -> | + Analyze | -> | (7 priority | -> | + clipboard |
103
+ | | | + Enrich | | layers) | | |
104
+ +-----------------+ +--------------+ +-----------------+ +--------------+
105
+ ```
106
+
107
+ 1. **Capture** -- Reads session data from the agent's native storage (JSONL for Claude Code/Codex, SQLite for Cursor)
108
+ 2. **Analyze** -- Extracts task state, decisions, blockers, and completed steps from the conversation
109
+ 3. **Enrich** -- Adds project context: git branch/status/log, directory tree, memory files
110
+ 4. **Compress** -- Priority-layered compression to fit any context window
111
+ 5. **Generate** -- Builds a self-summarizing resume prompt that tells the new agent to pick up exactly where the last one left off
112
+ 6. **Deliver** -- Writes to `.handoff/RESUME.md` and copies to clipboard
113
+
114
+ ## Compression Priority Layers
115
+
116
+ | Priority | Layer | Always included? |
117
+ |----------|-------|-----------------|
118
+ | 1 | Task state (what's done, in progress, remaining) | Yes |
119
+ | 2 | Active files (diffs/content of changed files) | Yes |
120
+ | 3 | Decisions and blockers | Yes |
121
+ | 4 | Project context (git, directory tree, memory files) | If room |
122
+ | 5 | Session overview (stats, first/last message) | If room |
123
+ | 6 | Recent messages (last 20) | If room |
124
+ | 7 | Full history (older messages) | If room |
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ npm install # Install dependencies
130
+ npm run dev -- detect # Run in dev mode
131
+ npm test # Run tests (watch mode)
132
+ npm run test:run # Run tests (single run)
133
+ npm run lint # Type check
134
+ npm run build # Build to dist/
135
+ ```
136
+
137
+ ## Project Structure
138
+
139
+ ```
140
+ src/
141
+ ├── adapters/ # Agent-specific session readers
142
+ │ ├── claude-code/adapter.ts # JSONL parser for ~/.claude/projects/
143
+ │ ├── cursor/adapter.ts # SQLite reader for Cursor workspaceStorage
144
+ │ └── codex/adapter.ts # JSONL parser for ~/.codex/sessions/
145
+ ├── core/
146
+ │ ├── compression.ts # Priority-layered compression engine
147
+ │ ├── conversation-analyzer.ts # Extracts tasks, decisions, blockers
148
+ │ ├── prompt-builder.ts # RESUME.md template assembly
149
+ │ ├── token-estimator.ts # Character-based token estimation
150
+ │ ├── project-context.ts # Git info, directory tree, memory files
151
+ │ ├── registry.ts # Agent metadata (paths, context windows)
152
+ │ └── watcher.ts # Polling-based session watcher
153
+ ├── providers/
154
+ │ ├── file-provider.ts # Writes .handoff/RESUME.md
155
+ │ └── clipboard-provider.ts # Copies to system clipboard
156
+ ├── types/index.ts # All TypeScript interfaces
157
+ └── cli/index.ts # Commander.js CLI entry point
158
+ ```
159
+
160
+ ## Tests
161
+
162
+ 70 tests passing across 8 test files:
163
+ - Adapter tests (Claude Code, Cursor, Codex) with real JSONL/SQLite parsing
164
+ - Compression engine tests across all priority layers
165
+ - Conversation analyzer tests
166
+ - Prompt builder tests including target-agent hints
167
+ - Watcher tests with mocked adapters and fake timers
168
+ - End-to-end handoff flow integration tests
169
+
170
+ ## CI
171
+
172
+ GitHub Actions runs on every PR and push to main:
173
+ - TypeScript type check
174
+ - Tests (vitest)
175
+ - Build
176
+ - Node.js 18, 20, 22
177
+
178
+ ## License
179
+
180
+ MIT
@@ -0,0 +1,13 @@
1
+ import type { AgentAdapter, AgentId, CapturedSession, SessionInfo } from "../types/index.js";
2
+ /**
3
+ * Base class with shared utilities for all adapters.
4
+ * Concrete adapters extend this and implement the abstract methods.
5
+ */
6
+ export declare abstract class BaseAdapter implements AgentAdapter {
7
+ abstract agentId: AgentId;
8
+ abstract detect(): Promise<boolean>;
9
+ abstract listSessions(projectPath?: string): Promise<SessionInfo[]>;
10
+ abstract capture(sessionId: string): Promise<CapturedSession>;
11
+ abstract captureLatest(projectPath?: string): Promise<CapturedSession>;
12
+ }
13
+ //# sourceMappingURL=base-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/base-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAE7F;;;GAGG;AACH,8BAAsB,WAAY,YAAW,YAAY;IACvD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAE1B,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IACnC,QAAQ,CAAC,YAAY,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IACnE,QAAQ,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAC7D,QAAQ,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CACvE"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Base class with shared utilities for all adapters.
3
+ * Concrete adapters extend this and implement the abstract methods.
4
+ */
5
+ export class BaseAdapter {
6
+ }
7
+ //# sourceMappingURL=base-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-adapter.js","sourceRoot":"","sources":["../../src/adapters/base-adapter.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,OAAgB,WAAW;CAOhC"}
@@ -0,0 +1,47 @@
1
+ import { BaseAdapter } from "../base-adapter.js";
2
+ import type { AgentId, CapturedSession, SessionInfo } from "../../types/index.js";
3
+ /**
4
+ * Adapter for Claude Code sessions.
5
+ * Reads JSONL files from ~/.claude/projects/<path-hash>/<session-uuid>.jsonl
6
+ */
7
+ export declare class ClaudeCodeAdapter extends BaseAdapter {
8
+ agentId: AgentId;
9
+ /** Root directory where Claude Code stores project sessions. */
10
+ private get projectsDir();
11
+ detect(): Promise<boolean>;
12
+ listSessions(projectPath?: string): Promise<SessionInfo[]>;
13
+ capture(sessionId: string): Promise<CapturedSession>;
14
+ captureLatest(projectPath?: string): Promise<CapturedSession>;
15
+ /**
16
+ * Convert an absolute project path to the hash format Claude Code uses for
17
+ * subdirectory names. Slashes (and on Windows, backslashes and the drive
18
+ * colon) are replaced with hyphens.
19
+ *
20
+ * Example (Linux): /home/user/project -> -home-user-project
21
+ * Example (Windows): C:\Users\kusha\proj -> C--Users-kusha-proj
22
+ */
23
+ private pathToHash;
24
+ /**
25
+ * Reverse the hash back to a plausible absolute path.
26
+ *
27
+ * Example (Linux): -home-user-project -> /home/user/project
28
+ * Example (Windows): C--Users-kusha-proj -> C:\Users\kusha\proj
29
+ */
30
+ private hashToPath;
31
+ /**
32
+ * Read basic session info from a .jsonl file without loading it all into memory.
33
+ * Reads the first line for startedAt/preview and the last line for lastActiveAt,
34
+ * and counts lines for messageCount.
35
+ */
36
+ private readSessionInfo;
37
+ /**
38
+ * Locate the session file for a given session UUID by searching all
39
+ * subdirectories under ~/.claude/projects/.
40
+ */
41
+ private findSessionFile;
42
+ /**
43
+ * Stream-read a JSONL file and return all non-empty lines.
44
+ */
45
+ private readJsonlLines;
46
+ }
47
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/claude-code/adapter.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIjD,OAAO,KAAK,EACV,OAAO,EACP,eAAe,EAGf,WAAW,EACZ,MAAM,sBAAsB,CAAC;AAE9B;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IAChD,OAAO,EAAE,OAAO,CAAiB;IAEjC,gEAAgE;IAChE,OAAO,KAAK,WAAW,GAEtB;IAMK,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAkB1B,YAAY,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IA8C1D,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IA4KpD,aAAa,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAiBnE;;;;;;;OAOG;IACH,OAAO,CAAC,UAAU;IAclB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAclB;;;;OAIG;YACW,eAAe;IAyE7B;;;OAGG;YACW,eAAe;IAc7B;;OAEG;YACW,cAAc;CAiB7B"}
@@ -0,0 +1,382 @@
1
+ import fs from "node:fs";
2
+ import { createReadStream } from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import readline from "node:readline";
6
+ import { glob } from "glob";
7
+ import { BaseAdapter } from "../base-adapter.js";
8
+ import { analyzeConversation } from "../../core/conversation-analyzer.js";
9
+ import { extractProjectContext } from "../../core/project-context.js";
10
+ import { validateSession } from "../../core/validation.js";
11
+ /**
12
+ * Adapter for Claude Code sessions.
13
+ * Reads JSONL files from ~/.claude/projects/<path-hash>/<session-uuid>.jsonl
14
+ */
15
+ export class ClaudeCodeAdapter extends BaseAdapter {
16
+ agentId = "claude-code";
17
+ /** Root directory where Claude Code stores project sessions. */
18
+ get projectsDir() {
19
+ return path.join(os.homedir(), ".claude", "projects");
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // detect
23
+ // ---------------------------------------------------------------------------
24
+ async detect() {
25
+ if (!fs.existsSync(this.projectsDir)) {
26
+ return false;
27
+ }
28
+ const files = await glob("**/*.jsonl", {
29
+ cwd: this.projectsDir,
30
+ nodir: true,
31
+ absolute: false,
32
+ });
33
+ return files.length > 0;
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // listSessions
37
+ // ---------------------------------------------------------------------------
38
+ async listSessions(projectPath) {
39
+ if (!fs.existsSync(this.projectsDir)) {
40
+ return [];
41
+ }
42
+ let pattern;
43
+ if (projectPath) {
44
+ const hash = this.pathToHash(projectPath);
45
+ pattern = `${hash}/*.jsonl`;
46
+ }
47
+ else {
48
+ pattern = "**/*.jsonl";
49
+ }
50
+ const files = await glob(pattern, {
51
+ cwd: this.projectsDir,
52
+ nodir: true,
53
+ absolute: true,
54
+ });
55
+ const sessions = [];
56
+ for (const filePath of files) {
57
+ try {
58
+ const info = await this.readSessionInfo(filePath);
59
+ if (info) {
60
+ sessions.push(info);
61
+ }
62
+ }
63
+ catch {
64
+ // Skip unreadable files
65
+ }
66
+ }
67
+ // Sort by lastActiveAt descending (most recent first)
68
+ sessions.sort((a, b) => {
69
+ const aTime = a.lastActiveAt ?? "";
70
+ const bTime = b.lastActiveAt ?? "";
71
+ return bTime.localeCompare(aTime);
72
+ });
73
+ return sessions;
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // capture
77
+ // ---------------------------------------------------------------------------
78
+ async capture(sessionId) {
79
+ const filePath = await this.findSessionFile(sessionId);
80
+ if (!filePath) {
81
+ throw new Error(`Session not found: ${sessionId}`);
82
+ }
83
+ const lines = await this.readJsonlLines(filePath);
84
+ const messages = [];
85
+ const fileChanges = new Map();
86
+ const seenMessageIds = new Set();
87
+ let totalTokens = 0;
88
+ let lastAssistantMessage = "";
89
+ let sessionStartedAt;
90
+ for (const line of lines) {
91
+ let entry;
92
+ try {
93
+ entry = JSON.parse(line);
94
+ }
95
+ catch {
96
+ // Skip malformed lines
97
+ continue;
98
+ }
99
+ if (!entry.message) {
100
+ continue;
101
+ }
102
+ if (entry.message.id && seenMessageIds.has(entry.message.id)) {
103
+ continue;
104
+ }
105
+ if (entry.message.id) {
106
+ seenMessageIds.add(entry.message.id);
107
+ }
108
+ // Track session start time from the first entry's timestamp
109
+ if (!sessionStartedAt && entry.timestamp) {
110
+ sessionStartedAt = entry.timestamp;
111
+ }
112
+ const role = entry.message.role === "user" ? "user" : "assistant";
113
+ const contentBlocks = Array.isArray(entry.message.content)
114
+ ? entry.message.content
115
+ : [];
116
+ // Accumulate token counts
117
+ if (entry.message.usage) {
118
+ totalTokens +=
119
+ (entry.message.usage.input_tokens ?? 0) +
120
+ (entry.message.usage.output_tokens ?? 0);
121
+ }
122
+ // Extract text content from this message
123
+ const textParts = [];
124
+ for (const block of contentBlocks) {
125
+ if (block.type === "text" && block.text) {
126
+ textParts.push(block.text);
127
+ }
128
+ }
129
+ const textContent = textParts.join("\n");
130
+ // Track last assistant message for in-progress summary
131
+ if (role === "assistant" && textContent) {
132
+ lastAssistantMessage = textContent;
133
+ }
134
+ // Create conversation message for text content
135
+ if (textContent) {
136
+ messages.push({
137
+ role,
138
+ content: textContent,
139
+ timestamp: entry.timestamp,
140
+ tokenCount: entry.message.usage
141
+ ? (entry.message.usage.input_tokens ?? 0) +
142
+ (entry.message.usage.output_tokens ?? 0)
143
+ : undefined,
144
+ });
145
+ }
146
+ // Process tool_use blocks
147
+ for (const block of contentBlocks) {
148
+ if (block.type === "tool_use" && block.name) {
149
+ // Add a tool message to the conversation
150
+ messages.push({
151
+ role: "tool",
152
+ content: typeof block.input === "object"
153
+ ? JSON.stringify(block.input)
154
+ : String(block.input ?? ""),
155
+ toolName: block.name,
156
+ timestamp: entry.timestamp,
157
+ });
158
+ // Extract file changes from Write and Edit tool blocks
159
+ if ((block.name === "Write" || block.name === "Edit") &&
160
+ block.input &&
161
+ typeof block.input === "object") {
162
+ const input = block.input;
163
+ const filePth = input.file_path ??
164
+ input.path;
165
+ if (filePth) {
166
+ const ext = path.extname(filePth).slice(1);
167
+ fileChanges.set(filePth, {
168
+ path: filePth,
169
+ changeType: block.name === "Write" ? "created" : "modified",
170
+ diff: input.content,
171
+ language: ext || undefined,
172
+ });
173
+ }
174
+ }
175
+ }
176
+ // Also handle tool_result blocks
177
+ if (block.type === "tool_result") {
178
+ const resultContent = typeof block.content === "string"
179
+ ? block.content
180
+ : JSON.stringify(block.content ?? "");
181
+ messages.push({
182
+ role: "tool",
183
+ content: resultContent,
184
+ timestamp: entry.timestamp,
185
+ });
186
+ }
187
+ }
188
+ }
189
+ // Infer project path from the directory structure
190
+ const parentDir = path.basename(path.dirname(filePath));
191
+ const inferredProjectPath = this.hashToPath(parentDir);
192
+ const projectContext = await extractProjectContext(inferredProjectPath);
193
+ const analysis = analyzeConversation(messages);
194
+ const session = {
195
+ version: "1.0",
196
+ source: "claude-code",
197
+ capturedAt: new Date().toISOString(),
198
+ sessionId,
199
+ sessionStartedAt,
200
+ project: {
201
+ ...projectContext,
202
+ path: projectContext.path || inferredProjectPath,
203
+ name: projectContext.name || path.basename(inferredProjectPath),
204
+ },
205
+ conversation: {
206
+ messageCount: messages.length,
207
+ estimatedTokens: totalTokens,
208
+ messages,
209
+ },
210
+ filesChanged: Array.from(fileChanges.values()),
211
+ decisions: analysis.decisions,
212
+ blockers: analysis.blockers,
213
+ task: {
214
+ description: analysis.taskDescription,
215
+ completed: analysis.completedSteps,
216
+ remaining: [],
217
+ inProgress: lastAssistantMessage
218
+ ? lastAssistantMessage.substring(0, 200)
219
+ : undefined,
220
+ blockers: analysis.blockers,
221
+ },
222
+ };
223
+ return validateSession(session);
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // captureLatest
227
+ // ---------------------------------------------------------------------------
228
+ async captureLatest(projectPath) {
229
+ const sessions = await this.listSessions(projectPath);
230
+ if (sessions.length === 0) {
231
+ throw new Error(projectPath
232
+ ? `No Claude Code sessions found for project: ${projectPath}`
233
+ : "No Claude Code sessions found");
234
+ }
235
+ return this.capture(sessions[0].id);
236
+ }
237
+ // ---------------------------------------------------------------------------
238
+ // Private helpers
239
+ // ---------------------------------------------------------------------------
240
+ /**
241
+ * Convert an absolute project path to the hash format Claude Code uses for
242
+ * subdirectory names. Slashes (and on Windows, backslashes and the drive
243
+ * colon) are replaced with hyphens.
244
+ *
245
+ * Example (Linux): /home/user/project -> -home-user-project
246
+ * Example (Windows): C:\Users\kusha\proj -> C--Users-kusha-proj
247
+ */
248
+ pathToHash(projectPath) {
249
+ let normalized = projectPath;
250
+ if (process.platform === "win32") {
251
+ // Replace backslashes with forward slashes first for uniform handling
252
+ normalized = normalized.replace(/\\/g, "/");
253
+ // Handle drive letter: C: -> C-
254
+ normalized = normalized.replace(/^([A-Za-z]):/, "$1-");
255
+ }
256
+ // Replace all forward slashes with hyphens
257
+ return normalized.replace(/\//g, "-");
258
+ }
259
+ /**
260
+ * Reverse the hash back to a plausible absolute path.
261
+ *
262
+ * Example (Linux): -home-user-project -> /home/user/project
263
+ * Example (Windows): C--Users-kusha-proj -> C:\Users\kusha\proj
264
+ */
265
+ hashToPath(hash) {
266
+ // Detect Windows-style hash: starts with a drive letter followed by a dash
267
+ const windowsDriveMatch = hash.match(/^([A-Za-z])-(.*)$/);
268
+ if (windowsDriveMatch) {
269
+ const drive = windowsDriveMatch[1];
270
+ const rest = windowsDriveMatch[2];
271
+ // The rest has leading dash from root, replace dashes with backslash
272
+ return `${drive}:${rest.replace(/-/g, "\\")}`;
273
+ }
274
+ // Unix-style: leading dash represents root /
275
+ return hash.replace(/-/g, "/");
276
+ }
277
+ /**
278
+ * Read basic session info from a .jsonl file without loading it all into memory.
279
+ * Reads the first line for startedAt/preview and the last line for lastActiveAt,
280
+ * and counts lines for messageCount.
281
+ */
282
+ async readSessionInfo(filePath) {
283
+ const fileName = path.basename(filePath, ".jsonl");
284
+ // Infer project path from parent directory name
285
+ const parentDir = path.basename(path.dirname(filePath));
286
+ const projectPath = this.hashToPath(parentDir);
287
+ let firstLine;
288
+ let lastLine;
289
+ let lineCount = 0;
290
+ const rl = readline.createInterface({
291
+ input: createReadStream(filePath, { encoding: "utf-8" }),
292
+ crlfDelay: Infinity,
293
+ });
294
+ for await (const line of rl) {
295
+ if (!line.trim())
296
+ continue;
297
+ lineCount++;
298
+ if (lineCount === 1) {
299
+ firstLine = line;
300
+ }
301
+ lastLine = line;
302
+ }
303
+ if (lineCount === 0 || !firstLine) {
304
+ return null;
305
+ }
306
+ let startedAt;
307
+ let lastActiveAt;
308
+ let preview;
309
+ try {
310
+ const firstEntry = JSON.parse(firstLine);
311
+ startedAt = firstEntry.timestamp;
312
+ // Extract preview from first message text content
313
+ if (firstEntry.message?.content) {
314
+ const blocks = Array.isArray(firstEntry.message.content)
315
+ ? firstEntry.message.content
316
+ : [];
317
+ for (const block of blocks) {
318
+ if (block.type === "text" && block.text) {
319
+ preview = block.text.substring(0, 200);
320
+ break;
321
+ }
322
+ }
323
+ }
324
+ }
325
+ catch {
326
+ // Malformed first line — still include the session with what we have
327
+ }
328
+ if (lastLine && lastLine !== firstLine) {
329
+ try {
330
+ const lastEntry = JSON.parse(lastLine);
331
+ lastActiveAt = lastEntry.timestamp;
332
+ }
333
+ catch {
334
+ // Ignore malformed last line
335
+ }
336
+ }
337
+ else {
338
+ lastActiveAt = startedAt;
339
+ }
340
+ return {
341
+ id: fileName,
342
+ startedAt,
343
+ lastActiveAt,
344
+ messageCount: lineCount,
345
+ projectPath,
346
+ preview,
347
+ };
348
+ }
349
+ /**
350
+ * Locate the session file for a given session UUID by searching all
351
+ * subdirectories under ~/.claude/projects/.
352
+ */
353
+ async findSessionFile(sessionId) {
354
+ if (!fs.existsSync(this.projectsDir)) {
355
+ return null;
356
+ }
357
+ const matches = await glob(`**/${sessionId}.jsonl`, {
358
+ cwd: this.projectsDir,
359
+ nodir: true,
360
+ absolute: true,
361
+ });
362
+ return matches.length > 0 ? matches[0] : null;
363
+ }
364
+ /**
365
+ * Stream-read a JSONL file and return all non-empty lines.
366
+ */
367
+ async readJsonlLines(filePath) {
368
+ const lines = [];
369
+ const rl = readline.createInterface({
370
+ input: createReadStream(filePath, { encoding: "utf-8" }),
371
+ crlfDelay: Infinity,
372
+ });
373
+ for await (const line of rl) {
374
+ const trimmed = line.trim();
375
+ if (trimmed) {
376
+ lines.push(trimmed);
377
+ }
378
+ }
379
+ return lines;
380
+ }
381
+ }
382
+ //# sourceMappingURL=adapter.js.map