@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.
Files changed (120) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +636 -613
  4. package/apps/agent-terminal/app.js +125 -65
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +171 -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 +187 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +623 -519
  11. package/apps/ai-profile-settings-host/index.html +25 -25
  12. package/apps/ai-profile-settings-host/page.css +1 -1
  13. package/apps/ai-profile-settings-host/page.js +113 -113
  14. package/apps/minimal-test-host/README.md +15 -15
  15. package/apps/minimal-test-host/host.css +2 -2
  16. package/apps/minimal-test-host/host.js +35 -35
  17. package/apps/minimal-test-host/index.html +58 -58
  18. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  19. package/docs/sdk/FILE_LAYOUT.md +31 -31
  20. package/docs/sdk/INTEGRATION.md +126 -126
  21. package/docs/sdk/README.md +21 -21
  22. package/docs/sdk/SESSION_EVENTS.md +13 -13
  23. package/docs/sdk/openapi.yaml +8 -1
  24. package/package.json +1 -1
  25. package/packages/client/README.md +29 -29
  26. package/packages/client/index.d.ts +122 -122
  27. package/packages/client/index.js +260 -260
  28. package/packages/client/package.json +2 -2
  29. package/packages/contracts/README.md +9 -9
  30. package/packages/contracts/index.d.ts +519 -517
  31. package/packages/contracts/index.js +312 -312
  32. package/packages/contracts/package.json +1 -1
  33. package/packages/core/README.md +8 -8
  34. package/packages/core/index.d.ts +76 -76
  35. package/packages/core/index.js +222 -222
  36. package/packages/core/package.json +2 -2
  37. package/packages/server/README.md +17 -17
  38. package/packages/server/bin/agent-terminal-server.js +42 -42
  39. package/packages/server/bin/claude-workbench.js +225 -225
  40. package/packages/server/index.d.ts +171 -171
  41. package/packages/server/index.js +230 -230
  42. package/packages/server/lib/agent-discovery.js +133 -133
  43. package/packages/server/lib/agent-registry.js +143 -143
  44. package/packages/server/lib/agent-spawn-env.js +17 -17
  45. package/packages/server/lib/attachment-store.js +78 -78
  46. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  47. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  48. package/packages/server/lib/capture-settings-store.js +35 -35
  49. package/packages/server/lib/claude-history-recovery.js +87 -87
  50. package/packages/server/lib/cli-session-recovery.js +28 -28
  51. package/packages/server/lib/context-resolver.js +55 -55
  52. package/packages/server/lib/conversation-capture.js +51 -51
  53. package/packages/server/lib/delegation-phase-store.js +357 -357
  54. package/packages/server/lib/delegation-store.js +474 -468
  55. package/packages/server/lib/diagnostics.js +32 -32
  56. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  57. package/packages/server/lib/drivers/codex-driver.js +1442 -1442
  58. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  59. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  60. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  61. package/packages/server/lib/event-stream.js +138 -138
  62. package/packages/server/lib/folder-picker.js +623 -623
  63. package/packages/server/lib/http-handler.js +339 -339
  64. package/packages/server/lib/json-file.js +26 -26
  65. package/packages/server/lib/knowledge-context.js +33 -33
  66. package/packages/server/lib/model-detection.js +295 -295
  67. package/packages/server/lib/parent-context.js +44 -0
  68. package/packages/server/lib/profile-store.js +283 -283
  69. package/packages/server/lib/project-store.js +102 -102
  70. package/packages/server/lib/runtime-loader.js +120 -120
  71. package/packages/server/lib/session-archive-store.js +53 -53
  72. package/packages/server/lib/session-history.js +612 -612
  73. package/packages/server/lib/system-doctor.js +125 -125
  74. package/packages/server/lib/ui-preferences-store.js +48 -48
  75. package/packages/server/lib/workbench-service.js +1696 -1675
  76. package/packages/server/package.json +2 -2
  77. package/packages/ui/README.md +30 -30
  78. package/packages/ui/ai-profile-settings.d.ts +45 -45
  79. package/packages/ui/ai-profile-settings.js +283 -283
  80. package/packages/ui/claude-terminal.d.ts +31 -31
  81. package/packages/ui/claude-terminal.js +343 -343
  82. package/packages/ui/index.d.ts +18 -18
  83. package/packages/ui/index.js +51 -51
  84. package/packages/ui/package.json +2 -2
  85. package/packages/ui/theme.css +13 -13
  86. package/src/assets/claude-prompts.json +21 -21
  87. package/src/backend/http/ai-profile-routes.js +61 -61
  88. package/src/backend/http/claude-routes.js +170 -170
  89. package/src/backend/http/node-helpers.js +45 -45
  90. package/src/backend/index.js +21 -21
  91. package/src/backend/lib/ai-adapter.js +144 -144
  92. package/src/backend/lib/ai-profile-store.js +259 -259
  93. package/src/backend/lib/ai-vendor-presets.js +83 -83
  94. package/src/backend/lib/ai-workspace.js +140 -140
  95. package/src/backend/lib/automation-config.js +253 -253
  96. package/src/backend/lib/claude-cli-runner.js +2392 -2392
  97. package/src/backend/lib/data-dir.js +169 -169
  98. package/src/backend/lib/llm-client.js +176 -176
  99. package/src/backend/lib/model-context-windows.js +82 -82
  100. package/src/backend/lib/native-task-control.js +217 -217
  101. package/src/backend/lib/native-task-history-index.js +475 -475
  102. package/src/backend/lib/native-task-history-reader.js +526 -526
  103. package/src/backend/lib/native-task-messages.js +457 -457
  104. package/src/backend/lib/native-task-registry.js +325 -325
  105. package/src/backend/lib/native-task-runtime.js +242 -242
  106. package/src/backend/lib/native-task-trace.js +440 -440
  107. package/src/backend/lib/native-task-transcript.js +393 -393
  108. package/src/backend/lib/prompt-registry.js +166 -166
  109. package/src/backend/lib/runtime-config.js +121 -121
  110. package/src/backend/lib/sdk-session-stream.js +143 -143
  111. package/src/backend/lib/sdk-turn-stream.js +16 -16
  112. package/src/backend/lib/store-scope.js +131 -131
  113. package/src/backend/lib/task-identity.js +104 -104
  114. package/src/backend/lib/task-state-projection.js +58 -58
  115. package/src/index.js +7 -7
  116. package/src/sdk-wave-a.ts +1 -1
  117. package/src/sdk-wave-b.ts +1 -1
  118. package/src/sdk-wave-c.ts +1 -1
  119. package/src/sdk-wave-d.ts +1 -1
  120. 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 };