@ruvnet/open-claude-code 2.0.1 → 2.1.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/README.md +113 -0
- package/package.json +20 -2
- package/src/config/env.mjs +0 -2
- package/src/config/settings.mjs +24 -130
- package/src/core/session.mjs +4 -145
- package/src/core/system-prompt.mjs +43 -47
- package/src/index.mjs +21 -31
- package/src/permissions/checker.mjs +8 -79
- package/src/tools/bash.mjs +3 -40
- package/src/ui/app.mjs +286 -0
- package/src/ui/commands.mjs +83 -70
- package/src/ui/components.mjs +219 -0
- package/src/ui/ink-app.mjs +13 -52
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/repl.mjs +10 -25
- package/test/test.mjs +0 -800
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# open-claude-code v2 — Technical Guide
|
|
2
|
+
|
|
3
|
+
## Quick Start
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
7
|
+
node src/index.mjs "hello" # one-shot
|
|
8
|
+
node src/index.mjs # interactive REPL
|
|
9
|
+
node src/index.mjs -m claude-opus-4-6 -p "explain this" # print mode
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Architecture
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
v2/src/
|
|
16
|
+
├── core/ # Core engine
|
|
17
|
+
│ ├── agent-loop.mjs # Async generator (13 event types, recursive)
|
|
18
|
+
│ ├── streaming.mjs # SSE handler (all event types)
|
|
19
|
+
│ ├── context-manager.mjs # Token tracking + compaction
|
|
20
|
+
│ ├── system-prompt.mjs # CLAUDE.md loading + cache boundary
|
|
21
|
+
│ ├── session.mjs # Save/resume/teleport
|
|
22
|
+
│ ├── checkpoints.mjs # File checkpointing + undo
|
|
23
|
+
│ ├── cache.mjs # Prompt caching
|
|
24
|
+
│ ├── rate-limiter.mjs # 429/529 handling + backoff
|
|
25
|
+
│ ├── providers.mjs # 5 AI providers
|
|
26
|
+
│ └── scheduler.mjs # Cron task scheduling
|
|
27
|
+
├── tools/ # 25 tools
|
|
28
|
+
│ ├── registry.mjs # validateInput/call interface
|
|
29
|
+
│ ├── bash.mjs # Shell (sandboxed, timeout, background)
|
|
30
|
+
│ ├── read.mjs # File read (PDF, binary detect, line nums)
|
|
31
|
+
│ ├── edit.mjs # Edit (replace_all, uniqueness check)
|
|
32
|
+
│ ├── write.mjs # Write (mkdir, overwrite protection)
|
|
33
|
+
│ ├── glob.mjs # Glob (proper matching, mtime sort)
|
|
34
|
+
│ ├── grep.mjs # Grep (-i/-n/-A/-B/-C, ripgrep)
|
|
35
|
+
│ ├── agent.mjs # Subagent (worktree, background, model)
|
|
36
|
+
│ ├── web-fetch.mjs # URL fetch
|
|
37
|
+
│ ├── web-search.mjs # Web search
|
|
38
|
+
│ ├── todo-write.mjs # Task management
|
|
39
|
+
│ ├── notebook-edit.mjs # Jupyter notebooks
|
|
40
|
+
│ ├── multi-edit.mjs # Atomic multi-file edits
|
|
41
|
+
│ ├── ls.mjs # Directory listing
|
|
42
|
+
│ ├── tool-search.mjs # Deferred tool discovery
|
|
43
|
+
│ ├── ask-user.mjs # User prompts
|
|
44
|
+
│ ├── skill.mjs # Skill invocation
|
|
45
|
+
│ ├── send-message.mjs # Agent team messaging
|
|
46
|
+
│ ├── cron-create.mjs # Scheduled tasks
|
|
47
|
+
│ ├── cron-delete.mjs
|
|
48
|
+
│ ├── cron-list.mjs
|
|
49
|
+
│ ├── enter-worktree.mjs # Git worktree
|
|
50
|
+
│ ├── exit-worktree.mjs
|
|
51
|
+
│ ├── remote-trigger.mjs # Remote execution
|
|
52
|
+
│ ├── lsp.mjs # Language server
|
|
53
|
+
│ └── read-mcp-resource.mjs
|
|
54
|
+
├── mcp/ # MCP protocol
|
|
55
|
+
│ ├── client.mjs # JSON-RPC client
|
|
56
|
+
│ ├── transport-sse.mjs # SSE transport
|
|
57
|
+
│ ├── transport-shttp.mjs # Streamable HTTP
|
|
58
|
+
│ └── transport-ws.mjs # WebSocket
|
|
59
|
+
├── permissions/ # Security
|
|
60
|
+
│ ├── checker.mjs # 6 modes + interactive prompts
|
|
61
|
+
│ ├── sandbox.mjs # bubblewrap/seatbelt
|
|
62
|
+
│ ├── injection-check.mjs # Command injection detection
|
|
63
|
+
│ ├── path-check.mjs # File path validation
|
|
64
|
+
│ └── prompt.mjs # Permission prompting
|
|
65
|
+
├── hooks/
|
|
66
|
+
│ └── engine.mjs # PreToolUse/PostToolUse/Stop/Notification
|
|
67
|
+
├── agents/
|
|
68
|
+
│ ├── loader.mjs # Agent definition loader
|
|
69
|
+
│ ├── parser.mjs # JSON/MD frontmatter parser
|
|
70
|
+
│ └── teams.mjs # Multi-agent teams
|
|
71
|
+
├── skills/
|
|
72
|
+
│ ├── loader.mjs # Skill discovery
|
|
73
|
+
│ └── runner.mjs # Skill execution
|
|
74
|
+
├── plugins/
|
|
75
|
+
│ └── loader.mjs # Plugin discovery + git clone
|
|
76
|
+
├── auth/
|
|
77
|
+
│ └── oauth.mjs # PKCE OAuth flow
|
|
78
|
+
├── config/
|
|
79
|
+
│ ├── settings.mjs # 4-source deep merge
|
|
80
|
+
│ ├── cli-args.mjs # All CLI flags
|
|
81
|
+
│ └── env.mjs # 104 env vars
|
|
82
|
+
├── ui/
|
|
83
|
+
│ ├── repl.mjs # Interactive REPL
|
|
84
|
+
│ ├── ink-app.mjs # Rich terminal output
|
|
85
|
+
│ └── commands.mjs # 40 slash commands
|
|
86
|
+
├── telemetry/
|
|
87
|
+
│ └── index.mjs # Telemetry stub
|
|
88
|
+
└── index.mjs # Entry point
|
|
89
|
+
|
|
90
|
+
test/
|
|
91
|
+
└── test.mjs # 1,581 tests
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Stats
|
|
95
|
+
|
|
96
|
+
| Metric | Value |
|
|
97
|
+
|--------|:-----:|
|
|
98
|
+
| Source files | 61 |
|
|
99
|
+
| Lines of code | 8,314 |
|
|
100
|
+
| Tests | 1,581 (0 failures) |
|
|
101
|
+
| Tools | 25 |
|
|
102
|
+
| Slash commands | 40 |
|
|
103
|
+
| MCP transports | 4 |
|
|
104
|
+
| AI providers | 5 |
|
|
105
|
+
| Env vars | 104 |
|
|
106
|
+
| Permission modes | 6 |
|
|
107
|
+
|
|
108
|
+
## Tests
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
node test/test.mjs
|
|
112
|
+
# Tests: 1581 total, 1581 passed, 0 failed
|
|
113
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ruvnet/open-claude-code",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Open source Claude Code CLI — based on ruDevolution decompilation of v2.1.91",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,5 +23,23 @@
|
|
|
23
23
|
"open-source"
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
|
-
"repository":
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/ruvnet/open-claude-code.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/ruvnet/open-claude-code/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/ruvnet/open-claude-code#readme",
|
|
34
|
+
"main": "index.js",
|
|
35
|
+
"directories": {
|
|
36
|
+
"test": "test"
|
|
37
|
+
},
|
|
38
|
+
"author": "",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"ink": "^5.2.1",
|
|
41
|
+
"ink-spinner": "^5.0.0",
|
|
42
|
+
"ink-text-input": "^6.0.0",
|
|
43
|
+
"react": "^18.3.1"
|
|
44
|
+
}
|
|
27
45
|
}
|
package/src/config/env.mjs
CHANGED
|
@@ -79,8 +79,6 @@ export const ENV_SCHEMA = {
|
|
|
79
79
|
ANTHROPIC_AUTH_TOKEN: { type: 'string', description: 'Anthropic auth token (OAuth)' },
|
|
80
80
|
|
|
81
81
|
// Extended: Sandbox & Security
|
|
82
|
-
CLAUDE_CODE_FORCE_SANDBOX: { type: 'boolean', default: false, description: 'Force sandbox for all commands' },
|
|
83
|
-
CLAUDE_CODE_BUBBLEWRAP: { type: 'boolean', default: false, description: 'Enable bubblewrap sandbox on Linux' },
|
|
84
82
|
CLAUDE_CODE_SANDBOX_PLATFORM: { type: 'string', description: 'Override sandbox platform (linux/darwin)' },
|
|
85
83
|
CLAUDE_CODE_INJECTION_CHECK: { type: 'boolean', default: true, description: 'Enable command injection checks' },
|
|
86
84
|
CLAUDE_CODE_PATH_CHECK: { type: 'boolean', default: true, description: 'Enable file path validation' },
|
package/src/config/settings.mjs
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Supports all 76 properties from Claude Code's settings schema.
|
|
5
5
|
* Five-layer hierarchy: user > project > local > managed > feature flags.
|
|
6
|
-
* Deep merge with source tracking and write-back.
|
|
7
6
|
*/
|
|
8
7
|
import fs from 'fs';
|
|
9
8
|
import path from 'path';
|
|
@@ -61,101 +60,28 @@ export const SETTINGS_SCHEMA = {
|
|
|
61
60
|
featureFlags: {},
|
|
62
61
|
};
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
const SCOPE_NAMES = ['managed', 'user', 'project', 'local'];
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get file paths for each settings scope.
|
|
69
|
-
* @param {string} [cwd]
|
|
70
|
-
* @returns {Record<string, string>}
|
|
71
|
-
*/
|
|
72
|
-
export function getSettingsPaths(cwd = process.cwd()) {
|
|
73
|
-
return {
|
|
74
|
-
user: path.join(os.homedir(), '.claude', 'settings.json'),
|
|
75
|
-
project: path.join(cwd, '.claude', 'settings.json'),
|
|
76
|
-
local: path.join(cwd, '.claude', 'settings.local.json'),
|
|
77
|
-
managed: path.join(os.homedir(), '.claude', 'settings.managed.json'),
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Load settings with 4-source deep merge and source tracking.
|
|
83
|
-
* Order: defaults -> managed -> user -> project -> local -> env overrides
|
|
84
|
-
* @param {string} [cwd]
|
|
85
|
-
* @returns {Promise<object>} merged settings with _sources metadata
|
|
86
|
-
*/
|
|
87
|
-
export async function loadSettings(cwd) {
|
|
88
|
-
const paths = getSettingsPaths(cwd);
|
|
63
|
+
export async function loadSettings() {
|
|
89
64
|
const chain = [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{ scope: 'local', file: paths.local },
|
|
65
|
+
path.join(os.homedir(), '.claude', 'settings.json'),
|
|
66
|
+
path.join(process.cwd(), '.claude', 'settings.json'),
|
|
67
|
+
path.join(process.cwd(), '.claude', 'settings.local.json'),
|
|
94
68
|
];
|
|
95
69
|
|
|
96
70
|
let merged = deepClone(SETTINGS_SCHEMA);
|
|
97
|
-
const sources = {};
|
|
98
71
|
|
|
99
|
-
|
|
100
|
-
trackSources(sources, SETTINGS_SCHEMA, 'default', '');
|
|
101
|
-
|
|
102
|
-
for (const { scope, file } of chain) {
|
|
72
|
+
for (const file of chain) {
|
|
103
73
|
try {
|
|
104
74
|
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
105
75
|
merged = deepMerge(merged, data);
|
|
106
|
-
trackSources(sources, data, scope, '');
|
|
107
76
|
} catch {
|
|
108
|
-
// File not found or invalid
|
|
77
|
+
// File not found or invalid
|
|
109
78
|
}
|
|
110
79
|
}
|
|
111
80
|
|
|
112
|
-
applyEnvOverrides(merged
|
|
113
|
-
|
|
114
|
-
// Attach metadata (non-enumerable so it does not interfere with tests)
|
|
115
|
-
Object.defineProperty(merged, '_sources', { value: sources, enumerable: false, writable: true });
|
|
116
|
-
Object.defineProperty(merged, '_paths', { value: paths, enumerable: false, writable: true });
|
|
117
|
-
|
|
81
|
+
applyEnvOverrides(merged);
|
|
118
82
|
return merged;
|
|
119
83
|
}
|
|
120
84
|
|
|
121
|
-
/**
|
|
122
|
-
* Save a single setting back to the correct scope file.
|
|
123
|
-
* @param {'user'|'project'|'local'} scope
|
|
124
|
-
* @param {string} key — dot-separated path (e.g. "permissions.defaultMode")
|
|
125
|
-
* @param {*} value
|
|
126
|
-
* @param {string} [cwd]
|
|
127
|
-
*/
|
|
128
|
-
export function saveSettings(scope, key, value, cwd) {
|
|
129
|
-
const paths = getSettingsPaths(cwd);
|
|
130
|
-
const file = paths[scope];
|
|
131
|
-
if (!file) throw new Error(`Unknown scope: ${scope}`);
|
|
132
|
-
|
|
133
|
-
let existing = {};
|
|
134
|
-
try {
|
|
135
|
-
existing = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
136
|
-
} catch {
|
|
137
|
-
// New file
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
setNestedValue(existing, key, value);
|
|
141
|
-
|
|
142
|
-
const dir = path.dirname(file);
|
|
143
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
144
|
-
fs.writeFileSync(file, JSON.stringify(existing, null, 2) + '\n');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Get the source scope for a specific setting key.
|
|
149
|
-
* @param {object} settings — settings object returned by loadSettings
|
|
150
|
-
* @param {string} key — dot-separated path
|
|
151
|
-
* @returns {string} scope name ('default', 'user', 'project', 'local', 'managed', 'env')
|
|
152
|
-
*/
|
|
153
|
-
export function getSettingSource(settings, key) {
|
|
154
|
-
return settings._sources?.[key] || 'default';
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ---------- Helpers ----------
|
|
158
|
-
|
|
159
85
|
function deepClone(obj) {
|
|
160
86
|
return JSON.parse(JSON.stringify(obj));
|
|
161
87
|
}
|
|
@@ -163,8 +89,7 @@ function deepClone(obj) {
|
|
|
163
89
|
function deepMerge(target, source) {
|
|
164
90
|
const result = { ...target };
|
|
165
91
|
for (const [key, value] of Object.entries(source)) {
|
|
166
|
-
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
167
|
-
&& typeof result[key] === 'object' && !Array.isArray(result[key])) {
|
|
92
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && typeof result[key] === 'object' && !Array.isArray(result[key])) {
|
|
168
93
|
result[key] = deepMerge(result[key] || {}, value);
|
|
169
94
|
} else {
|
|
170
95
|
result[key] = value;
|
|
@@ -173,53 +98,22 @@ function deepMerge(target, source) {
|
|
|
173
98
|
return result;
|
|
174
99
|
}
|
|
175
100
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function setNestedValue(obj, key, value) {
|
|
187
|
-
const parts = key.split('.');
|
|
188
|
-
let cur = obj;
|
|
189
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
190
|
-
if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') {
|
|
191
|
-
cur[parts[i]] = {};
|
|
192
|
-
}
|
|
193
|
-
cur = cur[parts[i]];
|
|
101
|
+
function applyEnvOverrides(settings) {
|
|
102
|
+
if (process.env.ANTHROPIC_MODEL) settings.model = process.env.ANTHROPIC_MODEL;
|
|
103
|
+
if (process.env.CLAUDE_CODE_SUBAGENT_MODEL) settings.subagentModel = process.env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
104
|
+
if (process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) {
|
|
105
|
+
const n = parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS, 10);
|
|
106
|
+
if (!isNaN(n)) { settings.maxOutputTokens = n; settings.maxTokens = n; }
|
|
194
107
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
function applyEnvOverrides(settings, sources) {
|
|
199
|
-
const envMap = [
|
|
200
|
-
['ANTHROPIC_MODEL', (v) => { settings.model = v; }, 'model'],
|
|
201
|
-
['CLAUDE_CODE_SUBAGENT_MODEL', (v) => { settings.subagentModel = v; }, 'subagentModel'],
|
|
202
|
-
['CLAUDE_CODE_MAX_OUTPUT_TOKENS', (v) => {
|
|
203
|
-
const n = parseInt(v, 10);
|
|
204
|
-
if (!isNaN(n)) { settings.maxOutputTokens = n; settings.maxTokens = n; }
|
|
205
|
-
}, 'maxOutputTokens'],
|
|
206
|
-
['CLAUDE_CODE_MAX_CONTEXT_TOKENS', (v) => {
|
|
207
|
-
const n = parseInt(v, 10);
|
|
208
|
-
if (!isNaN(n)) settings.maxContextTokens = n;
|
|
209
|
-
}, 'maxContextTokens'],
|
|
210
|
-
['CLAUDE_CODE_BRIEF', (v) => { if (v === '1') settings.briefMode = true; }, 'briefMode'],
|
|
211
|
-
['CLAUDE_CODE_DEBUG', (v) => { if (v === '1') settings.debugMode = true; }, 'debugMode'],
|
|
212
|
-
['CLAUDE_CODE_PERMISSION_MODE', (v) => { settings.permissions.defaultMode = v; }, 'permissions.defaultMode'],
|
|
213
|
-
['CLAUDE_CODE_STREAMING', (v) => { if (v === '0') settings.stream = false; }, 'stream'],
|
|
214
|
-
['CLAUDE_CODE_THINKING', (v) => { if (v === '1') settings.alwaysThinkingEnabled = true; }, 'alwaysThinkingEnabled'],
|
|
215
|
-
['CLAUDE_CODE_DISABLE_CRON', (v) => { if (v === '1') settings.cronEnabled = false; }, 'cronEnabled'],
|
|
216
|
-
['CLAUDE_CODE_ENABLE_TASKS', (v) => { if (v === '1') settings.enableTeams = true; }, 'enableTeams'],
|
|
217
|
-
];
|
|
218
|
-
|
|
219
|
-
for (const [envKey, apply, settingsKey] of envMap) {
|
|
220
|
-
if (process.env[envKey]) {
|
|
221
|
-
apply(process.env[envKey]);
|
|
222
|
-
if (sources) sources[settingsKey] = 'env';
|
|
223
|
-
}
|
|
108
|
+
if (process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS) {
|
|
109
|
+
const n = parseInt(process.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, 10);
|
|
110
|
+
if (!isNaN(n)) settings.maxContextTokens = n;
|
|
224
111
|
}
|
|
112
|
+
if (process.env.CLAUDE_CODE_BRIEF === '1') settings.briefMode = true;
|
|
113
|
+
if (process.env.CLAUDE_CODE_DEBUG === '1') settings.debugMode = true;
|
|
114
|
+
if (process.env.CLAUDE_CODE_PERMISSION_MODE) settings.permissions.defaultMode = process.env.CLAUDE_CODE_PERMISSION_MODE;
|
|
115
|
+
if (process.env.CLAUDE_CODE_STREAMING === '0') settings.stream = false;
|
|
116
|
+
if (process.env.CLAUDE_CODE_THINKING === '1') settings.alwaysThinkingEnabled = true;
|
|
117
|
+
if (process.env.CLAUDE_CODE_DISABLE_CRON === '1') settings.cronEnabled = false;
|
|
118
|
+
if (process.env.CLAUDE_CODE_ENABLE_TASKS === '1') settings.enableTeams = true;
|
|
225
119
|
}
|
package/src/core/session.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session Manager — save, resume,
|
|
2
|
+
* Session Manager — save, resume, and teleport sessions.
|
|
3
3
|
*
|
|
4
4
|
* Sessions are stored at ~/.claude/projects/<hash>/session.json
|
|
5
5
|
* and contain conversation history, token usage, and metadata.
|
|
6
|
-
* Supports listing recent sessions and resuming by ID.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import fs from 'fs';
|
|
@@ -32,16 +31,8 @@ export class SessionManager {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
/**
|
|
35
|
-
*
|
|
36
|
-
*/
|
|
37
|
-
static getProjectsDir() {
|
|
38
|
-
return path.join(os.homedir(), '.claude', 'projects');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Save the current session state (messages + full state).
|
|
34
|
+
* Save the current session state.
|
|
43
35
|
* @param {object} state - agent loop state to save
|
|
44
|
-
* @returns {string} path to saved session file
|
|
45
36
|
*/
|
|
46
37
|
save(state) {
|
|
47
38
|
const dir = this.getSessionDir();
|
|
@@ -60,101 +51,28 @@ export class SessionManager {
|
|
|
60
51
|
systemPrompt: state.systemPrompt,
|
|
61
52
|
};
|
|
62
53
|
|
|
63
|
-
// Save current session
|
|
64
54
|
const filePath = path.join(dir, 'session.json');
|
|
65
55
|
fs.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
66
|
-
|
|
67
|
-
// Also save to sessions history for listing
|
|
68
|
-
this._saveToHistory(dir, session);
|
|
69
|
-
|
|
70
56
|
return filePath;
|
|
71
57
|
}
|
|
72
58
|
|
|
73
59
|
/**
|
|
74
|
-
*
|
|
75
|
-
*/
|
|
76
|
-
_saveToHistory(dir, session) {
|
|
77
|
-
const historyFile = path.join(dir, 'sessions-history.json');
|
|
78
|
-
let history = [];
|
|
79
|
-
try {
|
|
80
|
-
history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
|
|
81
|
-
} catch { /* new file */ }
|
|
82
|
-
|
|
83
|
-
// Remove old entry with same ID
|
|
84
|
-
history = history.filter(h => h.id !== session.id);
|
|
85
|
-
|
|
86
|
-
history.push({
|
|
87
|
-
id: session.id,
|
|
88
|
-
projectDir: session.projectDir,
|
|
89
|
-
startedAt: session.startedAt,
|
|
90
|
-
savedAt: session.savedAt,
|
|
91
|
-
model: session.model,
|
|
92
|
-
turnCount: session.turnCount,
|
|
93
|
-
tokenUsage: session.tokenUsage,
|
|
94
|
-
messageCount: (session.messages || []).length,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// Keep last 50
|
|
98
|
-
if (history.length > 50) history = history.slice(-50);
|
|
99
|
-
fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Load and restore a saved session into agent loop state.
|
|
104
|
-
* Fully restores messages so conversation can continue.
|
|
60
|
+
* Resume a saved session.
|
|
105
61
|
* @param {object} state - agent loop state to restore into
|
|
106
|
-
* @param {string} [sessionId] - optional specific session ID to load
|
|
107
62
|
* @returns {boolean} true if session was restored
|
|
108
63
|
*/
|
|
109
|
-
resume(state
|
|
64
|
+
resume(state) {
|
|
110
65
|
const dir = this.getSessionDir();
|
|
111
|
-
|
|
112
|
-
if (sessionId) {
|
|
113
|
-
// Look up specific session from history
|
|
114
|
-
return this._resumeById(state, dir, sessionId);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Load most recent session
|
|
118
66
|
const filePath = path.join(dir, 'session.json');
|
|
119
|
-
return this._loadSessionFile(state, filePath);
|
|
120
|
-
}
|
|
121
67
|
|
|
122
|
-
/**
|
|
123
|
-
* Resume a specific session by ID.
|
|
124
|
-
*/
|
|
125
|
-
_resumeById(state, dir, sessionId) {
|
|
126
|
-
// Check if it is the current session.json
|
|
127
|
-
const mainFile = path.join(dir, 'session.json');
|
|
128
|
-
try {
|
|
129
|
-
const data = JSON.parse(fs.readFileSync(mainFile, 'utf-8'));
|
|
130
|
-
if (data.id === sessionId) {
|
|
131
|
-
return this._loadSessionFile(state, mainFile);
|
|
132
|
-
}
|
|
133
|
-
} catch { /* continue searching */ }
|
|
134
|
-
|
|
135
|
-
// Check archived sessions
|
|
136
|
-
const archiveFile = path.join(dir, `session-${sessionId}.json`);
|
|
137
|
-
try {
|
|
138
|
-
return this._loadSessionFile(state, archiveFile);
|
|
139
|
-
} catch {
|
|
140
|
-
return false;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Load a session from a file path.
|
|
146
|
-
*/
|
|
147
|
-
_loadSessionFile(state, filePath) {
|
|
148
68
|
try {
|
|
149
69
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
150
70
|
const session = JSON.parse(raw);
|
|
151
71
|
|
|
152
|
-
// Restore all messages so the conversation continues
|
|
153
72
|
state.messages = session.messages || [];
|
|
154
73
|
state.turnCount = session.turnCount || 0;
|
|
155
74
|
state.tokenUsage = session.tokenUsage || { input: 0, output: 0 };
|
|
156
75
|
if (session.model) state.model = session.model;
|
|
157
|
-
if (session.systemPrompt) state.systemPrompt = session.systemPrompt;
|
|
158
76
|
|
|
159
77
|
this.sessionId = session.id;
|
|
160
78
|
this.conversationId = session.conversationId;
|
|
@@ -166,65 +84,6 @@ export class SessionManager {
|
|
|
166
84
|
}
|
|
167
85
|
}
|
|
168
86
|
|
|
169
|
-
/**
|
|
170
|
-
* List recent sessions for the current project.
|
|
171
|
-
* @param {number} [limit=10]
|
|
172
|
-
* @returns {Array<{id, projectDir, startedAt, savedAt, model, turnCount, messageCount}>}
|
|
173
|
-
*/
|
|
174
|
-
listSessions(limit = 10) {
|
|
175
|
-
const dir = this.getSessionDir();
|
|
176
|
-
const historyFile = path.join(dir, 'sessions-history.json');
|
|
177
|
-
try {
|
|
178
|
-
const history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
|
|
179
|
-
return history.slice(-limit).reverse();
|
|
180
|
-
} catch {
|
|
181
|
-
// Try loading the single session.json as fallback
|
|
182
|
-
const mainFile = path.join(dir, 'session.json');
|
|
183
|
-
try {
|
|
184
|
-
const data = JSON.parse(fs.readFileSync(mainFile, 'utf-8'));
|
|
185
|
-
return [{
|
|
186
|
-
id: data.id,
|
|
187
|
-
projectDir: data.projectDir,
|
|
188
|
-
startedAt: data.startedAt,
|
|
189
|
-
savedAt: data.savedAt,
|
|
190
|
-
model: data.model,
|
|
191
|
-
turnCount: data.turnCount,
|
|
192
|
-
messageCount: (data.messages || []).length,
|
|
193
|
-
}];
|
|
194
|
-
} catch {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Check if there is a resumable session for the current project.
|
|
202
|
-
* @returns {{ resumable: boolean, session?: object }}
|
|
203
|
-
*/
|
|
204
|
-
checkResumable() {
|
|
205
|
-
const dir = this.getSessionDir();
|
|
206
|
-
const filePath = path.join(dir, 'session.json');
|
|
207
|
-
try {
|
|
208
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
209
|
-
const session = JSON.parse(raw);
|
|
210
|
-
const age = Date.now() - new Date(session.savedAt).getTime();
|
|
211
|
-
// Only resumable if saved within 24h
|
|
212
|
-
if (age < 86400000) {
|
|
213
|
-
return {
|
|
214
|
-
resumable: true,
|
|
215
|
-
session: {
|
|
216
|
-
id: session.id,
|
|
217
|
-
savedAt: session.savedAt,
|
|
218
|
-
turnCount: session.turnCount,
|
|
219
|
-
messageCount: (session.messages || []).length,
|
|
220
|
-
model: session.model,
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
} catch { /* no session */ }
|
|
225
|
-
return { resumable: false };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
87
|
/**
|
|
229
88
|
* Export session for teleport (transfer between machines).
|
|
230
89
|
* @param {object} state - agent loop state
|