chainlesschain 0.45.12 → 0.45.20
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AppLayout-BfLjLMsm.js → AppLayout-B00RARl2.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-DP7PO9Li.js → Chat-DXtvKoM0.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-DyQF-7R1.js → Cron-BJ4ODHOy.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-3iIpp3zd.js +3 -0
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Logs-BOii-AoO.js → Logs-CSeKZEG_.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DmiJtJYr.js → McpTools-BYQAK11r.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-CDRMMobU.js → Memory-gkUAPyuZ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CVhqqoS1.js → Notes-bjNrQgAo.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-Dkt7021l.js → Providers-Dbf57Tbv.js} +1 -1
- package/src/assets/web-panel/assets/{Services-DUDL_UGb.js → Services-CS0oMdxh.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DXXELJc3.js → Skills-B2fgruv8.js} +1 -1
- package/src/assets/web-panel/assets/Tasks-BJjN_YEm.css +1 -0
- package/src/assets/web-panel/assets/Tasks-qULws8pc.js +1 -0
- package/src/assets/web-panel/assets/chat-DnH09sSR.js +1 -0
- package/src/assets/web-panel/assets/{index-vW799KpE.js → index-CF2CqPYX.js} +2 -2
- package/src/assets/web-panel/assets/ws-DjelKkD6.js +1 -0
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +7 -8
- package/src/commands/chat.js +9 -11
- package/src/commands/serve.js +11 -106
- package/src/commands/session.js +101 -0
- package/src/commands/ui.js +10 -151
- package/src/gateways/repl/agent-repl.js +1 -0
- package/src/gateways/repl/chat-repl.js +1 -0
- package/src/gateways/ui/web-ui-server.js +1 -0
- package/src/gateways/ws/action-protocol.js +83 -0
- package/src/gateways/ws/message-dispatcher.js +73 -0
- package/src/gateways/ws/session-protocol.js +396 -0
- package/src/gateways/ws/task-protocol.js +55 -0
- package/src/gateways/ws/worktree-protocol.js +315 -0
- package/src/gateways/ws/ws-server.js +4 -0
- package/src/gateways/ws/ws-session-gateway.js +1 -0
- package/src/harness/background-task-manager.js +506 -0
- package/src/harness/background-task-worker.js +48 -0
- package/src/harness/compression-telemetry.js +214 -0
- package/src/harness/feature-flags.js +157 -0
- package/src/harness/jsonl-session-store.js +452 -0
- package/src/harness/prompt-compressor.js +416 -0
- package/src/harness/worktree-isolator.js +845 -0
- package/src/lib/agent-core.js +246 -45
- package/src/lib/background-task-manager.js +1 -305
- package/src/lib/background-task-worker.js +1 -50
- package/src/lib/compression-telemetry.js +5 -0
- package/src/lib/feature-flags.js +7 -182
- package/src/lib/interaction-adapter.js +32 -6
- package/src/lib/jsonl-session-store.js +21 -237
- package/src/lib/prompt-compressor.js +10 -481
- package/src/lib/sub-agent-context.js +21 -1
- package/src/lib/worktree-isolator.js +13 -231
- package/src/lib/ws-agent-handler.js +1 -0
- package/src/lib/ws-server.js +138 -387
- package/src/lib/ws-session-manager.js +82 -1
- package/src/repl/agent-repl.js +11 -0
- package/src/runtime/agent-runtime.js +417 -0
- package/src/runtime/contracts/agent-turn.js +11 -0
- package/src/runtime/contracts/session-record.js +31 -0
- package/src/runtime/contracts/task-record.js +18 -0
- package/src/runtime/contracts/telemetry-record.js +23 -0
- package/src/runtime/contracts/worktree-record.js +14 -0
- package/src/runtime/index.js +13 -0
- package/src/runtime/policies/agent-policy.js +45 -0
- package/src/runtime/runtime-context.js +14 -0
- package/src/runtime/runtime-events.js +37 -0
- package/src/runtime/runtime-factory.js +50 -0
- package/src/tools/index.js +22 -0
- package/src/tools/legacy-agent-tools.js +171 -0
- package/src/tools/registry.js +141 -0
- package/src/tools/tool-context.js +28 -0
- package/src/tools/tool-permissions.js +28 -0
- package/src/tools/tool-telemetry.js +39 -0
- package/src/assets/web-panel/assets/Dashboard-BGGdnr6t.js +0 -3
- package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +0 -1
- package/src/assets/web-panel/assets/Tasks-BwZ63-mq.js +0 -1
- package/src/assets/web-panel/assets/Tasks-Cr_XXNyQ.css +0 -1
- package/src/assets/web-panel/assets/chat-C_hu-qNs.js +0 -1
- package/src/assets/web-panel/assets/ws-DwluTqT5.js +0 -1
package/src/commands/chat.js
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
* chainlesschain chat [--model] [--provider] [--agent]
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
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
|
|
28
|
-
const
|
|
29
|
-
model: options.model
|
|
30
|
-
provider: options.provider
|
|
31
|
-
baseUrl: options.baseUrl
|
|
32
|
-
apiKey: options.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
|
|
35
|
+
await factory.createAgentRuntime(runtimeOptions).startAgentSession();
|
|
38
36
|
} else {
|
|
39
|
-
await
|
|
37
|
+
await factory.createChatRuntime(runtimeOptions).startChatSession();
|
|
40
38
|
}
|
|
41
39
|
});
|
|
42
40
|
}
|
package/src/commands/serve.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
);
|
|
130
|
-
|
|
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);
|
package/src/commands/session.js
CHANGED
|
@@ -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
|
}
|
package/src/commands/ui.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
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
|
+
}
|