dsh-thoughtdag 0.4.9 → 0.4.11
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/dist-app/assets/{canonical-Df0ckIdq.js → canonical-Cw94rYnE.js} +2 -2
- package/dist-app/assets/{canvas-record-CEy9ts1U.js → canvas-record-BYGoWCSi.js} +1 -1
- package/dist-app/assets/{claude-code-session-gxApIV4E.js → claude-code-session-D7_MMzYc.js} +1 -1
- package/dist-app/assets/{codex-session-rjwyINLc.js → codex-session-DVFc0JXs.js} +1 -1
- package/dist-app/assets/{dsh-session-9FrjljDY.js → dsh-session-BsZRbFip.js} +1 -1
- package/dist-app/assets/{experiment-loop-B8B2uvfc.js → experiment-loop-C9Q1W_wR.js} +1 -1
- package/dist-app/assets/http-bridge-mmwHcDuo.js +1 -0
- package/dist-app/assets/index-2pggDGtC.css +1 -0
- package/dist-app/assets/{index-Dbevq-w7.js → index-BEqQ5wV9.js} +1 -1
- package/dist-app/assets/{index-CCSkzpjj.js → index-Dyq9QwYZ.js} +3 -3
- package/dist-app/assets/index-Y8weIZcr.js +788 -0
- package/dist-app/assets/{index-DsWaXdBk.js → index-z0ZTt6U1.js} +1 -1
- package/dist-app/assets/{live-mirror-Bs_UiNg0.js → live-mirror-BlUlIINC.js} +2 -2
- package/dist-app/assets/{pi-session-_RCMhkfD.js → pi-session-wpL6N_gb.js} +1 -1
- package/dist-app/assets/{sensitive-scan-Dm_K56VK.js → sensitive-scan-Kz-yplyP.js} +1 -1
- package/dist-app/assets/{session-handoff-asoyabQy.js → session-handoff-BLHyTTl7.js} +2 -2
- package/dist-app/assets/{shared-BFDd9vhH.js → shared-BoxE2SdQ.js} +1 -1
- package/dist-app/assets/{turndown-plugin-gfm.cjs-DvWuwycM.js → turndown-plugin-gfm.cjs-2uYHu5V7.js} +1 -1
- package/dist-app/assets/{update-check-BhinDEBL.js → update-check-B1wesdxM.js} +1 -1
- package/dist-app/index.html +2 -2
- package/lib/client.js +8 -3
- package/lib/index.js +93 -7
- package/lib/runtime/agents/README.md +26 -0
- package/lib/runtime/agents/claude.cjs +303 -0
- package/lib/runtime/agents/codex.cjs +380 -0
- package/lib/runtime/agents/fs-diff.cjs +44 -0
- package/lib/runtime/agents/http.cjs +95 -0
- package/lib/runtime/agents/ops.cjs +54 -0
- package/lib/runtime/agents/pi-guard.mjs +81 -0
- package/lib/runtime/agents/pi.cjs +358 -0
- package/lib/runtime/sessions-fs.cjs +100 -0
- package/lib/runtime/terminal.cjs +105 -0
- package/package.json +1 -1
- package/dist-app/assets/index-BfIl0Oie.css +0 -1
- package/dist-app/assets/index-J-uxm9B7.js +0 -787
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// Claude Code as an agent runtime: one `claude -p` process per turn, spoken
|
|
2
|
+
// to in stream-json both ways. The turn's text and thinking arrive as
|
|
3
|
+
// content-block deltas, tool calls as tool_use blocks with their
|
|
4
|
+
// tool_result, permission prompts as control requests answered on stdin
|
|
5
|
+
// (`--permission-prompt-tool stdio`), the end as a result message. A session
|
|
6
|
+
// continues with `--resume`, branches with `--fork-session`. Claude Code
|
|
7
|
+
// keeps its session files under ~/.claude/projects, its login and its model
|
|
8
|
+
// aliases; the shell only asks. Same event kinds as the other runtimes.
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { spawn, execFile } = require('node:child_process');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const { randomUUID } = require('node:crypto');
|
|
16
|
+
const { snapshotDir, diffSnapshots } = require('./fs-diff.cjs');
|
|
17
|
+
|
|
18
|
+
const fsp = fs.promises;
|
|
19
|
+
const CANDIDATE_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin', path.join(os.homedir(), '.local', 'bin'), path.join(os.homedir(), '.claude', 'local'), path.join(os.homedir(), '.npm-global', 'bin'), path.join(os.homedir(), '.bun', 'bin')];
|
|
20
|
+
|
|
21
|
+
async function executable(p) { try { await fsp.access(p, fs.constants.X_OK); return true; } catch { return false; } }
|
|
22
|
+
async function findClaude() {
|
|
23
|
+
const names = process.platform === 'win32' ? ['claude.cmd', 'claude.exe', 'claude'] : ['claude'];
|
|
24
|
+
const dirs = [...(process.env.PATH || '').split(path.delimiter), ...CANDIDATE_DIRS].filter(Boolean);
|
|
25
|
+
for (const d of dirs) for (const n of names) { const p = path.join(d, n); if (await executable(p)) return p; }
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function childEnv() {
|
|
29
|
+
const seen = new Set((process.env.PATH || '').split(path.delimiter).filter(Boolean));
|
|
30
|
+
return { ...process.env, PATH: [...seen, ...CANDIDATE_DIRS.filter((d) => !seen.has(d))].join(path.delimiter) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Claude Code has no model list; its aliases resolve to the newest model
|
|
34
|
+
* of each family inside the CLI, so they never go stale. The CLI's own
|
|
35
|
+
* "default" is not offered: which model it means lives in the CLI's settings
|
|
36
|
+
* and can change under the person, so the picker names families only. */
|
|
37
|
+
/** The effort levels this CLI accepts, in its own words, read from its own
|
|
38
|
+
* `--help` (the `--effort <level>` entry lists them in parentheses) — so a
|
|
39
|
+
* CLI that adds a level shows it without a change here. Empty when the
|
|
40
|
+
* CLI has no such flag. Read once per launch. */
|
|
41
|
+
let effortsPromise = null;
|
|
42
|
+
function effortsOf(bin) {
|
|
43
|
+
if (!effortsPromise) effortsPromise = new Promise((resolve) => {
|
|
44
|
+
execFile(bin, ['--help'], { env: childEnv(), timeout: 8000, maxBuffer: 1 << 20 }, (_err, stdout) => {
|
|
45
|
+
const help = String(stdout || '');
|
|
46
|
+
const at = help.indexOf('--effort');
|
|
47
|
+
if (at < 0) return resolve([]);
|
|
48
|
+
const next = help.indexOf('\n --', at + 1);
|
|
49
|
+
const m = help.slice(at, next > 0 ? next : undefined).match(/\(([a-z0-9_, \n-]+)\)/i);
|
|
50
|
+
resolve(m ? m[1].split(',').map((s) => s.trim()).filter(Boolean) : []);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
return effortsPromise;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const MODELS = [
|
|
57
|
+
{ provider: 'claude-code', id: 'fable', name: 'Fable', reasoning: true, vision: true },
|
|
58
|
+
{ provider: 'claude-code', id: 'opus', name: 'Opus', reasoning: true, vision: true },
|
|
59
|
+
{ provider: 'claude-code', id: 'sonnet', name: 'Sonnet', reasoning: true, vision: true },
|
|
60
|
+
{ provider: 'claude-code', id: 'haiku', name: 'Haiku', reasoning: true, vision: true },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** The effort a turn ran at, as the transcript records it on every
|
|
64
|
+
* assistant entry (`effort`) — the CLI's own setting when the canvas named none. */
|
|
65
|
+
async function turnEffortOf(file) {
|
|
66
|
+
if (!file) return null;
|
|
67
|
+
try {
|
|
68
|
+
const lines = (await fsp.readFile(file, 'utf8')).split('\n');
|
|
69
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
70
|
+
if (!lines[i].includes('"effort"')) continue;
|
|
71
|
+
try { const o = JSON.parse(lines[i]); if (o.type === 'assistant' && typeof o.effort === 'string') return o.effort; } catch { /* torn line */ }
|
|
72
|
+
}
|
|
73
|
+
} catch { /* no file */ }
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The CLI's configured effort (`effortLevel` in ~/.claude/settings.json), when set. */
|
|
78
|
+
async function configuredEffort() {
|
|
79
|
+
try { const s = JSON.parse(await fsp.readFile(path.join(os.homedir(), '.claude', 'settings.json'), 'utf8')); return typeof s.effortLevel === 'string' ? s.effortLevel : null; } catch { return null; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** ~/.claude/projects/<slug>/<sessionId>.jsonl. The slug is the cwd with
|
|
83
|
+
* '/' and '.' turned into '-', but a long path is cut and given a short
|
|
84
|
+
* suffix by Claude Code itself — so the file is found by its id across the
|
|
85
|
+
* project directories when the computed name is not there. */
|
|
86
|
+
const PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
87
|
+
async function sessionFileFor(cwd, sessionId) {
|
|
88
|
+
const guess = path.join(PROJECTS_DIR, cwd.replace(/[/.]/g, '-'), `${sessionId}.jsonl`);
|
|
89
|
+
if (await fsp.access(guess).then(() => true, () => false)) return guess;
|
|
90
|
+
try {
|
|
91
|
+
const dirs = await fsp.readdir(PROJECTS_DIR, { withFileTypes: true });
|
|
92
|
+
for (const d of dirs) {
|
|
93
|
+
if (!d.isDirectory()) continue;
|
|
94
|
+
const candidate = path.join(PROJECTS_DIR, d.name, `${sessionId}.jsonl`);
|
|
95
|
+
if (await fsp.access(candidate).then(() => true, () => false)) return candidate;
|
|
96
|
+
}
|
|
97
|
+
} catch { /* no projects yet */ }
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// the session id is the file's name; a directory on the way may carry a
|
|
101
|
+
// UUID of its own (a project path that names a session), so take the LAST
|
|
102
|
+
const sessionIdOf = (sessionPath) => { const all = String(sessionPath ?? '').match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi); return all && all.length ? all[all.length - 1] : null; };
|
|
103
|
+
|
|
104
|
+
/** The guard file the canvas writes per working directory, as a permission mode. */
|
|
105
|
+
async function permissionModeFor(cwd) {
|
|
106
|
+
try {
|
|
107
|
+
const raw = JSON.parse(await fsp.readFile(path.join(cwd, '.thoughtdag', 'guard.json'), 'utf8'));
|
|
108
|
+
if (raw?.mode === 'allow') return 'bypassPermissions';
|
|
109
|
+
} catch { /* default */ }
|
|
110
|
+
return 'default';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createClaudeRuntime({ log } = {}) {
|
|
114
|
+
const say = log || (() => {});
|
|
115
|
+
const runs = new Map(); // runId → state
|
|
116
|
+
let binPromise = null;
|
|
117
|
+
const bin = () => { if (!binPromise) binPromise = findClaude(); return binPromise; };
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
available: () => bin(),
|
|
121
|
+
|
|
122
|
+
async models() {
|
|
123
|
+
const b = await bin();
|
|
124
|
+
if (!b) return { installed: false, models: [], default: null };
|
|
125
|
+
const [efforts, defaultEffort] = await Promise.all([effortsOf(b), configuredEffort()]);
|
|
126
|
+
return { installed: true, models: MODELS.map((m) => ({ ...m, efforts, defaultEffort })), default: 'claude/claude-code/opus' };
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async run(req, onEvent) {
|
|
130
|
+
const cwd = String(req.cwd || '');
|
|
131
|
+
if (!cwd || !path.isAbsolute(cwd)) throw new Error('run needs an absolute cwd');
|
|
132
|
+
await fsp.mkdir(cwd, { recursive: true });
|
|
133
|
+
const b = await bin();
|
|
134
|
+
if (!b) throw new Error('claude is not installed (no `claude` on PATH or in the usual places)');
|
|
135
|
+
const runId = randomUUID();
|
|
136
|
+
const emit = (event) => { try { onEvent({ runId, event }); } catch { /* renderer gone */ } };
|
|
137
|
+
const state = { runId, proc: null, asks: new Map(), text: '', final: null, sessionId: null, waiting: false, ended: false, aborted: false,
|
|
138
|
+
// what this conversation already allowed for good (see README: `rule`)
|
|
139
|
+
allowRules: new Set(Array.isArray(req.allowRules) ? req.allowRules.filter((s) => typeof s === 'string' && s.startsWith('claude:')) : []) };
|
|
140
|
+
runs.set(runId, state);
|
|
141
|
+
|
|
142
|
+
void (async () => {
|
|
143
|
+
const IDLE = Number(process.env.TD_AGENT_IDLE_MS) || 10 * 60 * 1000;
|
|
144
|
+
let lastEvent = Date.now();
|
|
145
|
+
const mark = (event) => { lastEvent = Date.now(); if (event.type === 'question') state.waiting = true; if (event.type === 'question_answered') state.waiting = false; emit(event); };
|
|
146
|
+
let watchdog = null;
|
|
147
|
+
const before = await snapshotDir(cwd).catch(() => null);
|
|
148
|
+
try {
|
|
149
|
+
const mode = await permissionModeFor(cwd);
|
|
150
|
+
const model = req.model && typeof req.model === 'object' && req.model.id && req.model.id !== 'default' ? req.model.id : null;
|
|
151
|
+
const resumeId = req.sessionPath ? sessionIdOf(req.sessionPath) : null;
|
|
152
|
+
const args = ['-p', '--output-format', 'stream-json', '--input-format', 'stream-json', '--verbose', '--include-partial-messages', '--permission-mode', mode];
|
|
153
|
+
if (mode !== 'bypassPermissions') args.push('--permission-prompt-tool', 'stdio');
|
|
154
|
+
if (model) args.push('--model', model);
|
|
155
|
+
// only a level the CLI itself lists; anything else means its default
|
|
156
|
+
if (req.effort && (await effortsOf(b)).includes(req.effort)) args.push('--effort', req.effort);
|
|
157
|
+
if (resumeId) { args.push('--resume', resumeId); if (req.forkEntryId) args.push('--fork-session'); }
|
|
158
|
+
const proc = spawn(b, args, { cwd, env: childEnv(), stdio: ['pipe', 'pipe', 'pipe'] });
|
|
159
|
+
state.proc = proc;
|
|
160
|
+
let buf = '';
|
|
161
|
+
const finished = new Promise((resolve) => {
|
|
162
|
+
const done = (how) => { if (state.ended) return; state.ended = true; resolve(how); };
|
|
163
|
+
proc.stdout.on('data', (d) => {
|
|
164
|
+
buf += d.toString();
|
|
165
|
+
let i;
|
|
166
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
167
|
+
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
168
|
+
if (!line.trim()) continue;
|
|
169
|
+
let m; try { m = JSON.parse(line); } catch { continue; }
|
|
170
|
+
handle(m, done);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
proc.stderr.on('data', (d) => say('[claude stderr] ' + d.toString().trim().slice(0, 400)));
|
|
174
|
+
proc.on('error', (e) => { mark({ type: 'run_error', message: 'spawn error: ' + e.message }); done('error'); });
|
|
175
|
+
proc.on('exit', (code, signal) => { if (!state.ended) { if (!state.aborted && code) mark({ type: 'run_error', message: `claude exited (${signal || code})` }); done(state.aborted ? 'aborted' : (code ? 'error' : 'end')); } });
|
|
176
|
+
});
|
|
177
|
+
watchdog = setInterval(() => {
|
|
178
|
+
if (state.waiting || Date.now() - lastEvent < IDLE) return;
|
|
179
|
+
mark({ type: 'run_error', message: `no activity for ${IDLE >= 60000 ? Math.round(IDLE / 60000) + ' min' : Math.round(IDLE / 1000) + ' s'}; the turn was stopped` });
|
|
180
|
+
state.aborted = true; try { proc.kill(); } catch { /* gone */ }
|
|
181
|
+
}, 15000);
|
|
182
|
+
|
|
183
|
+
const handle = (m, done) => {
|
|
184
|
+
switch (m.type) {
|
|
185
|
+
case 'system':
|
|
186
|
+
if (m.subtype === 'init') {
|
|
187
|
+
state.sessionId = m.session_id;
|
|
188
|
+
// the file appears once the first lines are written; the
|
|
189
|
+
// completion re-emits the session with its real path
|
|
190
|
+
mark({ type: 'session', sessionId: m.session_id, sessionFile: null, model: model ? { provider: 'claude-code', id: model, name: model } : null, cwd });
|
|
191
|
+
} else if (m.subtype === 'permission_denied') {
|
|
192
|
+
mark({ type: 'tool_execution_end', toolCallId: m.tool_use_id ?? '', toolName: m.tool_name ?? 'tool', result: 'permission denied', isError: true });
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case 'stream_event': {
|
|
196
|
+
const ev = m.event ?? {};
|
|
197
|
+
if (ev.type === 'content_block_delta') {
|
|
198
|
+
const d = ev.delta ?? {};
|
|
199
|
+
if (d.type === 'text_delta' && d.text) { state.text += d.text; mark({ type: 'message_update', assistantMessageEvent: { type: 'text_delta', delta: d.text } }); }
|
|
200
|
+
else if (d.type === 'thinking_delta' && d.thinking) mark({ type: 'message_update', assistantMessageEvent: { type: 'thinking_delta', delta: d.thinking } });
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'assistant':
|
|
205
|
+
// the alias resolved to a real model inside the CLI; the final session event carries it
|
|
206
|
+
if (!state.resolvedModel && typeof m.message?.model === 'string') state.resolvedModel = m.message.model;
|
|
207
|
+
for (const b of m.message?.content ?? []) if (b.type === 'tool_use') mark({ type: 'tool_execution_start', toolCallId: b.id, toolName: b.name, args: b.input ?? {} });
|
|
208
|
+
break;
|
|
209
|
+
case 'user':
|
|
210
|
+
for (const b of m.message?.content ?? []) if (b.type === 'tool_result') mark({ type: 'tool_execution_end', toolCallId: b.tool_use_id, toolName: '', result: typeof b.content === 'string' ? b.content : JSON.stringify(b.content ?? null).slice(0, 4000), isError: !!b.is_error });
|
|
211
|
+
break;
|
|
212
|
+
case 'control_request': {
|
|
213
|
+
const r = m.request ?? {};
|
|
214
|
+
if (r.subtype !== 'can_use_tool') { write(proc, { type: 'control_response', response: { subtype: 'error', request_id: m.request_id, error: 'unsupported request' } }); break; }
|
|
215
|
+
const id = `q-${m.request_id}`;
|
|
216
|
+
const input = r.input ?? {};
|
|
217
|
+
const line = typeof input.command === 'string' ? input.command : (input.file_path ?? input.path ?? JSON.stringify(input).slice(0, 200));
|
|
218
|
+
// the CLI's own "don't ask again" suggestion names what a standing
|
|
219
|
+
// allowance covers; sent back scoped to the session, never to settings
|
|
220
|
+
const suggest = (Array.isArray(r.permission_suggestions) ? r.permission_suggestions : []).find((s) => s && s.type === 'addRules' && Array.isArray(s.rules) && s.rules.length);
|
|
221
|
+
const rule = suggest ? 'claude:' + JSON.stringify(suggest.rules.map((x) => ({ toolName: x.toolName, ...(x.ruleContent ? { ruleContent: x.ruleContent } : {}) }))) : null;
|
|
222
|
+
const allow = (forSession) => write(proc, { type: 'control_response', response: { subtype: 'success', request_id: m.request_id, response: { behavior: 'allow', updatedInput: input, ...(forSession && suggest ? { updatedPermissions: [{ ...suggest, destination: 'session' }] } : {}) } } });
|
|
223
|
+
const title = `${r.tool_name ?? 'tool'} needs approval`;
|
|
224
|
+
if (rule && state.allowRules.has(rule)) {
|
|
225
|
+
allow(true);
|
|
226
|
+
mark({ type: 'question_answered', id, outcome: 'allowed-session', value: null, auto: true, title, message: line, rule });
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
state.asks.set(id, (answer) => {
|
|
230
|
+
const yes = !!answer?.confirmed;
|
|
231
|
+
const forSession = yes && answer?.scope === 'session' && !!rule;
|
|
232
|
+
if (forSession) state.allowRules.add(rule);
|
|
233
|
+
if (yes) allow(forSession); else write(proc, { type: 'control_response', response: { subtype: 'success', request_id: m.request_id, response: { behavior: 'deny', message: 'the person did not allow this step' } } });
|
|
234
|
+
mark({ type: 'question_answered', id, outcome: yes ? (forSession ? 'allowed-session' : 'allowed-once') : 'rejected', value: null, ...(forSession ? { rule } : {}) });
|
|
235
|
+
});
|
|
236
|
+
mark({ type: 'question', kind: 'confirm', id, rule, title, message: [line, r.description].filter(Boolean).join('\n'), options: [], placeholder: null, prefill: null, paths: [], suggest: null });
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'result':
|
|
240
|
+
state.final = typeof m.result === 'string' ? m.result : null;
|
|
241
|
+
if (m.subtype && m.subtype !== 'success') mark({ type: 'run_error', message: String(m.result ?? m.subtype) });
|
|
242
|
+
done(m.subtype === 'success' ? 'end' : 'error');
|
|
243
|
+
break;
|
|
244
|
+
default: break;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// the prompt goes in as one user message; stdin stays open for answers
|
|
249
|
+
const content = [{ type: 'text', text: String(req.prompt ?? '') }];
|
|
250
|
+
for (const img of Array.isArray(req.images) ? req.images : []) if (img && typeof img.data === 'string') content.push({ type: 'image', source: { type: 'base64', media_type: img.mimeType, data: img.data } });
|
|
251
|
+
write(proc, { type: 'user', message: { role: 'user', content } });
|
|
252
|
+
|
|
253
|
+
const how = await finished;
|
|
254
|
+
// let the CLI finish writing its transcript: it exits on its own
|
|
255
|
+
// after the result; a kill is only the fallback
|
|
256
|
+
await new Promise((resolve) => { if (proc.exitCode !== null || proc.signalCode) return resolve(); const t = setTimeout(() => { try { proc.kill(); } catch { /* gone */ } resolve(); }, 8000); proc.once('exit', () => { clearTimeout(t); resolve(); }); });
|
|
257
|
+
if (state.sessionId) {
|
|
258
|
+
const file = await sessionFileFor(cwd, state.sessionId);
|
|
259
|
+
const ran = await turnEffortOf(file);
|
|
260
|
+
mark({ type: 'session', effort: ran, sessionId: state.sessionId, sessionFile: file, model: { provider: 'claude-code', id: model ?? 'default', name: model ?? 'default', ...(state.resolvedModel ? { resolved: state.resolvedModel } : {}) }, cwd });
|
|
261
|
+
}
|
|
262
|
+
if (before) {
|
|
263
|
+
const after = await snapshotDir(cwd).catch(() => null);
|
|
264
|
+
if (after) { const d = diffSnapshots(before, after); if (d.changed.length || d.added.length || d.removed.length || d.truncated) mark({ type: 'fs_changes', ...d }); }
|
|
265
|
+
}
|
|
266
|
+
mark({ type: 'run_end', text: state.final ?? state.text, how });
|
|
267
|
+
} catch (e) {
|
|
268
|
+
mark({ type: 'run_error', message: e instanceof Error ? e.message : String(e) });
|
|
269
|
+
mark({ type: 'run_end', text: state.final ?? state.text, how: 'error' });
|
|
270
|
+
} finally {
|
|
271
|
+
if (watchdog) clearInterval(watchdog);
|
|
272
|
+
runs.delete(runId);
|
|
273
|
+
}
|
|
274
|
+
})();
|
|
275
|
+
return runId;
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
abort(runId) {
|
|
279
|
+
const r = runs.get(runId);
|
|
280
|
+
if (!r) return false;
|
|
281
|
+
r.aborted = true;
|
|
282
|
+
for (const [id, respond] of r.asks) { respond({ confirmed: false }); r.asks.delete(id); }
|
|
283
|
+
try { r.proc?.kill(); } catch { /* gone */ }
|
|
284
|
+
return true;
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
answer(runId, requestId, response) {
|
|
288
|
+
const r = runs.get(runId);
|
|
289
|
+
const respond = r?.asks.get(String(requestId));
|
|
290
|
+
if (!respond) return false;
|
|
291
|
+
r.asks.delete(String(requestId));
|
|
292
|
+
const a = response && typeof response === 'object' ? response : { confirmed: !!response };
|
|
293
|
+
respond(a.cancelled ? { confirmed: false } : a);
|
|
294
|
+
return true;
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
shutdown() { for (const r of runs.values()) { try { r.proc?.kill(); } catch { /* gone */ } } },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function write(proc, obj) { try { proc.stdin.write(JSON.stringify(obj) + '\n'); return true; } catch { return false; } }
|
|
302
|
+
|
|
303
|
+
module.exports = { createClaudeRuntime, findClaude };
|