@baanish/hydra-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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -0
  3. package/dist/config.d.ts +29 -0
  4. package/dist/config.d.ts.map +1 -0
  5. package/dist/config.js +338 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/db/client.d.ts +10 -0
  8. package/dist/db/client.d.ts.map +1 -0
  9. package/dist/db/client.js +93 -0
  10. package/dist/db/client.js.map +1 -0
  11. package/dist/db/queries.d.ts +67 -0
  12. package/dist/db/queries.d.ts.map +1 -0
  13. package/dist/db/queries.js +336 -0
  14. package/dist/db/queries.js.map +1 -0
  15. package/dist/engine/concurrency.d.ts +3 -0
  16. package/dist/engine/concurrency.d.ts.map +1 -0
  17. package/dist/engine/concurrency.js +42 -0
  18. package/dist/engine/concurrency.js.map +1 -0
  19. package/dist/engine/eta.d.ts +16 -0
  20. package/dist/engine/eta.d.ts.map +1 -0
  21. package/dist/engine/eta.js +54 -0
  22. package/dist/engine/eta.js.map +1 -0
  23. package/dist/engine/model.d.ts +57 -0
  24. package/dist/engine/model.d.ts.map +1 -0
  25. package/dist/engine/model.js +445 -0
  26. package/dist/engine/model.js.map +1 -0
  27. package/dist/engine/personas.d.ts +30 -0
  28. package/dist/engine/personas.d.ts.map +1 -0
  29. package/dist/engine/personas.js +336 -0
  30. package/dist/engine/personas.js.map +1 -0
  31. package/dist/engine/pipeline.d.ts +61 -0
  32. package/dist/engine/pipeline.d.ts.map +1 -0
  33. package/dist/engine/pipeline.js +638 -0
  34. package/dist/engine/pipeline.js.map +1 -0
  35. package/dist/engine/prompts.d.ts +10 -0
  36. package/dist/engine/prompts.d.ts.map +1 -0
  37. package/dist/engine/prompts.js +49 -0
  38. package/dist/engine/prompts.js.map +1 -0
  39. package/dist/engine/search.d.ts +46 -0
  40. package/dist/engine/search.d.ts.map +1 -0
  41. package/dist/engine/search.js +159 -0
  42. package/dist/engine/search.js.map +1 -0
  43. package/dist/index.d.ts +5 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +648 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/security.d.ts +18 -0
  48. package/dist/security.d.ts.map +1 -0
  49. package/dist/security.js +168 -0
  50. package/dist/security.js.map +1 -0
  51. package/dist/types.d.ts +143 -0
  52. package/dist/types.d.ts.map +1 -0
  53. package/dist/types.js +2 -0
  54. package/dist/types.js.map +1 -0
  55. package/dist/ui/agent-mode.d.ts +8 -0
  56. package/dist/ui/agent-mode.d.ts.map +1 -0
  57. package/dist/ui/agent-mode.js +138 -0
  58. package/dist/ui/agent-mode.js.map +1 -0
  59. package/dist/ui/animations.d.ts +8 -0
  60. package/dist/ui/animations.d.ts.map +1 -0
  61. package/dist/ui/animations.js +19 -0
  62. package/dist/ui/animations.js.map +1 -0
  63. package/dist/ui/components/agent-list.d.ts +2 -0
  64. package/dist/ui/components/agent-list.d.ts.map +1 -0
  65. package/dist/ui/components/agent-list.js +2 -0
  66. package/dist/ui/components/agent-list.js.map +1 -0
  67. package/dist/ui/components/header.d.ts +2 -0
  68. package/dist/ui/components/header.d.ts.map +1 -0
  69. package/dist/ui/components/header.js +2 -0
  70. package/dist/ui/components/header.js.map +1 -0
  71. package/dist/ui/components/phase-bar.d.ts +2 -0
  72. package/dist/ui/components/phase-bar.d.ts.map +1 -0
  73. package/dist/ui/components/phase-bar.js +2 -0
  74. package/dist/ui/components/phase-bar.js.map +1 -0
  75. package/dist/ui/components/stats-bar.d.ts +2 -0
  76. package/dist/ui/components/stats-bar.d.ts.map +1 -0
  77. package/dist/ui/components/stats-bar.js +2 -0
  78. package/dist/ui/components/stats-bar.js.map +1 -0
  79. package/dist/ui/tui.d.ts +18 -0
  80. package/dist/ui/tui.d.ts.map +1 -0
  81. package/dist/ui/tui.js +464 -0
  82. package/dist/ui/tui.js.map +1 -0
  83. package/dist/web/app.html +1352 -0
  84. package/dist/web/index.d.ts +2 -0
  85. package/dist/web/index.d.ts.map +1 -0
  86. package/dist/web/index.js +2 -0
  87. package/dist/web/index.js.map +1 -0
  88. package/dist/web/server.d.ts +2 -0
  89. package/dist/web/server.d.ts.map +1 -0
  90. package/dist/web/server.js +864 -0
  91. package/dist/web/server.js.map +1 -0
  92. package/package.json +59 -0
package/dist/index.js ADDED
@@ -0,0 +1,648 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from "node:fs";
3
+ import { Command } from "commander";
4
+ import { MAX_DEBATE_ROUNDS, MIN_DEBATE_ROUNDS, clampInt, getConfigPath, loadConfig, maskConfigValue, sanitizeConfigValueForSet, writeConfig, } from "./config.js";
5
+ import { getRun, getRunAgentRuns, listRuns, removeRun } from "./db/queries.js";
6
+ import { PERSONAS, addCustomPersona, allPersonas, removeCustomPersona, } from "./engine/personas.js";
7
+ import { HydraPipeline } from "./engine/pipeline.js";
8
+ import { formatErrorMessage, sanitizeForTerminal } from "./security.js";
9
+ import { emitAgentProgress, setAgentModeConcurrency, setAgentModeDebateRounds, } from "./ui/agent-mode.js";
10
+ const command = new Command()
11
+ .name("hydra")
12
+ .description("multi-agent research and synthesis CLI")
13
+ .addHelpText("after", `
14
+ storage:
15
+ config: ${getConfigPath()}
16
+ `)
17
+ .showHelpAfterError(true);
18
+ const configKeyMap = {
19
+ "api-key": "apiKey",
20
+ "search-provider": "searchProvider",
21
+ "synthetic-api-key": "syntheticApiKey",
22
+ "exa-api-key": "exaApiKey",
23
+ "brave-api-key": "braveApiKey",
24
+ "base-url": "baseUrl",
25
+ model: "model",
26
+ "orchestrator-model": "orchestratorModel",
27
+ "research-model": "researchModel",
28
+ "default-agent-count": "defaultAgentCount",
29
+ "max-concurrency": "maxConcurrency",
30
+ "debate-rounds": "debateRounds",
31
+ "search-enabled": "searchEnabled",
32
+ "custom-personas-only": "customPersonasOnly",
33
+ };
34
+ function resolveLlmApiKey(config) {
35
+ return config.apiKey || config.syntheticApiKey || "";
36
+ }
37
+ function resolveSearchConfig(config) {
38
+ return {
39
+ provider: config.searchProvider,
40
+ syntheticApiKey: config.syntheticApiKey || "",
41
+ exaApiKey: config.exaApiKey || "",
42
+ braveApiKey: config.braveApiKey || "",
43
+ };
44
+ }
45
+ function resolveSearchApiKey(config) {
46
+ const searchConfig = resolveSearchConfig(config);
47
+ if (searchConfig.provider === "synthetic") {
48
+ return config.syntheticApiKey || resolveLlmApiKey(config);
49
+ }
50
+ if (searchConfig.provider === "exa") {
51
+ return config.exaApiKey || "";
52
+ }
53
+ return config.braveApiKey || "";
54
+ }
55
+ function createMaskedConfig(config) {
56
+ return {
57
+ ...config,
58
+ apiKey: maskConfigValue(config.apiKey),
59
+ syntheticApiKey: maskConfigValue(config.syntheticApiKey),
60
+ exaApiKey: maskConfigValue(config.exaApiKey),
61
+ braveApiKey: maskConfigValue(config.braveApiKey),
62
+ };
63
+ }
64
+ function statusSymbol(status) {
65
+ if (status === "complete") {
66
+ return "✓";
67
+ }
68
+ if (status === "error") {
69
+ return "✗";
70
+ }
71
+ return "⋯";
72
+ }
73
+ export function formatElapsed(ms) {
74
+ const totalSeconds = Math.max(0, Math.floor(ms / 1000));
75
+ const hours = Math.floor(totalSeconds / 3600);
76
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
77
+ const seconds = totalSeconds % 60;
78
+ if (hours > 0) {
79
+ return `${hours}h ${minutes}m`;
80
+ }
81
+ if (minutes > 0) {
82
+ return `${minutes}m ${seconds}s`;
83
+ }
84
+ return `${seconds}s`;
85
+ }
86
+ export function truncateQuery(query, maxChars = 60) {
87
+ const normalized = query.replace(/\s+/g, " ").trim();
88
+ if (normalized.length <= maxChars) {
89
+ return normalized;
90
+ }
91
+ return `${normalized.slice(0, maxChars - 1)}…`;
92
+ }
93
+ const ROOT_COMMANDS = new Set([
94
+ "run",
95
+ "history",
96
+ "view",
97
+ "delete",
98
+ "web",
99
+ "config",
100
+ "persona",
101
+ "help",
102
+ ]);
103
+ const RUN_BOOLEAN_OPTIONS = new Set(["--agent-mode", "--json", "-h", "--help"]);
104
+ const RUN_VALUE_OPTIONS = new Set([
105
+ "-a",
106
+ "--agents",
107
+ "-c",
108
+ "--concurrency",
109
+ "-d",
110
+ "--debate-rounds",
111
+ "--model",
112
+ "-o",
113
+ "--output",
114
+ ]);
115
+ function isRecognizedRunOptionToken(token) {
116
+ if (RUN_BOOLEAN_OPTIONS.has(token)) {
117
+ return { known: true, takesValue: false };
118
+ }
119
+ if (RUN_VALUE_OPTIONS.has(token)) {
120
+ return { known: true, takesValue: true };
121
+ }
122
+ if (token.startsWith("-") && !token.startsWith("--") && token.length > 2) {
123
+ const shortOption = token.slice(0, 2);
124
+ if (RUN_VALUE_OPTIONS.has(shortOption)) {
125
+ // support compact short options like -a5, -d3, -oout.txt during bare-run normalization.
126
+ return { known: true, takesValue: false };
127
+ }
128
+ }
129
+ if (!token.startsWith("--")) {
130
+ return { known: false, takesValue: false };
131
+ }
132
+ const separatorIndex = token.indexOf("=");
133
+ if (separatorIndex <= 2) {
134
+ return { known: false, takesValue: false };
135
+ }
136
+ const optionName = token.slice(0, separatorIndex);
137
+ if (RUN_VALUE_OPTIONS.has(optionName)) {
138
+ return { known: true, takesValue: false };
139
+ }
140
+ return { known: false, takesValue: false };
141
+ }
142
+ function isSingleEditOrSwapAway(input, target) {
143
+ if (input === target) {
144
+ return false;
145
+ }
146
+ if (Math.abs(input.length - target.length) > 1) {
147
+ return false;
148
+ }
149
+ if (input.length === target.length) {
150
+ let mismatchIndex = -1;
151
+ for (let index = 0; index < input.length; index += 1) {
152
+ if (input[index] !== target[index]) {
153
+ mismatchIndex = index;
154
+ break;
155
+ }
156
+ }
157
+ if (mismatchIndex === -1) {
158
+ return false;
159
+ }
160
+ if (input.slice(mismatchIndex + 1) === target.slice(mismatchIndex + 1)) {
161
+ return true;
162
+ }
163
+ return (mismatchIndex + 1 < input.length &&
164
+ input[mismatchIndex] === target[mismatchIndex + 1] &&
165
+ input[mismatchIndex + 1] === target[mismatchIndex] &&
166
+ input.slice(mismatchIndex + 2) === target.slice(mismatchIndex + 2));
167
+ }
168
+ const shorter = input.length < target.length ? input : target;
169
+ const longer = input.length < target.length ? target : input;
170
+ for (let index = 0; index < shorter.length; index += 1) {
171
+ if (shorter[index] !== longer[index]) {
172
+ return shorter.slice(index) === longer.slice(index + 1);
173
+ }
174
+ }
175
+ return true;
176
+ }
177
+ function isLikelyRootCommandTypo(token) {
178
+ if (/\s/.test(token)) {
179
+ return false;
180
+ }
181
+ const normalized = token.toLowerCase();
182
+ if (ROOT_COMMANDS.has(normalized)) {
183
+ return false;
184
+ }
185
+ for (const commandName of ROOT_COMMANDS) {
186
+ if (isSingleEditOrSwapAway(normalized, commandName)) {
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+ function shouldRewriteAsBareRun(argv) {
193
+ const [queryToken, ...rest] = argv;
194
+ if (!queryToken) {
195
+ return false;
196
+ }
197
+ if (rest.length === 0) {
198
+ return true;
199
+ }
200
+ for (let index = 0; index < rest.length; index += 1) {
201
+ const token = rest[index];
202
+ if (!token || !token.startsWith("-")) {
203
+ return false;
204
+ }
205
+ const { known, takesValue } = isRecognizedRunOptionToken(token);
206
+ const nextToken = rest[index + 1];
207
+ if (takesValue && nextToken && !nextToken.startsWith("-")) {
208
+ index += 1;
209
+ continue;
210
+ }
211
+ if (!known && nextToken && !nextToken.startsWith("-")) {
212
+ // Unknown options are still treated as run flags so the run parser can emit a precise error.
213
+ index += 1;
214
+ }
215
+ }
216
+ return true;
217
+ }
218
+ export function normalizeArgvForBareRun(argv) {
219
+ if (argv.length < 3) {
220
+ return argv;
221
+ }
222
+ const firstArg = argv[2];
223
+ const normalizedFirstArg = firstArg?.toLowerCase();
224
+ if (!firstArg || firstArg.startsWith("-")) {
225
+ return argv;
226
+ }
227
+ if (normalizedFirstArg && ROOT_COMMANDS.has(normalizedFirstArg)) {
228
+ if (normalizedFirstArg === firstArg) {
229
+ return argv;
230
+ }
231
+ return [argv[0], argv[1], normalizedFirstArg, ...argv.slice(3)];
232
+ }
233
+ const bareRunArgs = argv.slice(2);
234
+ if (isLikelyRootCommandTypo(firstArg)) {
235
+ return argv;
236
+ }
237
+ if (!shouldRewriteAsBareRun(bareRunArgs)) {
238
+ return argv;
239
+ }
240
+ return [argv[0], argv[1], "run", ...argv.slice(2)];
241
+ }
242
+ function printRunErrorGuidance(message) {
243
+ const normalized = message.toLowerCase();
244
+ if (normalized.includes("524")) {
245
+ console.error("Tip: Synthetic.new timed out. Try again or reduce --agents.");
246
+ return;
247
+ }
248
+ if (normalized.includes("all agents failed") ||
249
+ /all\s+\w+\s+agents failed/.test(normalized)) {
250
+ console.error("Tip: Check your API key with 'hydra config show'.");
251
+ }
252
+ }
253
+ function parseJsonLine(value) {
254
+ try {
255
+ return JSON.stringify(JSON.parse(value), null, 2);
256
+ }
257
+ catch {
258
+ return sanitizeForTerminal(value);
259
+ }
260
+ }
261
+ function slugifyPersonaId(value) {
262
+ return value
263
+ .trim()
264
+ .toLowerCase()
265
+ .replace(/\s+/g, "-")
266
+ .replace(/[^a-z0-9-]/g, "");
267
+ }
268
+ const runCommand = new Command("run")
269
+ .description("run a hydra query")
270
+ .argument("<query>", "query to process")
271
+ .option("-a, --agents <count>", "number of agents")
272
+ .option("-c, --concurrency <count>", "max concurrency (1-5, default 5)")
273
+ .option("-d, --debate-rounds <n>", `debate rounds for this run (${MIN_DEBATE_ROUNDS}-${MAX_DEBATE_ROUNDS})`)
274
+ .option("--model <model>", "model override for this run")
275
+ .option("--custom-personas-only", "use only custom personas (and generate ephemeral personas if needed)")
276
+ .option("-o, --output <file>", "write full synthesis output to file")
277
+ .option("--agent-mode", "emit machine-friendly logs")
278
+ .option("--json", "emit json payload")
279
+ .action(async (query, options) => {
280
+ const baseConfig = loadConfig();
281
+ const resolvedConcurrency = options.concurrency
282
+ ? clampInt(options.concurrency, 1, 5, baseConfig.maxConcurrency)
283
+ : baseConfig.maxConcurrency;
284
+ const resolvedDebateRounds = options.debateRounds
285
+ ? clampInt(options.debateRounds, MIN_DEBATE_ROUNDS, MAX_DEBATE_ROUNDS, baseConfig.debateRounds)
286
+ : baseConfig.debateRounds;
287
+ const resolvedModel = typeof options.model === "string" && options.model.trim().length > 0
288
+ ? options.model.trim()
289
+ : baseConfig.model;
290
+ const resolvedCustomPersonasOnly = options.customPersonasOnly
291
+ ? true
292
+ : baseConfig.customPersonasOnly;
293
+ const config = {
294
+ ...baseConfig,
295
+ maxConcurrency: resolvedConcurrency,
296
+ debateRounds: resolvedDebateRounds,
297
+ model: resolvedModel,
298
+ customPersonasOnly: resolvedCustomPersonasOnly,
299
+ };
300
+ const modelApiKey = resolveLlmApiKey(config);
301
+ const searchConfig = resolveSearchConfig(config);
302
+ const searchApiKey = resolveSearchApiKey(config);
303
+ if (!modelApiKey) {
304
+ console.error("[hydra] warning: no api-key configured. run calls may fail.");
305
+ }
306
+ if (!searchApiKey) {
307
+ console.error(`[hydra] warning: no search API key configured for provider ${searchConfig.provider}. search calls may fail.`);
308
+ }
309
+ if (!config.syntheticApiKey) {
310
+ console.error("[hydra] warning: no synthetic-api-key configured. synthetic search may be unavailable for synthetic provider.");
311
+ }
312
+ const agentCount = clampInt(options.agents ?? config.defaultAgentCount, 1, 20, config.defaultAgentCount);
313
+ const pipelineConfig = {
314
+ apiKey: modelApiKey,
315
+ baseUrl: config.baseUrl,
316
+ model: resolvedModel,
317
+ orchestratorModel: config.orchestratorModel ?? resolvedModel,
318
+ researchModel: config.researchModel ?? resolvedModel,
319
+ searchConfig,
320
+ agentCount,
321
+ maxConcurrency: config.maxConcurrency,
322
+ debateRounds: config.debateRounds,
323
+ searchEnabled: config.searchEnabled,
324
+ customPersonasOnly: config.customPersonasOnly,
325
+ };
326
+ const pipeline = new HydraPipeline(pipelineConfig);
327
+ const shouldUseTui = !options.json &&
328
+ !options.agentMode &&
329
+ process.stdout.isTTY === true &&
330
+ process.stderr.isTTY === true;
331
+ let useAgentProgress = options.agentMode || !shouldUseTui;
332
+ let ui = null;
333
+ if (shouldUseTui) {
334
+ try {
335
+ const tuiModule = await import("./ui/tui.js");
336
+ ui = new tuiModule.HydraUI({
337
+ concurrency: config.maxConcurrency,
338
+ totalDebateRounds: config.debateRounds,
339
+ });
340
+ await ui.start(query, agentCount);
341
+ useAgentProgress = false;
342
+ }
343
+ catch (error) {
344
+ ui?.stop();
345
+ const message = formatErrorMessage(error) || "unknown tui initialization error";
346
+ console.error(`[hydra] warning: failed to initialize TUI, falling back to non-interactive progress: ${message}`);
347
+ ui = null;
348
+ useAgentProgress = true;
349
+ }
350
+ }
351
+ if (!options.json) {
352
+ setAgentModeDebateRounds(config.debateRounds);
353
+ setAgentModeConcurrency(config.maxConcurrency);
354
+ if (useAgentProgress) {
355
+ pipeline.on("run-created", emitAgentProgress);
356
+ pipeline.on("run-status-changed", emitAgentProgress);
357
+ pipeline.on("agent-progress", emitAgentProgress);
358
+ pipeline.on("agent-complete", emitAgentProgress);
359
+ pipeline.on("run-complete", emitAgentProgress);
360
+ }
361
+ if (ui) {
362
+ const activeUi = ui;
363
+ pipeline.on("run-created", (event) => {
364
+ if (event.type === "run-created") {
365
+ activeUi.handleEvent(event);
366
+ }
367
+ });
368
+ pipeline.on("run-status-changed", (event) => {
369
+ if (event.type === "run-status-changed") {
370
+ activeUi.handleEvent(event);
371
+ }
372
+ });
373
+ pipeline.on("agent-progress", (event) => {
374
+ if (event.type === "agent-progress") {
375
+ activeUi.handleEvent(event);
376
+ }
377
+ });
378
+ pipeline.on("agent-complete", (event) => {
379
+ if (event.type === "agent-complete") {
380
+ activeUi.handleEvent(event);
381
+ }
382
+ });
383
+ pipeline.on("run-complete", (event) => {
384
+ if (event.type === "run-complete") {
385
+ activeUi.handleEvent(event);
386
+ }
387
+ });
388
+ }
389
+ }
390
+ let result = null;
391
+ try {
392
+ result = await pipeline.run(query);
393
+ }
394
+ catch (error) {
395
+ ui?.stop();
396
+ const message = formatErrorMessage(error) || "pipeline failed";
397
+ console.error(`[hydra] error: ${message}`);
398
+ printRunErrorGuidance(message);
399
+ process.exitCode = 1;
400
+ return;
401
+ }
402
+ if (!result) {
403
+ return;
404
+ }
405
+ const outputPath = typeof options.output === "string" && options.output.trim().length > 0
406
+ ? options.output.trim()
407
+ : null;
408
+ if (outputPath) {
409
+ try {
410
+ writeFileSync(outputPath, result.brief, "utf8");
411
+ }
412
+ catch (error) {
413
+ const message = formatErrorMessage(error) || "unknown error";
414
+ ui?.stop();
415
+ console.error("Error: could not write to file:", message);
416
+ process.exitCode = 1;
417
+ return;
418
+ }
419
+ }
420
+ if (options.json) {
421
+ const finishedRun = getRun(result.runId);
422
+ const agentRuns = finishedRun ? getRunAgentRuns(finishedRun.id) : [];
423
+ console.log(JSON.stringify({
424
+ runId: result.runId,
425
+ query,
426
+ agentCount: finishedRun?.agentCount ?? agentCount,
427
+ elapsedMs: finishedRun?.elapsedMs ?? 0,
428
+ synthesis: sanitizeForTerminal(result.brief),
429
+ agents: agentRuns.map((agentRun) => ({
430
+ persona: agentRun.persona,
431
+ phase: agentRun.phase,
432
+ output: sanitizeForTerminal(agentRun.output),
433
+ })),
434
+ }, null, 2));
435
+ if (outputPath) {
436
+ console.error(`Saved to ${outputPath}`);
437
+ }
438
+ return;
439
+ }
440
+ if (ui) {
441
+ ui.stop(result.brief);
442
+ }
443
+ else {
444
+ console.log("\nbrief:");
445
+ console.log(sanitizeForTerminal(result.brief));
446
+ }
447
+ if (outputPath) {
448
+ console.log(`Saved to ${outputPath}`);
449
+ }
450
+ });
451
+ const historyCommand = new Command("history")
452
+ .description("list past runs")
453
+ .option("--limit <n>", "max rows to show")
454
+ .action((options) => {
455
+ const limit = clampInt(options.limit ?? 30, 1, 200, 30);
456
+ const runs = listRuns(limit);
457
+ if (runs.length === 0) {
458
+ console.log("no runs found");
459
+ return;
460
+ }
461
+ const now = Date.now();
462
+ const rows = runs.map((run) => {
463
+ const elapsedMs = run.elapsedMs ??
464
+ (run.status === "complete" || run.status === "error"
465
+ ? Math.max(0, (run.completedAt ?? run.createdAt) - run.createdAt)
466
+ : Math.max(0, now - run.createdAt));
467
+ return {
468
+ status: `${statusSymbol(run.status)} ${run.status}`,
469
+ runId: run.id,
470
+ createdAt: new Date(run.createdAt).toISOString(),
471
+ agents: String(run.agentCount),
472
+ elapsed: formatElapsed(elapsedMs),
473
+ query: sanitizeForTerminal(truncateQuery(run.query, 60)),
474
+ };
475
+ });
476
+ const statusWidth = Math.max("status".length, ...rows.map((row) => row.status.length));
477
+ const runIdWidth = Math.max("run-id".length, ...rows.map((row) => row.runId.length));
478
+ const createdAtWidth = Math.max("created-at".length, ...rows.map((row) => row.createdAt.length));
479
+ const agentsWidth = Math.max("agents".length, ...rows.map((row) => row.agents.length));
480
+ const elapsedWidth = Math.max("elapsed".length, ...rows.map((row) => row.elapsed.length));
481
+ console.log([
482
+ "status".padEnd(statusWidth),
483
+ "run-id".padEnd(runIdWidth),
484
+ "created-at".padEnd(createdAtWidth),
485
+ "agents".padStart(agentsWidth),
486
+ "elapsed".padStart(elapsedWidth),
487
+ "query",
488
+ ].join(" | "));
489
+ for (const row of rows) {
490
+ console.log([
491
+ row.status.padEnd(statusWidth),
492
+ row.runId.padEnd(runIdWidth),
493
+ row.createdAt.padEnd(createdAtWidth),
494
+ row.agents.padStart(agentsWidth),
495
+ row.elapsed.padStart(elapsedWidth),
496
+ row.query,
497
+ ].join(" | "));
498
+ }
499
+ });
500
+ const viewCommand = new Command("view")
501
+ .description("view a past run")
502
+ .argument("<runId>", "run id")
503
+ .option("--transcripts", "show agent transcripts")
504
+ .action((runId, options) => {
505
+ const run = getRun(runId);
506
+ if (!run) {
507
+ throw new Error(`run ${runId} not found`);
508
+ }
509
+ console.log(`id: ${run.id}`);
510
+ console.log(`status: ${run.status}`);
511
+ console.log(`query: ${sanitizeForTerminal(run.query)}`);
512
+ console.log(`createdAt: ${new Date(run.createdAt).toISOString()}`);
513
+ if (run.error) {
514
+ console.log(`error: ${sanitizeForTerminal(run.error)}`);
515
+ }
516
+ if (run.brief) {
517
+ console.log("\nbrief:");
518
+ console.log(sanitizeForTerminal(run.brief));
519
+ }
520
+ if (options.transcripts) {
521
+ const transcripts = getRunAgentRuns(run.id);
522
+ console.log(`\nagent_runs: ${transcripts.length}`);
523
+ for (const transcript of transcripts) {
524
+ console.log(`\n-- ${sanitizeForTerminal(transcript.persona)} [${transcript.phase}]`);
525
+ console.log(`status: ${transcript.status}`);
526
+ console.log(`started: ${new Date(transcript.startedAt).toISOString()}`);
527
+ if (transcript.output) {
528
+ console.log(parseJsonLine(transcript.output));
529
+ }
530
+ }
531
+ }
532
+ });
533
+ const deleteCommand = new Command("delete")
534
+ .description("delete a run")
535
+ .argument("<runId>", "run id")
536
+ .action((runId) => {
537
+ const success = removeRun(runId);
538
+ if (!success) {
539
+ throw new Error(`run ${runId} not found`);
540
+ }
541
+ console.log(`deleted ${runId}`);
542
+ });
543
+ const personaListCommand = new Command("list")
544
+ .description("list built-in and custom personas")
545
+ .option("--json", "output personas as json")
546
+ .action((options) => {
547
+ const builtinPersonaIds = new Set(PERSONAS.map((persona) => persona.id));
548
+ const personas = allPersonas();
549
+ if (options.json) {
550
+ console.log(JSON.stringify(personas, null, 2));
551
+ return;
552
+ }
553
+ for (const persona of personas) {
554
+ const type = builtinPersonaIds.has(persona.id) ? "builtin" : "custom";
555
+ console.log(`[${type}] ${sanitizeForTerminal(persona.id)} ${sanitizeForTerminal(persona.name)} — ${sanitizeForTerminal(persona.description)}`);
556
+ }
557
+ });
558
+ const personaAddCommand = new Command("add")
559
+ .description("add a custom persona")
560
+ .requiredOption("--name <name>", "persona name")
561
+ .requiredOption("--description <desc>", "persona description")
562
+ .requiredOption("--methodology <methodology>", "persona methodology")
563
+ .option("--id <id>", "custom persona id")
564
+ .action((options) => {
565
+ const id = options.id?.trim() || slugifyPersonaId(options.name);
566
+ const persona = {
567
+ id,
568
+ name: options.name,
569
+ description: options.description,
570
+ methodology: options.methodology,
571
+ };
572
+ const result = addCustomPersona(persona);
573
+ if (result.error) {
574
+ throw new Error(result.error);
575
+ }
576
+ console.log(`added persona ${id}`);
577
+ });
578
+ const personaRemoveCommand = new Command("remove")
579
+ .description("remove a custom persona")
580
+ .argument("<id>", "custom persona id")
581
+ .action((id) => {
582
+ if (PERSONAS.some((persona) => persona.id === id)) {
583
+ throw new Error(`cannot remove builtin persona ${id}`);
584
+ }
585
+ const removed = removeCustomPersona(id);
586
+ if (!removed) {
587
+ throw new Error(`persona ${id} not found`);
588
+ }
589
+ console.log(`removed persona ${id}`);
590
+ });
591
+ const personaCommand = new Command("persona")
592
+ .description("manage personas")
593
+ .addCommand(personaListCommand)
594
+ .addCommand(personaAddCommand)
595
+ .addCommand(personaRemoveCommand);
596
+ const webCommand = new Command("web")
597
+ .description("launch local web UI")
598
+ .option("--port <n>", "port to listen on", "3737")
599
+ .action(async (options) => {
600
+ const port = clampInt(options.port, 1024, 65535, 3737);
601
+ loadConfig();
602
+ const { startWebServer } = await import("./web/index.js");
603
+ await startWebServer(port);
604
+ });
605
+ const configShowCommand = new Command("show").action(() => {
606
+ const config = loadConfig();
607
+ console.log(JSON.stringify(createMaskedConfig(config), null, 2));
608
+ });
609
+ const configSetCommand = new Command("set")
610
+ .description("set a config value")
611
+ .argument("<key>", "api-key | synthetic-api-key | search-provider | exa-api-key | brave-api-key | model | orchestrator-model | research-model | base-url | default-agent-count | max-concurrency | debate-rounds | search-enabled | custom-personas-only")
612
+ .argument("<value>")
613
+ .action((key, rawValue) => {
614
+ const mapped = configKeyMap[key];
615
+ if (!mapped) {
616
+ throw new Error("invalid key. valid keys: api-key, synthetic-api-key, search-provider, exa-api-key, brave-api-key, model, orchestrator-model, research-model, base-url, default-agent-count, max-concurrency, debate-rounds, search-enabled, custom-personas-only");
617
+ }
618
+ const parsed = sanitizeConfigValueForSet(mapped, rawValue);
619
+ if (parsed.error || parsed.value === undefined) {
620
+ throw new Error(parsed.error ?? "invalid value");
621
+ }
622
+ const config = writeConfig({
623
+ [mapped]: parsed.value,
624
+ });
625
+ const safeConfig = createMaskedConfig(config);
626
+ console.log(`updated ${key}`);
627
+ console.log(JSON.stringify(safeConfig, null, 2));
628
+ });
629
+ const configCommand = new Command("config")
630
+ .description("manage local hydra config")
631
+ .addCommand(configShowCommand)
632
+ .addCommand(configSetCommand);
633
+ command.addCommand(runCommand);
634
+ command.addCommand(historyCommand);
635
+ command.addCommand(viewCommand);
636
+ command.addCommand(deleteCommand);
637
+ command.addCommand(personaCommand);
638
+ command.addCommand(webCommand);
639
+ command.addCommand(configCommand);
640
+ if (import.meta.main) {
641
+ (async () => {
642
+ await command.parseAsync(normalizeArgvForBareRun(process.argv));
643
+ })().catch((e) => {
644
+ console.error(formatErrorMessage(e));
645
+ process.exitCode = 1;
646
+ });
647
+ }
648
+ //# sourceMappingURL=index.js.map