@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/core",
3
- "version": "0.13.2",
3
+ "version": "0.13.4",
4
4
  "type": "commonjs",
5
5
  "main": "index.js",
6
6
  "browser": "index.js",
@@ -20,7 +20,7 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@claude-ai-workbench/contracts": "0.13.2"
23
+ "@claude-ai-workbench/contracts": "0.13.4"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18"
@@ -1,17 +1,17 @@
1
- # @claude-ai-workbench/server
2
-
3
- 版本化 REST/SSE Server SDK,同时提供 `claude-workbench-server` 命令。
4
-
5
- ```js
6
- const { createWorkbenchServer } = require("@claude-ai-workbench/server");
7
- const app = createWorkbenchServer({ dataDir: "./workbench-data", serveHost: false });
8
- app.server.listen(5760, "127.0.0.1");
9
- ```
10
-
11
- 命令行启动:
12
-
13
- ```bash
14
- claude-workbench-server
15
- ```
16
-
17
- 默认只应监听本机地址。若要跨机器部署,应在外层增加 HTTPS、身份认证和访问控制。
1
+ # @claude-ai-workbench/server
2
+
3
+ 版本化 REST/SSE Server SDK,同时提供 `claude-workbench-server` 命令。
4
+
5
+ ```js
6
+ const { createWorkbenchServer } = require("@claude-ai-workbench/server");
7
+ const app = createWorkbenchServer({ dataDir: "./workbench-data", serveHost: false });
8
+ app.server.listen(5760, "127.0.0.1");
9
+ ```
10
+
11
+ 命令行启动:
12
+
13
+ ```bash
14
+ claude-workbench-server
15
+ ```
16
+
17
+ 默认只应监听本机地址。若要跨机器部署,应在外层增加 HTTPS、身份认证和访问控制。
@@ -1,42 +1,42 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- // Standalone Agent Terminal entry point. Fresh data directory (~/.agent-terminal
4
- // by default, AGENT_TERMINAL_DATA_DIR overrides) so the terminal product starts
5
- // independently from any legacy workbench data.
6
- const os = require('os');
7
- const path = require('path');
8
- const fs = require('fs');
9
- const { createWorkbenchServer } = require('..');
10
- const { warmFolderPicker } = require('../lib/folder-picker');
11
-
12
- const port = Number(process.env.AGENT_TERMINAL_PORT || process.env.CLAUDE_WORKBENCH_PORT || 5760);
13
- const host = process.env.AGENT_TERMINAL_HOST || '127.0.0.1';
14
- const dataDir = process.env.AGENT_TERMINAL_DATA_DIR || path.join(os.homedir(), '.agent-terminal');
15
- fs.mkdirSync(dataDir, { recursive: true });
16
- // The agent-session runner resolves its own store through these env vars;
17
- // without them it falls back to the legacy ~/.claude-ai-workbench layout.
18
- process.env.CLAUDE_AI_WORKBENCH_DATA_DIR = dataDir;
19
- process.env.KB_DATA_DIR = dataDir;
20
-
21
-
22
- const app = createWorkbenchServer({ dataDir });
23
- app.server.listen(port, host, () => {
24
- process.stdout.write(`Agent Terminal listening at http://${host}:${port}/agent-terminal/ (data: ${dataDir})\n`);
25
- // Compile the folder-picker helper up front so the first "browse…" click
26
- // pops the dialog in milliseconds instead of paying the powershell +
27
- // Add-Type startup. Never blocks or crashes the server.
28
- if (process.env.KB_WORKBENCH_DISABLE_PICKER !== '1') warmFolderPicker({ title: '选择项目文件夹' }).catch(() => {});
29
- });
30
-
31
- let stopping = false;
32
- async function shutdown() {
33
- if (stopping) return; stopping = true;
34
- setTimeout(() => process.exit(1), 10000).unref();
35
- try { await app.service.destroy(); }
36
- catch { process.stderr.write('SHUTDOWN_FAILED: 客户端运行环境未能完整关闭。\n'); process.exitCode = 1; }
37
- app.server.closeAllConnections?.();
38
- app.server.close(() => process.exit(process.exitCode || 0));
39
- }
40
- process.on('SIGINT', shutdown);
41
- process.on('SIGTERM', shutdown);
42
- process.on('message', message => { if (message?.type === 'kb:shutdown') shutdown(); });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Standalone Agent Terminal entry point. Fresh data directory (~/.agent-terminal
4
+ // by default, AGENT_TERMINAL_DATA_DIR overrides) so the terminal product starts
5
+ // independently from any legacy workbench data.
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const fs = require('fs');
9
+ const { createWorkbenchServer } = require('..');
10
+ const { warmFolderPicker } = require('../lib/folder-picker');
11
+
12
+ const port = Number(process.env.AGENT_TERMINAL_PORT || process.env.CLAUDE_WORKBENCH_PORT || 5760);
13
+ const host = process.env.AGENT_TERMINAL_HOST || '127.0.0.1';
14
+ const dataDir = process.env.AGENT_TERMINAL_DATA_DIR || path.join(os.homedir(), '.agent-terminal');
15
+ fs.mkdirSync(dataDir, { recursive: true });
16
+ // The agent-session runner resolves its own store through these env vars;
17
+ // without them it falls back to the legacy ~/.claude-ai-workbench layout.
18
+ process.env.CLAUDE_AI_WORKBENCH_DATA_DIR = dataDir;
19
+ process.env.KB_DATA_DIR = dataDir;
20
+
21
+
22
+ const app = createWorkbenchServer({ dataDir });
23
+ app.server.listen(port, host, () => {
24
+ process.stdout.write(`Agent Terminal listening at http://${host}:${port}/agent-terminal/ (data: ${dataDir})\n`);
25
+ // Compile the folder-picker helper up front so the first "browse…" click
26
+ // pops the dialog in milliseconds instead of paying the powershell +
27
+ // Add-Type startup. Never blocks or crashes the server.
28
+ if (process.env.KB_WORKBENCH_DISABLE_PICKER !== '1') warmFolderPicker({ title: '选择项目文件夹' }).catch(() => {});
29
+ });
30
+
31
+ let stopping = false;
32
+ async function shutdown() {
33
+ if (stopping) return; stopping = true;
34
+ setTimeout(() => process.exit(1), 10000).unref();
35
+ try { await app.service.destroy(); }
36
+ catch { process.stderr.write('SHUTDOWN_FAILED: 客户端运行环境未能完整关闭。\n'); process.exitCode = 1; }
37
+ app.server.closeAllConnections?.();
38
+ app.server.close(() => process.exit(process.exitCode || 0));
39
+ }
40
+ process.on('SIGINT', shutdown);
41
+ process.on('SIGTERM', shutdown);
42
+ process.on('message', message => { if (message?.type === 'kb:shutdown') shutdown(); });
@@ -1,225 +1,225 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- // Standalone CLI for the published claude-ai-workbench package:
5
- // claude-workbench start the server in the foreground
6
- // claude-workbench start [-d] start in the foreground, detached with -d
7
- // claude-workbench stop stop the running instance
8
- // claude-workbench status report whether an instance is running
9
- // claude-workbench -v print the package version
10
- // Wired up via the package.json bin field, so a global `npm i -g
11
- // claude-ai-workbench` puts the `claude-workbench` command on PATH.
12
-
13
- const { execFileSync, spawn } = require('child_process');
14
- const fs = require('fs');
15
- const net = require('net');
16
- const os = require('os');
17
- const path = require('path');
18
-
19
- const pkg = require(path.join(__dirname, '..', '..', '..', 'package.json'));
20
- // The standalone product is the Agent Terminal: current dark UI, its own
21
- // clean data directory — not the legacy workbench data at ~/.claude-ai-workbench.
22
- const serverScript = path.join(__dirname, 'agent-terminal-server.js');
23
- const DEFAULT_PORT = 5760;
24
-
25
- function dataDir() {
26
- return path.resolve(process.env.AGENT_TERMINAL_DATA_DIR || path.join(os.homedir(), '.agent-terminal'));
27
- }
28
- function pidFile() {
29
- return path.join(dataDir(), 'server.pid');
30
- }
31
- function port() {
32
- return Number(process.env.AGENT_TERMINAL_PORT || process.env.CLAUDE_WORKBENCH_PORT || DEFAULT_PORT);
33
- }
34
- function listenUrl() {
35
- const host = process.env.AGENT_TERMINAL_HOST || process.env.CLAUDE_WORKBENCH_HOST || '127.0.0.1';
36
- return `http://${host}:${port()}/agent-terminal/`;
37
- }
38
-
39
- function writePid(pid) {
40
- fs.mkdirSync(dataDir(), { recursive: true });
41
- fs.writeFileSync(pidFile(), String(pid), 'utf8');
42
- }
43
- function readPid() {
44
- try {
45
- const pid = Number(fs.readFileSync(pidFile(), 'utf8').trim());
46
- return Number.isInteger(pid) && pid > 0 ? pid : null;
47
- } catch {
48
- return null;
49
- }
50
- }
51
- function clearPid() {
52
- try { fs.unlinkSync(pidFile()); } catch { /* already gone */ }
53
- }
54
-
55
- function alive(pid) {
56
- if (process.platform === 'win32') {
57
- try {
58
- const out = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], {
59
- encoding: 'utf8', windowsHide: true, timeout: 10000,
60
- });
61
- return out.includes(`"${pid}"`);
62
- } catch {
63
- return false;
64
- }
65
- }
66
- try { process.kill(pid, 0); return true; } catch { return false; }
67
- }
68
-
69
- function killPid(pid) {
70
- if (process.platform === 'win32') {
71
- execFileSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore', timeout: 15000 });
72
- return;
73
- }
74
- process.kill(pid, 'SIGTERM');
75
- }
76
-
77
- // Fallback for instances started without this CLI (raw bin script, lost data
78
- // dir): whoever is LISTENING on the workbench port is the instance.
79
- function pidsListeningOnPort(portNum) {
80
- let out = '';
81
- try {
82
- out = execFileSync('netstat', ['-ano'], {
83
- encoding: 'utf8', windowsHide: true, timeout: 15000, maxBuffer: 16 * 1024 * 1024,
84
- });
85
- } catch {
86
- return [];
87
- }
88
- const pids = new Set();
89
- for (const line of out.split(/\r?\n/)) {
90
- if (!/LISTENING/i.test(line)) continue;
91
- const columns = line.trim().split(/\s+/);
92
- if (!String(columns[1] || '').endsWith(`:${portNum}`)) continue;
93
- const pid = Number(columns[columns.length - 1]);
94
- if (Number.isInteger(pid) && pid > 0) pids.add(pid);
95
- }
96
- return [...pids];
97
- }
98
-
99
- function stopTargets() {
100
- const pid = readPid();
101
- if (pid && alive(pid)) return [pid];
102
- return pidsListeningOnPort(port());
103
- }
104
-
105
- // True when something already listens on the workbench port. Checked before
106
- // every start so a busy port surfaces as guidance instead of the server's
107
- // raw EADDRINUSE stack trace.
108
- function probePortInUse(portNum, host) {
109
- return new Promise((resolve) => {
110
- const server = net.createServer();
111
- server.once('error', () => resolve(true));
112
- server.once('listening', () => server.close(() => resolve(false)));
113
- server.listen(portNum, host);
114
- });
115
- }
116
-
117
- async function cmdStart(argv) {
118
- const daemon = argv.includes('-d') || argv.includes('--daemon');
119
- const existing = readPid();
120
- if (existing && alive(existing)) {
121
- process.stdout.write(`claude-workbench is already running (pid ${existing}) at ${listenUrl()}\n`);
122
- return 0;
123
- }
124
- const host = process.env.CLAUDE_WORKBENCH_HOST || '127.0.0.1';
125
- if (await probePortInUse(port(), host)) {
126
- const holders = pidsListeningOnPort(port());
127
- process.stderr.write(
128
- `port ${port()} is already in use${holders.length ? ` (pid ${holders.join(', ')})` : ''} — `
129
- + `another instance may be running there.\n`
130
- + `Try: claude-workbench stop, or pick another port via CLAUDE_WORKBENCH_PORT.\n`);
131
- return 1;
132
- }
133
- if (daemon) {
134
- const child = spawn(process.execPath, [serverScript], { detached: true, stdio: 'ignore', windowsHide: true });
135
- child.unref();
136
- writePid(child.pid);
137
- process.stdout.write(`claude-workbench ${pkg.version} started in the background (pid ${child.pid}) at ${listenUrl()}\n`);
138
- return 0;
139
- }
140
- writePid(process.pid);
141
- process.on('exit', clearPid);
142
- process.stderr.write(`claude-workbench ${pkg.version} starting at ${listenUrl()} (Ctrl+C stops)\n`);
143
- require(serverScript); // constructs and listens in-process; owns the lifetime
144
- return null;
145
- }
146
-
147
- function cmdStop() {
148
- const targets = stopTargets();
149
- if (!targets.length) {
150
- clearPid(); // stale pid file from a crashed instance
151
- process.stdout.write(`claude-workbench is not running (port ${port()})\n`);
152
- return 1;
153
- }
154
- let failed = false;
155
- for (const pid of targets) {
156
- try {
157
- killPid(pid);
158
- process.stdout.write(`stopped pid ${pid}\n`);
159
- } catch (error) {
160
- failed = true;
161
- process.stderr.write(`failed to stop pid ${pid}: ${error.message}\n`);
162
- }
163
- }
164
- clearPid();
165
- return failed ? 1 : 0;
166
- }
167
-
168
- function cmdStatus() {
169
- const pid = readPid();
170
- if (pid && alive(pid)) {
171
- process.stdout.write(`claude-workbench is running (pid ${pid}) at ${listenUrl()}\n`);
172
- return 0;
173
- }
174
- const portPids = pidsListeningOnPort(port());
175
- if (portPids.length) {
176
- process.stdout.write(`claude-workbench is running without a pid file (pid ${portPids.join(', ')}) at ${listenUrl()}\n`);
177
- return 0;
178
- }
179
- clearPid();
180
- process.stdout.write(`claude-workbench is not running\n`);
181
- return 1;
182
- }
183
-
184
- function cmdVersion() {
185
- process.stdout.write(`${pkg.version}\n`);
186
- return 0;
187
- }
188
-
189
- function help() {
190
- return [
191
- `claude-ai-workbench ${pkg.version} — local workbench server for Claude Code, Codex, OpenCode`,
192
- '',
193
- 'usage: claude-workbench [command]',
194
- '',
195
- 'commands:',
196
- ' start [-d|--daemon] start the server (foreground by default, background with -d)',
197
- ' stop stop the running server',
198
- ' status show whether the server is running',
199
- ' -v | --version print the version',
200
- ' -h | --help show this help',
201
- '',
202
- 'environment:',
203
- ` AGENT_TERMINAL_PORT port to listen on (default ${DEFAULT_PORT}; CLAUDE_WORKBENCH_PORT also honored)`,
204
- ' AGENT_TERMINAL_HOST bind address (default 127.0.0.1)',
205
- ' AGENT_TERMINAL_DATA_DIR data directory (default ~/.agent-terminal)',
206
- ].join('\n');
207
- }
208
-
209
- async function main(argv) {
210
- const command = argv[0];
211
- if (command === undefined || command === 'start') return cmdStart(argv.slice(1));
212
- if (command === 'stop') return cmdStop();
213
- if (command === 'status') return cmdStatus();
214
- if (command === 'version' || command === '-v' || command === '--version') return cmdVersion();
215
- if (command === '-h' || command === '--help' || command === 'help') {
216
- process.stdout.write(`${help()}\n`);
217
- return 0;
218
- }
219
- process.stderr.write(`unknown command: ${command}\n\n${help()}\n`);
220
- return 1;
221
- }
222
-
223
- main(process.argv.slice(2)).then((exitCode) => {
224
- if (exitCode !== null && exitCode !== undefined) process.exitCode = exitCode;
225
- });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Standalone CLI for the published claude-ai-workbench package:
5
+ // claude-workbench start the server in the foreground
6
+ // claude-workbench start [-d] start in the foreground, detached with -d
7
+ // claude-workbench stop stop the running instance
8
+ // claude-workbench status report whether an instance is running
9
+ // claude-workbench -v print the package version
10
+ // Wired up via the package.json bin field, so a global `npm i -g
11
+ // claude-ai-workbench` puts the `claude-workbench` command on PATH.
12
+
13
+ const { execFileSync, spawn } = require('child_process');
14
+ const fs = require('fs');
15
+ const net = require('net');
16
+ const os = require('os');
17
+ const path = require('path');
18
+
19
+ const pkg = require(path.join(__dirname, '..', '..', '..', 'package.json'));
20
+ // The standalone product is the Agent Terminal: current dark UI, its own
21
+ // clean data directory — not the legacy workbench data at ~/.claude-ai-workbench.
22
+ const serverScript = path.join(__dirname, 'agent-terminal-server.js');
23
+ const DEFAULT_PORT = 5760;
24
+
25
+ function dataDir() {
26
+ return path.resolve(process.env.AGENT_TERMINAL_DATA_DIR || path.join(os.homedir(), '.agent-terminal'));
27
+ }
28
+ function pidFile() {
29
+ return path.join(dataDir(), 'server.pid');
30
+ }
31
+ function port() {
32
+ return Number(process.env.AGENT_TERMINAL_PORT || process.env.CLAUDE_WORKBENCH_PORT || DEFAULT_PORT);
33
+ }
34
+ function listenUrl() {
35
+ const host = process.env.AGENT_TERMINAL_HOST || process.env.CLAUDE_WORKBENCH_HOST || '127.0.0.1';
36
+ return `http://${host}:${port()}/agent-terminal/`;
37
+ }
38
+
39
+ function writePid(pid) {
40
+ fs.mkdirSync(dataDir(), { recursive: true });
41
+ fs.writeFileSync(pidFile(), String(pid), 'utf8');
42
+ }
43
+ function readPid() {
44
+ try {
45
+ const pid = Number(fs.readFileSync(pidFile(), 'utf8').trim());
46
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function clearPid() {
52
+ try { fs.unlinkSync(pidFile()); } catch { /* already gone */ }
53
+ }
54
+
55
+ function alive(pid) {
56
+ if (process.platform === 'win32') {
57
+ try {
58
+ const out = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'], {
59
+ encoding: 'utf8', windowsHide: true, timeout: 10000,
60
+ });
61
+ return out.includes(`"${pid}"`);
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+ try { process.kill(pid, 0); return true; } catch { return false; }
67
+ }
68
+
69
+ function killPid(pid) {
70
+ if (process.platform === 'win32') {
71
+ execFileSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore', timeout: 15000 });
72
+ return;
73
+ }
74
+ process.kill(pid, 'SIGTERM');
75
+ }
76
+
77
+ // Fallback for instances started without this CLI (raw bin script, lost data
78
+ // dir): whoever is LISTENING on the workbench port is the instance.
79
+ function pidsListeningOnPort(portNum) {
80
+ let out = '';
81
+ try {
82
+ out = execFileSync('netstat', ['-ano'], {
83
+ encoding: 'utf8', windowsHide: true, timeout: 15000, maxBuffer: 16 * 1024 * 1024,
84
+ });
85
+ } catch {
86
+ return [];
87
+ }
88
+ const pids = new Set();
89
+ for (const line of out.split(/\r?\n/)) {
90
+ if (!/LISTENING/i.test(line)) continue;
91
+ const columns = line.trim().split(/\s+/);
92
+ if (!String(columns[1] || '').endsWith(`:${portNum}`)) continue;
93
+ const pid = Number(columns[columns.length - 1]);
94
+ if (Number.isInteger(pid) && pid > 0) pids.add(pid);
95
+ }
96
+ return [...pids];
97
+ }
98
+
99
+ function stopTargets() {
100
+ const pid = readPid();
101
+ if (pid && alive(pid)) return [pid];
102
+ return pidsListeningOnPort(port());
103
+ }
104
+
105
+ // True when something already listens on the workbench port. Checked before
106
+ // every start so a busy port surfaces as guidance instead of the server's
107
+ // raw EADDRINUSE stack trace.
108
+ function probePortInUse(portNum, host) {
109
+ return new Promise((resolve) => {
110
+ const server = net.createServer();
111
+ server.once('error', () => resolve(true));
112
+ server.once('listening', () => server.close(() => resolve(false)));
113
+ server.listen(portNum, host);
114
+ });
115
+ }
116
+
117
+ async function cmdStart(argv) {
118
+ const daemon = argv.includes('-d') || argv.includes('--daemon');
119
+ const existing = readPid();
120
+ if (existing && alive(existing)) {
121
+ process.stdout.write(`claude-workbench is already running (pid ${existing}) at ${listenUrl()}\n`);
122
+ return 0;
123
+ }
124
+ const host = process.env.CLAUDE_WORKBENCH_HOST || '127.0.0.1';
125
+ if (await probePortInUse(port(), host)) {
126
+ const holders = pidsListeningOnPort(port());
127
+ process.stderr.write(
128
+ `port ${port()} is already in use${holders.length ? ` (pid ${holders.join(', ')})` : ''} — `
129
+ + `another instance may be running there.\n`
130
+ + `Try: claude-workbench stop, or pick another port via CLAUDE_WORKBENCH_PORT.\n`);
131
+ return 1;
132
+ }
133
+ if (daemon) {
134
+ const child = spawn(process.execPath, [serverScript], { detached: true, stdio: 'ignore', windowsHide: true });
135
+ child.unref();
136
+ writePid(child.pid);
137
+ process.stdout.write(`claude-workbench ${pkg.version} started in the background (pid ${child.pid}) at ${listenUrl()}\n`);
138
+ return 0;
139
+ }
140
+ writePid(process.pid);
141
+ process.on('exit', clearPid);
142
+ process.stderr.write(`claude-workbench ${pkg.version} starting at ${listenUrl()} (Ctrl+C stops)\n`);
143
+ require(serverScript); // constructs and listens in-process; owns the lifetime
144
+ return null;
145
+ }
146
+
147
+ function cmdStop() {
148
+ const targets = stopTargets();
149
+ if (!targets.length) {
150
+ clearPid(); // stale pid file from a crashed instance
151
+ process.stdout.write(`claude-workbench is not running (port ${port()})\n`);
152
+ return 1;
153
+ }
154
+ let failed = false;
155
+ for (const pid of targets) {
156
+ try {
157
+ killPid(pid);
158
+ process.stdout.write(`stopped pid ${pid}\n`);
159
+ } catch (error) {
160
+ failed = true;
161
+ process.stderr.write(`failed to stop pid ${pid}: ${error.message}\n`);
162
+ }
163
+ }
164
+ clearPid();
165
+ return failed ? 1 : 0;
166
+ }
167
+
168
+ function cmdStatus() {
169
+ const pid = readPid();
170
+ if (pid && alive(pid)) {
171
+ process.stdout.write(`claude-workbench is running (pid ${pid}) at ${listenUrl()}\n`);
172
+ return 0;
173
+ }
174
+ const portPids = pidsListeningOnPort(port());
175
+ if (portPids.length) {
176
+ process.stdout.write(`claude-workbench is running without a pid file (pid ${portPids.join(', ')}) at ${listenUrl()}\n`);
177
+ return 0;
178
+ }
179
+ clearPid();
180
+ process.stdout.write(`claude-workbench is not running\n`);
181
+ return 1;
182
+ }
183
+
184
+ function cmdVersion() {
185
+ process.stdout.write(`${pkg.version}\n`);
186
+ return 0;
187
+ }
188
+
189
+ function help() {
190
+ return [
191
+ `claude-ai-workbench ${pkg.version} — local workbench server for Claude Code, Codex, OpenCode`,
192
+ '',
193
+ 'usage: claude-workbench [command]',
194
+ '',
195
+ 'commands:',
196
+ ' start [-d|--daemon] start the server (foreground by default, background with -d)',
197
+ ' stop stop the running server',
198
+ ' status show whether the server is running',
199
+ ' -v | --version print the version',
200
+ ' -h | --help show this help',
201
+ '',
202
+ 'environment:',
203
+ ` AGENT_TERMINAL_PORT port to listen on (default ${DEFAULT_PORT}; CLAUDE_WORKBENCH_PORT also honored)`,
204
+ ' AGENT_TERMINAL_HOST bind address (default 127.0.0.1)',
205
+ ' AGENT_TERMINAL_DATA_DIR data directory (default ~/.agent-terminal)',
206
+ ].join('\n');
207
+ }
208
+
209
+ async function main(argv) {
210
+ const command = argv[0];
211
+ if (command === undefined || command === 'start') return cmdStart(argv.slice(1));
212
+ if (command === 'stop') return cmdStop();
213
+ if (command === 'status') return cmdStatus();
214
+ if (command === 'version' || command === '-v' || command === '--version') return cmdVersion();
215
+ if (command === '-h' || command === '--help' || command === 'help') {
216
+ process.stdout.write(`${help()}\n`);
217
+ return 0;
218
+ }
219
+ process.stderr.write(`unknown command: ${command}\n\n${help()}\n`);
220
+ return 1;
221
+ }
222
+
223
+ main(process.argv.slice(2)).then((exitCode) => {
224
+ if (exitCode !== null && exitCode !== undefined) process.exitCode = exitCode;
225
+ });