claude-ws 0.3.98 → 0.3.100

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 (100) hide show
  1. package/bin/claude-ws.js +36 -3
  2. package/bin/lib/cli-parser.js +109 -0
  3. package/bin/lib/commands/logs.js +73 -0
  4. package/bin/lib/commands/open.js +49 -0
  5. package/bin/lib/commands/start.js +73 -0
  6. package/bin/lib/commands/status.js +35 -0
  7. package/bin/lib/commands/stop.js +50 -0
  8. package/bin/lib/config.js +93 -0
  9. package/bin/lib/daemon.js +167 -0
  10. package/bin/lib/health.js +62 -0
  11. package/locales/de.json +376 -12
  12. package/locales/en.json +376 -12
  13. package/locales/es.json +400 -11
  14. package/locales/fr.json +400 -11
  15. package/locales/ja.json +400 -11
  16. package/locales/ko.json +400 -11
  17. package/locales/vi.json +376 -12
  18. package/locales/zh.json +400 -11
  19. package/package.json +1 -1
  20. package/server.ts +283 -6
  21. package/src/app/[locale]/not-found.tsx +6 -3
  22. package/src/app/[locale]/page.tsx +14 -4
  23. package/src/app/api/attempts/[id]/workflow/route.ts +76 -0
  24. package/src/app/api/questions/answer/route.ts +58 -0
  25. package/src/app/api/questions/route.ts +68 -0
  26. package/src/app/api/tasks/[id]/compact/route.ts +62 -0
  27. package/src/app/api/tasks/route.ts +27 -25
  28. package/src/components/access-anywhere/api-access-key-setup-modal.tsx +2 -2
  29. package/src/components/access-anywhere/tunnel-settings-dialog.tsx +6 -6
  30. package/src/components/access-anywhere/wizard-step-ctunnel.tsx +8 -8
  31. package/src/components/agent-factory/dependency-tree.tsx +5 -3
  32. package/src/components/agent-factory/discovery-dialog.tsx +26 -22
  33. package/src/components/agent-factory/plugin-detail-dialog.tsx +41 -38
  34. package/src/components/agent-factory/plugin-form-dialog.tsx +23 -20
  35. package/src/components/agent-factory/plugin-list.tsx +20 -17
  36. package/src/components/agent-factory/upload-dialog.tsx +17 -14
  37. package/src/components/auth/agent-provider-dialog.tsx +67 -65
  38. package/src/components/auth/api-key-dialog.tsx +14 -11
  39. package/src/components/auth/auth-error-message.tsx +6 -3
  40. package/src/components/editor/code-editor-with-inline-edit.tsx +4 -2
  41. package/src/components/editor/file-diff-resolver-modal.tsx +31 -26
  42. package/src/components/editor/inline-edit-dialog.tsx +9 -6
  43. package/src/components/editor/selection-mention-popup.tsx +3 -1
  44. package/src/components/header/project-selector.tsx +7 -4
  45. package/src/components/header.tsx +70 -4
  46. package/src/components/kanban/board.tsx +66 -18
  47. package/src/components/kanban/column.tsx +11 -0
  48. package/src/components/kanban/task-card.tsx +70 -4
  49. package/src/components/project-settings/component-selector.tsx +3 -1
  50. package/src/components/project-settings/plugin-upload-dialog.tsx +7 -5
  51. package/src/components/project-settings/project-settings-dialog.tsx +5 -3
  52. package/src/components/questions/questions-panel.tsx +136 -0
  53. package/src/components/settings/folder-browser-dialog.tsx +29 -25
  54. package/src/components/settings/settings-page.tsx +64 -18
  55. package/src/components/settings/setup-dialog.tsx +26 -23
  56. package/src/components/setup/unified-setup-wizard.tsx +12 -9
  57. package/src/components/sidebar/file-browser/file-create-buttons.tsx +7 -3
  58. package/src/components/sidebar/file-browser/file-tab-content.tsx +19 -15
  59. package/src/components/sidebar/file-browser/file-tabs-panel.tsx +7 -4
  60. package/src/components/sidebar/file-browser/file-tree.tsx +3 -1
  61. package/src/components/sidebar/git-changes/branch-checkout-modal.tsx +6 -4
  62. package/src/components/sidebar/git-changes/commit-details-modal.tsx +5 -3
  63. package/src/components/sidebar/git-changes/diff-tabs-panel.tsx +3 -1
  64. package/src/components/sidebar/git-changes/git-file-item.tsx +8 -6
  65. package/src/components/sidebar/git-changes/git-graph.tsx +8 -5
  66. package/src/components/sidebar/git-changes/git-panel.tsx +28 -27
  67. package/src/components/sidebar/git-changes/git-section.tsx +5 -3
  68. package/src/components/sidebar/shells/shell-panel.tsx +3 -1
  69. package/src/components/task/attachment-bar.tsx +4 -1
  70. package/src/components/task/attempt-item.tsx +7 -5
  71. package/src/components/task/conversation-view.tsx +21 -13
  72. package/src/components/task/floating-chat-window.tsx +14 -5
  73. package/src/components/task/interactive-command/checkpoint-list.tsx +5 -3
  74. package/src/components/task/interactive-command/confirm-dialog.tsx +9 -4
  75. package/src/components/task/interactive-command/interactive-command-overlay.tsx +23 -9
  76. package/src/components/task/interactive-command/question-prompt.tsx +12 -8
  77. package/src/components/task/pending-question-indicator.tsx +5 -3
  78. package/src/components/task/prompt-input.tsx +1 -1
  79. package/src/components/task/shell-log-view.tsx +3 -1
  80. package/src/components/task/status-line.tsx +84 -23
  81. package/src/components/task/task-detail-panel.tsx +83 -28
  82. package/src/components/task/task-shell-indicator.tsx +10 -6
  83. package/src/components/terminal/terminal-context-menu.tsx +6 -4
  84. package/src/components/terminal/terminal-instance.tsx +11 -3
  85. package/src/components/terminal/terminal-panel.tsx +6 -3
  86. package/src/components/terminal/terminal-shortcut-bar.tsx +3 -1
  87. package/src/components/terminal/terminal-tab-bar.tsx +5 -3
  88. package/src/components/workflow/workflow-panel.tsx +181 -0
  89. package/src/hooks/use-attempt-stream.ts +96 -3
  90. package/src/lib/agent-manager.ts +88 -2
  91. package/src/lib/db/index.ts +18 -0
  92. package/src/lib/db/schema.ts +29 -0
  93. package/src/lib/process-manager.ts +28 -7
  94. package/src/lib/session-manager.ts +60 -0
  95. package/src/lib/usage-tracker.ts +19 -19
  96. package/src/lib/workflow-tracker.ts +118 -20
  97. package/src/stores/panel-layout-store.ts +12 -1
  98. package/src/stores/questions-store.ts +76 -0
  99. package/src/stores/task-store.ts +34 -0
  100. package/src/stores/workflow-store.ts +71 -0
package/bin/claude-ws.js CHANGED
@@ -14,6 +14,18 @@ const path = require('path');
14
14
  const fs = require('fs');
15
15
  const os = require('os');
16
16
 
17
+ // ── Subcommand detection ──────────────────────────────────────────────
18
+ // If the first positional arg is a known subcommand, delegate and exit.
19
+ // Otherwise, fall through to the existing foreground startup logic.
20
+ const SUBCOMMANDS = ['start', 'stop', 'status', 'logs', 'open'];
21
+ const _firstArg = process.argv[2];
22
+ if (SUBCOMMANDS.includes(_firstArg)) {
23
+ require(`./lib/commands/${_firstArg}`).run(process.argv.slice(3));
24
+ // The command handles process.exit — nothing else to do here.
25
+ return;
26
+ }
27
+ // ── End subcommand detection ──────────────────────────────────────────
28
+
17
29
  const isWindows = process.platform === 'win32';
18
30
 
19
31
  /**
@@ -59,7 +71,23 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
59
71
  Claude Workspace - Visual workspace for Claude Code
60
72
 
61
73
  Usage:
62
- claude-ws [options]
74
+ claude-ws [options] Start server in foreground (blocks terminal)
75
+ claude-ws <command> [flags] Daemon management
76
+
77
+ Commands:
78
+ start Start as background daemon
79
+ --port, -p <port> Server port (default: 8556)
80
+ --host <host> Bind host (default: localhost)
81
+ --data-dir <dir> Data directory
82
+ --log-dir <dir> Log directory
83
+ --no-open Don't open browser after start
84
+ stop Stop the running daemon
85
+ status Show daemon PID, URL, and health
86
+ logs Tail daemon log files
87
+ -f, --follow Follow log output
88
+ -n, --lines <N> Number of lines (default: 50)
89
+ -e, --error Show error log instead
90
+ open Open browser to running instance
63
91
 
64
92
  Options:
65
93
  -v, --version Show version number
@@ -68,10 +96,15 @@ Options:
68
96
  Environment:
69
97
  .env: Loaded from current working directory (./.env)
70
98
  Database: Stored in ./data/claude-ws.db (or DATA_DIR env)
99
+ Config: ~/.claude-ws/config.json (port, host, dataDir, logDir)
71
100
 
72
101
  Examples:
73
- claude-ws Start server using .env from current directory
74
- cd ~/myproject && claude-ws Use ~/myproject/.env and database
102
+ claude-ws Start server in foreground
103
+ claude-ws start Start as daemon
104
+ claude-ws start --port 3000 Start daemon on port 3000
105
+ claude-ws status Check if daemon is running
106
+ claude-ws logs -f Follow daemon logs
107
+ claude-ws stop Stop the daemon
75
108
 
76
109
  For more info: https://github.com/Claude-Workspace/claude-ws
77
110
  `);
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Lightweight CLI argument parser.
3
+ * Zero dependencies - Node.js built-ins only.
4
+ *
5
+ * Supports:
6
+ * --port 3000, --port=3000
7
+ * --no-open (boolean negation)
8
+ * -f (short flags)
9
+ * -n 50 (short flags with values)
10
+ */
11
+
12
+ /**
13
+ * @param {string[]} argv - process.argv.slice(N) after subcommand
14
+ * @param {{ flags: Record<string, { type: 'string'|'boolean', alias?: string, default?: any }> }} schema
15
+ * @returns {{ flags: Record<string, any>, args: string[] }}
16
+ */
17
+ function parse(argv, schema) {
18
+ const flags = {};
19
+ const args = [];
20
+ const defs = schema.flags || {};
21
+
22
+ // Set defaults
23
+ for (const [key, def] of Object.entries(defs)) {
24
+ if (def.default !== undefined) {
25
+ flags[key] = def.default;
26
+ } else {
27
+ flags[key] = def.type === 'boolean' ? false : undefined;
28
+ }
29
+ }
30
+
31
+ // Build alias map: alias -> canonical name
32
+ const aliasMap = {};
33
+ for (const [key, def] of Object.entries(defs)) {
34
+ if (def.alias) {
35
+ aliasMap[def.alias] = key;
36
+ }
37
+ }
38
+
39
+ let i = 0;
40
+ while (i < argv.length) {
41
+ const arg = argv[i];
42
+
43
+ if (arg === '--') {
44
+ args.push(...argv.slice(i + 1));
45
+ break;
46
+ }
47
+
48
+ // --no-<flag> boolean negation
49
+ if (arg.startsWith('--no-')) {
50
+ const name = arg.slice(5);
51
+ if (defs[name] && defs[name].type === 'boolean') {
52
+ flags[name] = false;
53
+ i++;
54
+ continue;
55
+ }
56
+ }
57
+
58
+ // --flag=value
59
+ if (arg.startsWith('--') && arg.includes('=')) {
60
+ const eqIdx = arg.indexOf('=');
61
+ const name = arg.slice(2, eqIdx);
62
+ const value = arg.slice(eqIdx + 1);
63
+ if (defs[name]) {
64
+ flags[name] = defs[name].type === 'boolean' ? value !== 'false' : value;
65
+ }
66
+ i++;
67
+ continue;
68
+ }
69
+
70
+ // --flag [value]
71
+ if (arg.startsWith('--')) {
72
+ const name = arg.slice(2);
73
+ if (defs[name]) {
74
+ if (defs[name].type === 'boolean') {
75
+ flags[name] = true;
76
+ i++;
77
+ } else {
78
+ flags[name] = argv[i + 1];
79
+ i += 2;
80
+ }
81
+ continue;
82
+ }
83
+ }
84
+
85
+ // -f or -f value (short alias)
86
+ if (arg.startsWith('-') && !arg.startsWith('--') && arg.length === 2) {
87
+ const alias = arg.slice(1);
88
+ const canonical = aliasMap[alias];
89
+ if (canonical && defs[canonical]) {
90
+ if (defs[canonical].type === 'boolean') {
91
+ flags[canonical] = true;
92
+ i++;
93
+ } else {
94
+ flags[canonical] = argv[i + 1];
95
+ i += 2;
96
+ }
97
+ continue;
98
+ }
99
+ }
100
+
101
+ // Positional argument
102
+ args.push(arg);
103
+ i++;
104
+ }
105
+
106
+ return { flags, args };
107
+ }
108
+
109
+ module.exports = { parse };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `claude-ws logs` — Tail daemon log files.
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { spawn } = require('child_process');
8
+ const { parse } = require('../cli-parser');
9
+ const config = require('../config');
10
+
11
+ const FLAG_SCHEMA = {
12
+ flags: {
13
+ follow: { type: 'boolean', alias: 'f', default: false },
14
+ lines: { type: 'string', alias: 'n', default: '50' },
15
+ error: { type: 'boolean', alias: 'e', default: false },
16
+ },
17
+ };
18
+
19
+ function run(argv) {
20
+ const { flags } = parse(argv, FLAG_SCHEMA);
21
+ const conf = config.resolve({});
22
+
23
+ const logFile = flags.error
24
+ ? path.join(conf.logDir, 'claude-ws-error.log')
25
+ : path.join(conf.logDir, 'claude-ws.log');
26
+
27
+ if (!fs.existsSync(logFile)) {
28
+ console.log(`[claude-ws] No log file found at ${logFile}`);
29
+ console.log('[claude-ws] Has the daemon been started? Try: claude-ws start');
30
+ process.exit(1);
31
+ }
32
+
33
+ const tailArgs = [];
34
+ tailArgs.push('-n', flags.lines);
35
+ if (flags.follow) {
36
+ tailArgs.push('-f');
37
+ }
38
+ tailArgs.push(logFile);
39
+
40
+ console.log(`[claude-ws] ${flags.error ? 'Error log' : 'Log'}: ${logFile}`);
41
+ console.log('---');
42
+
43
+ const tail = spawn('tail', tailArgs, { stdio: 'inherit' });
44
+
45
+ tail.on('error', (err) => {
46
+ // tail not available (Windows) — fall back to reading file
47
+ if (err.code === 'ENOENT') {
48
+ const content = fs.readFileSync(logFile, 'utf-8');
49
+ const lines = content.split('\n');
50
+ const n = parseInt(flags.lines, 10) || 50;
51
+ const tail = lines.slice(-n);
52
+ console.log(tail.join('\n'));
53
+
54
+ if (flags.follow) {
55
+ console.log('[claude-ws] -f (follow) is not supported on this platform.');
56
+ }
57
+ } else {
58
+ console.error(`[claude-ws] Error: ${err.message}`);
59
+ }
60
+ process.exit(1);
61
+ });
62
+
63
+ tail.on('close', (code) => {
64
+ process.exit(code || 0);
65
+ });
66
+
67
+ // Forward Ctrl+C to tail
68
+ process.on('SIGINT', () => {
69
+ tail.kill('SIGINT');
70
+ });
71
+ }
72
+
73
+ module.exports = { run };
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `claude-ws open` — Open browser to the running instance.
3
+ */
4
+
5
+ const { exec } = require('child_process');
6
+ const config = require('../config');
7
+ const daemon = require('../daemon');
8
+
9
+ /**
10
+ * Open a URL in the default browser (cross-platform).
11
+ * @param {string} url
12
+ */
13
+ function openUrl(url) {
14
+ const platform = process.platform;
15
+ let cmd;
16
+ if (platform === 'darwin') {
17
+ cmd = `open "${url}"`;
18
+ } else if (platform === 'win32') {
19
+ cmd = `start "" "${url}"`;
20
+ } else {
21
+ cmd = `xdg-open "${url}"`;
22
+ }
23
+
24
+ exec(cmd, (err) => {
25
+ if (err) {
26
+ console.log(`[claude-ws] Could not open browser. Visit: ${url}`);
27
+ }
28
+ });
29
+ }
30
+
31
+ async function run(_argv) {
32
+ const { running, pid } = daemon.checkRunning();
33
+
34
+ if (!running) {
35
+ console.log('[claude-ws] No running daemon found.');
36
+ console.log('[claude-ws] Start one first: claude-ws start');
37
+ process.exit(1);
38
+ }
39
+
40
+ const conf = config.resolve({});
41
+ const url = `http://${conf.host}:${conf.port}`;
42
+
43
+ console.log(`[claude-ws] Opening ${url} (PID ${pid})`);
44
+ openUrl(url);
45
+
46
+ process.exit(0);
47
+ }
48
+
49
+ module.exports = { run, openUrl };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `claude-ws start` — Start claude-ws as a background daemon.
3
+ */
4
+
5
+ const { parse } = require('../cli-parser');
6
+ const config = require('../config');
7
+ const daemon = require('../daemon');
8
+ const health = require('../health');
9
+
10
+ const FLAG_SCHEMA = {
11
+ flags: {
12
+ port: { type: 'string', alias: 'p' },
13
+ host: { type: 'string' },
14
+ 'data-dir': { type: 'string' },
15
+ 'log-dir': { type: 'string' },
16
+ 'no-open': { type: 'boolean' },
17
+ },
18
+ };
19
+
20
+ async function run(argv) {
21
+ const { flags } = parse(argv, FLAG_SCHEMA);
22
+ const conf = config.resolve(flags);
23
+
24
+ // Check if already running
25
+ const { running, pid } = daemon.checkRunning();
26
+ if (running) {
27
+ console.log(`[claude-ws] Already running (PID ${pid})`);
28
+ console.log(`[claude-ws] URL: http://${conf.host}:${conf.port}`);
29
+ console.log('[claude-ws] Use "claude-ws stop" to stop it first.');
30
+ process.exit(1);
31
+ }
32
+
33
+ console.log('[claude-ws] Starting daemon...');
34
+ console.log(`[claude-ws] Port: ${conf.port}`);
35
+ console.log(`[claude-ws] Host: ${conf.host}`);
36
+ console.log(`[claude-ws] Data: ${conf.dataDir}`);
37
+ console.log(`[claude-ws] Logs: ${conf.logDir}`);
38
+
39
+ const childPid = daemon.daemonize({
40
+ port: conf.port,
41
+ host: conf.host,
42
+ dataDir: conf.dataDir,
43
+ logDir: conf.logDir,
44
+ noOpen: flags['no-open'],
45
+ });
46
+
47
+ console.log(`[claude-ws] Daemon started (PID ${childPid})`);
48
+ console.log('[claude-ws] Waiting for server to become ready...');
49
+
50
+ const ready = await health.waitUntilReady(conf.host, conf.port, 60000, 2000);
51
+
52
+ if (ready) {
53
+ console.log(`[claude-ws] Server is ready at http://${conf.host}:${conf.port}`);
54
+
55
+ // Open browser unless --no-open
56
+ if (!flags['no-open']) {
57
+ try {
58
+ const openCmd = require('../commands/open');
59
+ openCmd.openUrl(`http://${conf.host}:${conf.port}`);
60
+ } catch {
61
+ // Non-critical — don't fail the start
62
+ }
63
+ }
64
+ } else {
65
+ console.log('[claude-ws] Server did not respond within 60s.');
66
+ console.log('[claude-ws] Check logs: claude-ws logs');
67
+ console.log('[claude-ws] The daemon may still be starting up (building, installing deps, etc.).');
68
+ }
69
+
70
+ process.exit(0);
71
+ }
72
+
73
+ module.exports = { run };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `claude-ws status` — Show daemon status, PID, URL, and health.
3
+ */
4
+
5
+ const config = require('../config');
6
+ const daemon = require('../daemon');
7
+ const health = require('../health');
8
+
9
+ async function run(_argv) {
10
+ const { running, pid } = daemon.checkRunning();
11
+
12
+ if (!running) {
13
+ console.log('[claude-ws] Status: NOT RUNNING');
14
+ process.exit(0);
15
+ }
16
+
17
+ const conf = config.resolve({});
18
+ const result = await health.check(conf.host, conf.port);
19
+
20
+ console.log('[claude-ws] Status: RUNNING');
21
+ console.log(`[claude-ws] PID: ${pid}`);
22
+ console.log(`[claude-ws] URL: http://${conf.host}:${conf.port}`);
23
+
24
+ if (result.ok) {
25
+ console.log(`[claude-ws] Health: OK (HTTP ${result.statusCode})`);
26
+ } else {
27
+ console.log(`[claude-ws] Health: UNREACHABLE (${result.error})`);
28
+ console.log('[claude-ws] The server process is running but not responding to HTTP.');
29
+ console.log('[claude-ws] It may still be starting up. Check: claude-ws logs');
30
+ }
31
+
32
+ process.exit(0);
33
+ }
34
+
35
+ module.exports = { run };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `claude-ws stop` — Stop the running daemon.
3
+ */
4
+
5
+ const daemon = require('../daemon');
6
+
7
+ async function run(_argv) {
8
+ const { running, pid } = daemon.checkRunning();
9
+
10
+ if (!running) {
11
+ console.log('[claude-ws] No running daemon found.');
12
+ process.exit(0);
13
+ }
14
+
15
+ console.log(`[claude-ws] Stopping daemon (PID ${pid})...`);
16
+
17
+ // Send SIGTERM
18
+ const sent = daemon.sendSignal(pid, 'SIGTERM');
19
+ if (!sent) {
20
+ console.log('[claude-ws] Failed to send SIGTERM — process may have already exited.');
21
+ daemon.removePid();
22
+ process.exit(0);
23
+ }
24
+
25
+ // Poll for exit (up to 10s)
26
+ const exited = await daemon.waitForExit(pid, 10000);
27
+
28
+ if (exited) {
29
+ daemon.removePid();
30
+ console.log('[claude-ws] Daemon stopped.');
31
+ process.exit(0);
32
+ }
33
+
34
+ // Force kill
35
+ console.log('[claude-ws] Daemon did not exit gracefully, sending SIGKILL...');
36
+ daemon.sendSignal(pid, 'SIGKILL');
37
+
38
+ const killed = await daemon.waitForExit(pid, 5000);
39
+ daemon.removePid();
40
+
41
+ if (killed) {
42
+ console.log('[claude-ws] Daemon killed.');
43
+ } else {
44
+ console.log(`[claude-ws] Warning: Process ${pid} may still be running.`);
45
+ }
46
+
47
+ process.exit(0);
48
+ }
49
+
50
+ module.exports = { run };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Configuration loader for claude-ws daemon mode.
3
+ *
4
+ * Priority: CLI flags > env vars > config file > defaults.
5
+ * Config file: ~/.claude-ws/config.json
6
+ * PID file: ~/.claude-ws/claude-ws.pid
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+
13
+ const CLAUDE_WS_DIR = path.join(os.homedir(), '.claude-ws');
14
+ const CONFIG_PATH = path.join(CLAUDE_WS_DIR, 'config.json');
15
+ const PID_PATH = path.join(CLAUDE_WS_DIR, 'claude-ws.pid');
16
+
17
+ const DEFAULTS = {
18
+ port: 8556,
19
+ host: 'localhost',
20
+ dataDir: path.join(CLAUDE_WS_DIR, 'data'),
21
+ logDir: path.join(CLAUDE_WS_DIR, 'logs'),
22
+ };
23
+
24
+ /**
25
+ * Ensure ~/.claude-ws/ directory exists.
26
+ */
27
+ function ensureDir() {
28
+ if (!fs.existsSync(CLAUDE_WS_DIR)) {
29
+ fs.mkdirSync(CLAUDE_WS_DIR, { recursive: true });
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Load config file if it exists.
35
+ * @returns {object}
36
+ */
37
+ function loadConfigFile() {
38
+ if (!fs.existsSync(CONFIG_PATH)) {
39
+ return {};
40
+ }
41
+ try {
42
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
43
+ return JSON.parse(raw);
44
+ } catch (err) {
45
+ console.error(`[claude-ws] Warning: Failed to parse ${CONFIG_PATH}: ${err.message}`);
46
+ return {};
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Resolve the final configuration by merging defaults, config file, env vars,
52
+ * and CLI flags (passed as an object).
53
+ *
54
+ * @param {object} [cliFlags={}] - Parsed CLI flags (e.g. { port: '3000', host: '0.0.0.0' })
55
+ * @returns {{ port: number, host: string, dataDir: string, logDir: string }}
56
+ */
57
+ function resolve(cliFlags = {}) {
58
+ ensureDir();
59
+
60
+ const file = loadConfigFile();
61
+
62
+ const port = parseInt(
63
+ cliFlags.port || process.env.PORT || file.port || DEFAULTS.port,
64
+ 10,
65
+ );
66
+
67
+ const host =
68
+ cliFlags.host || process.env.HOST || file.host || DEFAULTS.host;
69
+
70
+ const dataDir =
71
+ cliFlags['data-dir'] || process.env.DATA_DIR || file.dataDir || DEFAULTS.dataDir;
72
+
73
+ const logDir =
74
+ cliFlags['log-dir'] || process.env.LOG_DIR || file.logDir || DEFAULTS.logDir;
75
+
76
+ // Ensure log and data dirs exist
77
+ for (const dir of [dataDir, logDir]) {
78
+ if (!fs.existsSync(dir)) {
79
+ fs.mkdirSync(dir, { recursive: true });
80
+ }
81
+ }
82
+
83
+ return { port, host, dataDir, logDir };
84
+ }
85
+
86
+ module.exports = {
87
+ CLAUDE_WS_DIR,
88
+ CONFIG_PATH,
89
+ PID_PATH,
90
+ DEFAULTS,
91
+ ensureDir,
92
+ resolve,
93
+ };