@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3
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 +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +636 -613
- package/apps/agent-terminal/app.js +125 -65
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +171 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +187 -159
- package/apps/agent-terminal/right-workspace-dock.js +623 -519
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +13 -13
- package/docs/sdk/openapi.yaml +8 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1442 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2392 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- package/src/sdk-wave-e.ts +1 -1
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { readJson, writeJson } = require('./json-file');
|
|
5
|
-
|
|
6
|
-
const SCHEMA = 'capture-settings/v1';
|
|
7
|
-
|
|
8
|
-
// Terminal capture scope. When terminalConversations is true, new Agent
|
|
9
|
-
// Terminal SDK sessions stop flagging themselves as internal-runtime-only and
|
|
10
|
-
// their hooks reach the Bridge journal (Development Conversation / 会话浏览).
|
|
11
|
-
// Default false: the terminal is the system talking to itself, not the
|
|
12
|
-
// developer's CLI conversation record.
|
|
13
|
-
class CaptureSettingsStore {
|
|
14
|
-
constructor(options = {}) {
|
|
15
|
-
if (!options.dataDir) throw new Error('CaptureSettingsStore dataDir is required');
|
|
16
|
-
this.settingsFile = options.settingsFile || path.join(options.dataDir, 'capture-settings.v1.json');
|
|
17
|
-
this.defaultConversations = options.defaultConversations === true;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
read() {
|
|
21
|
-
const cfg = readJson(this.settingsFile, { schema: SCHEMA, terminalConversations: this.defaultConversations });
|
|
22
|
-
return { schema: SCHEMA, terminalConversations: cfg.terminalConversations === true };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
save(input = {}) {
|
|
26
|
-
if (typeof input.terminalConversations !== 'boolean') {
|
|
27
|
-
throw Object.assign(new Error('terminalConversations must be a boolean'), { status: 400 });
|
|
28
|
-
}
|
|
29
|
-
const cfg = { schema: SCHEMA, terminalConversations: input.terminalConversations, updatedAt: new Date().toISOString() };
|
|
30
|
-
writeJson(this.settingsFile, cfg);
|
|
31
|
-
return cfg;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
module.exports = { CaptureSettingsStore, SCHEMA };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { readJson, writeJson } = require('./json-file');
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'capture-settings/v1';
|
|
7
|
+
|
|
8
|
+
// Terminal capture scope. When terminalConversations is true, new Agent
|
|
9
|
+
// Terminal SDK sessions stop flagging themselves as internal-runtime-only and
|
|
10
|
+
// their hooks reach the Bridge journal (Development Conversation / 会话浏览).
|
|
11
|
+
// Default false: the terminal is the system talking to itself, not the
|
|
12
|
+
// developer's CLI conversation record.
|
|
13
|
+
class CaptureSettingsStore {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
if (!options.dataDir) throw new Error('CaptureSettingsStore dataDir is required');
|
|
16
|
+
this.settingsFile = options.settingsFile || path.join(options.dataDir, 'capture-settings.v1.json');
|
|
17
|
+
this.defaultConversations = options.defaultConversations === true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
read() {
|
|
21
|
+
const cfg = readJson(this.settingsFile, { schema: SCHEMA, terminalConversations: this.defaultConversations });
|
|
22
|
+
return { schema: SCHEMA, terminalConversations: cfg.terminalConversations === true };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
save(input = {}) {
|
|
26
|
+
if (typeof input.terminalConversations !== 'boolean') {
|
|
27
|
+
throw Object.assign(new Error('terminalConversations must be a boolean'), { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
const cfg = { schema: SCHEMA, terminalConversations: input.terminalConversations, updatedAt: new Date().toISOString() };
|
|
30
|
+
writeJson(this.settingsFile, cfg);
|
|
31
|
+
return cfg;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { CaptureSettingsStore, SCHEMA };
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
|
|
7
|
-
// Recovery reads the owning CLI's transcript only. It never resumes a model,
|
|
8
|
-
// edits a native transcript, or treats captured tool calls as executable work.
|
|
9
|
-
function recoverClaudeHistory(session, { claudeHome = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude') } = {}) {
|
|
10
|
-
const id = session.claudeSessionId;
|
|
11
|
-
if (!/^[a-f0-9-]{36}$/i.test(id || '') || !path.isAbsolute(session.projectPath || '')) return null;
|
|
12
|
-
const project = session.projectPath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
13
|
-
const root = path.resolve(claudeHome, 'projects');
|
|
14
|
-
const directory = path.join(root, project);
|
|
15
|
-
const file = path.join(directory, `${id}.jsonl`);
|
|
16
|
-
try {
|
|
17
|
-
const stat = fs.lstatSync(file);
|
|
18
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024 * 1024) return null;
|
|
19
|
-
const realRoot = fs.realpathSync(root), realFile = fs.realpathSync(file);
|
|
20
|
-
if (!realFile.startsWith(realRoot + path.sep)) return null;
|
|
21
|
-
const rows = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
22
|
-
const owned = rows.filter(row => row.sessionId === id && row.isSidechain !== true);
|
|
23
|
-
const byId = new Map(owned.filter(row => row.uuid).map(row => [row.uuid, row]));
|
|
24
|
-
const latest = owned.findLast(row => ['user', 'assistant'].includes(row.type));
|
|
25
|
-
if (!latest) return null;
|
|
26
|
-
const lineage = new Set(), chain = [];
|
|
27
|
-
let current = latest;
|
|
28
|
-
while (current?.uuid && !lineage.has(current.uuid)) {
|
|
29
|
-
lineage.add(current.uuid);
|
|
30
|
-
chain.push(current);
|
|
31
|
-
const parent = current.parentUuid || (current.subtype === 'compact_boundary' ? current.logicalParentUuid : null);
|
|
32
|
-
if (parent && !byId.has(parent)) return null;
|
|
33
|
-
current = byId.get(parent);
|
|
34
|
-
}
|
|
35
|
-
if (current?.uuid && lineage.has(current.uuid)) return null;
|
|
36
|
-
// Preserve bridge/system nodes in the parent chain, but render only the
|
|
37
|
-
// selected branch's user and assistant messages, not abandoned retries.
|
|
38
|
-
const messages = chain.reverse().filter(row => ['user', 'assistant'].includes(row.type)
|
|
39
|
-
&& row.isMeta !== true && row.isCompactSummary !== true);
|
|
40
|
-
const events = [], seen = new Set();
|
|
41
|
-
let hasTurn = false, finalText = '', finalAt = '', finalModel = '';
|
|
42
|
-
const finish = () => {
|
|
43
|
-
if (!hasTurn) return;
|
|
44
|
-
events.push({ type: 'claude/result', result: finalText, at: finalAt, model: finalModel, recovered: true });
|
|
45
|
-
hasTurn = false; finalText = '';
|
|
46
|
-
};
|
|
47
|
-
for (const row of messages) {
|
|
48
|
-
if (row.uuid && seen.has(row.uuid)) continue;
|
|
49
|
-
if (row.uuid) seen.add(row.uuid);
|
|
50
|
-
const content = typeof row.message?.content === 'string'
|
|
51
|
-
? [{ type: 'text', text: row.message.content }] : row.message?.content;
|
|
52
|
-
if (!Array.isArray(content)) continue;
|
|
53
|
-
if (row.type === 'user') {
|
|
54
|
-
const text = content.filter(block => block.type === 'text').map(block => block.text || '').join('\n');
|
|
55
|
-
const attachments = content.filter(block => block.type === 'image' && block.source?.type === 'base64'
|
|
56
|
-
&& ['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(block.source.media_type))
|
|
57
|
-
.map(block => ({ mediaType: block.source.media_type, name: '原始图片', url: `data:${block.source.media_type};base64,${block.source.data}` }));
|
|
58
|
-
if (text || attachments.length) {
|
|
59
|
-
finish(); hasTurn = true;
|
|
60
|
-
events.push({ type: 'claude/user-prompt', id: row.uuid, text, attachments, at: row.timestamp, recovered: true });
|
|
61
|
-
}
|
|
62
|
-
for (const block of content.filter(block => block.type === 'tool_result')) {
|
|
63
|
-
const output = typeof block.content === 'string' ? block.content : (block.content || []).filter(item => item.type === 'text').map(item => item.text || '').join('\n');
|
|
64
|
-
events.push({ type: 'claude/tool-result', id: block.tool_use_id, output, isError: block.is_error === true, at: row.timestamp, recovered: true });
|
|
65
|
-
}
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (!hasTurn) continue;
|
|
69
|
-
for (const [index, block] of content.entries()) {
|
|
70
|
-
const blockId = `${row.uuid || row.message.id}-${index}`;
|
|
71
|
-
const meta = { id: blockId, at: row.timestamp, recovered: true };
|
|
72
|
-
if (block.type === 'text') {
|
|
73
|
-
events.push({ type: 'claude/text-start', ...meta }, { type: 'claude/text-delta', text: block.text || '', ...meta });
|
|
74
|
-
finalText = block.text || ''; finalAt = row.timestamp; finalModel = row.message.model;
|
|
75
|
-
} else if (block.type === 'thinking') {
|
|
76
|
-
events.push({ type: 'claude/thinking-start', ...meta }, { type: 'claude/thinking-delta', text: block.thinking || '', ...meta });
|
|
77
|
-
} else if (block.type === 'tool_use') {
|
|
78
|
-
events.push({ type: 'claude/tool-use', id: block.id, name: block.name, input: block.input || {}, at: row.timestamp, recovered: true });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
finish();
|
|
83
|
-
return events.some(event => event.type === 'claude/user-prompt') ? events : null;
|
|
84
|
-
} catch { return null; }
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
module.exports = { recoverClaudeHistory };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
|
|
7
|
+
// Recovery reads the owning CLI's transcript only. It never resumes a model,
|
|
8
|
+
// edits a native transcript, or treats captured tool calls as executable work.
|
|
9
|
+
function recoverClaudeHistory(session, { claudeHome = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude') } = {}) {
|
|
10
|
+
const id = session.claudeSessionId;
|
|
11
|
+
if (!/^[a-f0-9-]{36}$/i.test(id || '') || !path.isAbsolute(session.projectPath || '')) return null;
|
|
12
|
+
const project = session.projectPath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
13
|
+
const root = path.resolve(claudeHome, 'projects');
|
|
14
|
+
const directory = path.join(root, project);
|
|
15
|
+
const file = path.join(directory, `${id}.jsonl`);
|
|
16
|
+
try {
|
|
17
|
+
const stat = fs.lstatSync(file);
|
|
18
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 128 * 1024 * 1024) return null;
|
|
19
|
+
const realRoot = fs.realpathSync(root), realFile = fs.realpathSync(file);
|
|
20
|
+
if (!realFile.startsWith(realRoot + path.sep)) return null;
|
|
21
|
+
const rows = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
22
|
+
const owned = rows.filter(row => row.sessionId === id && row.isSidechain !== true);
|
|
23
|
+
const byId = new Map(owned.filter(row => row.uuid).map(row => [row.uuid, row]));
|
|
24
|
+
const latest = owned.findLast(row => ['user', 'assistant'].includes(row.type));
|
|
25
|
+
if (!latest) return null;
|
|
26
|
+
const lineage = new Set(), chain = [];
|
|
27
|
+
let current = latest;
|
|
28
|
+
while (current?.uuid && !lineage.has(current.uuid)) {
|
|
29
|
+
lineage.add(current.uuid);
|
|
30
|
+
chain.push(current);
|
|
31
|
+
const parent = current.parentUuid || (current.subtype === 'compact_boundary' ? current.logicalParentUuid : null);
|
|
32
|
+
if (parent && !byId.has(parent)) return null;
|
|
33
|
+
current = byId.get(parent);
|
|
34
|
+
}
|
|
35
|
+
if (current?.uuid && lineage.has(current.uuid)) return null;
|
|
36
|
+
// Preserve bridge/system nodes in the parent chain, but render only the
|
|
37
|
+
// selected branch's user and assistant messages, not abandoned retries.
|
|
38
|
+
const messages = chain.reverse().filter(row => ['user', 'assistant'].includes(row.type)
|
|
39
|
+
&& row.isMeta !== true && row.isCompactSummary !== true);
|
|
40
|
+
const events = [], seen = new Set();
|
|
41
|
+
let hasTurn = false, finalText = '', finalAt = '', finalModel = '';
|
|
42
|
+
const finish = () => {
|
|
43
|
+
if (!hasTurn) return;
|
|
44
|
+
events.push({ type: 'claude/result', result: finalText, at: finalAt, model: finalModel, recovered: true });
|
|
45
|
+
hasTurn = false; finalText = '';
|
|
46
|
+
};
|
|
47
|
+
for (const row of messages) {
|
|
48
|
+
if (row.uuid && seen.has(row.uuid)) continue;
|
|
49
|
+
if (row.uuid) seen.add(row.uuid);
|
|
50
|
+
const content = typeof row.message?.content === 'string'
|
|
51
|
+
? [{ type: 'text', text: row.message.content }] : row.message?.content;
|
|
52
|
+
if (!Array.isArray(content)) continue;
|
|
53
|
+
if (row.type === 'user') {
|
|
54
|
+
const text = content.filter(block => block.type === 'text').map(block => block.text || '').join('\n');
|
|
55
|
+
const attachments = content.filter(block => block.type === 'image' && block.source?.type === 'base64'
|
|
56
|
+
&& ['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(block.source.media_type))
|
|
57
|
+
.map(block => ({ mediaType: block.source.media_type, name: '原始图片', url: `data:${block.source.media_type};base64,${block.source.data}` }));
|
|
58
|
+
if (text || attachments.length) {
|
|
59
|
+
finish(); hasTurn = true;
|
|
60
|
+
events.push({ type: 'claude/user-prompt', id: row.uuid, text, attachments, at: row.timestamp, recovered: true });
|
|
61
|
+
}
|
|
62
|
+
for (const block of content.filter(block => block.type === 'tool_result')) {
|
|
63
|
+
const output = typeof block.content === 'string' ? block.content : (block.content || []).filter(item => item.type === 'text').map(item => item.text || '').join('\n');
|
|
64
|
+
events.push({ type: 'claude/tool-result', id: block.tool_use_id, output, isError: block.is_error === true, at: row.timestamp, recovered: true });
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!hasTurn) continue;
|
|
69
|
+
for (const [index, block] of content.entries()) {
|
|
70
|
+
const blockId = `${row.uuid || row.message.id}-${index}`;
|
|
71
|
+
const meta = { id: blockId, at: row.timestamp, recovered: true };
|
|
72
|
+
if (block.type === 'text') {
|
|
73
|
+
events.push({ type: 'claude/text-start', ...meta }, { type: 'claude/text-delta', text: block.text || '', ...meta });
|
|
74
|
+
finalText = block.text || ''; finalAt = row.timestamp; finalModel = row.message.model;
|
|
75
|
+
} else if (block.type === 'thinking') {
|
|
76
|
+
events.push({ type: 'claude/thinking-start', ...meta }, { type: 'claude/thinking-delta', text: block.thinking || '', ...meta });
|
|
77
|
+
} else if (block.type === 'tool_use') {
|
|
78
|
+
events.push({ type: 'claude/tool-use', id: block.id, name: block.name, input: block.input || {}, at: row.timestamp, recovered: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finish();
|
|
83
|
+
return events.some(event => event.type === 'claude/user-prompt') ? events : null;
|
|
84
|
+
} catch { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { recoverClaudeHistory };
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const path = require('node:path');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const SCHEMAS = new Set(['cli-json-runner-session/v1', 'codex-appserver-session/v1', 'opencode-serve-session/v1', 'zcode-appserver-session/v1']);
|
|
6
|
-
|
|
7
|
-
function recoverOwnedCliSessions({ dataDir, projects, sourceDir = path.join(os.homedir(), '.agent-terminal/cli-sessions') }) {
|
|
8
|
-
const targetDir = path.join(dataDir, 'cli-sessions');
|
|
9
|
-
if (path.resolve(sourceDir) === path.resolve(targetDir) || !fs.existsSync(sourceDir)) return;
|
|
10
|
-
const owners = new Map(projects.list().map(project => [project.id, path.resolve(project.rootPath)]));
|
|
11
|
-
// Earlier v5 wiring put CLI records in the standalone directory. Preserve
|
|
12
|
-
// only records whose project ID AND code directory belong to this product.
|
|
13
|
-
for (const name of fs.readdirSync(sourceDir)) {
|
|
14
|
-
if (!/^[\w.-]+\.json$/.test(name)) continue;
|
|
15
|
-
const source = path.join(sourceDir, name); const target = path.join(targetDir, name);
|
|
16
|
-
let record;
|
|
17
|
-
try {
|
|
18
|
-
const stat = fs.lstatSync(source);
|
|
19
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 8 * 1024 * 1024 || fs.existsSync(target)) continue;
|
|
20
|
-
record = JSON.parse(fs.readFileSync(source, 'utf8'));
|
|
21
|
-
} catch { continue; }
|
|
22
|
-
if (!SCHEMAS.has(record.schema) || !owners.has(record.projectSlug) || typeof record.projectPath !== 'string' || path.resolve(record.projectPath) !== owners.get(record.projectSlug) || `${record.sessionId}.json` !== name) continue;
|
|
23
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
24
|
-
try { fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL); }
|
|
25
|
-
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
module.exports = { recoverOwnedCliSessions };
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const SCHEMAS = new Set(['cli-json-runner-session/v1', 'codex-appserver-session/v1', 'opencode-serve-session/v1', 'zcode-appserver-session/v1']);
|
|
6
|
+
|
|
7
|
+
function recoverOwnedCliSessions({ dataDir, projects, sourceDir = path.join(os.homedir(), '.agent-terminal/cli-sessions') }) {
|
|
8
|
+
const targetDir = path.join(dataDir, 'cli-sessions');
|
|
9
|
+
if (path.resolve(sourceDir) === path.resolve(targetDir) || !fs.existsSync(sourceDir)) return;
|
|
10
|
+
const owners = new Map(projects.list().map(project => [project.id, path.resolve(project.rootPath)]));
|
|
11
|
+
// Earlier v5 wiring put CLI records in the standalone directory. Preserve
|
|
12
|
+
// only records whose project ID AND code directory belong to this product.
|
|
13
|
+
for (const name of fs.readdirSync(sourceDir)) {
|
|
14
|
+
if (!/^[\w.-]+\.json$/.test(name)) continue;
|
|
15
|
+
const source = path.join(sourceDir, name); const target = path.join(targetDir, name);
|
|
16
|
+
let record;
|
|
17
|
+
try {
|
|
18
|
+
const stat = fs.lstatSync(source);
|
|
19
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 8 * 1024 * 1024 || fs.existsSync(target)) continue;
|
|
20
|
+
record = JSON.parse(fs.readFileSync(source, 'utf8'));
|
|
21
|
+
} catch { continue; }
|
|
22
|
+
if (!SCHEMAS.has(record.schema) || !owners.has(record.projectSlug) || typeof record.projectPath !== 'string' || path.resolve(record.projectPath) !== owners.get(record.projectSlug) || `${record.sessionId}.json` !== name) continue;
|
|
23
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
24
|
+
try { fs.copyFileSync(source, target, fs.constants.COPYFILE_EXCL); }
|
|
25
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
module.exports = { recoverOwnedCliSessions };
|
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { normalizeContext } = require('../../contracts');
|
|
6
|
-
|
|
7
|
-
class ContextResolver {
|
|
8
|
-
constructor(options = {}) {
|
|
9
|
-
this.createMissing = options.createMissing === true;
|
|
10
|
-
this.workspaces = new Map();
|
|
11
|
-
for (const [ref, target] of Object.entries(options.workspaces || {})) this.register(ref, target);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
register(ref, target) {
|
|
15
|
-
if (!ref || !target) throw new Error('workspace ref and target are required');
|
|
16
|
-
if (path.isAbsolute(String(ref))) throw new Error('workspaceRef must be an opaque server reference');
|
|
17
|
-
this.workspaces.set(String(ref), path.resolve(String(target)));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
unregister(ref) { return this.workspaces.delete(String(ref)); }
|
|
21
|
-
|
|
22
|
-
resolve(input) {
|
|
23
|
-
const context = normalizeContext(input);
|
|
24
|
-
if (!context.workspaceRef) throw Object.assign(new Error('workspaceRef is required'), { status: 400 });
|
|
25
|
-
if (path.isAbsolute(context.workspaceRef) || /[\\/]/.test(context.workspaceRef)) {
|
|
26
|
-
throw Object.assign(new Error('absolute or path-like workspaceRef is not trusted'), { status: 403 });
|
|
27
|
-
}
|
|
28
|
-
const workspacePath = this.workspaces.get(context.workspaceRef);
|
|
29
|
-
if (!workspacePath) throw Object.assign(new Error(`workspaceRef is not allowed: ${context.workspaceRef}`), { status: 403 });
|
|
30
|
-
if (!fs.existsSync(workspacePath)) {
|
|
31
|
-
if (!this.createMissing) throw Object.assign(new Error('workspace does not exist'), { status: 404 });
|
|
32
|
-
fs.mkdirSync(workspacePath, { recursive: true });
|
|
33
|
-
}
|
|
34
|
-
const real = fs.realpathSync(workspacePath);
|
|
35
|
-
if (!fs.statSync(real).isDirectory()) throw Object.assign(new Error('workspace is not a directory'), { status: 400 });
|
|
36
|
-
return { context, workspacePath: real };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Unique-match lookup for legacy sessions recorded before workspaceRef was
|
|
40
|
-
// persisted: a real path only maps back to a workspace when exactly one
|
|
41
|
-
// registered ref resolves to it. Zero or multiple matches are the caller's
|
|
42
|
-
// explicit ambiguity, never a silent pick of the current UI project.
|
|
43
|
-
findByRealPath(realPath) {
|
|
44
|
-
const matches = [];
|
|
45
|
-
if (typeof realPath !== 'string' || !realPath) return matches;
|
|
46
|
-
for (const [ref, target] of this.workspaces) {
|
|
47
|
-
try {
|
|
48
|
-
if (fs.existsSync(target) && fs.realpathSync(target) === realPath) matches.push(ref);
|
|
49
|
-
} catch { /* unresolvable registration cannot match */ }
|
|
50
|
-
}
|
|
51
|
-
return matches;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
module.exports = { ContextResolver };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { normalizeContext } = require('../../contracts');
|
|
6
|
+
|
|
7
|
+
class ContextResolver {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.createMissing = options.createMissing === true;
|
|
10
|
+
this.workspaces = new Map();
|
|
11
|
+
for (const [ref, target] of Object.entries(options.workspaces || {})) this.register(ref, target);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
register(ref, target) {
|
|
15
|
+
if (!ref || !target) throw new Error('workspace ref and target are required');
|
|
16
|
+
if (path.isAbsolute(String(ref))) throw new Error('workspaceRef must be an opaque server reference');
|
|
17
|
+
this.workspaces.set(String(ref), path.resolve(String(target)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
unregister(ref) { return this.workspaces.delete(String(ref)); }
|
|
21
|
+
|
|
22
|
+
resolve(input) {
|
|
23
|
+
const context = normalizeContext(input);
|
|
24
|
+
if (!context.workspaceRef) throw Object.assign(new Error('workspaceRef is required'), { status: 400 });
|
|
25
|
+
if (path.isAbsolute(context.workspaceRef) || /[\\/]/.test(context.workspaceRef)) {
|
|
26
|
+
throw Object.assign(new Error('absolute or path-like workspaceRef is not trusted'), { status: 403 });
|
|
27
|
+
}
|
|
28
|
+
const workspacePath = this.workspaces.get(context.workspaceRef);
|
|
29
|
+
if (!workspacePath) throw Object.assign(new Error(`workspaceRef is not allowed: ${context.workspaceRef}`), { status: 403 });
|
|
30
|
+
if (!fs.existsSync(workspacePath)) {
|
|
31
|
+
if (!this.createMissing) throw Object.assign(new Error('workspace does not exist'), { status: 404 });
|
|
32
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
const real = fs.realpathSync(workspacePath);
|
|
35
|
+
if (!fs.statSync(real).isDirectory()) throw Object.assign(new Error('workspace is not a directory'), { status: 400 });
|
|
36
|
+
return { context, workspacePath: real };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Unique-match lookup for legacy sessions recorded before workspaceRef was
|
|
40
|
+
// persisted: a real path only maps back to a workspace when exactly one
|
|
41
|
+
// registered ref resolves to it. Zero or multiple matches are the caller's
|
|
42
|
+
// explicit ambiguity, never a silent pick of the current UI project.
|
|
43
|
+
findByRealPath(realPath) {
|
|
44
|
+
const matches = [];
|
|
45
|
+
if (typeof realPath !== 'string' || !realPath) return matches;
|
|
46
|
+
for (const [ref, target] of this.workspaces) {
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(target) && fs.realpathSync(target) === realPath) matches.push(ref);
|
|
49
|
+
} catch { /* unresolvable registration cannot match */ }
|
|
50
|
+
}
|
|
51
|
+
return matches;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { ContextResolver };
|
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const fs = require('node:fs');
|
|
3
|
-
const { writeJson } = require('./json-file');
|
|
4
|
-
const SCHEMA = 'workbench-capture-outbox/v1';
|
|
5
|
-
|
|
6
|
-
class ConversationCapture {
|
|
7
|
-
constructor({ file, bridgeUrl, token, fetchImpl = fetch, retryMs = 2000 }) {
|
|
8
|
-
this.file = file; this.token = token; this.fetch = fetchImpl; this.queue = []; this.pending = null; this.error = null; this.closed = false;
|
|
9
|
-
const url = new URL('/api/internal/workbench-event', bridgeUrl);
|
|
10
|
-
if (!['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || url.protocol !== 'http:' || !token) throw new Error('Trusted local bridge configuration required');
|
|
11
|
-
this.url = url.href;
|
|
12
|
-
try {
|
|
13
|
-
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
14
|
-
if (record.schema !== SCHEMA || !Array.isArray(record.events)) throw new Error('Capture outbox schema mismatch');
|
|
15
|
-
this.queue = record.events;
|
|
16
|
-
} catch (error) { if (error.code !== 'ENOENT') this.error = 'CAPTURE_OUTBOX_DAMAGED'; }
|
|
17
|
-
this.timer = setInterval(() => this.flush().catch(() => {}), retryMs); this.timer.unref();
|
|
18
|
-
this.flush().catch(() => {});
|
|
19
|
-
}
|
|
20
|
-
status() { return { pending: this.queue.length, error: this.error }; }
|
|
21
|
-
enqueue(turn, eventType, content, outcome) {
|
|
22
|
-
if (this.closed || this.error === 'CAPTURE_OUTBOX_DAMAGED') throw new Error(this.error || 'Capture closed');
|
|
23
|
-
if (content !== undefined && (typeof content !== 'string' || content.length > 200000)) throw new Error('CAPTURE_CONTENT_TOO_LARGE');
|
|
24
|
-
const origin = ['human', 'agent-delegation'].includes(turn.inputOrigin) ? turn.inputOrigin : 'unknown';
|
|
25
|
-
const event = { ...turn, eventType, ...(content === undefined ? {} : { content }), ...(outcome ? { outcome } : {}), capturedAt: new Date().toISOString(), eventKey: `workbench:${turn.source}:${turn.sessionId}:${turn.turnId}:${eventType}:origin=${origin}` };
|
|
26
|
-
const next = [...this.queue, event];
|
|
27
|
-
if (next.length > 1000 || Buffer.byteLength(JSON.stringify(next)) > 16 * 1024 * 1024) throw new Error('CAPTURE_OUTBOX_FULL');
|
|
28
|
-
writeJson(this.file, { schema: SCHEMA, events: next }); this.queue = next;
|
|
29
|
-
this.flush().catch(() => {});
|
|
30
|
-
}
|
|
31
|
-
flush() {
|
|
32
|
-
if (this.pending) return this.pending;
|
|
33
|
-
if (this.closed || this.error === 'CAPTURE_OUTBOX_DAMAGED') return Promise.resolve();
|
|
34
|
-
this.pending = (async () => {
|
|
35
|
-
while (!this.closed && this.queue.length) {
|
|
36
|
-
this.controller = new AbortController();
|
|
37
|
-
const timeout = setTimeout(() => this.controller?.abort(), 3000);
|
|
38
|
-
try {
|
|
39
|
-
const response = await this.fetch(this.url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }, body: JSON.stringify(this.queue[0]), signal: this.controller.signal });
|
|
40
|
-
if (!response.ok || (await response.json()).status !== 'captured') throw new Error('CAPTURE_BRIDGE_UNAVAILABLE');
|
|
41
|
-
const next = this.queue.slice(1);
|
|
42
|
-
writeJson(this.file, { schema: SCHEMA, events: next }); this.queue = next; this.error = null;
|
|
43
|
-
} catch { this.error = 'CAPTURE_BRIDGE_UNAVAILABLE'; break; }
|
|
44
|
-
finally { clearTimeout(timeout); this.controller = null; }
|
|
45
|
-
}
|
|
46
|
-
})().finally(() => { this.pending = null; });
|
|
47
|
-
return this.pending;
|
|
48
|
-
}
|
|
49
|
-
close() { this.closed = true; clearInterval(this.timer); this.controller?.abort(); }
|
|
50
|
-
}
|
|
51
|
-
module.exports = { ConversationCapture };
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const { writeJson } = require('./json-file');
|
|
4
|
+
const SCHEMA = 'workbench-capture-outbox/v1';
|
|
5
|
+
|
|
6
|
+
class ConversationCapture {
|
|
7
|
+
constructor({ file, bridgeUrl, token, fetchImpl = fetch, retryMs = 2000 }) {
|
|
8
|
+
this.file = file; this.token = token; this.fetch = fetchImpl; this.queue = []; this.pending = null; this.error = null; this.closed = false;
|
|
9
|
+
const url = new URL('/api/internal/workbench-event', bridgeUrl);
|
|
10
|
+
if (!['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || url.protocol !== 'http:' || !token) throw new Error('Trusted local bridge configuration required');
|
|
11
|
+
this.url = url.href;
|
|
12
|
+
try {
|
|
13
|
+
const record = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
14
|
+
if (record.schema !== SCHEMA || !Array.isArray(record.events)) throw new Error('Capture outbox schema mismatch');
|
|
15
|
+
this.queue = record.events;
|
|
16
|
+
} catch (error) { if (error.code !== 'ENOENT') this.error = 'CAPTURE_OUTBOX_DAMAGED'; }
|
|
17
|
+
this.timer = setInterval(() => this.flush().catch(() => {}), retryMs); this.timer.unref();
|
|
18
|
+
this.flush().catch(() => {});
|
|
19
|
+
}
|
|
20
|
+
status() { return { pending: this.queue.length, error: this.error }; }
|
|
21
|
+
enqueue(turn, eventType, content, outcome) {
|
|
22
|
+
if (this.closed || this.error === 'CAPTURE_OUTBOX_DAMAGED') throw new Error(this.error || 'Capture closed');
|
|
23
|
+
if (content !== undefined && (typeof content !== 'string' || content.length > 200000)) throw new Error('CAPTURE_CONTENT_TOO_LARGE');
|
|
24
|
+
const origin = ['human', 'agent-delegation'].includes(turn.inputOrigin) ? turn.inputOrigin : 'unknown';
|
|
25
|
+
const event = { ...turn, eventType, ...(content === undefined ? {} : { content }), ...(outcome ? { outcome } : {}), capturedAt: new Date().toISOString(), eventKey: `workbench:${turn.source}:${turn.sessionId}:${turn.turnId}:${eventType}:origin=${origin}` };
|
|
26
|
+
const next = [...this.queue, event];
|
|
27
|
+
if (next.length > 1000 || Buffer.byteLength(JSON.stringify(next)) > 16 * 1024 * 1024) throw new Error('CAPTURE_OUTBOX_FULL');
|
|
28
|
+
writeJson(this.file, { schema: SCHEMA, events: next }); this.queue = next;
|
|
29
|
+
this.flush().catch(() => {});
|
|
30
|
+
}
|
|
31
|
+
flush() {
|
|
32
|
+
if (this.pending) return this.pending;
|
|
33
|
+
if (this.closed || this.error === 'CAPTURE_OUTBOX_DAMAGED') return Promise.resolve();
|
|
34
|
+
this.pending = (async () => {
|
|
35
|
+
while (!this.closed && this.queue.length) {
|
|
36
|
+
this.controller = new AbortController();
|
|
37
|
+
const timeout = setTimeout(() => this.controller?.abort(), 3000);
|
|
38
|
+
try {
|
|
39
|
+
const response = await this.fetch(this.url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.token}` }, body: JSON.stringify(this.queue[0]), signal: this.controller.signal });
|
|
40
|
+
if (!response.ok || (await response.json()).status !== 'captured') throw new Error('CAPTURE_BRIDGE_UNAVAILABLE');
|
|
41
|
+
const next = this.queue.slice(1);
|
|
42
|
+
writeJson(this.file, { schema: SCHEMA, events: next }); this.queue = next; this.error = null;
|
|
43
|
+
} catch { this.error = 'CAPTURE_BRIDGE_UNAVAILABLE'; break; }
|
|
44
|
+
finally { clearTimeout(timeout); this.controller = null; }
|
|
45
|
+
}
|
|
46
|
+
})().finally(() => { this.pending = null; });
|
|
47
|
+
return this.pending;
|
|
48
|
+
}
|
|
49
|
+
close() { this.closed = true; clearInterval(this.timer); this.controller?.abort(); }
|
|
50
|
+
}
|
|
51
|
+
module.exports = { ConversationCapture };
|