chainlesschain 0.45.12 → 0.45.19

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 (78) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AppLayout-BfLjLMsm.js → AppLayout-B00RARl2.js} +1 -1
  3. package/src/assets/web-panel/assets/{Chat-DP7PO9Li.js → Chat-DXtvKoM0.js} +1 -1
  4. package/src/assets/web-panel/assets/{Cron-DyQF-7R1.js → Cron-BJ4ODHOy.js} +1 -1
  5. package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
  6. package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
  7. package/src/assets/web-panel/assets/{Logs-BOii-AoO.js → Logs-CSeKZEG_.js} +1 -1
  8. package/src/assets/web-panel/assets/{McpTools-DmiJtJYr.js → McpTools-BYQAK11r.js} +1 -1
  9. package/src/assets/web-panel/assets/{Memory-CDRMMobU.js → Memory-gkUAPyuZ.js} +1 -1
  10. package/src/assets/web-panel/assets/{Notes-CVhqqoS1.js → Notes-bjNrQgAo.js} +1 -1
  11. package/src/assets/web-panel/assets/{Providers-Dkt7021l.js → Providers-Dbf57Tbv.js} +1 -1
  12. package/src/assets/web-panel/assets/{Services-DUDL_UGb.js → Services-CS0oMdxh.js} +1 -1
  13. package/src/assets/web-panel/assets/{Skills-DXXELJc3.js → Skills-B2fgruv8.js} +1 -1
  14. package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
  15. package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
  16. package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
  17. package/src/assets/web-panel/assets/{index-vW799KpE.js → index-CF2CqPYX.js} +2 -2
  18. package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
  19. package/src/assets/web-panel/index.html +1 -1
  20. package/src/commands/agent.js +7 -8
  21. package/src/commands/chat.js +9 -11
  22. package/src/commands/serve.js +11 -106
  23. package/src/commands/session.js +101 -0
  24. package/src/commands/ui.js +10 -151
  25. package/src/gateways/repl/agent-repl.js +1 -0
  26. package/src/gateways/repl/chat-repl.js +1 -0
  27. package/src/gateways/ui/web-ui-server.js +1 -0
  28. package/src/gateways/ws/action-protocol.js +83 -0
  29. package/src/gateways/ws/message-dispatcher.js +73 -0
  30. package/src/gateways/ws/session-protocol.js +396 -0
  31. package/src/gateways/ws/task-protocol.js +55 -0
  32. package/src/gateways/ws/worktree-protocol.js +315 -0
  33. package/src/gateways/ws/ws-server.js +4 -0
  34. package/src/gateways/ws/ws-session-gateway.js +1 -0
  35. package/src/harness/background-task-manager.js +506 -0
  36. package/src/harness/background-task-worker.js +48 -0
  37. package/src/harness/compression-telemetry.js +214 -0
  38. package/src/harness/feature-flags.js +157 -0
  39. package/src/harness/jsonl-session-store.js +452 -0
  40. package/src/harness/prompt-compressor.js +416 -0
  41. package/src/harness/worktree-isolator.js +845 -0
  42. package/src/lib/agent-core.js +246 -45
  43. package/src/lib/background-task-manager.js +1 -305
  44. package/src/lib/background-task-worker.js +1 -50
  45. package/src/lib/compression-telemetry.js +5 -0
  46. package/src/lib/feature-flags.js +7 -182
  47. package/src/lib/interaction-adapter.js +32 -6
  48. package/src/lib/jsonl-session-store.js +21 -237
  49. package/src/lib/prompt-compressor.js +10 -481
  50. package/src/lib/sub-agent-context.js +21 -1
  51. package/src/lib/worktree-isolator.js +13 -231
  52. package/src/lib/ws-agent-handler.js +1 -0
  53. package/src/lib/ws-server.js +138 -387
  54. package/src/lib/ws-session-manager.js +82 -1
  55. package/src/repl/agent-repl.js +11 -0
  56. package/src/runtime/agent-runtime.js +417 -0
  57. package/src/runtime/contracts/agent-turn.js +11 -0
  58. package/src/runtime/contracts/session-record.js +31 -0
  59. package/src/runtime/contracts/task-record.js +18 -0
  60. package/src/runtime/contracts/telemetry-record.js +23 -0
  61. package/src/runtime/contracts/worktree-record.js +14 -0
  62. package/src/runtime/index.js +13 -0
  63. package/src/runtime/policies/agent-policy.js +45 -0
  64. package/src/runtime/runtime-context.js +14 -0
  65. package/src/runtime/runtime-events.js +37 -0
  66. package/src/runtime/runtime-factory.js +50 -0
  67. package/src/tools/index.js +22 -0
  68. package/src/tools/legacy-agent-tools.js +171 -0
  69. package/src/tools/registry.js +141 -0
  70. package/src/tools/tool-context.js +28 -0
  71. package/src/tools/tool-permissions.js +28 -0
  72. package/src/tools/tool-telemetry.js +39 -0
  73. package/src/assets/web-panel/assets/Dashboard-BGGdnr6t.js +0 -3
  74. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
  75. package/src/assets/web-panel/assets/Tasks-BwZ63-mq.js +0 -1
  76. package/src/assets/web-panel/assets/Tasks-Cr_XXNyQ.css +0 -1
  77. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
  78. package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
@@ -3,9 +3,7 @@
3
3
  * chainlesschain chat [--model] [--provider] [--agent]
4
4
  */
5
5
 
6
- import { startChatRepl } from "../repl/chat-repl.js";
7
- import { startAgentRepl } from "../repl/agent-repl.js";
8
- import { loadConfig } from "../lib/config-manager.js";
6
+ import { createAgentRuntimeFactory } from "../runtime/runtime-factory.js";
9
7
 
10
8
  export function registerChatCommand(program) {
11
9
  program
@@ -24,19 +22,19 @@ export function registerChatCommand(program) {
24
22
  )
25
23
  .option("--session <id>", "Resume a previous session (agent mode)")
26
24
  .action(async (options) => {
27
- const config = loadConfig();
28
- const replOptions = {
29
- model: options.model || config.llm?.model || "qwen2:7b",
30
- provider: options.provider || config.llm?.provider || "ollama",
31
- baseUrl: options.baseUrl || config.llm?.baseUrl,
32
- apiKey: options.apiKey || config.llm?.apiKey,
25
+ const factory = createAgentRuntimeFactory();
26
+ const runtimeOptions = {
27
+ model: options.model,
28
+ provider: options.provider,
29
+ baseUrl: options.baseUrl,
30
+ apiKey: options.apiKey,
33
31
  sessionId: options.session,
34
32
  };
35
33
 
36
34
  if (options.agent) {
37
- await startAgentRepl(replOptions);
35
+ await factory.createAgentRuntime(runtimeOptions).startAgentSession();
38
36
  } else {
39
- await startChatRepl(replOptions);
37
+ await factory.createChatRuntime(runtimeOptions).startChatSession();
40
38
  }
41
39
  });
42
40
  }
@@ -3,11 +3,8 @@
3
3
  * chainlesschain serve [--port] [--host] [--token] [--max-connections] [--timeout] [--allow-remote] [--project]
4
4
  */
5
5
 
6
- import chalk from "chalk";
7
6
  import { logger } from "../lib/logger.js";
8
- import { ChainlessChainWSServer } from "../lib/ws-server.js";
9
- import { WSSessionManager } from "../lib/ws-session-manager.js";
10
- import { bootstrap } from "../runtime/bootstrap.js";
7
+ import { createAgentRuntimeFactory } from "../runtime/runtime-factory.js";
11
8
 
12
9
  export function registerServeCommand(program) {
13
10
  program
@@ -31,109 +28,17 @@ export function registerServeCommand(program) {
31
28
  )
32
29
  .option("--project <path>", "Default project root for sessions")
33
30
  .action(async (opts) => {
34
- const port = parseInt(opts.port, 10);
35
- const maxConnections = parseInt(opts.maxConnections, 10);
36
- const timeout = parseInt(opts.timeout, 10);
37
- let host = opts.host;
38
-
39
- // Validation
40
- if (isNaN(port) || port < 1 || port > 65535) {
41
- logger.error("Invalid port number. Must be between 1 and 65535.");
42
- process.exit(1);
43
- }
44
-
45
- if (opts.allowRemote) {
46
- if (!opts.token) {
47
- logger.error("--allow-remote requires --token for security.");
48
- process.exit(1);
49
- }
50
- host = "0.0.0.0";
51
- }
52
-
53
- // Bootstrap headless runtime for DB access
54
- let db = null;
55
- try {
56
- const ctx = await bootstrap({ skipDb: false });
57
- db = ctx.db?.getDb?.() || null;
58
- } catch (_err) {
59
- logger.log(
60
- chalk.yellow(
61
- " Warning: Database not available, sessions will be in-memory only",
62
- ),
63
- );
64
- }
65
-
66
- // Create session manager
67
- const sessionManager = new WSSessionManager({
68
- db,
69
- defaultProjectRoot: opts.project || process.cwd(),
70
- });
71
-
72
- const server = new ChainlessChainWSServer({
73
- port,
74
- host,
75
- token: opts.token || null,
76
- maxConnections,
77
- timeout,
78
- sessionManager,
79
- });
80
-
81
- // Event logging
82
- server.on("connection", ({ clientId, ip }) => {
83
- logger.log(chalk.green(` + Client connected: ${clientId} (${ip})`));
84
- });
85
-
86
- server.on("disconnection", ({ clientId, reason }) => {
87
- const extra = reason ? ` (${reason})` : "";
88
- logger.log(
89
- chalk.yellow(` - Client disconnected: ${clientId}${extra}`),
90
- );
91
- });
92
-
93
- server.on("command:start", ({ id, command }) => {
94
- logger.log(chalk.cyan(` > [${id}] ${command}`));
95
- });
96
-
97
- server.on("command:end", ({ id, exitCode }) => {
98
- const color = exitCode === 0 ? chalk.green : chalk.red;
99
- logger.log(color(` < [${id}] exit ${exitCode}`));
100
- });
101
-
102
- server.on("session:create", ({ sessionId, type }) => {
103
- logger.log(chalk.green(` + Session created: ${sessionId} (${type})`));
104
- });
105
-
106
- server.on("session:close", ({ sessionId }) => {
107
- logger.log(chalk.yellow(` - Session closed: ${sessionId}`));
108
- });
109
-
110
- // Graceful shutdown
111
- const shutdown = async () => {
112
- logger.log("\n" + chalk.yellow("Shutting down WebSocket server..."));
113
- await server.stop();
114
- process.exit(0);
115
- };
116
-
117
- process.on("SIGINT", shutdown);
118
- process.on("SIGTERM", shutdown);
119
-
120
31
  try {
121
- await server.start();
122
-
123
- logger.log("");
124
- logger.log(chalk.bold(" ChainlessChain WebSocket Server"));
125
- logger.log("");
126
- logger.log(` Address: ${chalk.cyan(`ws://${host}:${port}`)}`);
127
- logger.log(
128
- ` Auth: ${opts.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
129
- );
130
- logger.log(` Sessions: ${chalk.green("enabled")}`);
131
- logger.log(` Project: ${opts.project || process.cwd()}`);
132
- logger.log(` Max conn: ${maxConnections}`);
133
- logger.log(` Timeout: ${timeout}ms`);
134
- logger.log("");
135
- logger.log(chalk.dim(" Press Ctrl+C to stop"));
136
- logger.log("");
32
+ const runtime = createAgentRuntimeFactory().createServerRuntime({
33
+ port: parseInt(opts.port, 10),
34
+ host: opts.host,
35
+ token: opts.token,
36
+ maxConnections: parseInt(opts.maxConnections, 10),
37
+ timeout: parseInt(opts.timeout, 10),
38
+ allowRemote: opts.allowRemote,
39
+ project: opts.project,
40
+ });
41
+ await runtime.startServer();
137
42
  } catch (err) {
138
43
  logger.error(`Failed to start server: ${err.message}`);
139
44
  process.exit(1);
@@ -18,6 +18,10 @@ import {
18
18
  rebuildMessages,
19
19
  sessionExists,
20
20
  readEvents,
21
+ migrateLegacySessions,
22
+ migrateLegacySessionsBatch,
23
+ validateJsonlSession,
24
+ validateAllJsonlSessions,
21
25
  } from "../lib/jsonl-session-store.js";
22
26
  import { feature } from "../lib/feature-flags.js";
23
27
 
@@ -301,4 +305,101 @@ export function registerSessionCommand(program) {
301
305
  process.exit(1);
302
306
  }
303
307
  });
308
+
309
+ session
310
+ .command("migrate")
311
+ .description("Migrate legacy JSON session files to JSONL")
312
+ .argument("[source]", "Directory containing legacy .json session files")
313
+ .option("--dry-run", "Show what would migrate without writing files")
314
+ .option("--force", "Overwrite existing JSONL sessions")
315
+ .option("--no-archive", "Do not keep .migrated.json backups")
316
+ .option("--sample-size <n>", "Validate N migrated sessions after migration", "3")
317
+ .option("--retry-failures", "Retry failed migrations once")
318
+ .option("--json", "Output as JSON")
319
+ .action(async (source, options) => {
320
+ try {
321
+ const report = migrateLegacySessionsBatch(source, {
322
+ dryRun: options.dryRun,
323
+ force: options.force,
324
+ archive: options.archive,
325
+ sampleSize: parseInt(options.sampleSize, 10) || 3,
326
+ retryFailures: options.retryFailures,
327
+ });
328
+ const results = report.results || migrateLegacySessions(source, options);
329
+
330
+ if (options.json) {
331
+ console.log(JSON.stringify(report, null, 2));
332
+ return;
333
+ }
334
+
335
+ if (results.length === 0) {
336
+ logger.info("No legacy JSON session files found.");
337
+ return;
338
+ }
339
+
340
+ for (const result of results) {
341
+ if (result.skipped) {
342
+ logger.log(
343
+ `${chalk.yellow("skip")} ${result.file} -> ${result.sessionId} (${result.reason})`,
344
+ );
345
+ continue;
346
+ }
347
+ logger.log(
348
+ `${chalk.green(options.dryRun ? "plan" : "migrated")} ${result.file} -> ${result.sessionId} (${result.messageCount} messages)`,
349
+ );
350
+ }
351
+
352
+ logger.log(
353
+ chalk.gray(
354
+ `summary: scanned ${report.summary.scanned}, migrated ${report.summary.migrated}, skipped ${report.summary.skipped}, failed ${report.summary.failed}, retries ${report.summary.retries}`,
355
+ ),
356
+ );
357
+
358
+ if (report.sampledValidation?.length) {
359
+ for (const item of report.sampledValidation) {
360
+ const label = item.valid && item.matchesExpectedMessages
361
+ ? chalk.green("sample-ok")
362
+ : chalk.red("sample-fail");
363
+ logger.log(
364
+ `${label} ${item.sessionId} (${item.messageCount}/${item.expectedMessageCount} messages)`,
365
+ );
366
+ }
367
+ }
368
+ } catch (err) {
369
+ logger.error(`Failed: ${err.message}`);
370
+ process.exit(1);
371
+ }
372
+ });
373
+
374
+ session
375
+ .command("validate")
376
+ .description("Validate JSONL session files")
377
+ .argument("[id]", "Session ID to validate")
378
+ .option("--json", "Output as JSON")
379
+ .action(async (id, options) => {
380
+ try {
381
+ const result = id
382
+ ? validateJsonlSession(id)
383
+ : validateAllJsonlSessions();
384
+
385
+ if (options.json) {
386
+ console.log(JSON.stringify(result, null, 2));
387
+ return;
388
+ }
389
+
390
+ const results = Array.isArray(result) ? result : [result];
391
+ for (const item of results) {
392
+ const label = item.valid ? chalk.green("valid") : chalk.red("invalid");
393
+ logger.log(
394
+ `${label} ${item.sessionId} (${item.eventCount} events, ${item.messageCount || 0} messages, malformed: ${item.malformedLines})`,
395
+ );
396
+ if (!item.valid && item.reason) {
397
+ logger.log(` ${chalk.gray(item.reason)}`);
398
+ }
399
+ }
400
+ } catch (err) {
401
+ logger.error(`Failed: ${err.message}`);
402
+ process.exit(1);
403
+ }
404
+ });
304
405
  }
@@ -1,39 +1,5 @@
1
- /**
2
- * ui command — start a local web management UI
3
- * chainlesschain ui [--port] [--ws-port] [--host] [--no-open] [--token]
4
- *
5
- * Project mode (run from a dir with .chainlesschain/): project-scoped chat UI
6
- * Global mode (run from any other dir): global management panel
7
- */
8
-
9
- import { execSync } from "child_process";
10
- import path from "path";
11
- import chalk from "chalk";
12
1
  import { logger } from "../lib/logger.js";
13
- import { ChainlessChainWSServer } from "../lib/ws-server.js";
14
- import { WSSessionManager } from "../lib/ws-session-manager.js";
15
- import { createWebUIServer } from "../lib/web-ui-server.js";
16
- import { bootstrap } from "../runtime/bootstrap.js";
17
- import { findProjectRoot, loadProjectConfig } from "../lib/project-detector.js";
18
- import { loadConfig } from "../lib/config-manager.js";
19
-
20
- /**
21
- * Open a URL in the system default browser (cross-platform).
22
- */
23
- function openBrowser(url) {
24
- try {
25
- const platform = process.platform;
26
- if (platform === "win32") {
27
- execSync(`start "" "${url}"`, { stdio: "ignore" });
28
- } else if (platform === "darwin") {
29
- execSync(`open "${url}"`, { stdio: "ignore" });
30
- } else {
31
- execSync(`xdg-open "${url}"`, { stdio: "ignore" });
32
- }
33
- } catch (_err) {
34
- // Non-critical — user can open manually
35
- }
36
- }
2
+ import { createAgentRuntimeFactory } from "../runtime/runtime-factory.js";
37
3
 
38
4
  export function registerUiCommand(program) {
39
5
  program
@@ -52,126 +18,19 @@ export function registerUiCommand(program) {
52
18
  "Path to built web-panel dist/ directory (auto-detected by default)",
53
19
  )
54
20
  .action(async (opts) => {
55
- const httpPort = parseInt(opts.port, 10);
56
- const wsPort = parseInt(opts.wsPort, 10);
57
- const host = opts.host;
58
-
59
- if (isNaN(httpPort) || httpPort < 1 || httpPort > 65535) {
60
- logger.error("Invalid --port. Must be between 1 and 65535.");
61
- process.exit(1);
62
- }
63
- if (isNaN(wsPort) || wsPort < 1 || wsPort > 65535) {
64
- logger.error("Invalid --ws-port. Must be between 1 and 65535.");
65
- process.exit(1);
66
- }
67
-
68
- // ── Detect project context ────────────────────────────────────────────
69
- const projectRoot = findProjectRoot(process.cwd());
70
- const projectConfig = projectRoot ? loadProjectConfig(projectRoot) : null;
71
- const projectName =
72
- projectConfig?.name ||
73
- (projectRoot ? path.basename(projectRoot) : null);
74
- const mode = projectRoot ? "project" : "global";
75
-
76
- // ── Bootstrap headless runtime ────────────────────────────────────────
77
- let db = null;
78
- try {
79
- const ctx = await bootstrap({ skipDb: false });
80
- db = ctx.db?.getDb?.() || null;
81
- } catch (_err) {
82
- logger.log(
83
- chalk.yellow(
84
- " Warning: Database not available, sessions will be in-memory only",
85
- ),
86
- );
87
- }
88
-
89
- // ── Start WebSocket server ────────────────────────────────────────────
90
- const appConfig = loadConfig();
91
- const sessionManager = new WSSessionManager({
92
- db,
93
- defaultProjectRoot: projectRoot || process.cwd(),
94
- config: appConfig,
95
- });
96
-
97
- const wsServer = new ChainlessChainWSServer({
98
- port: wsPort,
99
- host,
100
- token: opts.token || null,
101
- maxConnections: 20,
102
- timeout: 60000,
103
- sessionManager,
104
- });
105
-
106
- try {
107
- await wsServer.start();
108
- } catch (err) {
109
- logger.error(`Failed to start WebSocket server: ${err.message}`);
110
- process.exit(1);
111
- }
112
-
113
- // ── Start HTTP server ─────────────────────────────────────────────────
114
- const httpServer = createWebUIServer({
115
- wsPort,
116
- wsToken: opts.token || null,
117
- wsHost: host === "0.0.0.0" ? "127.0.0.1" : host,
118
- projectRoot,
119
- projectName,
120
- mode,
121
- staticDir: opts.webPanelDir || null,
122
- });
123
-
124
21
  try {
125
- await new Promise((resolve, reject) => {
126
- httpServer.listen(httpPort, host, () => resolve());
127
- httpServer.on("error", reject);
22
+ const runtime = createAgentRuntimeFactory().createUiRuntime({
23
+ port: parseInt(opts.port, 10),
24
+ wsPort: parseInt(opts.wsPort, 10),
25
+ host: opts.host,
26
+ open: opts.open,
27
+ token: opts.token || null,
28
+ webPanelDir: opts.webPanelDir || null,
128
29
  });
30
+ await runtime.startUiServer();
129
31
  } catch (err) {
130
- logger.error(`Failed to start HTTP server: ${err.message}`);
32
+ logger.error(`Failed to start UI server: ${err.message}`);
131
33
  process.exit(1);
132
34
  }
133
-
134
- // ── Print startup info ────────────────────────────────────────────────
135
- const uiUrl = `http://${host === "0.0.0.0" ? "127.0.0.1" : host}:${httpPort}`;
136
-
137
- logger.log("");
138
- logger.log(chalk.bold(" ChainlessChain 管理面板"));
139
- logger.log("");
140
- if (mode === "project") {
141
- logger.log(
142
- ` Mode: ${chalk.cyan("project")} ${chalk.dim(projectRoot)}`,
143
- );
144
- if (projectName) {
145
- logger.log(` Project: ${chalk.green(projectName)}`);
146
- }
147
- } else {
148
- logger.log(` Mode: ${chalk.cyan("global")}`);
149
- }
150
- logger.log(` UI: ${chalk.cyan(uiUrl)}`);
151
- logger.log(` WS: ${chalk.dim(`ws://${host}:${wsPort}`)}`);
152
- logger.log(
153
- ` Auth: ${opts.token ? chalk.green("enabled") : chalk.yellow("disabled")}`,
154
- );
155
- logger.log("");
156
- logger.log(chalk.dim(" Press Ctrl+C to stop"));
157
- logger.log("");
158
-
159
- // ── Open browser ──────────────────────────────────────────────────────
160
- if (opts.open !== false) {
161
- openBrowser(uiUrl);
162
- }
163
-
164
- // ── Graceful shutdown ─────────────────────────────────────────────────
165
- const shutdown = async () => {
166
- logger.log("\n" + chalk.yellow("Shutting down UI server..."));
167
- await Promise.all([
168
- new Promise((resolve) => httpServer.close(resolve)),
169
- wsServer.stop(),
170
- ]);
171
- process.exit(0);
172
- };
173
-
174
- process.on("SIGINT", shutdown);
175
- process.on("SIGTERM", shutdown);
176
35
  });
177
36
  }
@@ -0,0 +1 @@
1
+ export { startAgentRepl } from "../../repl/agent-repl.js";
@@ -0,0 +1 @@
1
+ export { startChatRepl } from "../../repl/chat-repl.js";
@@ -0,0 +1 @@
1
+ export { createWebUIServer } from "../../lib/web-ui-server.js";
@@ -0,0 +1,83 @@
1
+ export function handleSlashCommand(server, id, ws, message) {
2
+ const { sessionId, command } = message;
3
+ const handler = server.sessionHandlers.get(sessionId);
4
+
5
+ if (!handler) {
6
+ server._send(ws, {
7
+ id,
8
+ type: "error",
9
+ code: "SESSION_NOT_FOUND",
10
+ message: `No active session handler for: ${sessionId}`,
11
+ });
12
+ return;
13
+ }
14
+
15
+ handler.handleSlashCommand(command, id);
16
+ }
17
+
18
+ export async function handleOrchestrate(server, id, ws, message) {
19
+ const {
20
+ task,
21
+ cwd,
22
+ agents = 3,
23
+ ci = "npm test",
24
+ noCi = false,
25
+ strategy,
26
+ } = message;
27
+
28
+ if (!task || typeof task !== "string") {
29
+ server._send(ws, {
30
+ id,
31
+ type: "error",
32
+ code: "INVALID_TASK",
33
+ message: "task field required",
34
+ });
35
+ return;
36
+ }
37
+
38
+ try {
39
+ const { Orchestrator, TASK_SOURCE } = await import("../../lib/orchestrator.js");
40
+
41
+ const orch = new Orchestrator({
42
+ cwd: cwd || server.projectRoot || process.cwd(),
43
+ maxParallel: Math.min(parseInt(agents, 10) || 3, 10),
44
+ ciCommand: ci,
45
+ agents: strategy ? { strategy } : undefined,
46
+ verbose: false,
47
+ });
48
+
49
+ const wsNotifier = orch.notifier.addWebSocketChannel({
50
+ send: (data) => server._send(ws, data),
51
+ requestId: id,
52
+ });
53
+
54
+ orch.on("agent:output", (ev) => wsNotifier.sendAgentOutput(ev));
55
+ orch.on("task:added", (t) => wsNotifier.sendStatus(t));
56
+ orch.on("task:decomposing", (t) => wsNotifier.sendStatus(t));
57
+ orch.on("ci:checking", ({ task: t }) => wsNotifier.sendStatus(t));
58
+ orch.on("task:retrying", ({ task: t }) => wsNotifier.sendStatus(t));
59
+
60
+ const result = await orch.addTask(task, {
61
+ source: TASK_SOURCE.CLI,
62
+ cwd: cwd || server.projectRoot || process.cwd(),
63
+ runCI: !noCi,
64
+ notify: true,
65
+ });
66
+
67
+ server._send(ws, {
68
+ id,
69
+ type: "orchestrate:done",
70
+ taskId: result.id,
71
+ status: result.status,
72
+ retries: result.retries,
73
+ subtasks: result.subtasks?.length || 0,
74
+ });
75
+ } catch (err) {
76
+ server._send(ws, {
77
+ id,
78
+ type: "error",
79
+ code: "ORCHESTRATE_FAILED",
80
+ message: err.message,
81
+ });
82
+ }
83
+ }
@@ -0,0 +1,73 @@
1
+ export function createWsMessageDispatcher(server) {
2
+ return {
3
+ async dispatch(clientId, ws, message) {
4
+ const { id, type } = message;
5
+
6
+ if (!id) {
7
+ server._send(ws, {
8
+ type: "error",
9
+ code: "MISSING_ID",
10
+ message: 'Message must include an "id" field',
11
+ });
12
+ return;
13
+ }
14
+
15
+ const client = server.clients.get(clientId);
16
+ if (server.token && !client.authenticated && type !== "auth") {
17
+ server._send(ws, {
18
+ id,
19
+ type: "error",
20
+ code: "AUTH_REQUIRED",
21
+ message: "Authentication required. Send an auth message first.",
22
+ });
23
+ return;
24
+ }
25
+
26
+ const routes = {
27
+ auth: () => server._handleAuth(clientId, ws, message),
28
+ ping: () =>
29
+ server._send(ws, { id, type: "pong", serverTime: Date.now() }),
30
+ execute: () => server._executeCommand(id, ws, message.command, false),
31
+ stream: () => server._executeCommand(id, ws, message.command, true),
32
+ cancel: () => server._cancelRequest(id, ws),
33
+ "session-create": () => server._handleSessionCreate(id, ws, message),
34
+ "session-resume": () => server._handleSessionResume(id, ws, message),
35
+ "session-message": () => server._handleSessionMessage(id, ws, message),
36
+ "session-policy-update": () =>
37
+ server._handleSessionPolicyUpdate(id, ws, message),
38
+ "session-list": () => server._handleSessionList(id, ws),
39
+ "session-close": () => server._handleSessionClose(id, ws, message),
40
+ "slash-command": () => server._handleSlashCommand(id, ws, message),
41
+ "session-answer": () => server._handleSessionAnswer(id, ws, message),
42
+ "host-tool-result": () => server._handleHostToolResult(id, ws, message),
43
+ orchestrate: () => server._handleOrchestrate(id, ws, message),
44
+ "tasks-list": () => server._handleTasksList(id, ws),
45
+ "tasks-stop": () => server._handleTasksStop(id, ws, message),
46
+ "tasks-detail": () => server._handleTaskDetail(id, ws, message),
47
+ "tasks-history": () => server._handleTaskHistory(id, ws, message),
48
+ "worktree-diff": () => server._handleWorktreeDiff(id, ws, message),
49
+ "worktree-merge": () => server._handleWorktreeMerge(id, ws, message),
50
+ "worktree-merge-preview": () =>
51
+ server._handleWorktreeMergePreview(id, ws, message),
52
+ "worktree-automation-apply": () =>
53
+ server._handleWorktreeAutomationApply(id, ws, message),
54
+ "worktree-list": () => server._handleWorktreeList(id, ws),
55
+ "compression-stats": () =>
56
+ server._handleCompressionStats(id, ws, message),
57
+ };
58
+
59
+ const handler = routes[type];
60
+ if (!handler) {
61
+ server._send(ws, {
62
+ id,
63
+ type: "error",
64
+ code: "UNKNOWN_TYPE",
65
+ message: `Unknown message type: ${type}`,
66
+ });
67
+ return;
68
+ }
69
+
70
+ return handler();
71
+ },
72
+ };
73
+ }