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.
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/dist/adapters/base-adapter.d.ts +13 -0
- package/dist/adapters/base-adapter.d.ts.map +1 -0
- package/dist/adapters/base-adapter.js +7 -0
- package/dist/adapters/base-adapter.js.map +1 -0
- package/dist/adapters/claude-code/adapter.d.ts +47 -0
- package/dist/adapters/claude-code/adapter.d.ts.map +1 -0
- package/dist/adapters/claude-code/adapter.js +382 -0
- package/dist/adapters/claude-code/adapter.js.map +1 -0
- package/dist/adapters/codex/adapter.d.ts +26 -0
- package/dist/adapters/codex/adapter.d.ts.map +1 -0
- package/dist/adapters/codex/adapter.js +446 -0
- package/dist/adapters/codex/adapter.js.map +1 -0
- package/dist/adapters/cursor/adapter.d.ts +35 -0
- package/dist/adapters/cursor/adapter.d.ts.map +1 -0
- package/dist/adapters/cursor/adapter.js +675 -0
- package/dist/adapters/cursor/adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +19 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +65 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +424 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils.d.ts +10 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +21 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/core/compression.d.ts +11 -0
- package/dist/core/compression.d.ts.map +1 -0
- package/dist/core/compression.js +182 -0
- package/dist/core/compression.js.map +1 -0
- package/dist/core/conversation-analyzer.d.ts +9 -0
- package/dist/core/conversation-analyzer.d.ts.map +1 -0
- package/dist/core/conversation-analyzer.js +220 -0
- package/dist/core/conversation-analyzer.js.map +1 -0
- package/dist/core/project-context.d.ts +7 -0
- package/dist/core/project-context.d.ts.map +1 -0
- package/dist/core/project-context.js +136 -0
- package/dist/core/project-context.js.map +1 -0
- package/dist/core/prompt-builder.d.ts +7 -0
- package/dist/core/prompt-builder.d.ts.map +1 -0
- package/dist/core/prompt-builder.js +88 -0
- package/dist/core/prompt-builder.js.map +1 -0
- package/dist/core/registry.d.ts +10 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +51 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/token-estimator.d.ts +10 -0
- package/dist/core/token-estimator.d.ts.map +1 -0
- package/dist/core/token-estimator.js +14 -0
- package/dist/core/token-estimator.js.map +1 -0
- package/dist/core/validation.d.ts +188 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +61 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/core/watcher.d.ts +20 -0
- package/dist/core/watcher.d.ts.map +1 -0
- package/dist/core/watcher.js +208 -0
- package/dist/core/watcher.js.map +1 -0
- package/dist/providers/clipboard-provider.d.ts +8 -0
- package/dist/providers/clipboard-provider.d.ts.map +1 -0
- package/dist/providers/clipboard-provider.js +15 -0
- package/dist/providers/clipboard-provider.js.map +1 -0
- package/dist/providers/file-provider.d.ts +8 -0
- package/dist/providers/file-provider.d.ts.map +1 -0
- package/dist/providers/file-provider.js +14 -0
- package/dist/providers/file-provider.js.map +1 -0
- package/dist/providers/index.d.ts +9 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +18 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/types/index.d.ts +132 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- 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 @@
|
|
|
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
|