@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-ai-workbench/core",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
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.3"
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
+ });