@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +636 -613
- package/apps/agent-terminal/app.js +125 -65
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +171 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +187 -159
- package/apps/agent-terminal/right-workspace-dock.js +623 -519
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +13 -13
- package/docs/sdk/openapi.yaml +8 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1442 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2392 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- package/src/sdk-wave-e.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/core",
|
|
3
|
-
"version": "0.13.
|
|
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.
|
|
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
|
+
});
|