@sanqianx/claude-ai-workbench 0.13.2 → 0.13.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +652 -613
  4. package/apps/agent-terminal/app.js +143 -67
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +172 -170
  7. package/apps/agent-terminal/panel-host-proof.html +152 -152
  8. package/apps/agent-terminal/panel-proof-content.html +41 -41
  9. package/apps/agent-terminal/right-workspace-dock.css +189 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +652 -521
  11. package/apps/agent-terminal/user-question-card.js +68 -0
  12. package/apps/ai-profile-settings-host/index.html +25 -25
  13. package/apps/ai-profile-settings-host/page.css +1 -1
  14. package/apps/ai-profile-settings-host/page.js +113 -113
  15. package/apps/minimal-test-host/README.md +15 -15
  16. package/apps/minimal-test-host/host.css +2 -2
  17. package/apps/minimal-test-host/host.js +35 -35
  18. package/apps/minimal-test-host/index.html +58 -58
  19. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  20. package/docs/sdk/FILE_LAYOUT.md +31 -31
  21. package/docs/sdk/INTEGRATION.md +126 -126
  22. package/docs/sdk/README.md +21 -21
  23. package/docs/sdk/SESSION_EVENTS.md +15 -13
  24. package/docs/sdk/openapi.yaml +16 -1
  25. package/package.json +1 -1
  26. package/packages/client/README.md +29 -29
  27. package/packages/client/index.d.ts +122 -122
  28. package/packages/client/index.js +260 -260
  29. package/packages/client/package.json +2 -2
  30. package/packages/contracts/README.md +9 -9
  31. package/packages/contracts/index.d.ts +519 -517
  32. package/packages/contracts/index.js +312 -312
  33. package/packages/contracts/package.json +1 -1
  34. package/packages/core/README.md +8 -8
  35. package/packages/core/index.d.ts +76 -76
  36. package/packages/core/index.js +222 -222
  37. package/packages/core/package.json +2 -2
  38. package/packages/server/README.md +17 -17
  39. package/packages/server/bin/agent-terminal-server.js +42 -42
  40. package/packages/server/bin/claude-workbench.js +225 -225
  41. package/packages/server/index.d.ts +171 -171
  42. package/packages/server/index.js +230 -230
  43. package/packages/server/lib/agent-discovery.js +133 -133
  44. package/packages/server/lib/agent-registry.js +143 -143
  45. package/packages/server/lib/agent-spawn-env.js +17 -17
  46. package/packages/server/lib/attachment-store.js +78 -78
  47. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  48. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  49. package/packages/server/lib/capture-settings-store.js +35 -35
  50. package/packages/server/lib/claude-history-recovery.js +87 -87
  51. package/packages/server/lib/cli-session-recovery.js +28 -28
  52. package/packages/server/lib/context-resolver.js +55 -55
  53. package/packages/server/lib/conversation-capture.js +51 -51
  54. package/packages/server/lib/delegation-phase-store.js +357 -357
  55. package/packages/server/lib/delegation-store.js +474 -468
  56. package/packages/server/lib/diagnostics.js +32 -32
  57. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  58. package/packages/server/lib/drivers/codex-driver.js +1461 -1442
  59. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  60. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  61. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  62. package/packages/server/lib/event-stream.js +138 -138
  63. package/packages/server/lib/folder-picker.js +623 -623
  64. package/packages/server/lib/http-handler.js +339 -339
  65. package/packages/server/lib/json-file.js +26 -26
  66. package/packages/server/lib/knowledge-context.js +33 -33
  67. package/packages/server/lib/model-detection.js +295 -295
  68. package/packages/server/lib/parent-context.js +44 -0
  69. package/packages/server/lib/profile-store.js +283 -283
  70. package/packages/server/lib/project-store.js +102 -102
  71. package/packages/server/lib/runtime-loader.js +120 -120
  72. package/packages/server/lib/session-archive-store.js +53 -53
  73. package/packages/server/lib/session-history.js +612 -612
  74. package/packages/server/lib/system-doctor.js +125 -125
  75. package/packages/server/lib/ui-preferences-store.js +48 -48
  76. package/packages/server/lib/user-questions.js +67 -0
  77. package/packages/server/lib/workbench-service.js +1696 -1675
  78. package/packages/server/package.json +2 -2
  79. package/packages/ui/README.md +30 -30
  80. package/packages/ui/ai-profile-settings.d.ts +45 -45
  81. package/packages/ui/ai-profile-settings.js +283 -283
  82. package/packages/ui/claude-terminal.d.ts +31 -31
  83. package/packages/ui/claude-terminal.js +343 -343
  84. package/packages/ui/index.d.ts +18 -18
  85. package/packages/ui/index.js +51 -51
  86. package/packages/ui/package.json +2 -2
  87. package/packages/ui/theme.css +13 -13
  88. package/src/assets/claude-prompts.json +21 -21
  89. package/src/backend/http/ai-profile-routes.js +61 -61
  90. package/src/backend/http/claude-routes.js +170 -170
  91. package/src/backend/http/node-helpers.js +45 -45
  92. package/src/backend/index.js +21 -21
  93. package/src/backend/lib/ai-adapter.js +144 -144
  94. package/src/backend/lib/ai-profile-store.js +259 -259
  95. package/src/backend/lib/ai-vendor-presets.js +83 -83
  96. package/src/backend/lib/ai-workspace.js +140 -140
  97. package/src/backend/lib/automation-config.js +253 -253
  98. package/src/backend/lib/claude-cli-runner.js +2408 -2392
  99. package/src/backend/lib/data-dir.js +169 -169
  100. package/src/backend/lib/llm-client.js +176 -176
  101. package/src/backend/lib/model-context-windows.js +82 -82
  102. package/src/backend/lib/native-task-control.js +217 -217
  103. package/src/backend/lib/native-task-history-index.js +475 -475
  104. package/src/backend/lib/native-task-history-reader.js +526 -526
  105. package/src/backend/lib/native-task-messages.js +457 -457
  106. package/src/backend/lib/native-task-registry.js +325 -325
  107. package/src/backend/lib/native-task-runtime.js +242 -242
  108. package/src/backend/lib/native-task-trace.js +440 -440
  109. package/src/backend/lib/native-task-transcript.js +393 -393
  110. package/src/backend/lib/prompt-registry.js +166 -166
  111. package/src/backend/lib/runtime-config.js +121 -121
  112. package/src/backend/lib/sdk-session-stream.js +143 -143
  113. package/src/backend/lib/sdk-turn-stream.js +16 -16
  114. package/src/backend/lib/store-scope.js +131 -131
  115. package/src/backend/lib/task-identity.js +104 -104
  116. package/src/backend/lib/task-state-projection.js +58 -58
  117. package/src/index.js +7 -7
  118. package/src/sdk-wave-a.ts +1 -1
  119. package/src/sdk-wave-b.ts +1 -1
  120. package/src/sdk-wave-c.ts +1 -1
  121. package/src/sdk-wave-d.ts +1 -1
  122. package/src/sdk-wave-e.ts +1 -1
@@ -1,133 +1,133 @@
1
- 'use strict';
2
-
3
- const { spawn, spawnSync } = require('child_process');
4
- const { AGENTS, AGENT_IDS } = require('../../contracts');
5
- const { discoverZCodeCli } = require('./drivers/zcode-driver');
6
-
7
- const PROBE_TIMEOUT_MS = 5000;
8
- const CACHE_TTL_MS = 30000;
9
-
10
- // Probes locally installed agent CLIs. Results are cached briefly so
11
- // GET /agents stays cheap; a failed probe is a valid (negative) result.
12
- function parseVersionToken(text) {
13
- const trimmed = String(text || '').trim();
14
- const match = trimmed.match(/(\d+\.\d+[\w.-]*)/);
15
- return match ? match[1] : null;
16
- }
17
- // Spawns `<executable> --version` directly (no shell) and returns the first
18
- // version-looking token, or null when the probe fails. This is the only
19
- // version source: nothing here invents or hardcodes one.
20
- function probeExecutable(cmd) {
21
- try {
22
- const script = /\.(?:[cm]?js)$/i.test(cmd);
23
- const result = spawnSync(script ? process.execPath : cmd, script ? [cmd, '--version'] : ['--version'], {
24
- timeout: PROBE_TIMEOUT_MS,
25
- windowsHide: true,
26
- encoding: 'utf8',
27
- });
28
- if (result.error || result.status !== 0) return { available: false, version: null, code: result.error?.code || 'CLI_PROBE_FAILED', reason: String(result.error?.message || result.stderr || result.stdout || `exit ${result.status}`).slice(0, 2000), exitCode: result.status };
29
- const version = parseVersionToken(String(result.stdout || result.stderr || ''));
30
- return { available: Boolean(version), version, reason: version ? '' : 'Claude Code 未返回版本号。' };
31
- } catch {
32
- return { available: false, version: null, code: 'CLI_PROBE_FAILED', reason: 'Claude Code 版本探测失败。' };
33
- }
34
- }
35
- function probeExecutableVersion(cmd) { return probeExecutable(cmd).version; }
36
- function runProbe(command, args, { signal, timeoutMs = PROBE_TIMEOUT_MS, shell = false } = {}) {
37
- return new Promise(resolve => {
38
- if (signal?.aborted) return resolve({ available: false, code: 'PROBE_TIMEOUT', reason: '探测超时,保留上次结果。' });
39
- let child, output = '', errors = '', done = false;
40
- const finish = result => { if (done) return; done = true; clearTimeout(timer); signal?.removeEventListener('abort', cancel); resolve(result); };
41
- const cancel = () => {
42
- if (child?.pid && child.exitCode === null && process.platform === 'win32' && shell) {
43
- const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }); killer.once('error', () => child.kill());
44
- } else child?.kill();
45
- finish({ available: false, code: 'PROBE_TIMEOUT', reason: '探测超时,保留上次结果。' });
46
- };
47
- const timer = setTimeout(cancel, timeoutMs);
48
- signal?.addEventListener('abort', cancel, { once: true });
49
- try {
50
- child = spawn(command, args, { windowsHide: true, shell, stdio: ['ignore', 'pipe', 'pipe'] });
51
- child.stdout.on('data', b => { output = (output + b).slice(-8192); });
52
- child.stderr.on('data', b => { errors = (errors + b).slice(-8192); });
53
- child.once('error', error => finish({ available: false, version: null, code: error.code || 'CLI_PROBE_FAILED', reason: String(error.message).slice(0, 2000) }));
54
- child.once('close', code => {
55
- const version = code === 0 ? parseVersionToken(output || errors) : null;
56
- finish({ available: !!version, version, exitCode: code, code: code === 0 ? undefined : 'CLI_PROBE_FAILED', reason: version ? '' : (errors || output || `exit ${code}`).slice(0, 2000), stdout: output });
57
- });
58
- } catch (error) { finish({ available: false, code: error.code || 'CLI_PROBE_FAILED', reason: String(error.message).slice(0, 2000) }); }
59
- });
60
- }
61
- async function probeExecutableAsync(command, options = {}) {
62
- const script = /\.(?:[cm]?js)$/i.test(command);
63
- return runProbe(script ? process.execPath : command, script ? [command, '--version'] : ['--version'], { ...options, shell: !script && process.platform === 'win32' && !require('path').isAbsolute(command) });
64
- }
65
- async function resolveClaudeAsync(driver, options = {}) {
66
- if (typeof driver.findClaudeExecutableForSdkAsync === 'function') return driver.findClaudeExecutableForSdkAsync(options);
67
- let found = driver.findClaudeExecutableForSdk({ pathHits: [] });
68
- if (found?.cmd || typeof found === 'string' || process.platform !== 'win32') return found;
69
- const where = require('path').join(process.env.WINDIR || 'C:/Windows', 'System32', 'where.exe');
70
- const result = await runProbe(where, ['claude'], options);
71
- return driver.findClaudeExecutableForSdk({ pathHits: (result.stdout || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean) });
72
- }
73
- async function resolveZCodeAsync({ signal, discover = discoverZCodeCli, probe = runProbe, platform = process.platform } = {}) {
74
- const direct = discover({ regQuery: () => null });
75
- if (direct || platform !== 'win32') return direct;
76
- const entries = new Map();
77
- for (const hive of ['HKCU', 'HKLM']) {
78
- const key = `${hive}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\ZCode.exe`;
79
- entries.set(key, (await probe('reg', ['query', key], { signal })).stdout || '');
80
- }
81
- return discover({ regQuery: key => entries.get(key) });
82
- }
83
- function createAgentDiscovery(options = {}) {
84
- const clock = options.now || Date.now;
85
- const claudeProbe = options.claudeProbe || (() => ({ available: false, reason: 'Claude Code executable was not found' }));
86
- let cache = Object.fromEntries(AGENT_IDS.map(id => [id, { ...AGENTS[id], available: false, version: null, scanState: 'unknown', reason: '尚未检查' }]));
87
- let cacheAt = 0, pending = null, generation = 0, closed = false;
88
- let scan = { state: 'idle', generation, updatedAt: null };
89
- const active = new Set();
90
-
91
- async function inspect(id, signal) {
92
- if (options.probes?.[id]) return options.probes[id]({ signal });
93
- if (id === 'claude-code') return claudeProbe({ signal });
94
- if (id !== 'zcode') return probeExecutableAsync(id, { signal });
95
- const cli = await resolveZCodeAsync({ signal });
96
- if (!cli) return { available: false, reason: 'ZCode was not found on this machine' };
97
- return probeExecutableAsync(cli.path, { signal });
98
- }
99
- function discover() {
100
- if (!closed && clock() - cacheAt >= (options.cacheTtlMs ?? CACHE_TTL_MS)) void discover.refresh().catch(() => {});
101
- return cache;
102
- }
103
- discover.snapshot = () => ({ ...scan });
104
- discover.refresh = () => {
105
- if (pending) return pending;
106
- if (closed) return Promise.resolve(cache);
107
- scan = { ...scan, state: 'scanning', generation: ++generation };
108
- let cursor = 0;
109
- const deadline = Date.now() + (options.scanBudgetMs ?? 12000);
110
- const worker = async () => {
111
- while (cursor < AGENT_IDS.length && !closed) {
112
- const id = AGENT_IDS[cursor++], controller = new AbortController(); active.add(controller);
113
- const budget = Math.max(0, Math.min(options.probeTimeoutMs ?? 5000, deadline - Date.now()));
114
- let timer;
115
- const timeout = new Promise(resolve => { const expire = () => resolve({ code: 'PROBE_TIMEOUT', reason: '检查超时,保留上次结果。' }); controller.signal.addEventListener('abort', expire, { once: true }); timer = setTimeout(() => controller.abort(), budget); });
116
- let result;
117
- try { result = await Promise.race([Promise.resolve().then(() => inspect(id, controller.signal)), timeout]); }
118
- catch { result = { code: 'PROBE_FAILED', reason: '检查失败,保留上次结果。' }; }
119
- finally { clearTimeout(timer); active.delete(controller); }
120
- if (closed) return;
121
- const uncertain = ['PROBE_TIMEOUT', 'ETIMEDOUT', 'PROBE_FAILED'].includes(result.code);
122
- cache = { ...cache, [id]: { ...cache[id], ...(uncertain ? {} : { available: !!result.available, version: result.version || null }), reason: result.reason || '', scanState: uncertain ? 'error' : 'ready', checkedAt: new Date().toISOString(), errorCode: result.code || null } };
123
- }
124
- };
125
- pending = Promise.all([worker(), worker()]).then(() => { cacheAt = clock(); scan = { state: 'ready', generation, updatedAt: new Date(cacheAt).toISOString() }; return cache; }).finally(() => { pending = null; });
126
- return pending;
127
- };
128
- discover.invalidate = () => { cacheAt = 0; };
129
- discover.close = () => { closed = true; for (const controller of active) controller.abort(); };
130
- return discover;
131
- }
132
-
133
- module.exports = { createAgentDiscovery, probeExecutableVersion, probeExecutable, probeExecutableAsync, resolveClaudeAsync, resolveZCodeAsync, runProbe };
1
+ 'use strict';
2
+
3
+ const { spawn, spawnSync } = require('child_process');
4
+ const { AGENTS, AGENT_IDS } = require('../../contracts');
5
+ const { discoverZCodeCli } = require('./drivers/zcode-driver');
6
+
7
+ const PROBE_TIMEOUT_MS = 5000;
8
+ const CACHE_TTL_MS = 30000;
9
+
10
+ // Probes locally installed agent CLIs. Results are cached briefly so
11
+ // GET /agents stays cheap; a failed probe is a valid (negative) result.
12
+ function parseVersionToken(text) {
13
+ const trimmed = String(text || '').trim();
14
+ const match = trimmed.match(/(\d+\.\d+[\w.-]*)/);
15
+ return match ? match[1] : null;
16
+ }
17
+ // Spawns `<executable> --version` directly (no shell) and returns the first
18
+ // version-looking token, or null when the probe fails. This is the only
19
+ // version source: nothing here invents or hardcodes one.
20
+ function probeExecutable(cmd) {
21
+ try {
22
+ const script = /\.(?:[cm]?js)$/i.test(cmd);
23
+ const result = spawnSync(script ? process.execPath : cmd, script ? [cmd, '--version'] : ['--version'], {
24
+ timeout: PROBE_TIMEOUT_MS,
25
+ windowsHide: true,
26
+ encoding: 'utf8',
27
+ });
28
+ if (result.error || result.status !== 0) return { available: false, version: null, code: result.error?.code || 'CLI_PROBE_FAILED', reason: String(result.error?.message || result.stderr || result.stdout || `exit ${result.status}`).slice(0, 2000), exitCode: result.status };
29
+ const version = parseVersionToken(String(result.stdout || result.stderr || ''));
30
+ return { available: Boolean(version), version, reason: version ? '' : 'Claude Code 未返回版本号。' };
31
+ } catch {
32
+ return { available: false, version: null, code: 'CLI_PROBE_FAILED', reason: 'Claude Code 版本探测失败。' };
33
+ }
34
+ }
35
+ function probeExecutableVersion(cmd) { return probeExecutable(cmd).version; }
36
+ function runProbe(command, args, { signal, timeoutMs = PROBE_TIMEOUT_MS, shell = false } = {}) {
37
+ return new Promise(resolve => {
38
+ if (signal?.aborted) return resolve({ available: false, code: 'PROBE_TIMEOUT', reason: '探测超时,保留上次结果。' });
39
+ let child, output = '', errors = '', done = false;
40
+ const finish = result => { if (done) return; done = true; clearTimeout(timer); signal?.removeEventListener('abort', cancel); resolve(result); };
41
+ const cancel = () => {
42
+ if (child?.pid && child.exitCode === null && process.platform === 'win32' && shell) {
43
+ const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }); killer.once('error', () => child.kill());
44
+ } else child?.kill();
45
+ finish({ available: false, code: 'PROBE_TIMEOUT', reason: '探测超时,保留上次结果。' });
46
+ };
47
+ const timer = setTimeout(cancel, timeoutMs);
48
+ signal?.addEventListener('abort', cancel, { once: true });
49
+ try {
50
+ child = spawn(command, args, { windowsHide: true, shell, stdio: ['ignore', 'pipe', 'pipe'] });
51
+ child.stdout.on('data', b => { output = (output + b).slice(-8192); });
52
+ child.stderr.on('data', b => { errors = (errors + b).slice(-8192); });
53
+ child.once('error', error => finish({ available: false, version: null, code: error.code || 'CLI_PROBE_FAILED', reason: String(error.message).slice(0, 2000) }));
54
+ child.once('close', code => {
55
+ const version = code === 0 ? parseVersionToken(output || errors) : null;
56
+ finish({ available: !!version, version, exitCode: code, code: code === 0 ? undefined : 'CLI_PROBE_FAILED', reason: version ? '' : (errors || output || `exit ${code}`).slice(0, 2000), stdout: output });
57
+ });
58
+ } catch (error) { finish({ available: false, code: error.code || 'CLI_PROBE_FAILED', reason: String(error.message).slice(0, 2000) }); }
59
+ });
60
+ }
61
+ async function probeExecutableAsync(command, options = {}) {
62
+ const script = /\.(?:[cm]?js)$/i.test(command);
63
+ return runProbe(script ? process.execPath : command, script ? [command, '--version'] : ['--version'], { ...options, shell: !script && process.platform === 'win32' && !require('path').isAbsolute(command) });
64
+ }
65
+ async function resolveClaudeAsync(driver, options = {}) {
66
+ if (typeof driver.findClaudeExecutableForSdkAsync === 'function') return driver.findClaudeExecutableForSdkAsync(options);
67
+ let found = driver.findClaudeExecutableForSdk({ pathHits: [] });
68
+ if (found?.cmd || typeof found === 'string' || process.platform !== 'win32') return found;
69
+ const where = require('path').join(process.env.WINDIR || 'C:/Windows', 'System32', 'where.exe');
70
+ const result = await runProbe(where, ['claude'], options);
71
+ return driver.findClaudeExecutableForSdk({ pathHits: (result.stdout || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean) });
72
+ }
73
+ async function resolveZCodeAsync({ signal, discover = discoverZCodeCli, probe = runProbe, platform = process.platform } = {}) {
74
+ const direct = discover({ regQuery: () => null });
75
+ if (direct || platform !== 'win32') return direct;
76
+ const entries = new Map();
77
+ for (const hive of ['HKCU', 'HKLM']) {
78
+ const key = `${hive}\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\ZCode.exe`;
79
+ entries.set(key, (await probe('reg', ['query', key], { signal })).stdout || '');
80
+ }
81
+ return discover({ regQuery: key => entries.get(key) });
82
+ }
83
+ function createAgentDiscovery(options = {}) {
84
+ const clock = options.now || Date.now;
85
+ const claudeProbe = options.claudeProbe || (() => ({ available: false, reason: 'Claude Code executable was not found' }));
86
+ let cache = Object.fromEntries(AGENT_IDS.map(id => [id, { ...AGENTS[id], available: false, version: null, scanState: 'unknown', reason: '尚未检查' }]));
87
+ let cacheAt = 0, pending = null, generation = 0, closed = false;
88
+ let scan = { state: 'idle', generation, updatedAt: null };
89
+ const active = new Set();
90
+
91
+ async function inspect(id, signal) {
92
+ if (options.probes?.[id]) return options.probes[id]({ signal });
93
+ if (id === 'claude-code') return claudeProbe({ signal });
94
+ if (id !== 'zcode') return probeExecutableAsync(id, { signal });
95
+ const cli = await resolveZCodeAsync({ signal });
96
+ if (!cli) return { available: false, reason: 'ZCode was not found on this machine' };
97
+ return probeExecutableAsync(cli.path, { signal });
98
+ }
99
+ function discover() {
100
+ if (!closed && clock() - cacheAt >= (options.cacheTtlMs ?? CACHE_TTL_MS)) void discover.refresh().catch(() => {});
101
+ return cache;
102
+ }
103
+ discover.snapshot = () => ({ ...scan });
104
+ discover.refresh = () => {
105
+ if (pending) return pending;
106
+ if (closed) return Promise.resolve(cache);
107
+ scan = { ...scan, state: 'scanning', generation: ++generation };
108
+ let cursor = 0;
109
+ const deadline = Date.now() + (options.scanBudgetMs ?? 12000);
110
+ const worker = async () => {
111
+ while (cursor < AGENT_IDS.length && !closed) {
112
+ const id = AGENT_IDS[cursor++], controller = new AbortController(); active.add(controller);
113
+ const budget = Math.max(0, Math.min(options.probeTimeoutMs ?? 5000, deadline - Date.now()));
114
+ let timer;
115
+ const timeout = new Promise(resolve => { const expire = () => resolve({ code: 'PROBE_TIMEOUT', reason: '检查超时,保留上次结果。' }); controller.signal.addEventListener('abort', expire, { once: true }); timer = setTimeout(() => controller.abort(), budget); });
116
+ let result;
117
+ try { result = await Promise.race([Promise.resolve().then(() => inspect(id, controller.signal)), timeout]); }
118
+ catch { result = { code: 'PROBE_FAILED', reason: '检查失败,保留上次结果。' }; }
119
+ finally { clearTimeout(timer); active.delete(controller); }
120
+ if (closed) return;
121
+ const uncertain = ['PROBE_TIMEOUT', 'ETIMEDOUT', 'PROBE_FAILED'].includes(result.code);
122
+ cache = { ...cache, [id]: { ...cache[id], ...(uncertain ? {} : { available: !!result.available, version: result.version || null }), reason: result.reason || '', scanState: uncertain ? 'error' : 'ready', checkedAt: new Date().toISOString(), errorCode: result.code || null } };
123
+ }
124
+ };
125
+ pending = Promise.all([worker(), worker()]).then(() => { cacheAt = clock(); scan = { state: 'ready', generation, updatedAt: new Date(cacheAt).toISOString() }; return cache; }).finally(() => { pending = null; });
126
+ return pending;
127
+ };
128
+ discover.invalidate = () => { cacheAt = 0; };
129
+ discover.close = () => { closed = true; for (const controller of active) controller.abort(); };
130
+ return discover;
131
+ }
132
+
133
+ module.exports = { createAgentDiscovery, probeExecutableVersion, probeExecutable, probeExecutableAsync, resolveClaudeAsync, resolveZCodeAsync, runProbe };
@@ -1,143 +1,143 @@
1
- 'use strict';
2
-
3
- const { AGENT_IDS, normalizeAgentId } = require('../../contracts');
4
-
5
- // Routes every WorkbenchRunner call to the driver that owns the session.
6
- // The claude-code driver is whatever runner the host injected (legacy
7
- // claude-cli-runner in production, fake runners in tests); CLI drivers plug
8
- // in beside it with the same interface.
9
- class AgentRegistry {
10
- constructor(options = {}) {
11
- this.drivers = new Map();
12
- this.sessionAgents = new Map();
13
- for (const [agentId, driver] of Object.entries(options.drivers || {})) {
14
- this.register(agentId, driver);
15
- }
16
- }
17
-
18
- register(agentId, driver) {
19
- const id = normalizeAgentId(agentId);
20
- if (!driver) throw new Error(`driver for ${id} is required`);
21
- this.drivers.set(id, driver);
22
- return driver;
23
- }
24
-
25
- driverFor(agentId) {
26
- const id = normalizeAgentId(agentId);
27
- const driver = this.drivers.get(id);
28
- if (!driver) {
29
- throw Object.assign(new Error(`agent "${id}" has no registered driver`), { status: 501 });
30
- }
31
- return driver;
32
- }
33
-
34
- agentOf(sessionId) {
35
- return this.sessionAgents.get(sessionId) || 'claude-code';
36
- }
37
-
38
- _route(sessionId) {
39
- try {
40
- return this.driverFor(this.agentOf(sessionId));
41
- } catch {
42
- throw Object.assign(new Error('session not found'), { status: 404 });
43
- }
44
- }
45
-
46
- asRunner() {
47
- const registry = this;
48
- return {
49
- async destroy() {
50
- await Promise.all([...registry.drivers.values()].map(driver => typeof driver.destroy === 'function' ? driver.destroy() : undefined));
51
- },
52
- startChatSession(args = {}) {
53
- // Reserved delegated identity reaches sessions only through the
54
- // trusted startDelegatedSession route below — never through ordinary
55
- // user-driven session creation, whatever the caller forwarded.
56
- if (args.reservedSessionId !== undefined || args.delegation !== undefined) {
57
- throw Object.assign(new Error('reserved delegated identity is not accepted on the ordinary session path'), {
58
- status: 400, code: 'DELEGATION_IDENTITY_FORBIDDEN',
59
- });
60
- }
61
- const agentId = normalizeAgentId(args.agentId);
62
- const driver = registry.driverFor(agentId);
63
- const started = driver.startChatSession({ ...args, agentId });
64
- registry.sessionAgents.set(started.sessionId, agentId);
65
- return started;
66
- },
67
- // Trusted reserved-identity factory route. A driver that cannot honor a
68
- // server-reserved childSessionId fails explicitly here — it must never
69
- // fall back to minting its own random child.
70
- startDelegatedSession(args = {}) {
71
- const agentId = normalizeAgentId(args.agentId);
72
- const driver = registry.driverFor(agentId);
73
- if (typeof driver.startDelegatedSession !== 'function') {
74
- throw Object.assign(new Error(`agent "${agentId}" cannot run delegated sessions with a server-reserved identity`), {
75
- status: 501, code: 'DELEGATION_AGENT_UNSUPPORTED',
76
- });
77
- }
78
- const started = driver.startDelegatedSession({ ...args, agentId });
79
- registry.sessionAgents.set(started.sessionId, agentId);
80
- return started;
81
- },
82
- // Capability probe that creates nothing: validates the command BEFORE
83
- // the ledger reserves an identity for an agent that could never run it.
84
- supportsDelegatedSessions(agentId) {
85
- const id = normalizeAgentId(agentId);
86
- const driver = registry.drivers.get(id);
87
- return Boolean(driver && typeof driver.startDelegatedSession === 'function');
88
- },
89
- listSessions(filter = {}) {
90
- const all = [];
91
- for (const id of AGENT_IDS) {
92
- const driver = registry.drivers.get(id);
93
- if (!driver) continue;
94
- for (const session of driver.listSessions(filter)) {
95
- registry.sessionAgents.set(session.sessionId, id);
96
- all.push({ ...session, agentId: registry.agentOf(session.sessionId) });
97
- }
98
- }
99
- return all;
100
- },
101
- getState(id) { return registry._route(id).getState(id); },
102
- getSession(id) { return registry._route(id).getSession(id); },
103
- getSessionTokenUsage(id) { return registry._route(id).getSessionTokenUsage(id); },
104
- subscribe(id, listener) { return registry._route(id).subscribe(id, listener); },
105
- sendInput(id, text, profile, options) { return registry._route(id).sendInput(id, text, profile, options); },
106
- updateSelection(id, profile, selectedModel) {
107
- const driver = registry._route(id);
108
- if (typeof driver.updateSelection === 'function') return driver.updateSelection(id, profile, selectedModel);
109
- // Bare runners without the method still get rebound so the service
110
- // fallback and the proxy stay interchangeable.
111
- const session = driver.getSession && driver.getSession(id);
112
- if (!session) throw Object.assign(new Error('session not found'), { status: 404 });
113
- session.aiProfileId = profile && profile.id || null;
114
- session.selectedModel = selectedModel || null;
115
- return session;
116
- },
117
- resolvePermission(id, requestId, decision) { return registry._route(id).resolvePermission(id, requestId, decision); },
118
- abort(id, reason) { return registry._route(id).abort(id, reason); },
119
- deleteSession(id) { return registry._route(id).deleteSession(id); },
120
- attachNativeTaskRuntime(id, identity) {
121
- // Native-task scope binding is claude-code-only today; CLI drivers
122
- // answer with an explicit unavailable result instead of throwing.
123
- const driver = registry._route(id);
124
- if (typeof driver.attachNativeTaskRuntime !== 'function') return { attached: false, reason: 'runner-without-native-tasks' };
125
- return driver.attachNativeTaskRuntime(id, identity);
126
- },
127
- detachNativeTaskRuntime(id, identity) {
128
- const driver = registry._route(id);
129
- if (typeof driver.detachNativeTaskRuntime !== 'function') return { detached: false, reason: 'runner-without-native-tasks' };
130
- return driver.detachNativeTaskRuntime(id, identity);
131
- },
132
- listSupportedCommands(id, profile) { return registry._route(id).listSupportedCommands(id, profile); },
133
- findClaudeExecutableForSdk() {
134
- const driver = registry.drivers.get('claude-code');
135
- return driver && typeof driver.findClaudeExecutableForSdk === 'function'
136
- ? driver.findClaudeExecutableForSdk()
137
- : null;
138
- },
139
- };
140
- }
141
- }
142
-
143
- module.exports = { AgentRegistry };
1
+ 'use strict';
2
+
3
+ const { AGENT_IDS, normalizeAgentId } = require('../../contracts');
4
+
5
+ // Routes every WorkbenchRunner call to the driver that owns the session.
6
+ // The claude-code driver is whatever runner the host injected (legacy
7
+ // claude-cli-runner in production, fake runners in tests); CLI drivers plug
8
+ // in beside it with the same interface.
9
+ class AgentRegistry {
10
+ constructor(options = {}) {
11
+ this.drivers = new Map();
12
+ this.sessionAgents = new Map();
13
+ for (const [agentId, driver] of Object.entries(options.drivers || {})) {
14
+ this.register(agentId, driver);
15
+ }
16
+ }
17
+
18
+ register(agentId, driver) {
19
+ const id = normalizeAgentId(agentId);
20
+ if (!driver) throw new Error(`driver for ${id} is required`);
21
+ this.drivers.set(id, driver);
22
+ return driver;
23
+ }
24
+
25
+ driverFor(agentId) {
26
+ const id = normalizeAgentId(agentId);
27
+ const driver = this.drivers.get(id);
28
+ if (!driver) {
29
+ throw Object.assign(new Error(`agent "${id}" has no registered driver`), { status: 501 });
30
+ }
31
+ return driver;
32
+ }
33
+
34
+ agentOf(sessionId) {
35
+ return this.sessionAgents.get(sessionId) || 'claude-code';
36
+ }
37
+
38
+ _route(sessionId) {
39
+ try {
40
+ return this.driverFor(this.agentOf(sessionId));
41
+ } catch {
42
+ throw Object.assign(new Error('session not found'), { status: 404 });
43
+ }
44
+ }
45
+
46
+ asRunner() {
47
+ const registry = this;
48
+ return {
49
+ async destroy() {
50
+ await Promise.all([...registry.drivers.values()].map(driver => typeof driver.destroy === 'function' ? driver.destroy() : undefined));
51
+ },
52
+ startChatSession(args = {}) {
53
+ // Reserved delegated identity reaches sessions only through the
54
+ // trusted startDelegatedSession route below — never through ordinary
55
+ // user-driven session creation, whatever the caller forwarded.
56
+ if (args.reservedSessionId !== undefined || args.delegation !== undefined) {
57
+ throw Object.assign(new Error('reserved delegated identity is not accepted on the ordinary session path'), {
58
+ status: 400, code: 'DELEGATION_IDENTITY_FORBIDDEN',
59
+ });
60
+ }
61
+ const agentId = normalizeAgentId(args.agentId);
62
+ const driver = registry.driverFor(agentId);
63
+ const started = driver.startChatSession({ ...args, agentId });
64
+ registry.sessionAgents.set(started.sessionId, agentId);
65
+ return started;
66
+ },
67
+ // Trusted reserved-identity factory route. A driver that cannot honor a
68
+ // server-reserved childSessionId fails explicitly here — it must never
69
+ // fall back to minting its own random child.
70
+ startDelegatedSession(args = {}) {
71
+ const agentId = normalizeAgentId(args.agentId);
72
+ const driver = registry.driverFor(agentId);
73
+ if (typeof driver.startDelegatedSession !== 'function') {
74
+ throw Object.assign(new Error(`agent "${agentId}" cannot run delegated sessions with a server-reserved identity`), {
75
+ status: 501, code: 'DELEGATION_AGENT_UNSUPPORTED',
76
+ });
77
+ }
78
+ const started = driver.startDelegatedSession({ ...args, agentId });
79
+ registry.sessionAgents.set(started.sessionId, agentId);
80
+ return started;
81
+ },
82
+ // Capability probe that creates nothing: validates the command BEFORE
83
+ // the ledger reserves an identity for an agent that could never run it.
84
+ supportsDelegatedSessions(agentId) {
85
+ const id = normalizeAgentId(agentId);
86
+ const driver = registry.drivers.get(id);
87
+ return Boolean(driver && typeof driver.startDelegatedSession === 'function');
88
+ },
89
+ listSessions(filter = {}) {
90
+ const all = [];
91
+ for (const id of AGENT_IDS) {
92
+ const driver = registry.drivers.get(id);
93
+ if (!driver) continue;
94
+ for (const session of driver.listSessions(filter)) {
95
+ registry.sessionAgents.set(session.sessionId, id);
96
+ all.push({ ...session, agentId: registry.agentOf(session.sessionId) });
97
+ }
98
+ }
99
+ return all;
100
+ },
101
+ getState(id) { return registry._route(id).getState(id); },
102
+ getSession(id) { return registry._route(id).getSession(id); },
103
+ getSessionTokenUsage(id) { return registry._route(id).getSessionTokenUsage(id); },
104
+ subscribe(id, listener) { return registry._route(id).subscribe(id, listener); },
105
+ sendInput(id, text, profile, options) { return registry._route(id).sendInput(id, text, profile, options); },
106
+ updateSelection(id, profile, selectedModel) {
107
+ const driver = registry._route(id);
108
+ if (typeof driver.updateSelection === 'function') return driver.updateSelection(id, profile, selectedModel);
109
+ // Bare runners without the method still get rebound so the service
110
+ // fallback and the proxy stay interchangeable.
111
+ const session = driver.getSession && driver.getSession(id);
112
+ if (!session) throw Object.assign(new Error('session not found'), { status: 404 });
113
+ session.aiProfileId = profile && profile.id || null;
114
+ session.selectedModel = selectedModel || null;
115
+ return session;
116
+ },
117
+ resolvePermission(id, requestId, decision) { return registry._route(id).resolvePermission(id, requestId, decision); },
118
+ abort(id, reason) { return registry._route(id).abort(id, reason); },
119
+ deleteSession(id) { return registry._route(id).deleteSession(id); },
120
+ attachNativeTaskRuntime(id, identity) {
121
+ // Native-task scope binding is claude-code-only today; CLI drivers
122
+ // answer with an explicit unavailable result instead of throwing.
123
+ const driver = registry._route(id);
124
+ if (typeof driver.attachNativeTaskRuntime !== 'function') return { attached: false, reason: 'runner-without-native-tasks' };
125
+ return driver.attachNativeTaskRuntime(id, identity);
126
+ },
127
+ detachNativeTaskRuntime(id, identity) {
128
+ const driver = registry._route(id);
129
+ if (typeof driver.detachNativeTaskRuntime !== 'function') return { detached: false, reason: 'runner-without-native-tasks' };
130
+ return driver.detachNativeTaskRuntime(id, identity);
131
+ },
132
+ listSupportedCommands(id, profile) { return registry._route(id).listSupportedCommands(id, profile); },
133
+ findClaudeExecutableForSdk() {
134
+ const driver = registry.drivers.get('claude-code');
135
+ return driver && typeof driver.findClaudeExecutableForSdk === 'function'
136
+ ? driver.findClaudeExecutableForSdk()
137
+ : null;
138
+ },
139
+ };
140
+ }
141
+ }
142
+
143
+ module.exports = { AgentRegistry };
@@ -1,17 +1,17 @@
1
- 'use strict';
2
-
3
- // KB_KNOWLEDGE_DATA_DIR names the product knowledge root the host product
4
- // launches the workbench with (explicit and non-secret by contract). The
5
- // workbench itself repoints KB_DATA_DIR at its own session storage; an agent
6
- // inheriting that value resolves project-knowledge-kb against the workbench
7
- // store and fails with PROJECT_NOT_FOUND. Map the product root onto KB_DATA_DIR
8
- // only in spawned agent environments: the workbench's own storage resolution
9
- // stays untouched, and a standalone workbench without the variable keeps the
10
- // inherited behavior.
11
- function knowledgeRootEnv(source) {
12
- const env = source || process.env;
13
- const root = typeof env.KB_KNOWLEDGE_DATA_DIR === 'string' ? env.KB_KNOWLEDGE_DATA_DIR.trim() : '';
14
- return root ? { KB_DATA_DIR: root } : {};
15
- }
16
-
17
- module.exports = { knowledgeRootEnv };
1
+ 'use strict';
2
+
3
+ // KB_KNOWLEDGE_DATA_DIR names the product knowledge root the host product
4
+ // launches the workbench with (explicit and non-secret by contract). The
5
+ // workbench itself repoints KB_DATA_DIR at its own session storage; an agent
6
+ // inheriting that value resolves project-knowledge-kb against the workbench
7
+ // store and fails with PROJECT_NOT_FOUND. Map the product root onto KB_DATA_DIR
8
+ // only in spawned agent environments: the workbench's own storage resolution
9
+ // stays untouched, and a standalone workbench without the variable keeps the
10
+ // inherited behavior.
11
+ function knowledgeRootEnv(source) {
12
+ const env = source || process.env;
13
+ const root = typeof env.KB_KNOWLEDGE_DATA_DIR === 'string' ? env.KB_KNOWLEDGE_DATA_DIR.trim() : '';
14
+ return root ? { KB_DATA_DIR: root } : {};
15
+ }
16
+
17
+ module.exports = { knowledgeRootEnv };