@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 +3419 -0
- package/package.json +46 -0
- package/public/assets/index-Cc8QkBCl.css +1 -0
- package/public/assets/index-DM1dnphb.js +250 -0
- package/public/index.html +13 -0
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();
|