@lydia-agent/cli 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,3419 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import "dotenv/config";
5
+ import { Command as Command2 } from "commander";
6
+ import chalk2 from "chalk";
7
+ import ora from "ora";
8
+ import { ReplayManager, StrategyRegistry as StrategyRegistry3, StrategyReviewer, StrategyApprovalService as StrategyApprovalService2, ShadowRouter as ShadowRouter2, ConfigLoader as ConfigLoader3, MemoryManager as MemoryManager2, BasicStrategyGate, StrategyUpdateGate, resolveCanonicalComputerUseToolName } from "@lydia-agent/core";
9
+ import { readFile as readFile3 } from "fs/promises";
10
+ import { join as join3, dirname as dirname3 } from "path";
11
+ import { fileURLToPath as fileURLToPath2 } from "url";
12
+ import * as readline from "readline/promises";
13
+ import { stdin as input, stdout as output } from "process";
14
+ import open from "open";
15
+
16
+ // src/server/index.ts
17
+ import { Hono } from "hono";
18
+ import { serve } from "@hono/node-server";
19
+ import { createNodeWebSocket } from "@hono/node-ws";
20
+ import {
21
+ MemoryManager,
22
+ ConfigLoader as ConfigLoader2,
23
+ Agent,
24
+ StrategyRegistry as StrategyRegistry2,
25
+ StrategyApprovalService,
26
+ ShadowRouter,
27
+ createLLMFromConfig
28
+ } from "@lydia-agent/core";
29
+ import { join as join2 } from "path";
30
+ import { homedir as homedir2 } from "os";
31
+ import { readFile as readFile2, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
32
+ import { existsSync as existsSync2 } from "fs";
33
+ import { fileURLToPath } from "url";
34
+ import { dirname as dirname2 } from "path";
35
+ import { randomUUID } from "crypto";
36
+
37
+ // src/mcp/health.ts
38
+ import { McpClientManager } from "@lydia-agent/core";
39
+ function timeoutError(timeoutMs) {
40
+ return new Error(`Timeout after ${timeoutMs}ms`);
41
+ }
42
+ async function withTimeout(promise, timeoutMs) {
43
+ let timer;
44
+ try {
45
+ return await Promise.race([
46
+ promise,
47
+ new Promise((_, reject) => {
48
+ timer = setTimeout(() => reject(timeoutError(timeoutMs)), timeoutMs);
49
+ })
50
+ ]);
51
+ } finally {
52
+ if (timer) clearTimeout(timer);
53
+ }
54
+ }
55
+ async function checkMcpServer(target, options = {}) {
56
+ const timeoutMs = options.timeoutMs ?? 15e3;
57
+ const retries = Math.max(0, options.retries ?? 0);
58
+ const start = Date.now();
59
+ let attempts = 0;
60
+ let lastError = null;
61
+ while (attempts <= retries) {
62
+ attempts += 1;
63
+ const manager = new McpClientManager();
64
+ try {
65
+ await withTimeout(
66
+ manager.connect({
67
+ id: target.id,
68
+ type: "stdio",
69
+ command: target.command,
70
+ args: target.args || [],
71
+ env: target.env
72
+ }),
73
+ timeoutMs
74
+ );
75
+ const tools = manager.getTools().map((t) => t.name);
76
+ return {
77
+ id: target.id,
78
+ ok: true,
79
+ tools,
80
+ durationMs: Date.now() - start,
81
+ attempts
82
+ };
83
+ } catch (error) {
84
+ lastError = error;
85
+ } finally {
86
+ await manager.closeAll().catch(() => {
87
+ });
88
+ }
89
+ }
90
+ return {
91
+ id: target.id,
92
+ ok: false,
93
+ tools: [],
94
+ durationMs: Date.now() - start,
95
+ attempts,
96
+ error: lastError instanceof Error ? lastError.message : String(lastError)
97
+ };
98
+ }
99
+ async function checkMcpServers(targets, options = {}) {
100
+ const results = [];
101
+ for (const target of targets) {
102
+ results.push(await checkMcpServer(target, options));
103
+ }
104
+ return results;
105
+ }
106
+
107
+ // src/service/constants.ts
108
+ var DEFAULT_HOST = "127.0.0.1";
109
+ var DEFAULT_PORT = 15536;
110
+ var STATUS_POLL_INTERVAL_MS = 300;
111
+ var STATUS_POLL_TIMEOUT_MS = 1e4;
112
+
113
+ // src/service/runtime.ts
114
+ import { ConfigLoader, StrategyRegistry } from "@lydia-agent/core";
115
+ import * as fs from "fs";
116
+ import * as fsPromises from "fs/promises";
117
+ import * as os from "os";
118
+ import * as path from "path";
119
+ function getLydiaPaths(home = os.homedir()) {
120
+ const baseDir = path.join(home, ".lydia");
121
+ const strategiesDir = path.join(baseDir, "strategies");
122
+ const skillsDir = path.join(baseDir, "skills");
123
+ const dataDir = path.join(baseDir, "data");
124
+ const logsDir = path.join(baseDir, "logs");
125
+ const runDir = path.join(baseDir, "run");
126
+ return {
127
+ home,
128
+ baseDir,
129
+ configPath: path.join(baseDir, "config.json"),
130
+ dataDir,
131
+ logsDir,
132
+ runDir,
133
+ skillsDir,
134
+ strategiesDir,
135
+ strategyPath: path.join(strategiesDir, "default.yml"),
136
+ serverPidPath: path.join(runDir, "server.pid"),
137
+ serverStatePath: path.join(runDir, "server.json"),
138
+ serverLogPath: path.join(logsDir, "server.log"),
139
+ serverErrorLogPath: path.join(logsDir, "server-error.log")
140
+ };
141
+ }
142
+ function getBaseUrl(port = DEFAULT_PORT, host = DEFAULT_HOST) {
143
+ return `http://${host}:${port}`;
144
+ }
145
+ async function initLocalWorkspace() {
146
+ const paths = getLydiaPaths();
147
+ const created = [];
148
+ const existing = [];
149
+ const dirs = [
150
+ paths.baseDir,
151
+ paths.strategiesDir,
152
+ paths.skillsDir,
153
+ paths.dataDir,
154
+ paths.logsDir,
155
+ paths.runDir
156
+ ];
157
+ for (const dir of dirs) {
158
+ if (fs.existsSync(dir)) {
159
+ existing.push(dir);
160
+ continue;
161
+ }
162
+ await fsPromises.mkdir(dir, { recursive: true });
163
+ created.push(dir);
164
+ }
165
+ const loader = new ConfigLoader();
166
+ if (!fs.existsSync(paths.configPath)) {
167
+ const config2 = await loader.load();
168
+ await fsPromises.writeFile(paths.configPath, JSON.stringify(config2, null, 2), "utf-8");
169
+ created.push(paths.configPath);
170
+ } else {
171
+ existing.push(paths.configPath);
172
+ }
173
+ if (!fs.existsSync(paths.strategyPath)) {
174
+ const registry = new StrategyRegistry();
175
+ const strategy = await registry.loadDefault();
176
+ const initial = {
177
+ ...strategy,
178
+ metadata: {
179
+ ...strategy.metadata,
180
+ id: "default",
181
+ version: "1.0.0",
182
+ name: "Default Strategy",
183
+ description: "Baseline strategy for safe execution."
184
+ }
185
+ };
186
+ await registry.saveToFile(initial, paths.strategyPath);
187
+ created.push(paths.strategyPath);
188
+ } else {
189
+ existing.push(paths.strategyPath);
190
+ }
191
+ const config = await loader.load();
192
+ if (!config.strategy.activePath) {
193
+ await loader.update({
194
+ strategy: {
195
+ activePath: paths.strategyPath
196
+ }
197
+ });
198
+ }
199
+ return { paths, created, existing };
200
+ }
201
+ async function readServiceState() {
202
+ const { serverStatePath } = getLydiaPaths();
203
+ try {
204
+ const raw = await fsPromises.readFile(serverStatePath, "utf-8");
205
+ return JSON.parse(raw);
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+ async function writeServiceState(state) {
211
+ const { serverStatePath, serverPidPath } = getLydiaPaths();
212
+ await fsPromises.mkdir(path.dirname(serverStatePath), { recursive: true });
213
+ await fsPromises.writeFile(serverStatePath, JSON.stringify(state, null, 2), "utf-8");
214
+ await fsPromises.writeFile(serverPidPath, `${state.pid}
215
+ `, "utf-8");
216
+ }
217
+ async function removeServiceState() {
218
+ const { serverStatePath, serverPidPath } = getLydiaPaths();
219
+ await Promise.all([
220
+ fsPromises.rm(serverStatePath, { force: true }),
221
+ fsPromises.rm(serverPidPath, { force: true })
222
+ ]);
223
+ }
224
+
225
+ // src/server/index.ts
226
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
227
+ function getSetupPaths() {
228
+ const paths = getLydiaPaths();
229
+ return {
230
+ baseDir: paths.baseDir,
231
+ strategiesDir: paths.strategiesDir,
232
+ skillsDir: paths.skillsDir,
233
+ configPath: paths.configPath,
234
+ strategyPath: paths.strategyPath
235
+ };
236
+ }
237
+ function maskSecret(value) {
238
+ if (!value) return "";
239
+ if (value.length <= 8) return "*".repeat(value.length);
240
+ return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
241
+ }
242
+ function pickString(value) {
243
+ if (typeof value !== "string") return void 0;
244
+ return value.trim();
245
+ }
246
+ async function ensureLocalWorkspace() {
247
+ const { strategiesDir, skillsDir, configPath, strategyPath } = getSetupPaths();
248
+ await mkdir2(strategiesDir, { recursive: true });
249
+ await mkdir2(skillsDir, { recursive: true });
250
+ const loader = new ConfigLoader2();
251
+ if (!existsSync2(configPath)) {
252
+ const config2 = await loader.load();
253
+ await writeFile2(configPath, JSON.stringify(config2, null, 2), "utf-8");
254
+ }
255
+ if (!existsSync2(strategyPath)) {
256
+ const registry = new StrategyRegistry2();
257
+ const strategy = await registry.loadDefault();
258
+ const initial = {
259
+ ...strategy,
260
+ metadata: {
261
+ ...strategy.metadata,
262
+ id: "default",
263
+ version: "1.0.0",
264
+ name: "Default Strategy"
265
+ }
266
+ };
267
+ await registry.saveToFile(initial, strategyPath);
268
+ }
269
+ const config = await loader.load();
270
+ if (!config.strategy.activePath) {
271
+ await loader.update({ strategy: { activePath: strategyPath } });
272
+ }
273
+ return getSetupPaths();
274
+ }
275
+ function createServer(port = DEFAULT_PORT, options) {
276
+ const app = new Hono();
277
+ const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app });
278
+ const configLoader = new ConfigLoader2();
279
+ const runs = /* @__PURE__ */ new Map();
280
+ let activeRunId = null;
281
+ const wsClients = /* @__PURE__ */ new Set();
282
+ const apiSessions = /* @__PURE__ */ new Map();
283
+ let authState = {
284
+ required: false,
285
+ apiToken: "",
286
+ sessionTtlMs: 24 * 60 * 60 * 1e3
287
+ };
288
+ let lastConfigRefreshAt = 0;
289
+ function broadcastWs(message) {
290
+ const data = JSON.stringify(message);
291
+ for (const ws of wsClients) {
292
+ try {
293
+ ws.send(data);
294
+ } catch {
295
+ wsClients.delete(ws);
296
+ }
297
+ }
298
+ }
299
+ function isPublicApiPath(path3) {
300
+ return path3 === "/api/status" || path3.startsWith("/api/setup") || path3 === "/api/auth/session";
301
+ }
302
+ function extractBearerToken(headerValue) {
303
+ if (!headerValue) return "";
304
+ const lower = headerValue.toLowerCase();
305
+ if (!lower.startsWith("bearer ")) return "";
306
+ return headerValue.slice(7).trim();
307
+ }
308
+ function pruneExpiredSessions(now) {
309
+ for (const [id, expiresAt] of apiSessions.entries()) {
310
+ if (expiresAt <= now) {
311
+ apiSessions.delete(id);
312
+ }
313
+ }
314
+ }
315
+ function isValidSession(sessionId) {
316
+ if (!sessionId) return false;
317
+ const now = Date.now();
318
+ pruneExpiredSessions(now);
319
+ const expiresAt = apiSessions.get(sessionId);
320
+ if (!expiresAt || expiresAt <= now) {
321
+ apiSessions.delete(sessionId);
322
+ return false;
323
+ }
324
+ return true;
325
+ }
326
+ app.get("/ws", upgradeWebSocket(() => ({
327
+ onOpen(_event, ws) {
328
+ wsClients.add(ws);
329
+ ws.send(JSON.stringify({
330
+ type: "connected",
331
+ data: { status: activeRunId ? "running" : "idle", activeRunId },
332
+ timestamp: Date.now()
333
+ }));
334
+ },
335
+ onMessage(event, ws) {
336
+ try {
337
+ const msg = JSON.parse(String(event.data));
338
+ if (msg.type === "ping") {
339
+ ws.send(JSON.stringify({ type: "pong", timestamp: Date.now() }));
340
+ }
341
+ } catch {
342
+ }
343
+ },
344
+ onClose(_event, ws) {
345
+ wsClients.delete(ws);
346
+ },
347
+ onError(_event, ws) {
348
+ wsClients.delete(ws);
349
+ }
350
+ })));
351
+ const dbPath = join2(homedir2(), ".lydia", "memory.db");
352
+ const soulPath = join2(homedir2(), ".lydia", "Soul.md");
353
+ const memoryManager = options?.memoryManager || new MemoryManager(dbPath);
354
+ const approvalService = new StrategyApprovalService(memoryManager, configLoader);
355
+ const shadowRouter = new ShadowRouter(memoryManager);
356
+ async function readSoulProfile() {
357
+ try {
358
+ if (!existsSync2(soulPath)) return {};
359
+ const content = await readFile2(soulPath, "utf-8");
360
+ const userDisplayName = content.match(/^- User display name: (.+)$/m)?.[1]?.trim();
361
+ const assistantDisplayName = content.match(/^- Assistant display name: (.+)$/m)?.[1]?.trim();
362
+ const updatedAt = content.match(/^- Updated at: (.+)$/m)?.[1]?.trim();
363
+ return {
364
+ userDisplayName: userDisplayName || void 0,
365
+ assistantDisplayName: assistantDisplayName || void 0,
366
+ updatedAt: updatedAt || void 0
367
+ };
368
+ } catch {
369
+ return {};
370
+ }
371
+ }
372
+ async function writeSoulProfile(profile) {
373
+ const next = {
374
+ ...profile,
375
+ assistantDisplayName: profile.assistantDisplayName || "Lydia",
376
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
377
+ };
378
+ const content = [
379
+ "# Soul",
380
+ "",
381
+ "Core identity notes for Lydia chat.",
382
+ "",
383
+ "## Identity",
384
+ `- User display name: ${next.userDisplayName || ""}`,
385
+ `- Assistant display name: ${next.assistantDisplayName}`,
386
+ `- Updated at: ${next.updatedAt}`,
387
+ ""
388
+ ].join("\n");
389
+ await writeFile2(soulPath, content, "utf-8");
390
+ }
391
+ function normalizeSoulName(value) {
392
+ if (!value) return void 0;
393
+ const cleaned = value.replace(/[*`"'“”‘’<>]/g, "").replace(/[。!!,.,~~::]+$/g, "").replace(/^(叫|是)/, "").replace(/了$/g, "").trim();
394
+ if (!cleaned || cleaned.length > 24) return void 0;
395
+ return cleaned;
396
+ }
397
+ function inferSoulFromMessage(message) {
398
+ const userPatterns = [
399
+ /(?:从现在起|以后)?我(?:就)?叫\s*[::]?\s*([^\n,。,.!!]+)/i,
400
+ /(?:从现在起|以后)?我(?:就)?是\s*[::]?\s*([^\n,。,.!!]+)/i,
401
+ /(?:从现在起|以后)?(?:你|您)(?:可以|就)?叫我\s*[::]?\s*([^\n,。,.!!]+)/i,
402
+ /(?:请|就)?称呼我\s*[::]?\s*([^\n,。,.!!]+)/i,
403
+ /\bcall me\s+([a-zA-Z][a-zA-Z0-9 _-]{0,23})/i
404
+ ];
405
+ const assistantPatterns = [
406
+ /(?:你|您)(?:以后|就)?叫\s*[::]?\s*([^\n,。,.!!]+)/i,
407
+ /(?:你的名字是|你叫)\s*[::]?\s*([^\n,。,.!!]+)/i,
408
+ /\byour name is\s+([a-zA-Z][a-zA-Z0-9 _-]{0,23})/i
409
+ ];
410
+ let userDisplayName;
411
+ let assistantDisplayName;
412
+ for (const pattern of userPatterns) {
413
+ const value = normalizeSoulName(message.match(pattern)?.[1]);
414
+ if (value) {
415
+ userDisplayName = value;
416
+ break;
417
+ }
418
+ }
419
+ for (const pattern of assistantPatterns) {
420
+ const value = normalizeSoulName(message.match(pattern)?.[1]);
421
+ if (value) {
422
+ assistantDisplayName = value;
423
+ break;
424
+ }
425
+ }
426
+ return { userDisplayName, assistantDisplayName };
427
+ }
428
+ async function updateSoulFromMessage(message) {
429
+ const current = await readSoulProfile();
430
+ const inferred = inferSoulFromMessage(message);
431
+ if (!inferred.userDisplayName && !inferred.assistantDisplayName) {
432
+ return current;
433
+ }
434
+ await writeSoulProfile({
435
+ userDisplayName: inferred.userDisplayName || current.userDisplayName,
436
+ assistantDisplayName: inferred.assistantDisplayName || current.assistantDisplayName || "Lydia"
437
+ });
438
+ return readSoulProfile();
439
+ }
440
+ async function createRoutedAgent(llm) {
441
+ const currentConfig = await configLoader.load();
442
+ const routedStrategy = await shadowRouter.selectStrategy(currentConfig);
443
+ const agent = new Agent(
444
+ llm,
445
+ routedStrategy.path ? { strategyPathOverride: routedStrategy.path } : {}
446
+ );
447
+ return { agent, routedStrategy };
448
+ }
449
+ function formatErrorMessage(error) {
450
+ if (error instanceof Error) return error.message;
451
+ if (typeof error === "string") return error;
452
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
453
+ return error.message;
454
+ }
455
+ return String(error);
456
+ }
457
+ async function refreshRuntimeConfig(force = false) {
458
+ const now = Date.now();
459
+ if (!force && now - lastConfigRefreshAt < 5e3) return;
460
+ const config = await configLoader.load();
461
+ lastConfigRefreshAt = now;
462
+ const configuredToken = String(config.server?.apiToken || "").trim();
463
+ const envToken = String(process.env.LYDIA_API_TOKEN || "").trim();
464
+ const apiToken = envToken || configuredToken;
465
+ const sessionTtlHours = Math.max(1, Number(config.server?.sessionTtlHours || 24));
466
+ authState = {
467
+ required: apiToken.length > 0,
468
+ apiToken,
469
+ sessionTtlMs: sessionTtlHours * 60 * 60 * 1e3
470
+ };
471
+ const checkpointTtlHours = Math.max(1, Number(config.memory?.checkpointTtlHours || 24));
472
+ const observationTtlHours = Math.max(1, Number(config.memory?.observationFrameTtlHours || 24 * 7));
473
+ memoryManager.cleanupStaleCheckpoints(checkpointTtlHours * 60 * 60 * 1e3);
474
+ memoryManager.cleanupStaleObservationFrames(observationTtlHours * 60 * 60 * 1e3);
475
+ }
476
+ refreshRuntimeConfig(true).catch(() => {
477
+ });
478
+ app.use("/api/*", async (c, next) => {
479
+ await refreshRuntimeConfig();
480
+ if (!authState.required || isPublicApiPath(c.req.path)) {
481
+ await next();
482
+ return;
483
+ }
484
+ const bearer = extractBearerToken(c.req.header("authorization"));
485
+ const sessionId = c.req.header("x-lydia-session");
486
+ if (bearer === authState.apiToken || isValidSession(sessionId)) {
487
+ await next();
488
+ return;
489
+ }
490
+ return c.json({ error: "Unauthorized" }, 401);
491
+ });
492
+ app.post("/api/auth/session", async (c) => {
493
+ await refreshRuntimeConfig();
494
+ if (!authState.required) {
495
+ return c.json({
496
+ ok: true,
497
+ authRequired: false,
498
+ message: "API token auth is disabled."
499
+ });
500
+ }
501
+ let body;
502
+ try {
503
+ body = await c.req.json();
504
+ } catch {
505
+ body = {};
506
+ }
507
+ const providedToken = typeof body?.token === "string" ? body.token.trim() : "";
508
+ if (!providedToken || providedToken !== authState.apiToken) {
509
+ return c.json({ error: "Invalid API token" }, 401);
510
+ }
511
+ const sessionId = `lsess-${randomUUID()}`;
512
+ const expiresAt = Date.now() + authState.sessionTtlMs;
513
+ apiSessions.set(sessionId, expiresAt);
514
+ return c.json({
515
+ ok: true,
516
+ authRequired: true,
517
+ sessionId,
518
+ expiresAt
519
+ });
520
+ });
521
+ app.get("/api/status", (c) => {
522
+ return c.json({
523
+ status: "ok",
524
+ version: "0.1.0",
525
+ pid: process.pid,
526
+ host: DEFAULT_HOST,
527
+ port,
528
+ baseUrl: getBaseUrl(port, DEFAULT_HOST),
529
+ uptimeMs: Math.round(process.uptime() * 1e3),
530
+ startedAt: new Date(Date.now() - Math.round(process.uptime() * 1e3)).toISOString(),
531
+ memory_db: dbPath
532
+ });
533
+ });
534
+ app.get("/api/setup", async (c) => {
535
+ const { configPath, strategyPath } = getSetupPaths();
536
+ const ready = existsSync2(configPath) && existsSync2(strategyPath);
537
+ const config = await configLoader.load();
538
+ const hasConfiguredKey = Boolean(config.llm.openaiApiKey || config.llm.anthropicApiKey);
539
+ const hasEnvKey = Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY);
540
+ return c.json({
541
+ ready,
542
+ configPath,
543
+ strategyPath,
544
+ llmConfigured: hasConfiguredKey || hasEnvKey,
545
+ provider: config.llm.provider || "auto"
546
+ });
547
+ });
548
+ app.post("/api/setup/init", async (c) => {
549
+ try {
550
+ const paths = await ensureLocalWorkspace();
551
+ return c.json({
552
+ ok: true,
553
+ ready: existsSync2(paths.configPath) && existsSync2(paths.strategyPath),
554
+ ...paths
555
+ });
556
+ } catch (error) {
557
+ return c.json({ error: error?.message || "Failed to initialize workspace." }, 500);
558
+ }
559
+ });
560
+ app.get("/api/soul", async (c) => {
561
+ const soul = await readSoulProfile();
562
+ return c.json({
563
+ userDisplayName: soul.userDisplayName,
564
+ assistantDisplayName: soul.assistantDisplayName || "Lydia",
565
+ updatedAt: soul.updatedAt
566
+ });
567
+ });
568
+ app.get("/api/setup/config", async (c) => {
569
+ const { configPath, strategyPath } = getSetupPaths();
570
+ const config = await configLoader.load();
571
+ const openaiKey = config.llm.openaiApiKey || process.env.OPENAI_API_KEY || "";
572
+ const anthropicKey = config.llm.anthropicApiKey || process.env.ANTHROPIC_API_KEY || "";
573
+ return c.json({
574
+ ready: existsSync2(configPath) && existsSync2(strategyPath),
575
+ llm: {
576
+ provider: config.llm.provider,
577
+ defaultModel: config.llm.defaultModel,
578
+ fallbackOrder: config.llm.fallbackOrder,
579
+ openaiBaseUrl: config.llm.openaiBaseUrl,
580
+ anthropicBaseUrl: config.llm.anthropicBaseUrl,
581
+ ollamaBaseUrl: config.llm.ollamaBaseUrl,
582
+ openaiApiKeySet: Boolean(openaiKey),
583
+ anthropicApiKeySet: Boolean(anthropicKey),
584
+ openaiApiKeyMasked: maskSecret(openaiKey),
585
+ anthropicApiKeyMasked: maskSecret(anthropicKey)
586
+ },
587
+ server: {
588
+ hasApiToken: Boolean(config.server.apiToken || process.env.LYDIA_API_TOKEN),
589
+ sessionTtlHours: config.server.sessionTtlHours
590
+ },
591
+ memory: {
592
+ checkpointTtlHours: config.memory.checkpointTtlHours,
593
+ observationFrameTtlHours: config.memory.observationFrameTtlHours
594
+ }
595
+ });
596
+ });
597
+ app.post("/api/setup/config", async (c) => {
598
+ let body;
599
+ try {
600
+ body = await c.req.json();
601
+ } catch {
602
+ body = {};
603
+ }
604
+ const llmInput = body?.llm || {};
605
+ const serverInput = body?.server || {};
606
+ const memoryInput = body?.memory || {};
607
+ const provider = pickString(llmInput.provider);
608
+ const defaultModel = pickString(llmInput.defaultModel);
609
+ const openaiBaseUrl = pickString(llmInput.openaiBaseUrl);
610
+ const anthropicBaseUrl = pickString(llmInput.anthropicBaseUrl);
611
+ const ollamaBaseUrl = pickString(llmInput.ollamaBaseUrl);
612
+ const fallbackOrder = Array.isArray(llmInput.fallbackOrder) ? llmInput.fallbackOrder.filter((item) => typeof item === "string") : void 0;
613
+ const providerSet = /* @__PURE__ */ new Set(["anthropic", "openai", "ollama", "mock", "auto"]);
614
+ if (provider && !providerSet.has(provider)) {
615
+ return c.json({ error: `Unsupported provider: ${provider}` }, 400);
616
+ }
617
+ const updateLlm = {};
618
+ if (provider !== void 0) updateLlm.provider = provider;
619
+ if (defaultModel !== void 0) updateLlm.defaultModel = defaultModel;
620
+ if (fallbackOrder !== void 0) updateLlm.fallbackOrder = fallbackOrder;
621
+ if (openaiBaseUrl !== void 0) updateLlm.openaiBaseUrl = openaiBaseUrl;
622
+ if (anthropicBaseUrl !== void 0) updateLlm.anthropicBaseUrl = anthropicBaseUrl;
623
+ if (ollamaBaseUrl !== void 0) updateLlm.ollamaBaseUrl = ollamaBaseUrl;
624
+ if (Object.prototype.hasOwnProperty.call(llmInput, "openaiApiKey")) {
625
+ updateLlm.openaiApiKey = String(llmInput.openaiApiKey || "").trim();
626
+ }
627
+ if (Object.prototype.hasOwnProperty.call(llmInput, "anthropicApiKey")) {
628
+ updateLlm.anthropicApiKey = String(llmInput.anthropicApiKey || "").trim();
629
+ }
630
+ const updateServer = {};
631
+ if (Object.prototype.hasOwnProperty.call(serverInput, "apiToken")) {
632
+ updateServer.apiToken = String(serverInput.apiToken || "").trim();
633
+ }
634
+ if (Object.prototype.hasOwnProperty.call(serverInput, "sessionTtlHours")) {
635
+ const sessionTtlHours = Number(serverInput.sessionTtlHours);
636
+ if (!Number.isNaN(sessionTtlHours) && sessionTtlHours > 0) {
637
+ updateServer.sessionTtlHours = sessionTtlHours;
638
+ }
639
+ }
640
+ const updateMemory = {};
641
+ if (Object.prototype.hasOwnProperty.call(memoryInput, "checkpointTtlHours")) {
642
+ const checkpointTtlHours = Number(memoryInput.checkpointTtlHours);
643
+ if (!Number.isNaN(checkpointTtlHours) && checkpointTtlHours > 0) {
644
+ updateMemory.checkpointTtlHours = checkpointTtlHours;
645
+ }
646
+ }
647
+ if (Object.prototype.hasOwnProperty.call(memoryInput, "observationFrameTtlHours")) {
648
+ const observationFrameTtlHours = Number(memoryInput.observationFrameTtlHours);
649
+ if (!Number.isNaN(observationFrameTtlHours) && observationFrameTtlHours > 0) {
650
+ updateMemory.observationFrameTtlHours = observationFrameTtlHours;
651
+ }
652
+ }
653
+ try {
654
+ const next = await configLoader.update({
655
+ llm: updateLlm,
656
+ server: updateServer,
657
+ memory: updateMemory
658
+ });
659
+ await refreshRuntimeConfig(true);
660
+ return c.json({
661
+ ok: true,
662
+ llm: {
663
+ provider: next.llm.provider,
664
+ defaultModel: next.llm.defaultModel,
665
+ fallbackOrder: next.llm.fallbackOrder,
666
+ openaiBaseUrl: next.llm.openaiBaseUrl,
667
+ anthropicBaseUrl: next.llm.anthropicBaseUrl,
668
+ ollamaBaseUrl: next.llm.ollamaBaseUrl,
669
+ openaiApiKeySet: Boolean(next.llm.openaiApiKey),
670
+ anthropicApiKeySet: Boolean(next.llm.anthropicApiKey),
671
+ openaiApiKeyMasked: maskSecret(next.llm.openaiApiKey),
672
+ anthropicApiKeyMasked: maskSecret(next.llm.anthropicApiKey)
673
+ },
674
+ server: {
675
+ hasApiToken: Boolean(next.server.apiToken),
676
+ sessionTtlHours: next.server.sessionTtlHours
677
+ },
678
+ memory: {
679
+ checkpointTtlHours: next.memory.checkpointTtlHours,
680
+ observationFrameTtlHours: next.memory.observationFrameTtlHours
681
+ }
682
+ });
683
+ } catch (error) {
684
+ return c.json({ error: error?.message || "Failed to update setup config." }, 400);
685
+ }
686
+ });
687
+ app.post("/api/setup/test-llm", async (c) => {
688
+ let body;
689
+ try {
690
+ body = await c.req.json();
691
+ } catch {
692
+ body = {};
693
+ }
694
+ const probe = Boolean(body?.probe);
695
+ const llmInput = body?.llm || {};
696
+ const testProvider = pickString(llmInput.provider);
697
+ const testDefaultModel = pickString(llmInput.defaultModel);
698
+ const testOpenaiApiKey = pickString(llmInput.openaiApiKey);
699
+ const testAnthropicApiKey = pickString(llmInput.anthropicApiKey);
700
+ const testOpenaiBaseUrl = pickString(llmInput.openaiBaseUrl);
701
+ const testAnthropicBaseUrl = pickString(llmInput.anthropicBaseUrl);
702
+ const testOllamaBaseUrl = pickString(llmInput.ollamaBaseUrl);
703
+ const testFallbackOrder = Array.isArray(llmInput.fallbackOrder) ? llmInput.fallbackOrder.filter((item) => typeof item === "string") : void 0;
704
+ try {
705
+ const llm = await createLLMFromConfig({
706
+ provider: testProvider,
707
+ model: testDefaultModel,
708
+ llmOverrides: {
709
+ provider: testProvider,
710
+ defaultModel: testDefaultModel,
711
+ fallbackOrder: testFallbackOrder,
712
+ openaiApiKey: Object.prototype.hasOwnProperty.call(llmInput, "openaiApiKey") ? testOpenaiApiKey : void 0,
713
+ anthropicApiKey: Object.prototype.hasOwnProperty.call(llmInput, "anthropicApiKey") ? testAnthropicApiKey : void 0,
714
+ openaiBaseUrl: testOpenaiBaseUrl,
715
+ anthropicBaseUrl: testAnthropicBaseUrl,
716
+ ollamaBaseUrl: testOllamaBaseUrl
717
+ }
718
+ });
719
+ if (probe) {
720
+ const timeoutMs = Number(body?.timeoutMs) > 0 ? Number(body.timeoutMs) : 15e3;
721
+ const response = await Promise.race([
722
+ llm.generate({
723
+ messages: [{ role: "user", content: "Reply with exactly: OK" }],
724
+ max_tokens: 16,
725
+ temperature: 0
726
+ }),
727
+ new Promise((_, reject) => setTimeout(() => reject(new Error("LLM probe timeout")), timeoutMs))
728
+ ]);
729
+ const text = response?.content?.filter((block) => block.type === "text")?.map((block) => block.text)?.join("\n")?.trim?.() || "";
730
+ return c.json({ ok: true, provider: llm.id, probeText: text.slice(0, 200) });
731
+ }
732
+ return c.json({ ok: true, provider: llm.id });
733
+ } catch (error) {
734
+ return c.json({ ok: false, error: error?.message || "Failed to initialize provider." }, 400);
735
+ }
736
+ });
737
+ app.get("/api/mcp/check", async (c) => {
738
+ const config = await configLoader.load();
739
+ const allServers = Object.entries(config.mcpServers || {});
740
+ const targetServer = c.req.query("server");
741
+ const timeoutMs = Number(c.req.query("timeoutMs")) || 15e3;
742
+ const retries = Math.max(0, Number(c.req.query("retries")) || 0);
743
+ const targets = targetServer ? allServers.filter(([id]) => id === targetServer) : allServers;
744
+ if (targets.length === 0) {
745
+ return c.json({
746
+ ok: false,
747
+ error: targetServer ? `MCP server "${targetServer}" not found in config.` : "No external MCP servers configured.",
748
+ results: []
749
+ }, 404);
750
+ }
751
+ const results = await checkMcpServers(
752
+ targets.map(([id, s]) => ({ id, command: s.command, args: s.args, env: s.env })),
753
+ { timeoutMs, retries }
754
+ );
755
+ return c.json({
756
+ ok: results.every((r) => r.ok),
757
+ timeoutMs,
758
+ retries,
759
+ results
760
+ });
761
+ });
762
+ app.get("/api/memory/facts", (c) => {
763
+ const limit = Number(c.req.query("limit")) || 100;
764
+ const tag = c.req.query("tag");
765
+ const facts = tag ? memoryManager.getFactsByTag(tag, limit) : memoryManager.getAllFacts(limit);
766
+ return c.json(facts);
767
+ });
768
+ app.get("/api/memory/approvals", (c) => {
769
+ const limit = Number(c.req.query("limit")) || 100;
770
+ const facts = memoryManager.getFactsByTag("risk_approval", limit);
771
+ return c.json(facts);
772
+ });
773
+ app.delete("/api/memory/approvals/:id", (c) => {
774
+ const id = Number(c.req.param("id"));
775
+ if (Number.isNaN(id)) return c.json({ error: "Invalid id" }, 400);
776
+ const ok = memoryManager.deleteFactById(id);
777
+ return c.json({ ok });
778
+ });
779
+ app.delete("/api/memory/approvals", (c) => {
780
+ const signature = c.req.query("signature");
781
+ if (!signature) return c.json({ error: "signature is required" }, 400);
782
+ const key = `risk_approval:${signature}`;
783
+ const ok = memoryManager.deleteFactByKey(key);
784
+ return c.json({ ok });
785
+ });
786
+ app.get("/api/replay", (c) => {
787
+ const limit = Number(c.req.query("limit")) || 50;
788
+ const episodes = memoryManager.listEpisodes(limit);
789
+ return c.json(episodes);
790
+ });
791
+ app.get("/api/strategy/proposals", (c) => {
792
+ const limit = Number(c.req.query("limit")) || 50;
793
+ const proposals = memoryManager.listStrategyProposals(limit).map((proposal) => {
794
+ let evaluationSummary = null;
795
+ if (proposal.evaluation_json) {
796
+ try {
797
+ const evaluation = JSON.parse(proposal.evaluation_json);
798
+ const replay = evaluation?.replay;
799
+ if (replay?.candidateSummary && replay?.baselineSummary && replay?.delta) {
800
+ evaluationSummary = {
801
+ candidateScore: replay.candidateScore,
802
+ baselineScore: replay.baselineScore,
803
+ tasksEvaluated: replay.tasksEvaluated,
804
+ candidateSummary: replay.candidateSummary,
805
+ baselineSummary: replay.baselineSummary,
806
+ delta: replay.delta,
807
+ validation: evaluation?.validation || null
808
+ };
809
+ } else if (evaluation?.validation) {
810
+ evaluationSummary = { validation: evaluation.validation };
811
+ }
812
+ } catch {
813
+ evaluationSummary = null;
814
+ }
815
+ }
816
+ return {
817
+ ...proposal,
818
+ evaluationSummary
819
+ };
820
+ });
821
+ return c.json(proposals);
822
+ });
823
+ app.get("/api/reports", (c) => {
824
+ const limit = Number(c.req.query("limit")) || 50;
825
+ const reports = memoryManager.listTaskReports(limit);
826
+ return c.json(reports);
827
+ });
828
+ app.get("/api/tasks", (c) => {
829
+ const limit = Number(c.req.query("limit")) || 50;
830
+ const offset = Number(c.req.query("offset")) || 0;
831
+ const statusFilter = c.req.query("status") || "";
832
+ const search = c.req.query("search") || "";
833
+ const liveItems = [];
834
+ for (const run of runs.values()) {
835
+ liveItems.push({
836
+ id: run.runId,
837
+ input: run.input,
838
+ status: run.status,
839
+ createdAt: run.startedAt,
840
+ duration: run.completedAt ? run.completedAt - run.startedAt : void 0,
841
+ summary: run.result?.substring(0, 120),
842
+ persisted: false
843
+ });
844
+ }
845
+ const dbReports = memoryManager.listTaskReports(limit + 50);
846
+ const dbItems = dbReports.map((r) => {
847
+ let report = null;
848
+ try {
849
+ report = JSON.parse(r.report_json);
850
+ } catch {
851
+ }
852
+ return {
853
+ id: `report-${r.id}`,
854
+ input: report?.intentSummary || r.task_id || "Unknown task",
855
+ status: report?.success ? "completed" : "failed",
856
+ createdAt: r.created_at,
857
+ duration: report?.duration,
858
+ summary: report?.summary || report?.intentSummary,
859
+ persisted: true
860
+ };
861
+ });
862
+ const liveTaskIds = new Set(liveItems.map((i) => i.id));
863
+ const merged = [
864
+ ...liveItems,
865
+ ...dbItems.filter((d) => !liveTaskIds.has(d.id))
866
+ ];
867
+ let filtered = merged;
868
+ if (statusFilter) {
869
+ filtered = filtered.filter((i) => i.status === statusFilter);
870
+ }
871
+ if (search) {
872
+ const q = search.toLowerCase();
873
+ filtered = filtered.filter(
874
+ (i) => i.input?.toLowerCase().includes(q) || i.summary?.toLowerCase().includes(q)
875
+ );
876
+ }
877
+ filtered.sort((a, b) => {
878
+ if (a.status === "running" && b.status !== "running") return -1;
879
+ if (b.status === "running" && a.status !== "running") return 1;
880
+ return (b.createdAt || 0) - (a.createdAt || 0);
881
+ });
882
+ const paginated = filtered.slice(offset, offset + limit);
883
+ return c.json({
884
+ items: paginated,
885
+ total: filtered.length,
886
+ activeRunId
887
+ });
888
+ });
889
+ app.get("/api/tasks/:id/detail", (c) => {
890
+ const id = c.req.param("id");
891
+ const liveRun = runs.get(id);
892
+ if (liveRun) {
893
+ const evidence = liveRun.taskId ? memoryManager.listObservationFramesByTask(liveRun.taskId, 100) : [];
894
+ return c.json({
895
+ id: liveRun.runId,
896
+ input: liveRun.input,
897
+ status: liveRun.status,
898
+ createdAt: liveRun.startedAt,
899
+ completedAt: liveRun.completedAt,
900
+ duration: liveRun.completedAt ? liveRun.completedAt - liveRun.startedAt : void 0,
901
+ result: liveRun.result,
902
+ error: liveRun.error,
903
+ pendingPrompt: liveRun.pendingPrompt,
904
+ report: null,
905
+ traces: null,
906
+ episode: null,
907
+ evidence
908
+ });
909
+ }
910
+ if (id.startsWith("report-")) {
911
+ const reportId = Number(id.slice("report-".length));
912
+ if (Number.isNaN(reportId)) return c.json({ error: "Invalid id" }, 400);
913
+ const dbReports = memoryManager.listTaskReports(500);
914
+ const dbReport = dbReports.find((r) => r.id === reportId);
915
+ if (!dbReport) return c.json({ error: "Task not found" }, 404);
916
+ let report = null;
917
+ try {
918
+ report = JSON.parse(dbReport.report_json);
919
+ } catch {
920
+ }
921
+ let episode = null;
922
+ let traces = [];
923
+ if (dbReport.task_id) {
924
+ const episodes = memoryManager.listEpisodes(200);
925
+ episode = episodes.find((e) => e.input?.includes(dbReport.task_id)) || null;
926
+ if (episode?.id) {
927
+ const rawTraces = memoryManager.getTraces(episode.id);
928
+ traces = rawTraces.map((t) => {
929
+ let args = null;
930
+ let output2 = null;
931
+ try {
932
+ args = JSON.parse(t.tool_args);
933
+ } catch {
934
+ }
935
+ try {
936
+ output2 = JSON.parse(t.tool_output);
937
+ } catch {
938
+ }
939
+ return { ...t, args, output: output2 };
940
+ });
941
+ }
942
+ }
943
+ const evidence = dbReport.task_id ? memoryManager.listObservationFramesByTask(dbReport.task_id, 200) : [];
944
+ return c.json({
945
+ id,
946
+ input: report?.intentSummary || dbReport.task_id || "Unknown task",
947
+ status: report?.success ? "completed" : "failed",
948
+ createdAt: dbReport.created_at,
949
+ completedAt: dbReport.created_at,
950
+ duration: report?.duration,
951
+ report,
952
+ traces: traces.length > 0 ? traces : null,
953
+ episode,
954
+ evidence
955
+ });
956
+ }
957
+ return c.json({ error: "Task not found" }, 404);
958
+ });
959
+ app.post("/api/tasks/run", async (c) => {
960
+ if (activeRunId) {
961
+ return c.json({ error: "A task is already running. Please wait." }, 409);
962
+ }
963
+ let body;
964
+ try {
965
+ body = await c.req.json();
966
+ } catch {
967
+ body = {};
968
+ }
969
+ const inputText = typeof body?.input === "string" ? body.input.trim() : "";
970
+ if (!inputText) {
971
+ return c.json({ error: "input is required" }, 400);
972
+ }
973
+ let llm;
974
+ try {
975
+ llm = await createLLMFromConfig();
976
+ } catch (error) {
977
+ return c.json({ error: error.message || "Failed to initialize provider." }, 500);
978
+ }
979
+ const runId = `run-${Date.now()}`;
980
+ const runState = {
981
+ runId,
982
+ input: inputText,
983
+ status: "running",
984
+ startedAt: Date.now()
985
+ };
986
+ runs.set(runId, runState);
987
+ activeRunId = runId;
988
+ try {
989
+ const { agent, routedStrategy } = await createRoutedAgent(llm);
990
+ runState.agent = agent;
991
+ runState.strategyPath = routedStrategy.path;
992
+ runState.strategyRole = routedStrategy.role;
993
+ runState.strategyId = routedStrategy.strategyId;
994
+ runState.strategyVersion = routedStrategy.strategyVersion;
995
+ agent.on("task:start", (task) => {
996
+ runState.taskId = task.id;
997
+ broadcastWs({
998
+ type: "task:start",
999
+ data: {
1000
+ runId,
1001
+ taskId: task.id,
1002
+ description: task.description,
1003
+ strategy: {
1004
+ role: routedStrategy.role,
1005
+ path: routedStrategy.path,
1006
+ id: routedStrategy.strategyId,
1007
+ version: routedStrategy.strategyVersion,
1008
+ reason: routedStrategy.reason
1009
+ }
1010
+ },
1011
+ timestamp: Date.now()
1012
+ });
1013
+ });
1014
+ agent.on("stream:text", (text) => {
1015
+ broadcastWs({ type: "stream:text", data: { runId, text }, timestamp: Date.now() });
1016
+ });
1017
+ agent.on("stream:thinking", (thinking) => {
1018
+ broadcastWs({ type: "stream:thinking", data: { runId, thinking }, timestamp: Date.now() });
1019
+ });
1020
+ agent.on("message", (msg) => {
1021
+ broadcastWs({ type: "message", data: { runId, ...msg }, timestamp: Date.now() });
1022
+ });
1023
+ agent.on("tool:start", (data) => {
1024
+ broadcastWs({ type: "tool:start", data: { runId, ...data }, timestamp: Date.now() });
1025
+ });
1026
+ agent.on("tool:complete", (data) => {
1027
+ broadcastWs({ type: "tool:complete", data: { runId, ...data }, timestamp: Date.now() });
1028
+ });
1029
+ agent.on("tool:error", (data) => {
1030
+ broadcastWs({ type: "tool:error", data: { runId, ...data }, timestamp: Date.now() });
1031
+ });
1032
+ agent.on("retry", (data) => {
1033
+ broadcastWs({ type: "retry", data: { runId, ...data }, timestamp: Date.now() });
1034
+ });
1035
+ agent.on("interaction_request", (req) => {
1036
+ runState.pendingPrompt = { id: req.id, prompt: req.prompt };
1037
+ broadcastWs({ type: "interaction_request", data: { runId, id: req.id, prompt: req.prompt }, timestamp: Date.now() });
1038
+ });
1039
+ agent.on("checkpoint:saved", (data) => {
1040
+ broadcastWs({ type: "checkpoint:saved", data: { runId, ...data }, timestamp: Date.now() });
1041
+ });
1042
+ agent.on("checkpoint:error", (data) => {
1043
+ broadcastWs({
1044
+ type: "checkpoint:error",
1045
+ data: { runId, ...data || {}, error: formatErrorMessage(data?.error) },
1046
+ timestamp: Date.now()
1047
+ });
1048
+ });
1049
+ agent.on("phase:start", (phase) => {
1050
+ broadcastWs({ type: "phase:start", data: { runId, phase }, timestamp: Date.now() });
1051
+ });
1052
+ agent.on("phase:end", (phase) => {
1053
+ broadcastWs({ type: "phase:end", data: { runId, phase }, timestamp: Date.now() });
1054
+ });
1055
+ agent.on("intent", (intent) => {
1056
+ broadcastWs({ type: "intent", data: { runId, intent }, timestamp: Date.now() });
1057
+ });
1058
+ agent.on("plan", (steps) => {
1059
+ broadcastWs({ type: "plan", data: { runId, steps }, timestamp: Date.now() });
1060
+ });
1061
+ agent.on("plan:error", (error) => {
1062
+ broadcastWs({ type: "plan:error", data: { runId, error: formatErrorMessage(error) }, timestamp: Date.now() });
1063
+ });
1064
+ agent.on("max_iterations", (data) => {
1065
+ broadcastWs({ type: "max_iterations", data: { runId, ...data || {} }, timestamp: Date.now() });
1066
+ });
1067
+ agent.on("skill:error", (error) => {
1068
+ broadcastWs({ type: "skill:error", data: { runId, error: formatErrorMessage(error) }, timestamp: Date.now() });
1069
+ });
1070
+ agent.on("thinking", (thinking) => {
1071
+ broadcastWs({ type: "thinking", data: { runId, thinking }, timestamp: Date.now() });
1072
+ });
1073
+ agent.on("computer-use:session.start", (data) => {
1074
+ broadcastWs({ type: "computer-use:session.start", data: { runId, ...data }, timestamp: Date.now() });
1075
+ });
1076
+ agent.on("computer-use:action.dispatch", (data) => {
1077
+ broadcastWs({ type: "computer-use:action.dispatch", data: { runId, ...data }, timestamp: Date.now() });
1078
+ });
1079
+ agent.on("computer-use:observation.collect", (data) => {
1080
+ broadcastWs({ type: "computer-use:observation.collect", data: { runId, ...data }, timestamp: Date.now() });
1081
+ });
1082
+ agent.on("computer-use:checkpoint.save", (data) => {
1083
+ broadcastWs({ type: "computer-use:checkpoint.save", data: { runId, ...data }, timestamp: Date.now() });
1084
+ });
1085
+ agent.on("computer-use:verification", (data) => {
1086
+ broadcastWs({ type: "computer-use:verification", data: { runId, ...data }, timestamp: Date.now() });
1087
+ });
1088
+ agent.on("computer-use:session.end", (data) => {
1089
+ broadcastWs({ type: "computer-use:session.end", data: { runId, ...data }, timestamp: Date.now() });
1090
+ });
1091
+ agent.run(inputText, runId).then(async (task) => {
1092
+ runState.taskId = task.id;
1093
+ runState.status = task.status === "completed" ? "completed" : "failed";
1094
+ runState.result = task.result;
1095
+ runState.completedAt = Date.now();
1096
+ runState.pendingPrompt = void 0;
1097
+ try {
1098
+ const latestConfig = await configLoader.load();
1099
+ const promotion = await shadowRouter.evaluateAutoPromotion(latestConfig);
1100
+ if (promotion) {
1101
+ const nextCandidates = (latestConfig.strategy.shadowCandidatePaths || []).filter((p) => p !== promotion.candidatePath);
1102
+ await configLoader.update({
1103
+ strategy: {
1104
+ activePath: promotion.candidatePath,
1105
+ shadowCandidatePaths: nextCandidates
1106
+ }
1107
+ });
1108
+ memoryManager.rememberFact(
1109
+ JSON.stringify({
1110
+ promotedAt: Date.now(),
1111
+ candidatePath: promotion.candidatePath,
1112
+ candidateId: promotion.candidateId,
1113
+ candidateVersion: promotion.candidateVersion,
1114
+ baselineId: promotion.baselineId,
1115
+ baselineVersion: promotion.baselineVersion,
1116
+ successImprovement: promotion.successImprovement,
1117
+ pValue: promotion.pValue
1118
+ }),
1119
+ `strategy.autopromote.${Date.now()}`,
1120
+ ["strategy", "shadow", "autopromote"]
1121
+ );
1122
+ broadcastWs({
1123
+ type: "strategy:autopromote",
1124
+ data: {
1125
+ runId,
1126
+ candidatePath: promotion.candidatePath,
1127
+ candidateId: promotion.candidateId,
1128
+ candidateVersion: promotion.candidateVersion,
1129
+ successImprovement: promotion.successImprovement,
1130
+ pValue: promotion.pValue
1131
+ },
1132
+ timestamp: Date.now()
1133
+ });
1134
+ }
1135
+ } catch {
1136
+ }
1137
+ broadcastWs({ type: "task:complete", data: { runId, taskId: task.id, status: task.status, result: task.result }, timestamp: Date.now() });
1138
+ }).catch((error) => {
1139
+ runState.status = "failed";
1140
+ runState.error = error?.message || "Task failed.";
1141
+ runState.completedAt = Date.now();
1142
+ runState.pendingPrompt = void 0;
1143
+ broadcastWs({ type: "task:error", data: { runId, error: runState.error }, timestamp: Date.now() });
1144
+ }).finally(() => {
1145
+ activeRunId = null;
1146
+ runState.agent = void 0;
1147
+ });
1148
+ return c.json({ runId });
1149
+ } catch (error) {
1150
+ activeRunId = null;
1151
+ runs.delete(runId);
1152
+ return c.json({ error: error.message || "Task failed." }, 500);
1153
+ }
1154
+ });
1155
+ app.get("/api/tasks/:id/status", (c) => {
1156
+ const runId = c.req.param("id");
1157
+ const run = runs.get(runId);
1158
+ if (!run) {
1159
+ return c.json({ error: "Task not found" }, 404);
1160
+ }
1161
+ return c.json({
1162
+ runId: run.runId,
1163
+ input: run.input,
1164
+ status: run.status,
1165
+ taskId: run.taskId,
1166
+ strategyPath: run.strategyPath,
1167
+ strategyRole: run.strategyRole,
1168
+ strategyId: run.strategyId,
1169
+ strategyVersion: run.strategyVersion,
1170
+ result: run.result,
1171
+ error: run.error,
1172
+ startedAt: run.startedAt,
1173
+ completedAt: run.completedAt,
1174
+ duration: run.completedAt ? run.completedAt - run.startedAt : Date.now() - run.startedAt,
1175
+ pendingPrompt: run.pendingPrompt
1176
+ });
1177
+ });
1178
+ app.post("/api/tasks/:id/respond", async (c) => {
1179
+ const runId = c.req.param("id");
1180
+ const run = runs.get(runId);
1181
+ if (!run) {
1182
+ return c.json({ error: "Task not found" }, 404);
1183
+ }
1184
+ if (!run.pendingPrompt) {
1185
+ return c.json({ error: "No pending prompt" }, 409);
1186
+ }
1187
+ let body;
1188
+ try {
1189
+ body = await c.req.json();
1190
+ } catch {
1191
+ body = {};
1192
+ }
1193
+ const response = typeof body?.response === "string" ? body.response.trim() : "";
1194
+ if (!response) {
1195
+ return c.json({ error: "response is required" }, 400);
1196
+ }
1197
+ if (!run.agent) {
1198
+ return c.json({ error: "Agent not available" }, 500);
1199
+ }
1200
+ const promptId = run.pendingPrompt.id;
1201
+ run.pendingPrompt = void 0;
1202
+ run.agent.resolveInteraction(promptId, response);
1203
+ return c.json({ ok: true });
1204
+ });
1205
+ app.get("/api/tasks/resumable", (c) => {
1206
+ const checkpoints = memoryManager.listCheckpoints();
1207
+ const items = checkpoints.map((cp) => ({
1208
+ taskId: cp.taskId,
1209
+ runId: cp.runId,
1210
+ input: cp.input,
1211
+ iteration: cp.iteration,
1212
+ taskCreatedAt: cp.taskCreatedAt,
1213
+ updatedAt: cp.updatedAt
1214
+ }));
1215
+ return c.json({ items });
1216
+ });
1217
+ app.post("/api/tasks/:id/resume", async (c) => {
1218
+ if (activeRunId) {
1219
+ return c.json({ error: "A task is already running. Please wait." }, 409);
1220
+ }
1221
+ const taskId = c.req.param("id");
1222
+ const checkpoint = memoryManager.loadCheckpoint(taskId);
1223
+ if (!checkpoint) {
1224
+ return c.json({ error: "No checkpoint found for this task." }, 404);
1225
+ }
1226
+ let llm;
1227
+ try {
1228
+ llm = await createLLMFromConfig();
1229
+ } catch (error) {
1230
+ return c.json({ error: error.message || "Failed to initialize provider." }, 500);
1231
+ }
1232
+ const runId = checkpoint.runId || `resume-${Date.now()}`;
1233
+ const runState = {
1234
+ runId,
1235
+ input: checkpoint.input,
1236
+ status: "running",
1237
+ startedAt: Date.now(),
1238
+ taskId: checkpoint.taskId
1239
+ };
1240
+ runs.set(runId, runState);
1241
+ activeRunId = runId;
1242
+ try {
1243
+ const { agent, routedStrategy } = await createRoutedAgent(llm);
1244
+ runState.agent = agent;
1245
+ runState.strategyPath = routedStrategy.path;
1246
+ runState.strategyRole = routedStrategy.role;
1247
+ runState.strategyId = routedStrategy.strategyId;
1248
+ runState.strategyVersion = routedStrategy.strategyVersion;
1249
+ agent.on("task:resume", (data) => {
1250
+ broadcastWs({
1251
+ type: "task:resume",
1252
+ data: {
1253
+ runId,
1254
+ ...data,
1255
+ strategy: {
1256
+ role: routedStrategy.role,
1257
+ path: routedStrategy.path,
1258
+ id: routedStrategy.strategyId,
1259
+ version: routedStrategy.strategyVersion,
1260
+ reason: routedStrategy.reason
1261
+ }
1262
+ },
1263
+ timestamp: Date.now()
1264
+ });
1265
+ });
1266
+ agent.on("stream:text", (text) => {
1267
+ broadcastWs({ type: "stream:text", data: { runId, text }, timestamp: Date.now() });
1268
+ });
1269
+ agent.on("stream:thinking", (thinking) => {
1270
+ broadcastWs({ type: "stream:thinking", data: { runId, thinking }, timestamp: Date.now() });
1271
+ });
1272
+ agent.on("message", (msg) => {
1273
+ broadcastWs({ type: "message", data: { runId, ...msg }, timestamp: Date.now() });
1274
+ });
1275
+ agent.on("tool:start", (data) => {
1276
+ broadcastWs({ type: "tool:start", data: { runId, ...data }, timestamp: Date.now() });
1277
+ });
1278
+ agent.on("tool:complete", (data) => {
1279
+ broadcastWs({ type: "tool:complete", data: { runId, ...data }, timestamp: Date.now() });
1280
+ });
1281
+ agent.on("tool:error", (data) => {
1282
+ broadcastWs({ type: "tool:error", data: { runId, ...data }, timestamp: Date.now() });
1283
+ });
1284
+ agent.on("retry", (data) => {
1285
+ broadcastWs({ type: "retry", data: { runId, ...data }, timestamp: Date.now() });
1286
+ });
1287
+ agent.on("interaction_request", (req) => {
1288
+ runState.pendingPrompt = { id: req.id, prompt: req.prompt };
1289
+ broadcastWs({ type: "interaction_request", data: { runId, id: req.id, prompt: req.prompt }, timestamp: Date.now() });
1290
+ });
1291
+ agent.on("checkpoint:saved", (data) => {
1292
+ broadcastWs({ type: "checkpoint:saved", data: { runId, ...data }, timestamp: Date.now() });
1293
+ });
1294
+ agent.on("checkpoint:error", (data) => {
1295
+ broadcastWs({
1296
+ type: "checkpoint:error",
1297
+ data: { runId, ...data || {}, error: formatErrorMessage(data?.error) },
1298
+ timestamp: Date.now()
1299
+ });
1300
+ });
1301
+ agent.on("phase:start", (phase) => {
1302
+ broadcastWs({ type: "phase:start", data: { runId, phase }, timestamp: Date.now() });
1303
+ });
1304
+ agent.on("phase:end", (phase) => {
1305
+ broadcastWs({ type: "phase:end", data: { runId, phase }, timestamp: Date.now() });
1306
+ });
1307
+ agent.on("intent", (intent) => {
1308
+ broadcastWs({ type: "intent", data: { runId, intent }, timestamp: Date.now() });
1309
+ });
1310
+ agent.on("plan", (steps) => {
1311
+ broadcastWs({ type: "plan", data: { runId, steps }, timestamp: Date.now() });
1312
+ });
1313
+ agent.on("plan:error", (error) => {
1314
+ broadcastWs({ type: "plan:error", data: { runId, error: formatErrorMessage(error) }, timestamp: Date.now() });
1315
+ });
1316
+ agent.on("max_iterations", (data) => {
1317
+ broadcastWs({ type: "max_iterations", data: { runId, ...data || {} }, timestamp: Date.now() });
1318
+ });
1319
+ agent.on("skill:error", (error) => {
1320
+ broadcastWs({ type: "skill:error", data: { runId, error: formatErrorMessage(error) }, timestamp: Date.now() });
1321
+ });
1322
+ agent.on("thinking", (thinking) => {
1323
+ broadcastWs({ type: "thinking", data: { runId, thinking }, timestamp: Date.now() });
1324
+ });
1325
+ agent.on("computer-use:session.start", (data) => {
1326
+ broadcastWs({ type: "computer-use:session.start", data: { runId, ...data }, timestamp: Date.now() });
1327
+ });
1328
+ agent.on("computer-use:action.dispatch", (data) => {
1329
+ broadcastWs({ type: "computer-use:action.dispatch", data: { runId, ...data }, timestamp: Date.now() });
1330
+ });
1331
+ agent.on("computer-use:observation.collect", (data) => {
1332
+ broadcastWs({ type: "computer-use:observation.collect", data: { runId, ...data }, timestamp: Date.now() });
1333
+ });
1334
+ agent.on("computer-use:checkpoint.save", (data) => {
1335
+ broadcastWs({ type: "computer-use:checkpoint.save", data: { runId, ...data }, timestamp: Date.now() });
1336
+ });
1337
+ agent.on("computer-use:verification", (data) => {
1338
+ broadcastWs({ type: "computer-use:verification", data: { runId, ...data }, timestamp: Date.now() });
1339
+ });
1340
+ agent.on("computer-use:session.end", (data) => {
1341
+ broadcastWs({ type: "computer-use:session.end", data: { runId, ...data }, timestamp: Date.now() });
1342
+ });
1343
+ agent.resume(taskId).then((task) => {
1344
+ runState.taskId = task.id;
1345
+ runState.status = task.status === "completed" ? "completed" : "failed";
1346
+ runState.result = task.result;
1347
+ runState.completedAt = Date.now();
1348
+ runState.pendingPrompt = void 0;
1349
+ broadcastWs({ type: "task:complete", data: { runId, taskId: task.id, status: task.status, result: task.result }, timestamp: Date.now() });
1350
+ }).catch((error) => {
1351
+ runState.status = "failed";
1352
+ runState.error = error?.message || "Resume failed.";
1353
+ runState.completedAt = Date.now();
1354
+ runState.pendingPrompt = void 0;
1355
+ broadcastWs({ type: "task:error", data: { runId, error: runState.error }, timestamp: Date.now() });
1356
+ }).finally(() => {
1357
+ activeRunId = null;
1358
+ runState.agent = void 0;
1359
+ });
1360
+ return c.json({ runId, resumed: true, fromIteration: checkpoint.iteration });
1361
+ } catch (error) {
1362
+ activeRunId = null;
1363
+ runs.delete(runId);
1364
+ return c.json({ error: error.message || "Resume failed." }, 500);
1365
+ }
1366
+ });
1367
+ app.get("/api/tasks/:id/evidence", (c) => {
1368
+ const taskId = c.req.param("id");
1369
+ const frames = memoryManager.listObservationFramesByTask(taskId);
1370
+ return c.json({
1371
+ taskId,
1372
+ frames,
1373
+ total: frames.length
1374
+ });
1375
+ });
1376
+ app.get("/api/computer-use/sessions/:id", (c) => {
1377
+ const sessionId = c.req.param("id");
1378
+ const frames = memoryManager.listObservationFramesBySession(sessionId);
1379
+ const summary = memoryManager.getComputerUseSessionSummary(sessionId);
1380
+ const activeCheckpoint = memoryManager.listCheckpoints().find((item) => item.computerUseSessionId === sessionId);
1381
+ return c.json({
1382
+ sessionId,
1383
+ checkpoint: summary ? {
1384
+ taskId: summary.taskId,
1385
+ lastActionId: summary.lastActionId,
1386
+ latestFrameIds: summary.latestFrameIds,
1387
+ verificationFailures: summary.verificationFailures,
1388
+ updatedAt: summary.updatedAt,
1389
+ status: summary.status,
1390
+ startedAt: summary.startedAt,
1391
+ endedAt: summary.endedAt || null
1392
+ } : activeCheckpoint ? {
1393
+ taskId: activeCheckpoint.taskId,
1394
+ lastActionId: activeCheckpoint.computerUseLastActionId,
1395
+ latestFrameIds: (() => {
1396
+ try {
1397
+ return activeCheckpoint.computerUseLatestFrameIdsJson ? JSON.parse(activeCheckpoint.computerUseLatestFrameIdsJson) : [];
1398
+ } catch {
1399
+ return [];
1400
+ }
1401
+ })(),
1402
+ verificationFailures: activeCheckpoint.computerUseVerificationFailures || 0,
1403
+ updatedAt: activeCheckpoint.updatedAt,
1404
+ status: "active",
1405
+ startedAt: null,
1406
+ endedAt: null
1407
+ } : null,
1408
+ frames,
1409
+ total: frames.length
1410
+ });
1411
+ });
1412
+ const chatSessions = /* @__PURE__ */ new Map();
1413
+ app.post("/api/chat/start", async (c) => {
1414
+ let llm;
1415
+ try {
1416
+ llm = await createLLMFromConfig();
1417
+ } catch (error) {
1418
+ return c.json({ error: error.message }, 500);
1419
+ }
1420
+ try {
1421
+ const sessionId = `chat-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1422
+ const { agent } = await createRoutedAgent(llm);
1423
+ chatSessions.set(sessionId, { agent, createdAt: Date.now() });
1424
+ return c.json({ sessionId });
1425
+ } catch (error) {
1426
+ return c.json({ error: error?.message || "Failed to start chat session" }, 500);
1427
+ }
1428
+ });
1429
+ app.post("/api/chat/:id/message", async (c) => {
1430
+ const sessionId = c.req.param("id");
1431
+ const session = chatSessions.get(sessionId);
1432
+ if (!session) return c.json({ error: "Session not found" }, 404);
1433
+ let body;
1434
+ try {
1435
+ body = await c.req.json();
1436
+ } catch {
1437
+ body = {};
1438
+ }
1439
+ const message = typeof body?.message === "string" ? body.message.trim() : "";
1440
+ if (!message) return c.json({ error: "message is required" }, 400);
1441
+ try {
1442
+ await updateSoulFromMessage(message);
1443
+ const handleStreamText = (text) => {
1444
+ broadcastWs({ type: "chat:stream:text", data: { sessionId, text }, timestamp: Date.now() });
1445
+ };
1446
+ const handleStreamThinking = (thinking) => {
1447
+ broadcastWs({ type: "chat:stream:thinking", data: { sessionId, thinking }, timestamp: Date.now() });
1448
+ };
1449
+ const handleMessage = (msg) => {
1450
+ broadcastWs({ type: "chat:message", data: { sessionId, ...msg }, timestamp: Date.now() });
1451
+ };
1452
+ const handleThinking = (thinking) => {
1453
+ broadcastWs({ type: "chat:thinking", data: { sessionId, thinking }, timestamp: Date.now() });
1454
+ };
1455
+ session.agent.on("stream:text", handleStreamText);
1456
+ session.agent.on("stream:thinking", handleStreamThinking);
1457
+ session.agent.on("message", handleMessage);
1458
+ session.agent.on("thinking", handleThinking);
1459
+ try {
1460
+ const response = await session.agent.chat(message);
1461
+ return c.json({ response });
1462
+ } finally {
1463
+ session.agent.off("stream:text", handleStreamText);
1464
+ session.agent.off("stream:thinking", handleStreamThinking);
1465
+ session.agent.off("message", handleMessage);
1466
+ session.agent.off("thinking", handleThinking);
1467
+ }
1468
+ } catch (error) {
1469
+ return c.json({ error: error.message || "Chat failed." }, 500);
1470
+ }
1471
+ });
1472
+ app.delete("/api/chat/:id", (c) => {
1473
+ const sessionId = c.req.param("id");
1474
+ const deleted = chatSessions.delete(sessionId);
1475
+ return c.json({ ok: deleted });
1476
+ });
1477
+ app.get("/api/strategy/active", async (c) => {
1478
+ const config = await configLoader.load();
1479
+ const fallbackPath = join2(homedir2(), ".lydia", "strategies", "default.yml");
1480
+ const activePath = config.strategy?.activePath || fallbackPath;
1481
+ try {
1482
+ if (!existsSync2(activePath)) {
1483
+ return c.json({ error: "Active strategy not found", path: activePath }, 404);
1484
+ }
1485
+ const content = await readFile2(activePath, "utf-8");
1486
+ return c.json({ path: activePath, content });
1487
+ } catch {
1488
+ return c.json({ error: "Failed to read active strategy" }, 500);
1489
+ }
1490
+ });
1491
+ app.post("/api/strategy/proposals/:id/approve", async (c) => {
1492
+ const id = Number(c.req.param("id"));
1493
+ try {
1494
+ const result = await approvalService.approveProposal(id);
1495
+ return c.json({ ok: true, activePath: result.activePath });
1496
+ } catch (error) {
1497
+ const message = error?.message || "Approval failed";
1498
+ if (message === "Proposal not found") return c.json({ error: message }, 404);
1499
+ return c.json({ error: message }, 400);
1500
+ }
1501
+ });
1502
+ app.get("/api/strategy/shadow/status", async (c) => {
1503
+ const config = await configLoader.load();
1504
+ const registry = new StrategyRegistry2();
1505
+ const windowDays = config.strategy.shadowWindowDays ?? 14;
1506
+ const sinceMs = Date.now() - windowDays * 24 * 60 * 60 * 1e3;
1507
+ const baseline = config.strategy.activePath ? await registry.loadFromFile(config.strategy.activePath) : await registry.loadDefault();
1508
+ const baselineSummary = memoryManager.summarizeEpisodesByStrategy(
1509
+ baseline.metadata.id,
1510
+ baseline.metadata.version,
1511
+ { sinceMs, limit: 1e3 }
1512
+ );
1513
+ const candidates = [];
1514
+ for (const candidatePath of config.strategy.shadowCandidatePaths || []) {
1515
+ try {
1516
+ const strategy = await registry.loadFromFile(candidatePath);
1517
+ const summary = memoryManager.summarizeEpisodesByStrategy(
1518
+ strategy.metadata.id,
1519
+ strategy.metadata.version,
1520
+ { sinceMs, limit: 1e3 }
1521
+ );
1522
+ candidates.push({
1523
+ path: candidatePath,
1524
+ id: strategy.metadata.id,
1525
+ version: strategy.metadata.version,
1526
+ summary
1527
+ });
1528
+ } catch (error) {
1529
+ candidates.push({
1530
+ path: candidatePath,
1531
+ error: error?.message || String(error)
1532
+ });
1533
+ }
1534
+ }
1535
+ return c.json({
1536
+ enabled: config.strategy.shadowModeEnabled,
1537
+ mode: config.strategy.shadowRolloutMode,
1538
+ trafficRatio: config.strategy.shadowTrafficRatio,
1539
+ autoPromoteEnabled: config.strategy.autoPromoteEnabled,
1540
+ autoPromoteEvalInterval: config.strategy.autoPromoteEvalInterval,
1541
+ windowDays,
1542
+ baseline: {
1543
+ path: config.strategy.activePath || null,
1544
+ id: baseline.metadata.id,
1545
+ version: baseline.metadata.version,
1546
+ summary: baselineSummary
1547
+ },
1548
+ candidates
1549
+ });
1550
+ });
1551
+ app.post("/api/strategy/proposals/:id/reject", async (c) => {
1552
+ const id = Number(c.req.param("id"));
1553
+ let reason = "";
1554
+ try {
1555
+ const body = await c.req.json();
1556
+ reason = body?.reason || "";
1557
+ } catch {
1558
+ }
1559
+ try {
1560
+ await approvalService.rejectProposal(id, reason);
1561
+ return c.json({ ok: true });
1562
+ } catch (error) {
1563
+ const message = error?.message || "Rejection failed";
1564
+ if (message === "Proposal not found") return c.json({ error: message }, 404);
1565
+ return c.json({ error: message }, 400);
1566
+ }
1567
+ });
1568
+ app.get("/api/replay/:id", (c) => {
1569
+ const id = Number(c.req.param("id"));
1570
+ const episode = memoryManager.getEpisode(id);
1571
+ if (!episode) return c.json({ error: "Episode not found" }, 404);
1572
+ const traces = memoryManager.getTraces(id);
1573
+ const summary = {
1574
+ total: traces.length,
1575
+ success: traces.filter((t) => t.status === "success").length,
1576
+ failed: traces.filter((t) => t.status === "failed").length
1577
+ };
1578
+ const traceDetails = traces.map((t) => {
1579
+ let args = null;
1580
+ let output2 = null;
1581
+ try {
1582
+ args = JSON.parse(t.tool_args);
1583
+ } catch {
1584
+ }
1585
+ try {
1586
+ output2 = JSON.parse(t.tool_output);
1587
+ } catch {
1588
+ }
1589
+ return {
1590
+ ...t,
1591
+ args,
1592
+ output: output2
1593
+ };
1594
+ });
1595
+ return c.json({ episode, traces: traceDetails, summary });
1596
+ });
1597
+ app.get("/api/strategy/content", async (c) => {
1598
+ const filePath = c.req.query("path");
1599
+ if (!filePath) return c.json({ error: "path is required" }, 400);
1600
+ const strategiesDir = join2(homedir2(), ".lydia", "strategies");
1601
+ const resolvedPath = join2(dirname2(filePath), "..", filePath);
1602
+ if (!filePath.includes(".lydia") || !filePath.includes("strategies")) {
1603
+ return c.json({ error: "Access denied: Path must be within .lydia/strategies" }, 403);
1604
+ }
1605
+ try {
1606
+ if (!existsSync2(filePath)) return c.json({ error: "File not found" }, 404);
1607
+ const content = await readFile2(filePath, "utf-8");
1608
+ return c.json({ content });
1609
+ } catch (e) {
1610
+ return c.json({ error: "Failed to read file" }, 500);
1611
+ }
1612
+ });
1613
+ const publicDirCandidates = [
1614
+ join2(process.cwd(), "packages", "cli", "public"),
1615
+ join2(process.cwd(), "public"),
1616
+ join2(__dirname, "../public"),
1617
+ join2(__dirname, "../../public")
1618
+ ];
1619
+ const publicDir = publicDirCandidates.find((candidate) => existsSync2(candidate)) || publicDirCandidates[0];
1620
+ app.get("/*", async (c) => {
1621
+ const path3 = c.req.path === "/" ? "/index.html" : c.req.path;
1622
+ const filePath = join2(publicDir, path3);
1623
+ if (path3.startsWith("/api")) return c.json({ error: "Not found" }, 404);
1624
+ try {
1625
+ if (existsSync2(filePath)) {
1626
+ const content = await readFile2(filePath);
1627
+ if (path3.endsWith(".html")) c.header("Content-Type", "text/html");
1628
+ if (path3.endsWith(".js")) c.header("Content-Type", "application/javascript");
1629
+ if (path3.endsWith(".css")) c.header("Content-Type", "text/css");
1630
+ return c.body(content);
1631
+ } else {
1632
+ const indexHtml = join2(publicDir, "index.html");
1633
+ if (existsSync2(indexHtml)) {
1634
+ const content = await readFile2(indexHtml);
1635
+ c.header("Content-Type", "text/html");
1636
+ return c.body(content);
1637
+ }
1638
+ return c.text('Dashboard not found. Please run "pnpm build:dashboard" first.', 404);
1639
+ }
1640
+ } catch (e) {
1641
+ return c.text("Internal Server Error", 500);
1642
+ }
1643
+ });
1644
+ return {
1645
+ app,
1646
+ start: () => {
1647
+ if (!options?.silent) {
1648
+ console.log(`Starting server on port ${port}...`);
1649
+ }
1650
+ const server = serve({
1651
+ fetch: app.fetch,
1652
+ port
1653
+ });
1654
+ injectWebSocket(server);
1655
+ return server;
1656
+ }
1657
+ };
1658
+ }
1659
+
1660
+ // src/service/manager.ts
1661
+ import * as fs2 from "fs";
1662
+ import { spawn } from "child_process";
1663
+ function isPidRunning(pid) {
1664
+ if (!Number.isInteger(pid) || pid <= 0) return false;
1665
+ try {
1666
+ process.kill(pid, 0);
1667
+ return true;
1668
+ } catch {
1669
+ return false;
1670
+ }
1671
+ }
1672
+ async function isServerHealthy(port = DEFAULT_PORT, host = DEFAULT_HOST) {
1673
+ try {
1674
+ const res = await fetch(`${getBaseUrl(port, host)}/api/status`, {
1675
+ signal: AbortSignal.timeout(2e3)
1676
+ });
1677
+ return res.ok;
1678
+ } catch {
1679
+ return false;
1680
+ }
1681
+ }
1682
+ async function waitForServer(port = DEFAULT_PORT, host = DEFAULT_HOST) {
1683
+ const deadline = Date.now() + STATUS_POLL_TIMEOUT_MS;
1684
+ while (Date.now() < deadline) {
1685
+ if (await isServerHealthy(port, host)) return;
1686
+ await sleep(STATUS_POLL_INTERVAL_MS);
1687
+ }
1688
+ throw new Error(`Server failed to become healthy on ${host}:${port} within ${STATUS_POLL_TIMEOUT_MS}ms`);
1689
+ }
1690
+ async function getServiceStatus() {
1691
+ const state = await readServiceState();
1692
+ const host = state?.host || DEFAULT_HOST;
1693
+ const port = state?.port || DEFAULT_PORT;
1694
+ const baseUrl = getBaseUrl(port, host);
1695
+ if (!state) {
1696
+ const healthy2 = await isServerHealthy(port, host);
1697
+ return {
1698
+ running: healthy2,
1699
+ healthy: healthy2,
1700
+ pid: null,
1701
+ port,
1702
+ host,
1703
+ baseUrl,
1704
+ reason: healthy2 ? "Healthy service detected without local state file." : "Service is not running."
1705
+ };
1706
+ }
1707
+ const pidRunning = isPidRunning(state.pid);
1708
+ const healthy = await isServerHealthy(state.port, state.host);
1709
+ if (!pidRunning && !healthy) {
1710
+ await removeServiceState();
1711
+ return {
1712
+ running: false,
1713
+ healthy: false,
1714
+ pid: state.pid,
1715
+ port: state.port,
1716
+ host: state.host,
1717
+ baseUrl: state.baseUrl,
1718
+ startedAt: state.startedAt,
1719
+ version: state.version,
1720
+ reason: "Found stale Lydia state; cleaned it up."
1721
+ };
1722
+ }
1723
+ return {
1724
+ running: pidRunning || healthy,
1725
+ healthy,
1726
+ pid: state.pid,
1727
+ port: state.port,
1728
+ host: state.host,
1729
+ baseUrl: state.baseUrl,
1730
+ startedAt: state.startedAt,
1731
+ version: state.version,
1732
+ reason: healthy ? "Service is healthy." : "Process exists but health endpoint is not responding yet."
1733
+ };
1734
+ }
1735
+ async function ensureServiceStarted(port = DEFAULT_PORT, host = DEFAULT_HOST) {
1736
+ const status = await getServiceStatus();
1737
+ if (status.running && status.healthy) {
1738
+ return status;
1739
+ }
1740
+ return startService({ port, host });
1741
+ }
1742
+ async function startService(options = {}) {
1743
+ const port = options.port || DEFAULT_PORT;
1744
+ const host = options.host || DEFAULT_HOST;
1745
+ const version = options.version || "unknown";
1746
+ const status = await getServiceStatus();
1747
+ if (status.running && status.healthy) {
1748
+ return status;
1749
+ }
1750
+ await initLocalWorkspace();
1751
+ const paths = getLydiaPaths();
1752
+ const launch = resolveLaunchCommand(port, host);
1753
+ const outFd = fs2.openSync(paths.serverLogPath, "a");
1754
+ const errFd = fs2.openSync(paths.serverErrorLogPath, "a");
1755
+ const child = spawn(launch.command, launch.args, {
1756
+ cwd: process.cwd(),
1757
+ detached: true,
1758
+ stdio: ["ignore", outFd, errFd],
1759
+ windowsHide: true
1760
+ });
1761
+ fs2.closeSync(outFd);
1762
+ fs2.closeSync(errFd);
1763
+ child.unref();
1764
+ try {
1765
+ await waitForServer(port, host);
1766
+ const nextState = {
1767
+ pid: child.pid ?? 0,
1768
+ port,
1769
+ host,
1770
+ baseUrl: getBaseUrl(port, host),
1771
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1772
+ version
1773
+ };
1774
+ await writeServiceState(nextState);
1775
+ return {
1776
+ running: true,
1777
+ healthy: true,
1778
+ pid: nextState.pid,
1779
+ port,
1780
+ host,
1781
+ baseUrl: nextState.baseUrl,
1782
+ startedAt: nextState.startedAt,
1783
+ version,
1784
+ reason: "Service started successfully."
1785
+ };
1786
+ } catch (error) {
1787
+ try {
1788
+ if (child.pid) process.kill(child.pid);
1789
+ } catch {
1790
+ }
1791
+ throw error;
1792
+ }
1793
+ }
1794
+ async function stopService() {
1795
+ const state = await readServiceState();
1796
+ if (!state) {
1797
+ return {
1798
+ running: false,
1799
+ healthy: false,
1800
+ pid: null,
1801
+ port: DEFAULT_PORT,
1802
+ host: DEFAULT_HOST,
1803
+ baseUrl: getBaseUrl(),
1804
+ reason: "Service is not running."
1805
+ };
1806
+ }
1807
+ if (isPidRunning(state.pid)) {
1808
+ try {
1809
+ process.kill(state.pid);
1810
+ } catch {
1811
+ }
1812
+ }
1813
+ const deadline = Date.now() + STATUS_POLL_TIMEOUT_MS;
1814
+ while (Date.now() < deadline) {
1815
+ if (!isPidRunning(state.pid)) break;
1816
+ await sleep(STATUS_POLL_INTERVAL_MS);
1817
+ }
1818
+ await removeServiceState();
1819
+ return {
1820
+ running: false,
1821
+ healthy: false,
1822
+ pid: state.pid,
1823
+ port: state.port,
1824
+ host: state.host,
1825
+ baseUrl: state.baseUrl,
1826
+ startedAt: state.startedAt,
1827
+ version: state.version,
1828
+ reason: "Service stopped."
1829
+ };
1830
+ }
1831
+ function resolveLaunchCommand(port, host) {
1832
+ const entry = process.argv[1];
1833
+ if (!entry) {
1834
+ throw new Error("Unable to resolve Lydia CLI entry point.");
1835
+ }
1836
+ if (entry.endsWith(".ts")) {
1837
+ const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
1838
+ return {
1839
+ command: pnpmCommand,
1840
+ args: ["tsx", entry, "serve", "--port", String(port), "--host", host]
1841
+ };
1842
+ }
1843
+ return {
1844
+ command: process.execPath,
1845
+ args: [entry, "serve", "--port", String(port), "--host", host]
1846
+ };
1847
+ }
1848
+ function sleep(ms) {
1849
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1850
+ }
1851
+
1852
+ // src/client.ts
1853
+ function getServerUrl(port) {
1854
+ return `http://${DEFAULT_HOST}:${port || DEFAULT_PORT}`;
1855
+ }
1856
+ function getWsUrl(port) {
1857
+ return `ws://${DEFAULT_HOST}:${port || DEFAULT_PORT}/ws`;
1858
+ }
1859
+ async function ensureServer(port) {
1860
+ const p = port || DEFAULT_PORT;
1861
+ await ensureServiceStarted(p);
1862
+ return p;
1863
+ }
1864
+ function buildAuthHeaders(base = {}) {
1865
+ const token = (process.env.LYDIA_API_TOKEN || "").trim();
1866
+ if (!token) return base;
1867
+ return {
1868
+ ...base,
1869
+ Authorization: `Bearer ${token}`
1870
+ };
1871
+ }
1872
+ async function resolveWebSocketCtor() {
1873
+ const globalWs = globalThis.WebSocket;
1874
+ if (typeof globalWs === "function") {
1875
+ return globalWs;
1876
+ }
1877
+ try {
1878
+ const mod = await import("ws");
1879
+ return mod.default;
1880
+ } catch {
1881
+ throw new Error('WebSocket runtime not found. Install dependency "ws" or use a Node.js runtime with global WebSocket.');
1882
+ }
1883
+ }
1884
+ function bindWsEvent(ws, event, handler) {
1885
+ if (typeof ws.on === "function") {
1886
+ ws.on(event, handler);
1887
+ return;
1888
+ }
1889
+ if (typeof ws.addEventListener === "function") {
1890
+ ws.addEventListener(event, handler);
1891
+ }
1892
+ }
1893
+ async function apiGet(path3, port) {
1894
+ const res = await fetch(`${getServerUrl(port)}${path3}`, {
1895
+ headers: buildAuthHeaders()
1896
+ });
1897
+ if (!res.ok) {
1898
+ const err = await res.json().catch(() => ({}));
1899
+ throw new Error(err.error || `GET ${path3} failed: ${res.status}`);
1900
+ }
1901
+ return res.json();
1902
+ }
1903
+ async function apiPost(path3, body, port) {
1904
+ const headers = buildAuthHeaders(body ? { "Content-Type": "application/json" } : {});
1905
+ const res = await fetch(`${getServerUrl(port)}${path3}`, {
1906
+ method: "POST",
1907
+ headers,
1908
+ body: body ? JSON.stringify(body) : void 0
1909
+ });
1910
+ if (!res.ok) {
1911
+ const err = await res.json().catch(() => ({}));
1912
+ throw new Error(err.error || `POST ${path3} failed: ${res.status}`);
1913
+ }
1914
+ return res.json();
1915
+ }
1916
+ function connectTaskStream(runId, handlers, port) {
1917
+ return (async () => {
1918
+ const WebSocketCtor = await resolveWebSocketCtor();
1919
+ return await new Promise((resolve2, reject) => {
1920
+ const ws = new WebSocketCtor(getWsUrl(port));
1921
+ let resolved = false;
1922
+ bindWsEvent(ws, "open", () => {
1923
+ resolved = true;
1924
+ resolve2({ close: () => ws.close() });
1925
+ });
1926
+ bindWsEvent(ws, "error", (err) => {
1927
+ if (!resolved) {
1928
+ reject(err);
1929
+ }
1930
+ });
1931
+ bindWsEvent(ws, "message", (raw) => {
1932
+ try {
1933
+ const rawText = typeof raw === "string" ? raw : raw?.data ? String(raw.data) : raw?.toString?.() || "";
1934
+ const msg = JSON.parse(rawText);
1935
+ if (msg.data?.runId && msg.data.runId !== runId) return;
1936
+ switch (msg.type) {
1937
+ case "stream:text":
1938
+ handlers.onText?.(msg.data?.text || "");
1939
+ break;
1940
+ case "stream:thinking":
1941
+ handlers.onThinking?.(msg.data?.thinking || "");
1942
+ break;
1943
+ case "tool:start":
1944
+ handlers.onToolStart?.(msg.data?.name || "unknown");
1945
+ break;
1946
+ case "tool:complete":
1947
+ handlers.onToolComplete?.(msg.data?.name || "unknown", msg.data?.duration || 0, msg.data?.result);
1948
+ break;
1949
+ case "tool:error":
1950
+ handlers.onToolError?.(msg.data?.name || "unknown", msg.data?.error || "unknown error");
1951
+ break;
1952
+ case "retry":
1953
+ handlers.onRetry?.(msg.data?.attempt, msg.data?.maxRetries, msg.data?.delay, msg.data?.error);
1954
+ break;
1955
+ case "interaction_request":
1956
+ if (handlers.onInteraction) {
1957
+ handlers.onInteraction(msg.data?.id, msg.data?.prompt).then((response) => {
1958
+ apiPost(`/api/tasks/${runId}/respond`, { response }, port).catch(() => {
1959
+ });
1960
+ });
1961
+ }
1962
+ break;
1963
+ case "task:complete":
1964
+ handlers.onComplete?.(msg.data?.taskId || "", msg.data?.result || "");
1965
+ ws.close();
1966
+ break;
1967
+ case "task:error":
1968
+ handlers.onError?.(msg.data?.error || "Task failed.");
1969
+ ws.close();
1970
+ break;
1971
+ default:
1972
+ handlers.onMessage?.(msg.type, msg.data);
1973
+ break;
1974
+ }
1975
+ } catch {
1976
+ }
1977
+ });
1978
+ });
1979
+ })();
1980
+ }
1981
+
1982
+ // src/index.ts
1983
+ import * as os2 from "os";
1984
+ import * as path2 from "path";
1985
+ import * as fs3 from "fs";
1986
+ import * as fsPromises2 from "fs/promises";
1987
+
1988
+ // src/commands/review.ts
1989
+ import { Command } from "commander";
1990
+ import { ReviewManager, StrategyBranchManager } from "@lydia-agent/core";
1991
+ import inquirer from "inquirer";
1992
+ import chalk from "chalk";
1993
+ function reviewCommand() {
1994
+ const command = new Command("review");
1995
+ command.description("Review pending strategy updates").action(async () => {
1996
+ const reviewManager = new ReviewManager();
1997
+ const branchManager = new StrategyBranchManager();
1998
+ await reviewManager.init();
1999
+ await branchManager.init();
2000
+ const pending = await reviewManager.listPending();
2001
+ if (pending.length === 0) {
2002
+ console.log(chalk.green("No pending reviews."));
2003
+ return;
2004
+ }
2005
+ console.log(chalk.bold(`Found ${pending.length} pending reviews:
2006
+ `));
2007
+ for (const req of pending) {
2008
+ console.log(chalk.cyan(`ID: ${req.id}`));
2009
+ console.log(`Source: ${req.source}`);
2010
+ console.log(`Branch: ${req.branchName}`);
2011
+ console.log(`Summary: ${req.diffSummary}`);
2012
+ console.log(chalk.gray(`Validation: ${req.validationResult.status} ${req.validationResult.reason || ""}`));
2013
+ console.log("-".repeat(40));
2014
+ }
2015
+ const { action } = await inquirer.prompt([
2016
+ {
2017
+ type: "list",
2018
+ name: "action",
2019
+ message: "What would you like to do?",
2020
+ choices: [
2021
+ { name: "Approve a request", value: "approve" },
2022
+ { name: "Reject a request", value: "reject" },
2023
+ { name: "Exit", value: "exit" }
2024
+ ]
2025
+ }
2026
+ ]);
2027
+ if (action === "exit") return;
2028
+ const { reqId } = await inquirer.prompt([
2029
+ {
2030
+ type: "list",
2031
+ name: "reqId",
2032
+ message: "Select request:",
2033
+ choices: pending.map((r) => ({ name: `${r.id} - ${r.diffSummary}`, value: r.id }))
2034
+ }
2035
+ ]);
2036
+ if (action === "approve") {
2037
+ const req = pending.find((r) => r.id === reqId);
2038
+ try {
2039
+ console.log(chalk.yellow("Merging branch..."));
2040
+ await branchManager.mergeBranch(req.branchName);
2041
+ await reviewManager.updateStatus(reqId, "approved");
2042
+ console.log(chalk.green(`Request ${reqId} approved and merged.`));
2043
+ } catch (e) {
2044
+ console.error(chalk.red(`Failed to merge: ${e}`));
2045
+ }
2046
+ } else if (action === "reject") {
2047
+ await reviewManager.updateStatus(reqId, "rejected");
2048
+ console.log(chalk.yellow(`Request ${reqId} rejected.`));
2049
+ }
2050
+ });
2051
+ return command;
2052
+ }
2053
+
2054
+ // src/index.ts
2055
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2056
+ async function getVersion() {
2057
+ try {
2058
+ const pkg = JSON.parse(await readFile3(join3(__dirname2, "../package.json"), "utf-8"));
2059
+ return pkg.version;
2060
+ } catch (e) {
2061
+ return "unknown";
2062
+ }
2063
+ }
2064
+ function formatDurationMs(ms) {
2065
+ if (ms < 1e3) return `${ms}ms`;
2066
+ const seconds = Math.floor(ms / 1e3);
2067
+ if (seconds < 60) return `${seconds}s`;
2068
+ const minutes = Math.floor(seconds / 60);
2069
+ const remainSeconds = seconds % 60;
2070
+ return `${minutes}m ${remainSeconds}s`;
2071
+ }
2072
+ function bumpPatchVersion(version) {
2073
+ const parts = version.split(".").map((p) => parseInt(p, 10));
2074
+ if (parts.length !== 3 || parts.some((p) => Number.isNaN(p))) {
2075
+ return `${version}-next`;
2076
+ }
2077
+ parts[2] += 1;
2078
+ return parts.join(".");
2079
+ }
2080
+ function buildLocalApiAuthHeaders(base = {}) {
2081
+ const token = (process.env.LYDIA_API_TOKEN || "").trim();
2082
+ if (!token) return base;
2083
+ return { ...base, Authorization: `Bearer ${token}` };
2084
+ }
2085
+ async function main() {
2086
+ const program = new Command2();
2087
+ const version = await getVersion();
2088
+ program.name("lydia").description("Lydia - AI Agent with Strategic Evolution").version(version);
2089
+ program.command("serve").description("Run the Lydia local service").option("--port <number>", "Server port", String(DEFAULT_PORT)).option("--host <host>", "Server host", DEFAULT_HOST).action(async (options) => {
2090
+ const port = parseInt(options.port, 10) || DEFAULT_PORT;
2091
+ const host = String(options.host || DEFAULT_HOST);
2092
+ await initLocalWorkspace();
2093
+ const server = createServer(port);
2094
+ server.start();
2095
+ await writeServiceState({
2096
+ pid: process.pid,
2097
+ port,
2098
+ host,
2099
+ baseUrl: getBaseUrl(port, host),
2100
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2101
+ version
2102
+ });
2103
+ });
2104
+ program.command("start").description("Start the Lydia local service").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (options) => {
2105
+ const port = parseInt(options.port, 10) || DEFAULT_PORT;
2106
+ const status = await startService({ port, version });
2107
+ console.log(chalk2.green(`Lydia service running at ${status.baseUrl}`));
2108
+ if (status.pid) {
2109
+ console.log(chalk2.dim(`pid=${status.pid}`));
2110
+ }
2111
+ });
2112
+ program.command("stop").description("Stop the Lydia local service").action(async () => {
2113
+ const status = await stopService();
2114
+ console.log(chalk2.green(status.reason || "Service stopped."));
2115
+ });
2116
+ program.command("restart").description("Restart the Lydia local service").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (options) => {
2117
+ await stopService();
2118
+ const port = parseInt(options.port, 10) || DEFAULT_PORT;
2119
+ const status = await startService({ port, version });
2120
+ console.log(chalk2.green(`Lydia service restarted at ${status.baseUrl}`));
2121
+ if (status.pid) {
2122
+ console.log(chalk2.dim(`pid=${status.pid}`));
2123
+ }
2124
+ });
2125
+ program.command("status").description("Show Lydia service status").action(async () => {
2126
+ const status = await getServiceStatus();
2127
+ const stateText = status.healthy ? chalk2.green("healthy") : status.running ? chalk2.yellow("starting") : chalk2.red("stopped");
2128
+ console.log(`Service: ${stateText}`);
2129
+ console.log(`URL: ${status.baseUrl}`);
2130
+ if (status.pid) console.log(`PID: ${status.pid}`);
2131
+ if (status.version) console.log(`Version: ${status.version}`);
2132
+ if (status.startedAt) console.log(`Started: ${status.startedAt}`);
2133
+ if (status.reason) console.log(chalk2.dim(status.reason));
2134
+ });
2135
+ program.command("doctor").description("Run local health checks for Lydia").action(async () => {
2136
+ const checks = [];
2137
+ const init = await initLocalWorkspace();
2138
+ const config = await new ConfigLoader3().load();
2139
+ const service = await getServiceStatus();
2140
+ checks.push({
2141
+ name: "Workspace",
2142
+ ok: true,
2143
+ detail: init.paths.baseDir
2144
+ });
2145
+ checks.push({
2146
+ name: "Config",
2147
+ ok: fs3.existsSync(init.paths.configPath),
2148
+ detail: init.paths.configPath
2149
+ });
2150
+ checks.push({
2151
+ name: "Strategy",
2152
+ ok: fs3.existsSync(init.paths.strategyPath),
2153
+ detail: init.paths.strategyPath
2154
+ });
2155
+ checks.push({
2156
+ name: "Service",
2157
+ ok: service.healthy,
2158
+ detail: service.healthy ? service.baseUrl : service.reason || service.baseUrl
2159
+ });
2160
+ checks.push({
2161
+ name: "LLM Provider",
2162
+ ok: Boolean(config.llm.provider),
2163
+ detail: config.llm.provider || "auto"
2164
+ });
2165
+ checks.push({
2166
+ name: "API Keys",
2167
+ ok: Boolean(
2168
+ config.llm.openaiApiKey || config.llm.anthropicApiKey || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY
2169
+ ),
2170
+ detail: "OpenAI/Anthropic key configured"
2171
+ });
2172
+ for (const check of checks) {
2173
+ const icon = check.ok ? chalk2.green("*") : chalk2.red("x");
2174
+ console.log(`${icon} ${check.name}: ${check.detail}`);
2175
+ }
2176
+ if (checks.some((check) => !check.ok)) {
2177
+ process.exitCode = 1;
2178
+ }
2179
+ });
2180
+ program.command("run").description("Execute a task").argument("<task>", "The task description").option("-m, --model <model>", "Override default model").option("-p, --provider <provider>", "LLM provider (anthropic|openai|ollama|mock|auto)").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (taskDescription, options) => {
2181
+ console.log(chalk2.bold.blue("\nLydia is starting...\n"));
2182
+ const spinner = ora("Connecting to server...").start();
2183
+ try {
2184
+ const port = await ensureServer(parseInt(options.port, 10));
2185
+ spinner.succeed(chalk2.green("Server connected"));
2186
+ spinner.start("Submitting task...");
2187
+ const { runId } = await apiPost("/api/tasks/run", { input: taskDescription }, port);
2188
+ spinner.succeed(chalk2.green(`Task submitted (${runId})`));
2189
+ console.log(chalk2.bold(`
2190
+ Task: ${taskDescription}
2191
+ `));
2192
+ spinner.start("Thinking...");
2193
+ let isStreaming = false;
2194
+ await new Promise((resolve2, reject) => {
2195
+ connectTaskStream(runId, {
2196
+ onText(text) {
2197
+ if (!isStreaming) {
2198
+ spinner.stop();
2199
+ isStreaming = true;
2200
+ }
2201
+ process.stdout.write(chalk2.white(text));
2202
+ },
2203
+ onThinking() {
2204
+ if (!isStreaming) {
2205
+ spinner.stop();
2206
+ isStreaming = true;
2207
+ }
2208
+ spinner.text = chalk2.dim("Thinking...");
2209
+ },
2210
+ onToolStart(name) {
2211
+ if (isStreaming) {
2212
+ process.stdout.write("\n");
2213
+ isStreaming = false;
2214
+ }
2215
+ spinner.start(`Using tool: ${name}`);
2216
+ },
2217
+ onToolComplete(name, duration, result) {
2218
+ spinner.stopAndPersist({
2219
+ symbol: chalk2.green("*"),
2220
+ text: `${chalk2.green(name)} ${chalk2.dim(`(${duration}ms)`)}`
2221
+ });
2222
+ if (result) {
2223
+ const resultLines = String(result).split("\n");
2224
+ const preview = resultLines.slice(0, 5).join("\n");
2225
+ console.log(chalk2.dim(preview.replace(/^/gm, " ")));
2226
+ if (resultLines.length > 5) {
2227
+ console.log(chalk2.dim(` ... (${resultLines.length - 5} more lines)`));
2228
+ }
2229
+ }
2230
+ spinner.start("Thinking...");
2231
+ },
2232
+ onToolError(name, error) {
2233
+ spinner.stopAndPersist({
2234
+ symbol: chalk2.red("x"),
2235
+ text: `${chalk2.red(name)}: ${error}`
2236
+ });
2237
+ spinner.start("Thinking...");
2238
+ },
2239
+ onRetry(attempt, maxRetries, delay, error) {
2240
+ spinner.text = chalk2.yellow(`Retry ${attempt}/${maxRetries} after ${delay}ms: ${error}`);
2241
+ },
2242
+ async onInteraction(_id, prompt) {
2243
+ if (isStreaming) {
2244
+ process.stdout.write("\n");
2245
+ isStreaming = false;
2246
+ }
2247
+ spinner.stopAndPersist({ symbol: "!", text: "User Input Required" });
2248
+ const rl = readline.createInterface({ input, output });
2249
+ console.log(chalk2.yellow(`
2250
+ Agent asks: ${prompt}`));
2251
+ const answer = await rl.question(chalk2.bold("> "));
2252
+ rl.close();
2253
+ spinner.start("Resuming...");
2254
+ return answer;
2255
+ },
2256
+ onComplete(_taskId, _result) {
2257
+ if (isStreaming) {
2258
+ process.stdout.write("\n");
2259
+ isStreaming = false;
2260
+ }
2261
+ spinner.succeed(chalk2.bold.green("Task Completed."));
2262
+ resolve2();
2263
+ },
2264
+ onError(error) {
2265
+ if (isStreaming) {
2266
+ process.stdout.write("\n");
2267
+ isStreaming = false;
2268
+ }
2269
+ spinner.fail(chalk2.red("Task Failed"));
2270
+ console.error(chalk2.red(`
2271
+ Error details: ${error}`));
2272
+ resolve2();
2273
+ },
2274
+ onMessage(type, _data) {
2275
+ if (type === "message" && _data?.text) {
2276
+ spinner.stop();
2277
+ console.log(chalk2.white(_data.text));
2278
+ spinner.start("Thinking...");
2279
+ }
2280
+ }
2281
+ }, port).catch(reject);
2282
+ });
2283
+ } catch (error) {
2284
+ spinner.fail(chalk2.red("Fatal Error"));
2285
+ console.error(chalk2.red(error.message || error));
2286
+ process.exit(1);
2287
+ }
2288
+ });
2289
+ program.command("chat").description("Start an interactive chat session with Lydia").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (options) => {
2290
+ console.log(chalk2.bold.blue("\nLydia Chat Mode\n"));
2291
+ console.log(chalk2.dim("Commands: /exit, /reset, /tasks, /task <id>, /help"));
2292
+ console.log(chalk2.dim("\u2500".repeat(40) + "\n"));
2293
+ try {
2294
+ const port = await ensureServer(parseInt(options.port, 10));
2295
+ let sessionId = (await apiPost("/api/chat/start", {}, port)).sessionId;
2296
+ const rl = readline.createInterface({ input, output });
2297
+ while (true) {
2298
+ const userInput = await rl.question(chalk2.bold.cyan("You> "));
2299
+ const trimmed = userInput.trim();
2300
+ if (!trimmed) continue;
2301
+ if (trimmed === "/exit" || trimmed === "/quit") {
2302
+ await apiPost(`/api/chat/${sessionId}`, void 0, port).catch(() => {
2303
+ });
2304
+ console.log(chalk2.dim("\nGoodbye!"));
2305
+ rl.close();
2306
+ break;
2307
+ }
2308
+ if (trimmed === "/reset") {
2309
+ await fetch(`http://localhost:${port}/api/chat/${sessionId}`, {
2310
+ method: "DELETE",
2311
+ headers: buildLocalApiAuthHeaders()
2312
+ }).catch(() => {
2313
+ });
2314
+ sessionId = (await apiPost("/api/chat/start", {}, port)).sessionId;
2315
+ console.log(chalk2.dim("Session reset.\n"));
2316
+ continue;
2317
+ }
2318
+ if (trimmed === "/help") {
2319
+ console.log(chalk2.dim(" /exit - End the chat session"));
2320
+ console.log(chalk2.dim(" /reset - Clear conversation history"));
2321
+ console.log(chalk2.dim(" /tasks - List recent task history"));
2322
+ console.log(chalk2.dim(" /task <id> - Show task detail"));
2323
+ console.log(chalk2.dim(" /help - Show this help message\n"));
2324
+ continue;
2325
+ }
2326
+ if (trimmed === "/tasks") {
2327
+ try {
2328
+ const result = await apiGet("/api/tasks?limit=10", port);
2329
+ if (!result.items?.length) {
2330
+ console.log(chalk2.dim("No tasks found.\n"));
2331
+ } else {
2332
+ console.log(chalk2.bold("\nRecent Tasks:"));
2333
+ for (const item of result.items) {
2334
+ const icon = item.status === "completed" ? chalk2.green("\u2713") : item.status === "running" ? chalk2.blue("\u25CB") : chalk2.red("\u2717");
2335
+ const date = new Date(item.createdAt).toLocaleString();
2336
+ console.log(` ${icon} ${item.input?.substring(0, 60) || "Unknown"} ${chalk2.dim(`\xB7 ${date} \xB7 ${item.id}`)}`);
2337
+ }
2338
+ console.log("");
2339
+ }
2340
+ } catch (err) {
2341
+ console.log(chalk2.red(`Failed to fetch tasks: ${err.message}
2342
+ `));
2343
+ }
2344
+ continue;
2345
+ }
2346
+ if (trimmed.startsWith("/task ")) {
2347
+ const taskId = trimmed.slice("/task ".length).trim();
2348
+ try {
2349
+ const detail = await apiGet(`/api/tasks/${encodeURIComponent(taskId)}/detail`, port);
2350
+ const statusText = detail.status === "completed" ? chalk2.green("SUCCESS") : detail.status === "running" ? chalk2.blue("RUNNING") : chalk2.red("FAILED");
2351
+ console.log(chalk2.bold(`
2352
+ ${detail.report?.intentSummary || detail.input || "Task"}`));
2353
+ console.log(` Status: ${statusText} \xB7 ${new Date(detail.createdAt).toLocaleString()}`);
2354
+ if (detail.report?.summary) console.log(` ${detail.report.summary}`);
2355
+ if (detail.report?.outputs?.length) {
2356
+ for (const out of detail.report.outputs) console.log(` \u2192 ${out}`);
2357
+ }
2358
+ console.log("");
2359
+ } catch (err) {
2360
+ console.log(chalk2.red(`Failed to fetch task: ${err.message}
2361
+ `));
2362
+ }
2363
+ continue;
2364
+ }
2365
+ try {
2366
+ const { response } = await apiPost(
2367
+ `/api/chat/${sessionId}/message`,
2368
+ { message: trimmed },
2369
+ port
2370
+ );
2371
+ if (response) {
2372
+ console.log(chalk2.white(response));
2373
+ }
2374
+ console.log("");
2375
+ } catch (error) {
2376
+ console.error(chalk2.red(`Error: ${error.message}
2377
+ `));
2378
+ }
2379
+ }
2380
+ } catch (error) {
2381
+ console.error(chalk2.red("Fatal Error:"), error.message);
2382
+ process.exit(1);
2383
+ }
2384
+ });
2385
+ program.command("replay").description("Replay a past episode").argument("<episodeId>", "The ID of the episode to replay").option("--runs <n>", "Run replay determinism check N times (default: 1)", "1").option("--min-consistency <n>", "Minimum consistency rate for determinism check [0,1] (default: 0.99)", "0.99").action(async (episodeId, options) => {
2386
+ try {
2387
+ const id = parseInt(episodeId, 10);
2388
+ if (isNaN(id)) throw new Error("Episode ID must be a number");
2389
+ const runs = Math.max(1, Number(options.runs) || 1);
2390
+ const minConsistency = Math.min(1, Math.max(0, Number(options.minConsistency) || 0.99));
2391
+ const replayer = new ReplayManager();
2392
+ const evaluation = await replayer.replay(id);
2393
+ console.log(chalk2.bold(`
2394
+ Replay Result for Episode #${id}`));
2395
+ console.log(` Success: ${evaluation.success ? chalk2.green("yes") : chalk2.red("no")}`);
2396
+ console.log(` Score: ${evaluation.score.toFixed(3)}`);
2397
+ console.log(` Duration: ${formatDurationMs(evaluation.metrics.duration)}`);
2398
+ console.log(` Steps: ${evaluation.metrics.steps}`);
2399
+ console.log(` Drift: ${evaluation.metrics.driftDetected ? chalk2.yellow("detected") : chalk2.green("none")}`);
2400
+ console.log(` Risk events: ${evaluation.metrics.riskEvents}`);
2401
+ console.log(` Human interrupts: ${evaluation.metrics.humanInterrupts}`);
2402
+ console.log(` Observation frames: ${evaluation.metrics.observationFrames}`);
2403
+ console.log(` Multimodal frames: ${evaluation.metrics.multimodalFrames}`);
2404
+ if (runs > 1) {
2405
+ const det = await replayer.replayDeterminism(id, {
2406
+ runs,
2407
+ minConsistencyRate: minConsistency
2408
+ });
2409
+ const rateText = `${(det.consistencyRate * 100).toFixed(1)}% (${det.consistentRuns}/${det.runs})`;
2410
+ console.log(` Determinism: ${det.ok ? chalk2.green(rateText) : chalk2.red(rateText)}`);
2411
+ console.log(` Determinism threshold: ${(minConsistency * 100).toFixed(1)}%`);
2412
+ if (!det.ok) {
2413
+ process.exitCode = 1;
2414
+ }
2415
+ }
2416
+ console.log("");
2417
+ } catch (error) {
2418
+ console.error(chalk2.red("Replay Error:"), error.message);
2419
+ }
2420
+ });
2421
+ program.command("dashboard").description("Launch the Web Dashboard").option("-p, --port <number>", "Port to run on", String(DEFAULT_PORT)).option("--no-open", "Do not open browser automatically").action(async (options) => {
2422
+ const port = parseInt(options.port, 10) || DEFAULT_PORT;
2423
+ const status = await startService({ port, version });
2424
+ const url = status.baseUrl;
2425
+ console.log(chalk2.green(`
2426
+ Dashboard running at: ${chalk2.bold(url)}
2427
+ `));
2428
+ if (options.open) {
2429
+ await open(url);
2430
+ }
2431
+ });
2432
+ program.addCommand(reviewCommand());
2433
+ const mcpCmd = program.command("mcp").description("Inspect external MCP server connectivity");
2434
+ mcpCmd.command("check").description("Check configured external MCP servers and list discovered tools").option("-s, --server <id>", "Check only one configured server id (e.g. browser)").option("--timeout-ms <ms>", "Connection timeout per server (default: 15000)", "15000").option("--retries <n>", "Retry attempts per server (default: 0)", "0").option("--json", "Output JSON only").action(async (options) => {
2435
+ const config = await new ConfigLoader3().load();
2436
+ const allServers = Object.entries(config.mcpServers || {});
2437
+ if (allServers.length === 0) {
2438
+ console.log(chalk2.yellow("No external MCP servers configured in ~/.lydia/config.json"));
2439
+ return;
2440
+ }
2441
+ const targets = options.server ? allServers.filter(([id]) => id === options.server) : allServers;
2442
+ if (targets.length === 0) {
2443
+ const message = `MCP server "${options.server}" not found in config.`;
2444
+ if (options.json) {
2445
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
2446
+ } else {
2447
+ console.error(chalk2.red(message));
2448
+ }
2449
+ process.exitCode = 1;
2450
+ return;
2451
+ }
2452
+ const checkTargets = targets.map(([id, serverConfig]) => ({
2453
+ id,
2454
+ command: serverConfig.command,
2455
+ args: serverConfig.args,
2456
+ env: serverConfig.env
2457
+ }));
2458
+ const timeoutMs = Number(options.timeoutMs) || 15e3;
2459
+ const retries = Math.max(0, Number(options.retries) || 0);
2460
+ const results = await checkMcpServers(checkTargets, { timeoutMs, retries });
2461
+ if (options.json) {
2462
+ const failed2 = results.filter((r) => !r.ok).length;
2463
+ console.log(JSON.stringify({
2464
+ ok: failed2 === 0,
2465
+ timeoutMs,
2466
+ retries,
2467
+ results
2468
+ }, null, 2));
2469
+ if (failed2 > 0) process.exitCode = 1;
2470
+ return;
2471
+ }
2472
+ console.log(chalk2.bold(`
2473
+ Checking ${targets.length} MCP server(s)...
2474
+ `));
2475
+ let failed = 0;
2476
+ for (const result of results) {
2477
+ if (!result.ok) {
2478
+ failed += 1;
2479
+ console.log(chalk2.red(`x ${result.id} (${result.durationMs}ms, attempts=${result.attempts})`));
2480
+ console.log(chalk2.dim(` ${result.error}`));
2481
+ continue;
2482
+ }
2483
+ console.log(chalk2.green(`* ${result.id} (${result.durationMs}ms, attempts=${result.attempts})`));
2484
+ if (result.tools.length === 0) {
2485
+ console.log(chalk2.dim(" tools: (none discovered)"));
2486
+ } else {
2487
+ console.log(chalk2.dim(` tools (${result.tools.length}): ${result.tools.join(", ")}`));
2488
+ }
2489
+ }
2490
+ if (failed > 0) {
2491
+ process.exitCode = 1;
2492
+ console.log(chalk2.red(`
2493
+ ${failed} server(s) failed health check.`));
2494
+ } else {
2495
+ console.log(chalk2.green("\nAll checked MCP servers are reachable."));
2496
+ }
2497
+ });
2498
+ mcpCmd.command("tools").description("List discovered tools from configured external MCP servers").option("-s, --server <id>", "Inspect one configured server id").option("--timeout-ms <ms>", "Connection timeout per server (default: 15000)", "15000").option("--retries <n>", "Retry attempts per server (default: 0)", "0").option("--json", "Output JSON only").action(async (options) => {
2499
+ const config = await new ConfigLoader3().load();
2500
+ const allServers = Object.entries(config.mcpServers || {});
2501
+ const targets = options.server ? allServers.filter(([id]) => id === options.server) : allServers;
2502
+ if (targets.length === 0) {
2503
+ const message = options.server ? `MCP server "${options.server}" not found in config.` : "No external MCP servers configured in ~/.lydia/config.json";
2504
+ if (options.json) {
2505
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
2506
+ } else {
2507
+ console.error(chalk2.red(message));
2508
+ }
2509
+ process.exitCode = 1;
2510
+ return;
2511
+ }
2512
+ const timeoutMs = Number(options.timeoutMs) || 15e3;
2513
+ const retries = Math.max(0, Number(options.retries) || 0);
2514
+ const results = await checkMcpServers(
2515
+ targets.map(([id, s]) => ({ id, command: s.command, args: s.args, env: s.env })),
2516
+ { timeoutMs, retries }
2517
+ );
2518
+ if (options.json) {
2519
+ console.log(JSON.stringify({
2520
+ ok: results.every((r) => r.ok),
2521
+ toolsByServer: results.map((r) => ({ id: r.id, ok: r.ok, tools: r.tools, error: r.error }))
2522
+ }, null, 2));
2523
+ if (!results.every((r) => r.ok)) process.exitCode = 1;
2524
+ return;
2525
+ }
2526
+ for (const result of results) {
2527
+ if (!result.ok) {
2528
+ console.log(chalk2.red(`x ${result.id}: ${result.error}`));
2529
+ continue;
2530
+ }
2531
+ console.log(chalk2.green(`${result.id}`));
2532
+ if (result.tools.length === 0) {
2533
+ console.log(chalk2.dim(" (no tools)"));
2534
+ continue;
2535
+ }
2536
+ for (const tool of result.tools) {
2537
+ console.log(` - ${tool}`);
2538
+ }
2539
+ }
2540
+ if (!results.every((r) => r.ok)) process.exitCode = 1;
2541
+ });
2542
+ program.command("init").description("Initialize Lydia config, strategy, and folders").action(async () => {
2543
+ try {
2544
+ const result = await initLocalWorkspace();
2545
+ for (const filePath of result.created) {
2546
+ console.log(chalk2.green(`Created: ${filePath}`));
2547
+ }
2548
+ for (const filePath of result.existing) {
2549
+ console.log(chalk2.gray(`Exists: ${filePath}`));
2550
+ }
2551
+ console.log(chalk2.green("Lydia initialization complete."));
2552
+ } catch (error) {
2553
+ console.error(chalk2.red("Initialization failed:"), error.message);
2554
+ }
2555
+ });
2556
+ const strategyCmd = program.command("strategy").description("Manage strategies");
2557
+ strategyCmd.command("list").description("List available strategies").action(async () => {
2558
+ const registry = new StrategyRegistry3();
2559
+ const dir = path2.join(os2.homedir(), ".lydia", "strategies");
2560
+ try {
2561
+ const strategies = await registry.listFromDirectory(dir);
2562
+ if (strategies.length === 0) {
2563
+ console.log(chalk2.yellow("No strategies found."));
2564
+ return;
2565
+ }
2566
+ strategies.forEach((s) => {
2567
+ console.log(`${chalk2.green(s.metadata.id)} v${s.metadata.version} - ${s.metadata.name}`);
2568
+ });
2569
+ } catch (error) {
2570
+ console.error(chalk2.red("Failed to list strategies:"), error.message);
2571
+ }
2572
+ });
2573
+ strategyCmd.command("use").description("Set active strategy by file path").argument("<file>", "Path to strategy file").action(async (file) => {
2574
+ const loader = new ConfigLoader3();
2575
+ try {
2576
+ const absPath = path2.resolve(file);
2577
+ const registry = new StrategyRegistry3();
2578
+ await registry.loadFromFile(absPath);
2579
+ await loader.update({ strategy: { activePath: absPath } });
2580
+ console.log(chalk2.green(`Active strategy set to: ${absPath}`));
2581
+ } catch (error) {
2582
+ console.error(chalk2.red("Failed to set active strategy:"), error.message);
2583
+ }
2584
+ });
2585
+ strategyCmd.command("propose").description("Propose a strategy update for review").argument("<file>", "Path to strategy file").action(async (file) => {
2586
+ const absPath = path2.resolve(file);
2587
+ const dbPath = path2.join(os2.homedir(), ".lydia", "memory.db");
2588
+ const memory = new MemoryManager2(dbPath);
2589
+ const registry = new StrategyRegistry3();
2590
+ const updateGate = new StrategyUpdateGate();
2591
+ const replayManager = new ReplayManager(memory);
2592
+ try {
2593
+ const config = await new ConfigLoader3().load();
2594
+ const strategy = await registry.loadFromFile(absPath);
2595
+ const baselinePath = config.strategy?.activePath;
2596
+ const baseline = baselinePath ? await registry.loadFromFile(baselinePath) : await registry.loadDefault();
2597
+ const replayCount = config.strategy?.replayEpisodes ?? 10;
2598
+ const replayEpisodeIds = memory.listEpisodes(replayCount).map((ep) => ep.id).filter((id2) => typeof id2 === "number");
2599
+ const replayComparison = replayEpisodeIds.length > 0 ? await replayManager.replayCompare(replayEpisodeIds, baseline, strategy) : null;
2600
+ const validation = await updateGate.process(
2601
+ strategy,
2602
+ {
2603
+ name: strategy.metadata.id,
2604
+ version: strategy.metadata.version,
2605
+ path: absPath,
2606
+ parent: strategy.metadata.inheritFrom,
2607
+ createdAt: Date.now()
2608
+ },
2609
+ replayComparison?.details || [],
2610
+ baseline
2611
+ );
2612
+ const proposalStatus = validation.status === "REJECT" ? "invalid" : "pending_human";
2613
+ const evaluation = {
2614
+ validation,
2615
+ replay: replayComparison,
2616
+ sampledEpisodeIds: replayEpisodeIds,
2617
+ baseline: {
2618
+ id: baseline.metadata.id,
2619
+ version: baseline.metadata.version
2620
+ },
2621
+ candidate: {
2622
+ id: strategy.metadata.id,
2623
+ version: strategy.metadata.version
2624
+ }
2625
+ };
2626
+ const id = memory.recordStrategyProposal({
2627
+ strategy_path: absPath,
2628
+ status: proposalStatus,
2629
+ reason: validation.reason,
2630
+ evaluation_json: JSON.stringify(evaluation),
2631
+ created_at: Date.now(),
2632
+ decided_at: proposalStatus === "invalid" ? Date.now() : void 0
2633
+ });
2634
+ if (proposalStatus === "invalid") {
2635
+ console.error(chalk2.red(`Proposal rejected by gate: ${id}`));
2636
+ console.error(chalk2.red(validation.reason || "Rejected by automated validation"));
2637
+ return;
2638
+ }
2639
+ console.log(chalk2.green(`Proposal created: ${id}`));
2640
+ if (replayComparison) {
2641
+ console.log(
2642
+ chalk2.gray(
2643
+ `Replay compared ${replayComparison.tasksEvaluated} episodes (candidate ${(replayComparison.candidateScore * 100).toFixed(1)}%, baseline ${(replayComparison.baselineScore * 100).toFixed(1)}%).`
2644
+ )
2645
+ );
2646
+ } else {
2647
+ console.log(chalk2.yellow("No replay episodes found. Proposal requires manual review."));
2648
+ }
2649
+ } catch (error) {
2650
+ const id = memory.recordStrategyProposal({
2651
+ strategy_path: absPath,
2652
+ status: "invalid",
2653
+ reason: error.message,
2654
+ created_at: Date.now(),
2655
+ decided_at: Date.now()
2656
+ });
2657
+ console.error(chalk2.red(`Proposal invalid: ${id}`));
2658
+ console.error(chalk2.red(error.message));
2659
+ }
2660
+ });
2661
+ strategyCmd.command("review").description("Review recent episodes and generate a strategy proposal").option("-n, --limit <limit>", "Max episodes to review", "50").option("--min-failures <count>", "Min failures per tool", "1").option("--min-failure-rate <rate>", "Min failure rate per tool", "0.2").action(async (options) => {
2662
+ const config = await new ConfigLoader3().load();
2663
+ const registry = new StrategyRegistry3();
2664
+ const dbPath = path2.join(os2.homedir(), ".lydia", "memory.db");
2665
+ const memory = new MemoryManager2(dbPath);
2666
+ try {
2667
+ const activePath = config.strategy?.activePath;
2668
+ const active = activePath ? await registry.loadFromFile(activePath) : await registry.loadDefault();
2669
+ const reviewer = new StrategyReviewer(memory);
2670
+ const summary = reviewer.review(active, {
2671
+ episodeLimit: Number(options.limit) || 50,
2672
+ minFailures: Number(options.minFailures) || 1,
2673
+ minFailureRate: Number(options.minFailureRate) || 0.2
2674
+ });
2675
+ if (summary.findings.length === 0) {
2676
+ console.log(chalk2.yellow("No actionable findings."));
2677
+ return;
2678
+ }
2679
+ const proposed = JSON.parse(JSON.stringify(active));
2680
+ proposed.metadata = {
2681
+ ...active.metadata,
2682
+ version: bumpPatchVersion(active.metadata.version),
2683
+ inheritFrom: active.metadata.id,
2684
+ description: `${active.metadata.description || "Strategy"} (auto review)`
2685
+ };
2686
+ const existingConfirmations = new Set(active.execution?.requiresConfirmation || []);
2687
+ for (const tool of summary.suggestedConfirmations) {
2688
+ existingConfirmations.add(tool);
2689
+ }
2690
+ proposed.execution = {
2691
+ ...active.execution || {},
2692
+ requiresConfirmation: Array.from(existingConfirmations)
2693
+ };
2694
+ const proposalsDir = path2.join(os2.homedir(), ".lydia", "strategies", "proposals");
2695
+ await fsPromises2.mkdir(proposalsDir, { recursive: true });
2696
+ const proposalPath = path2.join(
2697
+ proposalsDir,
2698
+ `${proposed.metadata.id}-v${proposed.metadata.version}.yml`
2699
+ );
2700
+ await registry.saveToFile(proposed, proposalPath);
2701
+ const gate = BasicStrategyGate.validate(proposed);
2702
+ const status = gate.ok ? "pending_human" : "invalid";
2703
+ const id = memory.recordStrategyProposal({
2704
+ strategy_path: proposalPath,
2705
+ status,
2706
+ reason: gate.reason,
2707
+ evaluation_json: JSON.stringify({ review: summary }),
2708
+ created_at: Date.now(),
2709
+ decided_at: gate.ok ? void 0 : Date.now()
2710
+ });
2711
+ if (gate.ok) {
2712
+ console.log(chalk2.green(`Review proposal created: ${id}`));
2713
+ console.log(chalk2.green(`Proposal file: ${proposalPath}`));
2714
+ } else {
2715
+ console.error(chalk2.red(`Proposal rejected by gate: ${id}`));
2716
+ console.error(chalk2.red(gate.reason || "Invalid strategy"));
2717
+ }
2718
+ } catch (error) {
2719
+ console.error(chalk2.red("Review failed:"), error.message);
2720
+ }
2721
+ });
2722
+ strategyCmd.command("approve").description("Approve a strategy proposal").argument("<id>", "Proposal id").action(async (id) => {
2723
+ const proposalId = Number(id);
2724
+ const approval = new StrategyApprovalService2();
2725
+ try {
2726
+ const result = await approval.approveProposal(proposalId);
2727
+ console.log(chalk2.green(`Approved proposal ${proposalId}`));
2728
+ console.log(chalk2.gray(`Active strategy: ${result.activePath}`));
2729
+ } catch (error) {
2730
+ console.error(chalk2.red(error.message || String(error)));
2731
+ }
2732
+ });
2733
+ strategyCmd.command("reject").description("Reject a strategy proposal").argument("<id>", "Proposal id").option("-r, --reason <reason>", "Rejection reason").action(async (id, options) => {
2734
+ const proposalId = Number(id);
2735
+ const approval = new StrategyApprovalService2();
2736
+ try {
2737
+ await approval.rejectProposal(proposalId, options.reason);
2738
+ console.log(chalk2.yellow(`Rejected proposal ${proposalId}`));
2739
+ } catch (error) {
2740
+ console.error(chalk2.red(error.message || String(error)));
2741
+ }
2742
+ });
2743
+ strategyCmd.command("proposals").description("List recent strategy proposals").option("-n, --limit <limit>", "Max number of proposals", "20").action(async (options) => {
2744
+ const limit = Number(options.limit) || 20;
2745
+ const dbPath = path2.join(os2.homedir(), ".lydia", "memory.db");
2746
+ const memory = new MemoryManager2(dbPath);
2747
+ const proposals = memory.listStrategyProposals(limit);
2748
+ if (proposals.length === 0) {
2749
+ console.log(chalk2.yellow("No proposals found."));
2750
+ return;
2751
+ }
2752
+ proposals.forEach((p) => {
2753
+ let details = "";
2754
+ if (p.evaluation_json) {
2755
+ try {
2756
+ const evalData = JSON.parse(p.evaluation_json);
2757
+ const replay = evalData?.replay;
2758
+ const validation = evalData?.validation;
2759
+ if (replay?.candidateSummary && replay?.baselineSummary && replay?.delta) {
2760
+ const candidate = replay.candidateSummary;
2761
+ const baseline = replay.baselineSummary;
2762
+ const delta = replay.delta;
2763
+ details = ` | score ${((replay.candidateScore || 0) * 100).toFixed(1)}% vs ${((replay.baselineScore || 0) * 100).toFixed(1)}% | dur ${Math.round(candidate.averageDuration || 0)}ms (${Math.round(delta.averageDuration || 0)}ms) | risk ${(candidate.averageRiskEvents || 0).toFixed(2)} (${(delta.averageRiskEvents || 0).toFixed(2)}) | human ${(candidate.averageHumanInterrupts || 0).toFixed(2)} (${(delta.averageHumanInterrupts || 0).toFixed(2)}) | drift ${(candidate.driftRate || 0).toFixed(2)} (${(delta.driftRate || 0).toFixed(2)})`;
2764
+ } else if (validation?.status) {
2765
+ details = ` | validation ${validation.status}${validation.reason ? `: ${validation.reason}` : ""}`;
2766
+ }
2767
+ } catch {
2768
+ }
2769
+ }
2770
+ const summary = p.evaluation_json ? "has_eval" : "no_eval";
2771
+ console.log(`${p.id} | ${p.status} | ${summary}${details} | ${p.strategy_path}`);
2772
+ });
2773
+ });
2774
+ strategyCmd.command("report").description("Export proposal evaluation to a JSON file").argument("<id>", "Proposal id").argument("<file>", "Output file path").action(async (id, file) => {
2775
+ const proposalId = Number(id);
2776
+ if (Number.isNaN(proposalId)) {
2777
+ console.error(chalk2.red("Proposal id must be a number"));
2778
+ return;
2779
+ }
2780
+ const dbPath = path2.join(os2.homedir(), ".lydia", "memory.db");
2781
+ const memory = new MemoryManager2(dbPath);
2782
+ const proposal = memory.getStrategyProposal(proposalId);
2783
+ if (!proposal || !proposal.evaluation_json) {
2784
+ console.error(chalk2.red("Proposal evaluation not found"));
2785
+ return;
2786
+ }
2787
+ const outPath = path2.resolve(file);
2788
+ fs3.writeFileSync(outPath, proposal.evaluation_json, "utf-8");
2789
+ console.log(chalk2.green(`Report saved: ${outPath}`));
2790
+ });
2791
+ const shadowCmd = strategyCmd.command("shadow").description("Manage shadow/canary rollout and auto-promotion settings");
2792
+ shadowCmd.command("status").description("Show current shadow rollout status and recent metrics").action(async () => {
2793
+ const config = await new ConfigLoader3().load();
2794
+ const registry = new StrategyRegistry3();
2795
+ const dbPath = path2.join(os2.homedir(), ".lydia", "memory.db");
2796
+ const memory = new MemoryManager2(dbPath);
2797
+ const active = config.strategy?.activePath ? await registry.loadFromFile(config.strategy.activePath) : await registry.loadDefault();
2798
+ const windowDays = config.strategy.shadowWindowDays ?? 14;
2799
+ const sinceMs = Date.now() - windowDays * 24 * 60 * 60 * 1e3;
2800
+ const baselineSummary = memory.summarizeEpisodesByStrategy(
2801
+ active.metadata.id,
2802
+ active.metadata.version,
2803
+ { sinceMs, limit: 1e3 }
2804
+ );
2805
+ console.log(chalk2.bold("\nShadow Rollout Status"));
2806
+ console.log(` Enabled: ${config.strategy.shadowModeEnabled ? chalk2.green("yes") : chalk2.gray("no")}`);
2807
+ console.log(` Mode: ${config.strategy.shadowRolloutMode}`);
2808
+ console.log(` Traffic Ratio: ${(config.strategy.shadowTrafficRatio * 100).toFixed(1)}%`);
2809
+ console.log(` Auto-Promote: ${config.strategy.autoPromoteEnabled ? chalk2.green("yes") : chalk2.gray("no")}`);
2810
+ console.log(` Promote Check Interval: ${config.strategy.autoPromoteEvalInterval} task(s)`);
2811
+ console.log(` Window: ${windowDays} day(s)`);
2812
+ console.log(` Baseline: ${active.metadata.id} v${active.metadata.version}`);
2813
+ console.log(
2814
+ ` tasks=${baselineSummary.total} success=${baselineSummary.success} failure=${baselineSummary.failure} avgDuration=${baselineSummary.avg_duration_ms}ms`
2815
+ );
2816
+ const candidates = config.strategy.shadowCandidatePaths || [];
2817
+ if (candidates.length === 0) {
2818
+ console.log(chalk2.yellow("\n No shadow candidates configured.\n"));
2819
+ return;
2820
+ }
2821
+ console.log(chalk2.bold("\nCandidates:"));
2822
+ for (const candidatePath of candidates) {
2823
+ try {
2824
+ const candidate = await registry.loadFromFile(candidatePath);
2825
+ const summary = memory.summarizeEpisodesByStrategy(
2826
+ candidate.metadata.id,
2827
+ candidate.metadata.version,
2828
+ { sinceMs, limit: 1e3 }
2829
+ );
2830
+ console.log(` - ${candidate.metadata.id} v${candidate.metadata.version}`);
2831
+ console.log(` path=${candidatePath}`);
2832
+ console.log(
2833
+ ` tasks=${summary.total} success=${summary.success} failure=${summary.failure} avgDuration=${summary.avg_duration_ms}ms`
2834
+ );
2835
+ } catch (error) {
2836
+ console.log(` - ${candidatePath}`);
2837
+ console.log(chalk2.red(` failed to load: ${error.message || String(error)}`));
2838
+ }
2839
+ }
2840
+ console.log("");
2841
+ });
2842
+ shadowCmd.command("enable").description("Enable shadow/canary rollout").action(async () => {
2843
+ await new ConfigLoader3().update({ strategy: { shadowModeEnabled: true } });
2844
+ console.log(chalk2.green("Shadow rollout enabled."));
2845
+ });
2846
+ shadowCmd.command("disable").description("Disable shadow/canary rollout").action(async () => {
2847
+ await new ConfigLoader3().update({ strategy: { shadowModeEnabled: false } });
2848
+ console.log(chalk2.yellow("Shadow rollout disabled."));
2849
+ });
2850
+ shadowCmd.command("mode").description("Set rollout mode: shadow (safe) or canary (real traffic)").argument("<mode>", "shadow | canary").action(async (mode) => {
2851
+ const normalized = String(mode || "").trim().toLowerCase();
2852
+ if (normalized !== "shadow" && normalized !== "canary") {
2853
+ console.error(chalk2.red("mode must be shadow or canary"));
2854
+ return;
2855
+ }
2856
+ await new ConfigLoader3().update({ strategy: { shadowRolloutMode: normalized } });
2857
+ console.log(chalk2.green(`Shadow rollout mode set to ${normalized}.`));
2858
+ });
2859
+ shadowCmd.command("ratio").description("Set shadow traffic ratio (0.0 - 1.0)").argument("<value>", "Traffic ratio").action(async (value) => {
2860
+ const ratio = Number(value);
2861
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1) {
2862
+ console.error(chalk2.red("ratio must be between 0.0 and 1.0"));
2863
+ return;
2864
+ }
2865
+ await new ConfigLoader3().update({ strategy: { shadowTrafficRatio: ratio } });
2866
+ console.log(chalk2.green(`Shadow traffic ratio set to ${(ratio * 100).toFixed(1)}%.`));
2867
+ });
2868
+ shadowCmd.command("eval-interval").description("Set auto-promotion evaluation interval in completed tasks").argument("<value>", "Positive integer").action(async (value) => {
2869
+ const interval = Number(value);
2870
+ if (!Number.isInteger(interval) || interval <= 0) {
2871
+ console.error(chalk2.red("eval interval must be a positive integer"));
2872
+ return;
2873
+ }
2874
+ await new ConfigLoader3().update({ strategy: { autoPromoteEvalInterval: interval } });
2875
+ console.log(chalk2.green(`Auto-promotion evaluation interval set to ${interval}.`));
2876
+ });
2877
+ shadowCmd.command("add").description("Add a shadow candidate strategy file").argument("<file>", "Path to strategy file").action(async (file) => {
2878
+ const absPath = path2.resolve(file);
2879
+ const registry = new StrategyRegistry3();
2880
+ try {
2881
+ await registry.loadFromFile(absPath);
2882
+ } catch (error) {
2883
+ console.error(chalk2.red(`Invalid strategy file: ${error.message || String(error)}`));
2884
+ return;
2885
+ }
2886
+ const loader = new ConfigLoader3();
2887
+ const config = await loader.load();
2888
+ const current = new Set(config.strategy.shadowCandidatePaths || []);
2889
+ current.add(absPath);
2890
+ await loader.update({ strategy: { shadowCandidatePaths: Array.from(current) } });
2891
+ console.log(chalk2.green(`Added shadow candidate: ${absPath}`));
2892
+ });
2893
+ shadowCmd.command("remove").description("Remove a shadow candidate strategy file").argument("<file>", "Path to strategy file").action(async (file) => {
2894
+ const absPath = path2.resolve(file);
2895
+ const loader = new ConfigLoader3();
2896
+ const config = await loader.load();
2897
+ const next = (config.strategy.shadowCandidatePaths || []).filter((item) => path2.resolve(item) !== absPath);
2898
+ await loader.update({ strategy: { shadowCandidatePaths: next } });
2899
+ console.log(chalk2.yellow(`Removed shadow candidate: ${absPath}`));
2900
+ });
2901
+ shadowCmd.command("promote-check").description("Evaluate whether a candidate should be auto-promoted now").action(async () => {
2902
+ const config = await new ConfigLoader3().load();
2903
+ const router = new ShadowRouter2();
2904
+ const decision = await router.evaluateAutoPromotion(config);
2905
+ if (!decision) {
2906
+ console.log(chalk2.yellow("No candidate currently meets auto-promotion criteria."));
2907
+ return;
2908
+ }
2909
+ console.log(chalk2.green("Auto-promotion candidate ready:"));
2910
+ console.log(` Candidate: ${decision.candidateId} v${decision.candidateVersion}`);
2911
+ console.log(` Path: ${decision.candidatePath}`);
2912
+ console.log(` Improvement: ${(decision.successImprovement * 100).toFixed(2)}%`);
2913
+ console.log(` p-value: ${decision.pValue.toFixed(6)}`);
2914
+ });
2915
+ const tasksCmd = program.command("tasks").description("View and manage task history");
2916
+ tasksCmd.command("list").description("List recent tasks").option("-n, --limit <limit>", "Max number of tasks", "20").option("--status <status>", "Filter by status (running|completed|failed)").option("-s, --search <query>", "Search tasks by keyword").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (options) => {
2917
+ try {
2918
+ const port = await ensureServer(parseInt(options.port, 10));
2919
+ const limit = Number(options.limit) || 20;
2920
+ const params = new URLSearchParams({ limit: String(limit) });
2921
+ if (options.status) params.set("status", options.status);
2922
+ if (options.search) params.set("search", options.search);
2923
+ const result = await apiGet(
2924
+ `/api/tasks?${params}`,
2925
+ port
2926
+ );
2927
+ if (!result.items?.length) {
2928
+ console.log(chalk2.yellow("No tasks found."));
2929
+ return;
2930
+ }
2931
+ console.log(chalk2.bold(`
2932
+ Recent Tasks (${result.items.length} of ${result.total}):
2933
+ `));
2934
+ for (const item of result.items) {
2935
+ const statusIcon = item.status === "completed" ? chalk2.green("\u2713") : item.status === "running" ? chalk2.blue("\u25CB") : chalk2.red("\u2717");
2936
+ const statusText = item.status === "completed" ? chalk2.green("completed") : item.status === "running" ? chalk2.blue("running") : chalk2.red("failed");
2937
+ const title = item.input?.substring(0, 80) || item.summary || "Unknown task";
2938
+ const date = new Date(item.createdAt).toLocaleString();
2939
+ const duration = item.duration ? ` (${formatDurationMs(item.duration)})` : "";
2940
+ console.log(` ${statusIcon} ${chalk2.bold(title)}`);
2941
+ console.log(` ${statusText}${duration} \xB7 ${chalk2.dim(date)} \xB7 ID: ${chalk2.dim(item.id)}`);
2942
+ if (item.summary && item.summary !== item.input) {
2943
+ console.log(` ${chalk2.dim(item.summary.substring(0, 100))}`);
2944
+ }
2945
+ console.log("");
2946
+ }
2947
+ } catch (error) {
2948
+ console.error(chalk2.red("Failed to list tasks:"), error.message);
2949
+ }
2950
+ });
2951
+ tasksCmd.command("show").description("Show detailed information about a task").argument("<id>", "Task ID (e.g., report-5 or run-...)").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (id, options) => {
2952
+ try {
2953
+ const port = await ensureServer(parseInt(options.port, 10));
2954
+ const detail = await apiGet(`/api/tasks/${encodeURIComponent(id)}/detail`, port);
2955
+ const statusText = detail.status === "completed" ? chalk2.green("SUCCESS") : detail.status === "running" ? chalk2.blue("RUNNING") : chalk2.red("FAILED");
2956
+ const title = detail.report?.intentSummary || detail.input || "Unknown task";
2957
+ const date = new Date(detail.createdAt).toLocaleString();
2958
+ const duration = detail.duration ? formatDurationMs(detail.duration) : "N/A";
2959
+ console.log(chalk2.bold(`
2960
+ Task: ${title}
2961
+ `));
2962
+ console.log(` Status: ${statusText}`);
2963
+ console.log(` Date: ${date}`);
2964
+ console.log(` Duration: ${duration}`);
2965
+ console.log(` ID: ${id}`);
2966
+ if (detail.report?.summary) {
2967
+ console.log(`
2968
+ ${chalk2.bold("Summary:")}`);
2969
+ console.log(` ${detail.report.summary}`);
2970
+ }
2971
+ if (detail.report?.outputs?.length) {
2972
+ console.log(`
2973
+ ${chalk2.bold("Outputs:")}`);
2974
+ for (const out of detail.report.outputs) {
2975
+ console.log(` \u2192 ${out}`);
2976
+ }
2977
+ }
2978
+ if (detail.report?.steps?.length) {
2979
+ console.log(`
2980
+ ${chalk2.bold("Steps:")}`);
2981
+ for (const step of detail.report.steps) {
2982
+ const stepIcon = step.status === "completed" ? chalk2.green("\u2713") : chalk2.red("\u2717");
2983
+ console.log(` ${stepIcon} ${step.stepId} (${step.status})`);
2984
+ }
2985
+ }
2986
+ if (detail.report?.followUps?.length) {
2987
+ console.log(`
2988
+ ${chalk2.bold("Follow-ups:")}`);
2989
+ for (const item of detail.report.followUps) {
2990
+ console.log(` \u2022 ${item}`);
2991
+ }
2992
+ }
2993
+ if (detail.traces?.length) {
2994
+ console.log(`
2995
+ ${chalk2.bold(`Tool Traces (${detail.traces.length} steps):`)}`);
2996
+ for (const trace of detail.traces) {
2997
+ const traceIcon = trace.status === "success" ? chalk2.green("\u2713") : chalk2.red("\u2717");
2998
+ console.log(` ${traceIcon} ${trace.tool_name} ${chalk2.dim(`(${trace.duration}ms)`)}`);
2999
+ }
3000
+ }
3001
+ if (detail.evidence?.length) {
3002
+ console.log(`
3003
+ ${chalk2.bold(`Evidence Frames (${detail.evidence.length}):`)}`);
3004
+ for (const frame of detail.evidence.slice(0, 10)) {
3005
+ const blockTypes = Array.isArray(frame.blocks) ? frame.blocks.map((block) => block.type).join(", ") : "unknown";
3006
+ console.log(` - ${frame.frameId} ${chalk2.dim(`(${blockTypes})`)}`);
3007
+ }
3008
+ }
3009
+ console.log("");
3010
+ } catch (error) {
3011
+ console.error(chalk2.red("Failed to show task:"), error.message);
3012
+ }
3013
+ });
3014
+ tasksCmd.command("resumable").description("List tasks that can be resumed from checkpoint").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (options) => {
3015
+ try {
3016
+ const port = await ensureServer(parseInt(options.port, 10));
3017
+ const result = await apiGet("/api/tasks/resumable", port);
3018
+ if (!result.items?.length) {
3019
+ console.log(chalk2.yellow("No resumable tasks found."));
3020
+ return;
3021
+ }
3022
+ console.log(chalk2.bold(`
3023
+ Resumable Tasks (${result.items.length}):
3024
+ `));
3025
+ for (const item of result.items) {
3026
+ const date = new Date(item.taskCreatedAt).toLocaleString();
3027
+ const updated = new Date(item.updatedAt).toLocaleString();
3028
+ const title = item.input?.substring(0, 80) || "Unknown task";
3029
+ console.log(` ${chalk2.blue("\u25CB")} ${chalk2.bold(title)}`);
3030
+ console.log(` Iteration: ${chalk2.cyan(String(item.iteration))} \xB7 Started: ${chalk2.dim(date)} \xB7 Last checkpoint: ${chalk2.dim(updated)}`);
3031
+ console.log(` ID: ${chalk2.dim(item.taskId)}`);
3032
+ console.log("");
3033
+ }
3034
+ } catch (error) {
3035
+ console.error(chalk2.red("Failed to list resumable tasks:"), error.message);
3036
+ }
3037
+ });
3038
+ tasksCmd.command("resume").description("Resume an interrupted task from its checkpoint").argument("<id>", 'Task ID (from "tasks resumable")').option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (taskId, options) => {
3039
+ const spinner = ora("Connecting to server...").start();
3040
+ try {
3041
+ const port = await ensureServer(parseInt(options.port, 10));
3042
+ spinner.succeed(chalk2.green("Server connected"));
3043
+ spinner.start("Resuming task from checkpoint...");
3044
+ const { runId, fromIteration } = await apiPost(
3045
+ `/api/tasks/${encodeURIComponent(taskId)}/resume`,
3046
+ {},
3047
+ port
3048
+ );
3049
+ spinner.succeed(chalk2.green(`Task resumed from iteration ${fromIteration} (${runId})`));
3050
+ spinner.start("Thinking...");
3051
+ let isStreaming = false;
3052
+ await new Promise((resolve2, reject) => {
3053
+ connectTaskStream(runId, {
3054
+ onText(text) {
3055
+ if (!isStreaming) {
3056
+ spinner.stop();
3057
+ isStreaming = true;
3058
+ }
3059
+ process.stdout.write(chalk2.white(text));
3060
+ },
3061
+ onThinking() {
3062
+ if (!isStreaming) {
3063
+ spinner.stop();
3064
+ isStreaming = true;
3065
+ }
3066
+ spinner.text = chalk2.dim("Thinking...");
3067
+ },
3068
+ onToolStart(name) {
3069
+ if (isStreaming) {
3070
+ process.stdout.write("\n");
3071
+ isStreaming = false;
3072
+ }
3073
+ spinner.start(`Using tool: ${name}`);
3074
+ },
3075
+ onToolComplete(name, duration, result) {
3076
+ spinner.stopAndPersist({
3077
+ symbol: chalk2.green("*"),
3078
+ text: `${chalk2.green(name)} ${chalk2.dim(`(${duration}ms)`)}`
3079
+ });
3080
+ if (result) {
3081
+ const resultLines = String(result).split("\n");
3082
+ const preview = resultLines.slice(0, 5).join("\n");
3083
+ console.log(chalk2.dim(preview.replace(/^/gm, " ")));
3084
+ if (resultLines.length > 5) {
3085
+ console.log(chalk2.dim(` ... (${resultLines.length - 5} more lines)`));
3086
+ }
3087
+ }
3088
+ spinner.start("Thinking...");
3089
+ },
3090
+ onToolError(name, error) {
3091
+ spinner.stopAndPersist({
3092
+ symbol: chalk2.red("x"),
3093
+ text: `${chalk2.red(name)}: ${error}`
3094
+ });
3095
+ spinner.start("Thinking...");
3096
+ },
3097
+ onRetry(attempt, maxRetries, delay, error) {
3098
+ spinner.text = chalk2.yellow(`Retry ${attempt}/${maxRetries} after ${delay}ms: ${error}`);
3099
+ },
3100
+ async onInteraction(_id, prompt) {
3101
+ if (isStreaming) {
3102
+ process.stdout.write("\n");
3103
+ isStreaming = false;
3104
+ }
3105
+ spinner.stopAndPersist({ symbol: "!", text: "User Input Required" });
3106
+ const rl = readline.createInterface({ input, output });
3107
+ console.log(chalk2.yellow(`
3108
+ Agent asks: ${prompt}`));
3109
+ const answer = await rl.question(chalk2.bold("> "));
3110
+ rl.close();
3111
+ spinner.start("Resuming...");
3112
+ return answer;
3113
+ },
3114
+ onComplete() {
3115
+ if (isStreaming) {
3116
+ process.stdout.write("\n");
3117
+ isStreaming = false;
3118
+ }
3119
+ spinner.succeed(chalk2.bold.green("Task Completed."));
3120
+ resolve2();
3121
+ },
3122
+ onError(error) {
3123
+ if (isStreaming) {
3124
+ process.stdout.write("\n");
3125
+ isStreaming = false;
3126
+ }
3127
+ spinner.fail(chalk2.red("Task Failed"));
3128
+ console.error(chalk2.red(`
3129
+ Error details: ${error}`));
3130
+ resolve2();
3131
+ }
3132
+ }, port).catch(reject);
3133
+ });
3134
+ } catch (error) {
3135
+ spinner.fail(chalk2.red("Resume Error"));
3136
+ console.error(chalk2.red(error.message || error));
3137
+ process.exit(1);
3138
+ }
3139
+ });
3140
+ const computerUseCmd = program.command("computer-use").description("Inspect computer-use sessions and evidence");
3141
+ computerUseCmd.command("task-evidence").description("Inspect observation frames for a task").argument("<taskId>", "Task ID").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (taskId, options) => {
3142
+ try {
3143
+ const port = await ensureServer(parseInt(options.port, 10));
3144
+ const result = await apiGet(
3145
+ `/api/tasks/${encodeURIComponent(taskId)}/evidence`,
3146
+ port
3147
+ );
3148
+ console.log(chalk2.bold(`
3149
+ Task Evidence: ${result.taskId}`));
3150
+ console.log(` Frames: ${result.total}`);
3151
+ for (const frame of result.frames) {
3152
+ const blockTypes = Array.isArray(frame.blocks) ? frame.blocks.map((block) => block.type).join(", ") : "unknown";
3153
+ console.log(` - ${frame.frameId} (${blockTypes})`);
3154
+ }
3155
+ console.log("");
3156
+ } catch (error) {
3157
+ console.error(chalk2.red("Failed to inspect task evidence:"), error.message);
3158
+ }
3159
+ });
3160
+ computerUseCmd.command("session").description("Inspect a computer-use session timeline").argument("<sessionId>", "Computer-use session ID").option("--port <number>", "Server port", String(DEFAULT_PORT)).action(async (sessionId, options) => {
3161
+ try {
3162
+ const port = await ensureServer(parseInt(options.port, 10));
3163
+ const result = await apiGet(`/api/computer-use/sessions/${encodeURIComponent(sessionId)}`, port);
3164
+ console.log(chalk2.bold(`
3165
+ Computer-Use Session: ${result.sessionId}`));
3166
+ if (result.checkpoint) {
3167
+ console.log(` Task: ${result.checkpoint.taskId}`);
3168
+ console.log(` Last action: ${result.checkpoint.lastActionId || "n/a"}`);
3169
+ console.log(` Verification failures: ${result.checkpoint.verificationFailures}`);
3170
+ } else {
3171
+ console.log(chalk2.yellow(" No active checkpoint found for this session."));
3172
+ }
3173
+ console.log(` Frames: ${result.total}`);
3174
+ for (const frame of result.frames) {
3175
+ const blockTypes = Array.isArray(frame.blocks) ? frame.blocks.map((block) => block.type).join(", ") : "unknown";
3176
+ console.log(` - ${frame.actionId} -> ${frame.frameId} (${blockTypes})`);
3177
+ }
3178
+ console.log("");
3179
+ } catch (error) {
3180
+ console.error(chalk2.red("Failed to inspect session:"), error.message);
3181
+ }
3182
+ });
3183
+ computerUseCmd.command("smoke").description("Run computer-use MCP smoke checks (health + canonical tool coverage + success rate)").option("-s, --server <id>", "Configured MCP server id", "browser").option("--timeout-ms <ms>", "Connection timeout per server (default: 15000)", "15000").option("--retries <n>", "Retry attempts (default: 1)", "1").option("--runs <n>", "Number of repeated smoke attempts (default: 1)", "1").option("--min-success-rate <n>", "Minimum pass ratio in [0,1] (default: 0.95)", "0.95").option("--json", "Output JSON only").action(async (options) => {
3184
+ const config = await new ConfigLoader3().load();
3185
+ const serverId = String(options.server || "browser");
3186
+ const server = (config.mcpServers || {})[serverId];
3187
+ if (!server) {
3188
+ const message = `MCP server "${serverId}" not found in ~/.lydia/config.json`;
3189
+ if (options.json) {
3190
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
3191
+ } else {
3192
+ console.error(chalk2.red(message));
3193
+ }
3194
+ process.exitCode = 1;
3195
+ return;
3196
+ }
3197
+ const timeoutMs = Number(options.timeoutMs) || 15e3;
3198
+ const retries = Math.max(0, Number(options.retries) || 1);
3199
+ const runs = Math.max(1, Number(options.runs) || 1);
3200
+ const minSuccessRate = Math.min(1, Math.max(0, Number(options.minSuccessRate) || 0.95));
3201
+ const attempts = [];
3202
+ for (let i = 0; i < runs; i += 1) {
3203
+ const [result] = await checkMcpServers(
3204
+ [{ id: serverId, command: server.command, args: server.args, env: server.env }],
3205
+ { timeoutMs, retries }
3206
+ );
3207
+ const tools = result?.tools || [];
3208
+ const canonicalTools = tools.filter((name) => Boolean(resolveCanonicalComputerUseToolName(name)));
3209
+ attempts.push({
3210
+ ok: Boolean(result?.ok) && canonicalTools.length > 0,
3211
+ health: result,
3212
+ totalTools: tools.length,
3213
+ canonicalTools
3214
+ });
3215
+ }
3216
+ const passed = attempts.filter((item) => item.ok).length;
3217
+ const successRate = runs > 0 ? passed / runs : 0;
3218
+ const last = attempts[attempts.length - 1];
3219
+ const smoke = {
3220
+ ok: successRate >= minSuccessRate,
3221
+ serverId,
3222
+ runs,
3223
+ passed,
3224
+ successRate,
3225
+ minSuccessRate,
3226
+ lastAttempt: last,
3227
+ attempts
3228
+ };
3229
+ if (options.json) {
3230
+ console.log(JSON.stringify(smoke, null, 2));
3231
+ } else {
3232
+ if (!last?.health?.ok) {
3233
+ console.log(chalk2.red(`x ${serverId}: ${last?.health?.error || "health check failed"}`));
3234
+ } else {
3235
+ console.log(chalk2.green(`* ${serverId}: latest health check passed (${last.health.durationMs}ms)`));
3236
+ console.log(chalk2.dim(` discovered tools: ${last.totalTools}`));
3237
+ console.log(chalk2.dim(` canonical computer-use aliases: ${last.canonicalTools.length}`));
3238
+ if (last.canonicalTools.length > 0) {
3239
+ console.log(chalk2.dim(` ${last.canonicalTools.join(", ")}`));
3240
+ }
3241
+ }
3242
+ console.log(
3243
+ `${smoke.ok ? chalk2.green("*") : chalk2.red("x")} smoke success rate: ${(successRate * 100).toFixed(1)}% (${passed}/${runs}), threshold ${(minSuccessRate * 100).toFixed(1)}%`
3244
+ );
3245
+ }
3246
+ if (!smoke.ok) {
3247
+ if (!options.json && last?.health?.ok && (last?.canonicalTools?.length || 0) === 0) {
3248
+ console.log(chalk2.red("Smoke failed: no canonical computer-use tools detected."));
3249
+ }
3250
+ process.exitCode = 1;
3251
+ }
3252
+ });
3253
+ const skillsCmd = program.command("skills").description("Manage skills");
3254
+ skillsCmd.command("list").description("List all loaded skills").action(async () => {
3255
+ const { SkillRegistry, SkillLoader } = await import("@lydia-agent/core");
3256
+ const registry = new SkillRegistry();
3257
+ const loader = new SkillLoader(registry);
3258
+ const config = await new ConfigLoader3().load();
3259
+ const extraDirs = config.skills?.extraDirs ?? [];
3260
+ await loader.loadAll(extraDirs);
3261
+ const skills = registry.list();
3262
+ if (skills.length === 0) {
3263
+ console.log(chalk2.yellow("No skills found."));
3264
+ return;
3265
+ }
3266
+ console.log(chalk2.bold(`
3267
+ Loaded Skills (${skills.length}):
3268
+ `));
3269
+ for (const skill of skills) {
3270
+ const version2 = "version" in skill && skill.version ? ` v${skill.version}` : "";
3271
+ const tags = skill.tags?.length ? chalk2.dim(` [${skill.tags.join(", ")}]`) : "";
3272
+ const source = skill.path ? chalk2.dim(` (${skill.path})`) : "";
3273
+ const isDynamic = "execute" in skill ? chalk2.cyan(" [dynamic]") : "";
3274
+ console.log(` ${chalk2.green(skill.name)}${version2}${isDynamic} - ${skill.description}${tags}`);
3275
+ if (source) console.log(` ${source}`);
3276
+ }
3277
+ console.log("");
3278
+ });
3279
+ skillsCmd.command("info").description("Show detailed information about a skill").argument("<name>", "Skill name").action(async (name) => {
3280
+ const { SkillRegistry, SkillLoader } = await import("@lydia-agent/core");
3281
+ const registry = new SkillRegistry();
3282
+ const loader = new SkillLoader(registry);
3283
+ const config = await new ConfigLoader3().load();
3284
+ const extraDirs = config.skills?.extraDirs ?? [];
3285
+ await loader.loadAll(extraDirs);
3286
+ const skill = registry.get(name);
3287
+ if (!skill) {
3288
+ console.error(chalk2.red(`Skill "${name}" not found.`));
3289
+ return;
3290
+ }
3291
+ console.log(chalk2.bold(`
3292
+ Skill: ${skill.name}
3293
+ `));
3294
+ console.log(` Description: ${skill.description}`);
3295
+ if ("version" in skill && skill.version) console.log(` Version: ${skill.version}`);
3296
+ if ("author" in skill && skill.author) console.log(` Author: ${skill.author}`);
3297
+ if (skill.tags?.length) console.log(` Tags: ${skill.tags.join(", ")}`);
3298
+ if (skill.allowedTools?.length) console.log(` Allowed Tools: ${skill.allowedTools.join(", ")}`);
3299
+ if (skill.path) console.log(` Path: ${skill.path}`);
3300
+ const content = await loader.loadContent(name);
3301
+ if (content) {
3302
+ console.log(chalk2.dim(`
3303
+ ${"\u2500".repeat(40)}
3304
+ `));
3305
+ console.log(content);
3306
+ console.log("");
3307
+ }
3308
+ });
3309
+ skillsCmd.command("install").description("Install a skill from a GitHub URL or local path").argument("<source>", "GitHub URL (github:user/repo/path) or local directory path").option("--project", "Install to project .lydia/skills/ instead of user global").action(async (source, options) => {
3310
+ const targetDir = options.project ? path2.join(process.cwd(), ".lydia", "skills") : path2.join(os2.homedir(), ".lydia", "skills");
3311
+ await fsPromises2.mkdir(targetDir, { recursive: true });
3312
+ if (source.startsWith("github:")) {
3313
+ const ghPath = source.slice("github:".length);
3314
+ const parts = ghPath.split("/");
3315
+ if (parts.length < 3) {
3316
+ console.error(chalk2.red("Invalid GitHub source. Format: github:owner/repo/path/to/skill"));
3317
+ return;
3318
+ }
3319
+ const owner = parts[0];
3320
+ const repo = parts[1];
3321
+ const skillPath = parts.slice(2).join("/");
3322
+ const skillName = parts[parts.length - 1].replace(/\.md$/, "");
3323
+ console.log(chalk2.dim(`Fetching from GitHub: ${owner}/${repo}/${skillPath}...`));
3324
+ try {
3325
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/${skillPath}`;
3326
+ const response = await fetch(rawUrl);
3327
+ if (response.ok) {
3328
+ const content = await response.text();
3329
+ const fileName = skillPath.endsWith(".md") ? path2.basename(skillPath) : `${skillName}.md`;
3330
+ const destPath = path2.join(targetDir, fileName);
3331
+ await fsPromises2.writeFile(destPath, content, "utf-8");
3332
+ console.log(chalk2.green(`Installed skill to: ${destPath}`));
3333
+ } else {
3334
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${skillPath}`;
3335
+ const apiResponse = await fetch(apiUrl, {
3336
+ headers: { "Accept": "application/vnd.github.v3+json" }
3337
+ });
3338
+ if (!apiResponse.ok) {
3339
+ console.error(chalk2.red(`Failed to fetch from GitHub: ${apiResponse.statusText}`));
3340
+ return;
3341
+ }
3342
+ const contents = await apiResponse.json();
3343
+ if (!Array.isArray(contents)) {
3344
+ console.error(chalk2.red("Source is not a valid file or directory."));
3345
+ return;
3346
+ }
3347
+ const skillDir = path2.join(targetDir, skillName);
3348
+ await fsPromises2.mkdir(skillDir, { recursive: true });
3349
+ for (const item of contents) {
3350
+ if (item.type === "file" && item.download_url) {
3351
+ const fileRes = await fetch(item.download_url);
3352
+ if (fileRes.ok) {
3353
+ const fileContent = await fileRes.text();
3354
+ const fileDest = path2.join(skillDir, item.name);
3355
+ await fsPromises2.writeFile(fileDest, fileContent, "utf-8");
3356
+ console.log(chalk2.dim(` Downloaded: ${item.name}`));
3357
+ }
3358
+ }
3359
+ }
3360
+ console.log(chalk2.green(`Installed skill directory to: ${skillDir}`));
3361
+ }
3362
+ } catch (error) {
3363
+ console.error(chalk2.red(`Installation failed: ${error.message}`));
3364
+ }
3365
+ } else {
3366
+ const sourcePath = path2.resolve(source);
3367
+ try {
3368
+ const stat2 = await fsPromises2.stat(sourcePath);
3369
+ if (stat2.isFile()) {
3370
+ const destPath = path2.join(targetDir, path2.basename(sourcePath));
3371
+ await fsPromises2.copyFile(sourcePath, destPath);
3372
+ console.log(chalk2.green(`Installed skill to: ${destPath}`));
3373
+ } else if (stat2.isDirectory()) {
3374
+ const dirName = path2.basename(sourcePath);
3375
+ const destDir = path2.join(targetDir, dirName);
3376
+ await fsPromises2.mkdir(destDir, { recursive: true });
3377
+ await copyDir(sourcePath, destDir);
3378
+ console.log(chalk2.green(`Installed skill directory to: ${destDir}`));
3379
+ }
3380
+ } catch (error) {
3381
+ console.error(chalk2.red(`Installation failed: ${error.message}`));
3382
+ }
3383
+ }
3384
+ });
3385
+ skillsCmd.command("remove").description("Remove an installed skill").argument("<name>", "Skill name to remove").option("--project", "Remove from project .lydia/skills/ instead of user global").action(async (name, options) => {
3386
+ const baseDir = options.project ? path2.join(process.cwd(), ".lydia", "skills") : path2.join(os2.homedir(), ".lydia", "skills");
3387
+ let removed = false;
3388
+ const mdPath = path2.join(baseDir, `${name}.md`);
3389
+ if (fs3.existsSync(mdPath)) {
3390
+ await fsPromises2.unlink(mdPath);
3391
+ console.log(chalk2.green(`Removed skill file: ${mdPath}`));
3392
+ removed = true;
3393
+ }
3394
+ const dirPath = path2.join(baseDir, name);
3395
+ if (fs3.existsSync(dirPath)) {
3396
+ await fsPromises2.rm(dirPath, { recursive: true });
3397
+ console.log(chalk2.green(`Removed skill directory: ${dirPath}`));
3398
+ removed = true;
3399
+ }
3400
+ if (!removed) {
3401
+ console.error(chalk2.red(`Skill "${name}" not found in ${baseDir}`));
3402
+ }
3403
+ });
3404
+ program.parse();
3405
+ }
3406
+ async function copyDir(src, dest) {
3407
+ const entries = await fsPromises2.readdir(src, { withFileTypes: true });
3408
+ for (const entry of entries) {
3409
+ const srcPath = path2.join(src, entry.name);
3410
+ const destPath = path2.join(dest, entry.name);
3411
+ if (entry.isDirectory()) {
3412
+ await fsPromises2.mkdir(destPath, { recursive: true });
3413
+ await copyDir(srcPath, destPath);
3414
+ } else {
3415
+ await fsPromises2.copyFile(srcPath, destPath);
3416
+ }
3417
+ }
3418
+ }
3419
+ main();