klyro 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +13 -0
  2. package/dist/agent/custom-agents.d.ts +3 -0
  3. package/dist/agent/custom-agents.js +96 -0
  4. package/dist/agent/orchestrator.d.ts +26 -0
  5. package/dist/agent/orchestrator.js +41 -4
  6. package/dist/agent/runtime.d.ts +15 -0
  7. package/dist/agent/runtime.js +232 -61
  8. package/dist/chat.d.ts +10 -0
  9. package/dist/chat.js +39 -7
  10. package/dist/checkpoints/store.d.ts +11 -0
  11. package/dist/checkpoints/store.js +32 -0
  12. package/dist/cli/auth.d.ts +10 -3
  13. package/dist/cli/auth.js +43 -5
  14. package/dist/cli/completion.js +2 -2
  15. package/dist/cli/config.d.ts +4 -4
  16. package/dist/cli/doctor.js +0 -1
  17. package/dist/cli/eval.d.ts +15 -1
  18. package/dist/cli/eval.js +43 -5
  19. package/dist/cli/hooks.d.ts +74 -5
  20. package/dist/cli/hooks.js +118 -7
  21. package/dist/cli/init.d.ts +6 -0
  22. package/dist/cli/init.js +60 -0
  23. package/dist/cli/keychain.d.ts +10 -0
  24. package/dist/cli/keychain.js +86 -0
  25. package/dist/cli/repl.js +188 -30
  26. package/dist/cli/run.d.ts +7 -1
  27. package/dist/cli/run.js +92 -50
  28. package/dist/cli/setup.js +3 -2
  29. package/dist/cli/slash/custom.d.ts +25 -0
  30. package/dist/cli/slash/custom.js +166 -0
  31. package/dist/cli/slash/parser.d.ts +9 -1
  32. package/dist/cli/slash/parser.js +34 -9
  33. package/dist/cli/update.d.ts +3 -1
  34. package/dist/cli/update.js +16 -1
  35. package/dist/context/accounting.d.ts +6 -0
  36. package/dist/context/accounting.js +8 -2
  37. package/dist/context/compaction.d.ts +2 -1
  38. package/dist/context/compaction.js +39 -12
  39. package/dist/context/memory.d.ts +11 -0
  40. package/dist/context/memory.js +59 -4
  41. package/dist/eval/harness.d.ts +40 -5
  42. package/dist/eval/harness.js +103 -10
  43. package/dist/eval/judge.d.ts +32 -0
  44. package/dist/eval/judge.js +63 -0
  45. package/dist/eval/tasks.js +134 -0
  46. package/dist/index.js +239 -130
  47. package/dist/mcp/auth.d.ts +85 -0
  48. package/dist/mcp/auth.js +249 -0
  49. package/dist/mcp/client.d.ts +15 -0
  50. package/dist/mcp/client.js +42 -2
  51. package/dist/mcp/config.d.ts +31 -1
  52. package/dist/mcp/config.js +84 -1
  53. package/dist/mcp/registry.d.ts +19 -0
  54. package/dist/mcp/registry.js +118 -2
  55. package/dist/mcp/remote.d.ts +36 -0
  56. package/dist/mcp/remote.js +207 -0
  57. package/dist/mcp/sse.d.ts +42 -0
  58. package/dist/mcp/sse.js +310 -0
  59. package/dist/persistence/audit.d.ts +15 -3
  60. package/dist/persistence/audit.js +84 -13
  61. package/dist/persistence/store.d.ts +9 -0
  62. package/dist/persistence/store.js +17 -0
  63. package/dist/policy/approval.d.ts +15 -1
  64. package/dist/policy/approval.js +8 -0
  65. package/dist/policy/engine.js +9 -0
  66. package/dist/providers/endpoints.d.ts +43 -0
  67. package/dist/providers/endpoints.js +104 -0
  68. package/dist/providers.js +17 -14
  69. package/dist/tools/shell/shell-exec.d.ts +13 -0
  70. package/dist/tools/shell/shell-exec.js +64 -2
  71. package/dist/tui/app.js +172 -15
  72. package/dist/tui/app.test.js +27 -2
  73. package/dist/tui/approval.js +55 -1
  74. package/dist/tui/scroll-model.d.ts +2 -2
  75. package/dist/tui/scroll-model.js +9 -3
  76. package/dist/tui/tokens.d.ts +8 -11
  77. package/dist/tui/tokens.js +18 -11
  78. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -60,8 +60,10 @@ async function main() {
60
60
  .option('--no-color', 'Disable colored output')
61
61
  .option('-p, --print <prompt>', 'Headless one-shot prompt (alias for run, --output json for machine)')
62
62
  .option('--output-format <fmt>', 'Headless output format: text|json|stream-json (default text)')
63
- .option('--no-stream', 'Disable streaming (buffer full response)')
64
- .option('--show-thinking', 'Show thinking blocks');
63
+ .option('--temperature <n>', 'Sampling temperature 0-2 (headless -p / run)', parseTemperature)
64
+ .option('--max-tokens <n>', 'Max output tokens per step (headless -p / run)', (v) => parsePositiveInt('--max-tokens', v))
65
+ .option('--no-stream', 'Disable streaming (TUI only; headless json always streams events)')
66
+ .option('--show-thinking', 'Show thinking blocks (TUI only)');
65
67
  // Unknown commands / unknown options are usage errors (exit 2 per PRD).
66
68
  // Help and version still exit 0; anything else rethrows to the last-resort
67
69
  // handler (exit 1).
@@ -143,8 +145,9 @@ async function main() {
143
145
  program
144
146
  .command('update')
145
147
  .description('Check for klyro updates (cached 24h, KLYRO_NO_UPDATE_CHECK=1 to disable)')
146
- .action(async () => {
147
- const code = await runUpdate();
148
+ .option('--apply', 'Apply the update now via npm i -g (opt-in; default is notify-only)')
149
+ .action(async (opts) => {
150
+ const code = await runUpdate({ apply: !!opts.apply });
148
151
  process.exit(code);
149
152
  });
150
153
  program
@@ -197,12 +200,30 @@ async function main() {
197
200
  const code = await runDoctor({ json: !!opts.json });
198
201
  process.exit(code);
199
202
  });
203
+ program
204
+ .command('init')
205
+ .description('Bootstrap this project: scan + KLYRO.md draft + .mcp.json skeleton (never overwrites)')
206
+ .action(async () => {
207
+ const { initProject, nextStepsText } = await import('./cli/init.js');
208
+ try {
209
+ const { created, skipped } = await initProject(process.cwd());
210
+ for (const f of created)
211
+ process.stdout.write(`created ${f}\n`);
212
+ for (const f of skipped)
213
+ process.stdout.write(`kept ${f}\n`);
214
+ process.stdout.write(nextStepsText() + '\n');
215
+ process.exit(0);
216
+ }
217
+ catch (err) {
218
+ process.stderr.write(`klyro: init failed: ${err instanceof Error ? err.message : String(err)}\n`);
219
+ process.exit(1);
220
+ }
221
+ });
200
222
  // Support `klyro "prompt"` positional headless (2.5)
201
223
  program.argument('[prompt]', 'Headless one-shot prompt (same as -p)');
202
224
  program
203
225
  .action(async (promptArg) => {
204
- const opts = program.opts();
205
- // 9.2 — --continue / --resume handling
226
+ const opts = program.opts(); // 9.2 — --continue / --resume handling
206
227
  if (opts.continue || typeof opts.resume === 'string') {
207
228
  const { getDefaultSessionStore } = await import('./persistence/session.js');
208
229
  const store = getDefaultSessionStore();
@@ -243,6 +264,7 @@ async function main() {
243
264
  cwd: process.cwd(),
244
265
  model: process.env.KLYRO_MODEL ?? 'gpt-4o-mini',
245
266
  maxTokens: opts.maxTokens,
267
+ temperature: opts.temperature,
246
268
  output: output,
247
269
  provider: process.env.KLYRO_PROVIDER,
248
270
  });
@@ -298,6 +320,7 @@ async function main() {
298
320
  .option('--require-verify', 'Fail with exit 8 if no verification passed after edits (6.5)')
299
321
  .option('--agent <name>', 'Run under an orchestrator context enabling spawn_agent/task_list/task_get (explorer|implementer|tester|reviewer)')
300
322
  .option('--max-depth <n>', 'Max spawn depth for child agents (default 1)', (v) => parsePositiveInt('--max-depth', v))
323
+ .option('--bare', 'Deterministic runs: skip MCP, hooks, memory/KLYRO.md/context, persistence')
301
324
  .action(async (prompt, opts) => {
302
325
  const model = opts.model ?? process.env.KLYRO_MODEL;
303
326
  if (!model) {
@@ -339,6 +362,7 @@ async function main() {
339
362
  requireVerify: !!opts.requireVerify,
340
363
  agent: opts.agent,
341
364
  maxDepth: opts.maxDepth,
365
+ bare: !!opts.bare,
342
366
  });
343
367
  process.exit(code);
344
368
  }
@@ -370,17 +394,18 @@ async function main() {
370
394
  .option('--runs <n>', 'Runs per fixture (default 1)', (v) => parsePositiveInt('--runs', v))
371
395
  .option('--parallel <n>', 'Parallelism (default 1)', (v) => parsePositiveInt('--parallel', v))
372
396
  .option('--model <id>', 'Model for eval')
397
+ .option('--judge-model <id>', 'Live model id for grading judge.rubric (needs endpoint + key)')
373
398
  .action(async (input, opts) => {
374
399
  const output = (opts.output ?? 'human');
375
400
  if (opts.suite) {
376
- const code = await runEval({ inputPath: input ?? '-', output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model });
401
+ const code = await runEval({ inputPath: input ?? '-', output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model, judgeModel: opts.judgeModel });
377
402
  process.exit(code);
378
403
  }
379
404
  if (!input) {
380
405
  process.stderr.write('klyro eval: missing input (provide <input> or --suite)\n');
381
406
  process.exit(2);
382
407
  }
383
- const code = await runEval({ inputPath: input, output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model });
408
+ const code = await runEval({ inputPath: input, output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model, judgeModel: opts.judgeModel });
384
409
  process.exit(code);
385
410
  });
386
411
  program
@@ -395,14 +420,11 @@ async function main() {
395
420
  process.stdout.write(out + '\n');
396
421
  process.exit(0);
397
422
  });
398
- // Level 9 — Session management
399
- const session = program.command('session').description('Session persistence (Level 9)');
400
- session
401
- .command('list')
402
- .description('List persisted sessions')
403
- .option('--status <s>', 'Filter by status: open|complete|verify_failed|aborted|max_steps')
404
- .option('--json', 'Output JSON')
405
- .action(async (opts) => {
423
+ // Level 9 — Session management.
424
+ // One namespace: `session` and `sessions` accept the SAME subcommands
425
+ // (list/show/resume/export/import/fork/delete). Handlers live here once
426
+ // and both command groups delegate to them.
427
+ async function sessionList(opts) {
406
428
  const { getDefaultSessionStore, formatSession } = await import('./persistence/session.js');
407
429
  const store = getDefaultSessionStore();
408
430
  const all = await store.list(opts.status ? { status: opts.status } : undefined);
@@ -419,25 +441,26 @@ async function main() {
419
441
  }
420
442
  }
421
443
  }
422
- });
423
- session
424
- .command('show <id>')
425
- .description('Show session transcript and observations')
426
- .option('--json', 'Output JSON')
427
- .action(async (id, opts) => {
444
+ }
445
+ async function resolveOrExit(id) {
428
446
  const { getDefaultSessionStore, resolveSessionId, matchSessionIds } = await import('./persistence/session.js');
429
447
  const store = getDefaultSessionStore();
430
448
  const full = await resolveSessionId(store, id);
431
- if (!full) {
432
- const matches = await matchSessionIds(store, id);
433
- if (matches.length > 1) {
434
- process.stderr.write(`ambiguous id "${id}" matches:\n${matches.map((r) => ` ${r.id.slice(0, 8)} ${r.task.slice(0, 50)}`).join('\n')}\n`);
435
- }
436
- else {
437
- process.stderr.write(`session not found: ${id}\n`);
438
- }
439
- process.exit(2);
449
+ if (full)
450
+ return full;
451
+ const matches = await matchSessionIds(store, id);
452
+ if (matches.length > 1) {
453
+ process.stderr.write(`ambiguous id "${id}" matches:\n${matches.map((r) => ` ${r.id.slice(0, 8)} ${r.task.slice(0, 50)}`).join('\n')}\n`);
454
+ }
455
+ else {
456
+ process.stderr.write(`session not found: ${id}\n`);
440
457
  }
458
+ process.exit(2);
459
+ }
460
+ async function sessionShow(id, opts) {
461
+ const { getDefaultSessionStore } = await import('./persistence/session.js');
462
+ const store = getDefaultSessionStore();
463
+ const full = await resolveOrExit(id);
441
464
  const rec = await store.get(full);
442
465
  const msgs = await store.loadMessages(full);
443
466
  const obs = await store.loadObservations(full);
@@ -453,28 +476,11 @@ async function main() {
453
476
  for (const o of obs)
454
477
  process.stdout.write(` ${o.toolName} -> ${o.isError ? 'ERR' : 'ok'} ${JSON.stringify(o.output).slice(0, 120)}\n`);
455
478
  }
456
- });
457
- session
458
- .command('resume <id>')
459
- .description('Resume a persisted session (requires KLYRO_MODEL etc.)')
460
- .option('-m, --model <id>', 'Model (default: from session or env)')
461
- .option('--max-steps <n>', 'Max steps (default 30)', (v) => parsePositiveInt('--max-steps', v))
462
- .option('--verify-command <cmd>', 'Override verification command')
463
- .option('--verify', 'Enable verification (default: enabled)')
464
- .action(async (id, opts) => {
465
- const { getDefaultSessionStore, resolveSessionId, matchSessionIds } = await import('./persistence/session.js');
479
+ }
480
+ async function sessionResume(id, opts) {
481
+ const { getDefaultSessionStore } = await import('./persistence/session.js');
466
482
  const store = getDefaultSessionStore();
467
- const full = await resolveSessionId(store, id);
468
- if (!full) {
469
- const matches = await matchSessionIds(store, id);
470
- if (matches.length > 1) {
471
- process.stderr.write(`ambiguous id "${id}" matches:\n${matches.map((r) => ` ${r.id.slice(0, 8)} ${r.task.slice(0, 50)}`).join('\n')}\n`);
472
- }
473
- else {
474
- process.stderr.write(`session not found: ${id}\n`);
475
- }
476
- process.exit(2);
477
- }
483
+ const full = await resolveOrExit(id);
478
484
  const rec = await store.get(full);
479
485
  if (!rec) {
480
486
  process.stderr.write(`session not found: ${id}\n`);
@@ -495,69 +501,19 @@ async function main() {
495
501
  verifyCommand: opts.verifyCommand,
496
502
  });
497
503
  process.exit(code);
498
- });
499
- // Alias: klyro resume <id> → klyro session resume <id>
500
- program
501
- .command('resume <id>')
502
- .description('Alias for `klyro session resume <id>`')
503
- .option('-m, --model <id>', 'Model')
504
- .option('--max-steps <n>', 'Max steps', (v) => parsePositiveInt('--max-steps', v))
505
- .action(async (id, opts) => {
506
- const { getDefaultSessionStore, resolveSessionId, matchSessionIds } = await import('./persistence/session.js');
507
- const store = getDefaultSessionStore();
508
- const full = await resolveSessionId(store, id);
509
- if (!full) {
510
- const matches = await matchSessionIds(store, id);
511
- if (matches.length > 1) {
512
- process.stderr.write(`ambiguous id "${id}" matches:\n${matches.map((r) => ` ${r.id.slice(0, 8)} ${r.task.slice(0, 50)}`).join('\n')}\n`);
513
- }
514
- else {
515
- process.stderr.write(`session not found: ${id}\n`);
516
- }
517
- process.exit(2);
518
- }
519
- const rec = await store.get(full);
520
- if (!rec) {
521
- process.stderr.write(`session not found: ${id}\n`);
522
- process.exit(2);
523
- }
524
- const model = opts.model ?? rec.config.model ?? process.env.KLYRO_MODEL;
525
- if (!model) {
526
- process.stderr.write('klyro: KLYRO_MODEL is not set (or pass --model)\n');
527
- process.exit(2);
528
- }
529
- const code = await runOnce({
530
- task: rec.task,
531
- cwd: rec.cwd,
532
- model,
533
- maxSteps: opts.maxSteps ?? rec.config.maxSteps,
534
- sessionId: full,
535
- });
536
- process.exit(code);
537
- });
538
- program.command('scan').description('Scan project (7.1) — languages, frameworks, commands, 300ms cached').option('--json', 'JSON output').action(async (opts) => { const { runScan } = await import('./cli/scan.js'); process.exit(await runScan({ cwd: process.cwd(), json: !!opts.json })); });
539
- program.command('project').description('Alias for scan').option('--json', 'JSON output').action(async (opts) => { const { runProject } = await import('./cli/scan.js'); process.exit(await runProject({ cwd: process.cwd(), json: !!opts.json })); });
540
- // 9.2 — Continue / resume top-level flags (also handled via session resume)
541
- program.option('-c, --continue', 'Continue most recent session in cwd (9.2)');
542
- program.option('-r, --resume [id]', 'Resume session by id or pick most recent');
543
- // 9.4 — Sessions extended: fork/rename/export/import/prune/history + locks
544
- const sessions = program.command('sessions').description('Alias for session');
545
- sessions.command('export <id> [file]').description('Export session to file (9.4)').action(async (id, file) => {
546
- const { getDefaultSessionStore, resolveSessionId } = await import('./persistence/session.js');
504
+ }
505
+ async function sessionExport(id, file) {
506
+ const { getDefaultSessionStore } = await import('./persistence/session.js');
547
507
  const store = getDefaultSessionStore();
548
- const full = await resolveSessionId(store, id);
549
- if (!full) {
550
- process.stderr.write(`session not found: ${id}\n`);
551
- process.exit(2);
552
- }
508
+ const full = await resolveOrExit(id);
553
509
  const rec = await store.get(full);
554
510
  const msgs = await store.loadMessages(full);
555
511
  const obs = await store.loadObservations(full);
556
512
  const out = file ?? `${full}.export.json`;
557
513
  await (await import('node:fs/promises')).writeFile(out, JSON.stringify({ record: rec, messages: msgs, observations: obs }, null, 2));
558
514
  process.stdout.write(`exported ${full} → ${out}\n`);
559
- });
560
- sessions.command('import <file>').description('Import session from file (restores record + messages + observations)').action(async (file) => {
515
+ }
516
+ async function sessionImport(file) {
561
517
  let data;
562
518
  try {
563
519
  data = JSON.parse(await (await import('node:fs/promises')).readFile(file, 'utf-8'));
@@ -604,8 +560,8 @@ async function main() {
604
560
  }
605
561
  }
606
562
  process.stdout.write(`imported → ${created.id} (${restored} messages restored)\n`);
607
- });
608
- sessions.command('fork <id>').description('Fork session with full context (9.4)').action(async (id) => {
563
+ }
564
+ async function sessionFork(id) {
609
565
  const { getDefaultSessionStore, matchSessionIds } = await import('./persistence/session.js');
610
566
  const store = getDefaultSessionStore();
611
567
  const matches = await matchSessionIds(store, id);
@@ -621,8 +577,8 @@ async function main() {
621
577
  const forked = await store.fork(full);
622
578
  const msgs = await store.loadMessages(forked.id);
623
579
  process.stdout.write(`forked ${full.slice(0, 8)} → ${forked.id.slice(0, 8)} (${msgs.length} messages carried over)\n`);
624
- });
625
- sessions.command('delete <id>').description('Delete a session and its artifacts').action(async (id) => {
580
+ }
581
+ async function sessionDelete(id) {
626
582
  const { getDefaultSessionStore, matchSessionIds } = await import('./persistence/session.js');
627
583
  const store = getDefaultSessionStore();
628
584
  const matches = await matchSessionIds(store, id);
@@ -637,7 +593,64 @@ async function main() {
637
593
  const full = matches[0].id;
638
594
  await store.delete(full);
639
595
  process.stdout.write(`deleted ${full.slice(0, 8)}\n`);
640
- });
596
+ }
597
+ const session = program.command('session').description('Session persistence (Level 9)');
598
+ session
599
+ .command('list')
600
+ .description('List persisted sessions')
601
+ .option('--status <s>', 'Filter by status: open|complete|verify_failed|aborted|max_steps')
602
+ .option('--json', 'Output JSON')
603
+ .action(async (opts) => { await sessionList(opts); });
604
+ session
605
+ .command('show <id>')
606
+ .description('Show session transcript and observations')
607
+ .option('--json', 'Output JSON')
608
+ .action(async (id, opts) => { await sessionShow(id, opts); });
609
+ session
610
+ .command('resume <id>')
611
+ .description('Resume a persisted session (requires KLYRO_MODEL etc.)')
612
+ .option('-m, --model <id>', 'Model (default: from session or env)')
613
+ .option('--max-steps <n>', 'Max steps (default 30)', (v) => parsePositiveInt('--max-steps', v))
614
+ .option('--verify-command <cmd>', 'Override verification command')
615
+ .option('--verify', 'Enable verification (default: enabled)')
616
+ .action(async (id, opts) => { await sessionResume(id, opts); });
617
+ session
618
+ .command('export <id> [file]')
619
+ .description('Export session to file (9.4)')
620
+ .action(async (id, file) => { await sessionExport(id, file); });
621
+ session
622
+ .command('import <file>')
623
+ .description('Import session from file (restores record + messages + observations)')
624
+ .action(async (file) => { await sessionImport(file); });
625
+ session
626
+ .command('fork <id>')
627
+ .description('Fork session with full context (9.4)')
628
+ .action(async (id) => { await sessionFork(id); });
629
+ session
630
+ .command('delete <id>')
631
+ .description('Delete a session and its artifacts')
632
+ .action(async (id) => { await sessionDelete(id); });
633
+ // Alias: klyro resume <id> → klyro session resume <id>
634
+ program
635
+ .command('resume <id>')
636
+ .description('Alias for `klyro session resume <id>`')
637
+ .option('-m, --model <id>', 'Model')
638
+ .option('--max-steps <n>', 'Max steps', (v) => parsePositiveInt('--max-steps', v))
639
+ .action(async (id, opts) => { await sessionResume(id, opts); });
640
+ program.command('scan').description('Scan project (7.1) — languages, frameworks, commands, 300ms cached').option('--json', 'JSON output').action(async (opts) => { const { runScan } = await import('./cli/scan.js'); process.exit(await runScan({ cwd: process.cwd(), json: !!opts.json })); });
641
+ program.command('project').description('Alias for scan').option('--json', 'JSON output').action(async (opts) => { const { runProject } = await import('./cli/scan.js'); process.exit(await runProject({ cwd: process.cwd(), json: !!opts.json })); });
642
+ // 9.2 — Continue / resume top-level flags (also handled via session resume)
643
+ program.option('-c, --continue', 'Continue most recent session in cwd (9.2)');
644
+ program.option('-r, --resume [id]', 'Resume session by id or pick most recent');
645
+ // 9.4 — same namespace as `session`: every subcommand works under both.
646
+ const sessions = program.command('sessions').description('Alias for session (same subcommands)');
647
+ sessions.command('list').description('List persisted sessions').option('--status <s>', 'Filter by status').option('--json', 'Output JSON').action(async (opts) => { await sessionList(opts); });
648
+ sessions.command('show <id>').description('Show session transcript and observations').option('--json', 'Output JSON').action(async (id, opts) => { await sessionShow(id, opts); });
649
+ sessions.command('resume <id>').description('Resume a persisted session').option('-m, --model <id>', 'Model').option('--max-steps <n>', 'Max steps', (v) => parsePositiveInt('--max-steps', v)).option('--verify-command <cmd>', 'Override verification command').option('--verify', 'Enable verification (default: enabled)').action(async (id, opts) => { await sessionResume(id, opts); });
650
+ sessions.command('export <id> [file]').description('Export session to file (9.4)').action(async (id, file) => { await sessionExport(id, file); });
651
+ sessions.command('import <file>').description('Import session from file (restores record + messages + observations)').action(async (file) => { await sessionImport(file); });
652
+ sessions.command('fork <id>').description('Fork session with full context (9.4)').action(async (id) => { await sessionFork(id); });
653
+ sessions.command('delete <id>').description('Delete a session and its artifacts').action(async (id) => { await sessionDelete(id); });
641
654
  // 10.1 — MCP
642
655
  const mcp = program.command('mcp').description('MCP client/server (10.1)');
643
656
  mcp.command('list').description('List MCP servers').action(async () => {
@@ -654,10 +667,14 @@ async function main() {
654
667
  process.stdout.write(`${name} source=${source}${spec?.disabled ? ' disabled' : ''}\n`);
655
668
  }
656
669
  });
657
- mcp.command('add <name> <command> [args...]').description('Add a project MCP server to .mcp.json (stdio command)').action(async (name, command, args) => {
670
+ mcp.command('add <name> <command> [args...]').description('Add a project MCP server to .mcp.json (stdio command, or https:// URL for remote)').action(async (name, command, args) => {
658
671
  const { addProjectServer, projectMcpPath } = await import('./mcp/config.js');
659
672
  try {
660
- addProjectServer(process.cwd(), name, { command, ...(args && args.length > 0 ? { args } : {}) });
673
+ // URL first arg → remote Streamable-HTTP server; else stdio command.
674
+ const spec = /^https?:\/\//i.test(command)
675
+ ? { url: command }
676
+ : { command, ...(args && args.length > 0 ? { args } : {}) };
677
+ addProjectServer(process.cwd(), name, spec);
661
678
  process.stdout.write(`added mcp server "${name}" → ${projectMcpPath(process.cwd())}\n`);
662
679
  }
663
680
  catch (err) {
@@ -687,18 +704,21 @@ async function main() {
687
704
  process.stderr.write(`klyro: mcp server not found: ${name}\n`);
688
705
  process.exit(2);
689
706
  }
690
- const { McpClient } = await import('./mcp/client.js');
691
- const client = new McpClient(name, spec);
692
- // 15s overall probe budget (connect has its own internal timeout too).
707
+ const { makeMcpClient } = await import('./mcp/registry.js');
708
+ const client = makeMcpClient(name, spec); // 15s overall probe budget (connect has its own internal timeout too).
693
709
  const timer = setTimeout(() => {
694
710
  process.stderr.write(`klyro: mcp probe ${name} timed out after 15s\n`);
695
711
  process.exit(2);
696
712
  }, 15_000);
697
713
  try {
698
- await client.connect();
714
+ const withConnect = client;
715
+ if (typeof withConnect.connect === 'function')
716
+ await withConnect.connect();
699
717
  const tools = await client.listTools();
718
+ const prompts = typeof client.promptsList === 'function' ? await client.promptsList().catch(() => []) : [];
700
719
  const names = tools.map((t) => t.name);
701
- process.stdout.write(`${name}: ${tools.length} tool(s)${names.length > 0 ? `: ${names.join(', ')}` : ''}\n`);
720
+ const extra = prompts.length > 0 ? `, ${prompts.length} prompt(s): ${prompts.map((p) => p.name).join(', ')}` : '';
721
+ process.stdout.write(`${name}: ${tools.length} tool(s)${names.length > 0 ? `: ${names.join(', ')}` : ''}${extra}\n`);
702
722
  process.exit(0);
703
723
  }
704
724
  catch (err) {
@@ -713,6 +733,58 @@ async function main() {
713
733
  catch { /* ignore */ }
714
734
  }
715
735
  });
736
+ mcp.command('trust <name>').description('Trust a project MCP server spec (records its hash, enables auto-connect)').action(async (name) => {
737
+ const { loadMcpServers } = await import('./mcp/config.js');
738
+ const { McpTrust, hashSpec } = await import('./mcp/trust.js');
739
+ const cfg = loadMcpServers(process.cwd());
740
+ const spec = cfg.servers[name];
741
+ if (!spec) {
742
+ process.stderr.write(`klyro: mcp server not found: ${name}\n`);
743
+ process.exit(2);
744
+ }
745
+ new McpTrust().approve(name, hashSpec(spec));
746
+ process.stdout.write(`trusted mcp server "${name}" (source=${cfg.sources[name] ?? 'unknown'})\n`);
747
+ });
748
+ mcp.command('prompts [server]').description('List MCP prompts as /mcp__<server>__<prompt> slash names').action(async (server) => {
749
+ const { loadMcpServers, enabledServers } = await import('./mcp/config.js');
750
+ const { makeMcpClient } = await import('./mcp/registry.js');
751
+ const cfg = loadMcpServers(process.cwd());
752
+ const targets = server ? [server] : Object.keys(enabledServers(cfg));
753
+ if (targets.length === 0) {
754
+ process.stdout.write('mcp prompts: none (no servers configured)\n');
755
+ return;
756
+ }
757
+ let shown = 0;
758
+ for (const name of targets) {
759
+ const spec = cfg.servers[name];
760
+ if (!spec) {
761
+ process.stderr.write(`klyro: mcp server not found: ${name}\n`);
762
+ continue;
763
+ }
764
+ const client = makeMcpClient(name, spec);
765
+ try {
766
+ const withConnect = client;
767
+ if (typeof withConnect.connect === 'function')
768
+ await withConnect.connect();
769
+ const prompts = typeof client.promptsList === 'function' ? await client.promptsList() : [];
770
+ for (const p of prompts) {
771
+ process.stdout.write(`/mcp__${name}__${p.name}${p.description ? ` — ${p.description}` : ''}\n`);
772
+ shown++;
773
+ }
774
+ }
775
+ catch (err) {
776
+ process.stderr.write(`klyro: mcp prompts ${name} failed: ${err instanceof Error ? err.message : String(err)}\n`);
777
+ }
778
+ finally {
779
+ try {
780
+ await client.close();
781
+ }
782
+ catch { /* ignore */ }
783
+ }
784
+ }
785
+ if (shown === 0)
786
+ process.stdout.write('mcp prompts: none exposed by configured servers\n');
787
+ });
716
788
  mcp.command('serve').description('Serve builtin tools as an MCP server over stdio (policy-gated)').action(async () => {
717
789
  const { serveStdio } = await import('./mcp/serve.js');
718
790
  const code = await serveStdio(process.cwd());
@@ -731,15 +803,49 @@ async function main() {
731
803
  return;
732
804
  }
733
805
  for (const h of hooks)
734
- process.stdout.write(`${h.name} ${h.event} ${h.command}\n`);
735
- });
736
- program.command('agents [name] [extra...]').description('List agents (10.2), show one, or run: agents run <name> <task...>').action(async (name, extra) => {
737
- const { BUILTIN_AGENTS } = await import('./agent/orchestrator.js');
806
+ process.stdout.write(`${h.name} ${h.event}${h.matcher ? ` (${h.matcher})` : ''} ${h.command}\n`);
807
+ });
808
+ program.command('agents [name] [extra...]').description('List agents (builtins + .klyro/agents/*.md), show one, lint files, or run: agents run <name> <task...>').action(async (name, extra) => {
809
+ const { listAllAgents } = await import('./agent/orchestrator.js');
810
+ const ALL = listAllAgents(process.cwd());
811
+ // `klyro agents lint`: validate custom agent files (ids, tool names).
812
+ if (name === 'lint') {
813
+ const { loadCustomAgents } = await import('./agent/custom-agents.js');
814
+ const { builtinRegistry } = await import('./tools/registry.js');
815
+ const known = new Set(builtinRegistry().list().map((t) => t.name));
816
+ const customs = loadCustomAgents(process.cwd());
817
+ if (customs.length === 0) {
818
+ process.stdout.write('agents lint: no custom agents in .klyro/agents/ (or ~/.klyro/agents/)\n');
819
+ return;
820
+ }
821
+ let bad = 0;
822
+ for (const a of customs) {
823
+ const problems = [];
824
+ if (!a.description || a.description === `Custom agent ${a.id}`)
825
+ problems.push('missing description');
826
+ for (const t of a.allowedTools ?? []) {
827
+ if (!known.has(t))
828
+ problems.push(`unknown tool "${t}" (will be dropped at spawn)`);
829
+ }
830
+ if (!a.prompt && !(a.allowedTools ?? []).length)
831
+ problems.push('no prompt body and no tools — agent has no specialization');
832
+ if (problems.length > 0) {
833
+ bad++;
834
+ process.stdout.write(`${a.id} (${a.source ?? 'custom'}):\n${problems.map((p) => ` ! ${p}`).join('\n')}\n`);
835
+ }
836
+ else {
837
+ process.stdout.write(`${a.id} (${a.source ?? 'custom'}): ok\n`);
838
+ }
839
+ }
840
+ if (bad > 0)
841
+ process.exit(2);
842
+ return;
843
+ }
738
844
  // `klyro agents run <name> <task...>`: one-shot run under a named agent.
739
845
  if (name === 'run') {
740
846
  const [agentName, ...taskParts] = extra ?? [];
741
- if (!agentName || !BUILTIN_AGENTS.some((a) => a.id === agentName)) {
742
- process.stderr.write(`klyro: unknown agent: ${agentName ?? '(missing)'} (known: ${BUILTIN_AGENTS.map((a) => a.id).join(', ')})\n`);
847
+ if (!agentName || !ALL.some((a) => a.id === agentName)) {
848
+ process.stderr.write(`klyro: unknown agent: ${agentName ?? '(missing)'} (known: ${ALL.map((a) => a.id).join(', ')})\n`);
743
849
  process.exit(2);
744
850
  }
745
851
  const task = (taskParts ?? []).join(' ').trim();
@@ -756,19 +862,20 @@ async function main() {
756
862
  process.exit(code);
757
863
  }
758
864
  if (!name) {
759
- for (const a of BUILTIN_AGENTS)
760
- process.stdout.write(`${a.id} — ${a.description}\n`);
865
+ for (const a of ALL)
866
+ process.stdout.write(`${a.id}${a.source && a.source !== 'builtin' ? ` (${a.source})` : ''} — ${a.description}\n`);
761
867
  return;
762
868
  }
763
- const def = BUILTIN_AGENTS.find((a) => a.id === name);
869
+ const def = ALL.find((a) => a.id === name);
764
870
  if (!def) {
765
- process.stderr.write(`klyro: unknown agent: ${name} (known: ${BUILTIN_AGENTS.map((a) => a.id).join(', ')})\n`);
871
+ process.stderr.write(`klyro: unknown agent: ${name} (known: ${ALL.map((a) => a.id).join(', ')})\n`);
766
872
  process.exit(2);
767
873
  }
768
874
  const tools = def.allowedTools ?? ['<inherited: all parent tools>'];
769
875
  const lines = [
770
876
  `agent: ${def.id}`,
771
877
  `description: ${def.description}`,
878
+ `source: ${def.source ?? 'builtin'}`,
772
879
  `tools (${tools.length}): ${tools.join(', ')}`,
773
880
  `readonly: ${def.readonly ?? false}`,
774
881
  `canSpawn: ${def.canSpawn ?? false}`,
@@ -776,6 +883,8 @@ async function main() {
776
883
  `maxSteps: ${def.maxSteps ?? '<default>'}`,
777
884
  `maxTokens: ${def.maxTokens ?? '<default>'}`,
778
885
  ];
886
+ if (def.prompt)
887
+ lines.push(`prompt: ${def.prompt.slice(0, 200)}${def.prompt.length > 200 ? '…' : ''}`);
779
888
  process.stdout.write(lines.join('\n') + '\n');
780
889
  });
781
890
  // 10.3 — Web / git workflows / SDK
@@ -0,0 +1,85 @@
1
+ /** Shape of the `auth` block on an MCP server spec (see config.ts). */
2
+ export interface McpAuthSpec {
3
+ /** OAuth 2.0 client identifier registered with the authorization server. */
4
+ clientId: string;
5
+ /** Token endpoint; when absent, discovered from the server's metadata. */
6
+ tokenEndpoint?: string;
7
+ /**
8
+ * Authorization endpoint; when absent, discovered from the server's
9
+ * metadata. The MCP server URL is the OAuth resource server, so its
10
+ * `/.well-known/oauth-authorization-server` metadata is the default
11
+ * discovery source (per the MCP 2025-06-18 auth spec).
12
+ */
13
+ authorizationEndpoint?: string;
14
+ /** Custom redirect URI; defaults to a loopback listener on 127.0.0.1. */
15
+ redirectUri?: string;
16
+ /** Space-separated additional scopes to request. */
17
+ scopes?: string;
18
+ }
19
+ export declare class AuthError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ export interface CachedToken {
23
+ access_token: string;
24
+ token_type: string;
25
+ /** Unix seconds when the token expires. 0 = unknown (treated as valid). */
26
+ expires_at: number;
27
+ refresh_token?: string;
28
+ scope?: string;
29
+ cachedAtMs: number;
30
+ }
31
+ export declare const TOKEN_STALE_MARGIN_S = 60;
32
+ export declare const TOKENS_FILE = "mcp-tokens.json";
33
+ /** Fully load the token cache as a plain object (missing/corrupt → {}). */
34
+ export declare function loadMcpTokens(): Record<string, CachedToken>;
35
+ /**
36
+ * Persist a token to the 0600 cache. Merges by key so concurrent server
37
+ * writes don't clobber each other. A symlinked cache path is refused (the
38
+ * write would land outside the klyro config dir or be swapped mid-write).
39
+ */
40
+ export declare function saveMcpToken(cacheKey: string, token: CachedToken): void;
41
+ /** Stable cache key for a server URL + clientId. */
42
+ export declare function mcpTokenCacheKey(url: string, clientId: string): string;
43
+ /** True when a cached token is still usable (with a clock-skew margin). */
44
+ export declare function tokenValid(t: CachedToken): boolean;
45
+ /** PKCE (RFC 7636 §4.1): cryptographically-random 43-128 char code verifier. */
46
+ export declare function generateCodeVerifier(): string;
47
+ /** PKCE §4.2: base64url SHA-256 challenge of the verifier. */
48
+ export declare function codeChallengeFromVerifier(verifier: string): string;
49
+ export interface AuthResolveResult {
50
+ /** Extra headers (e.g. `Authorization`) to attach to MCP requests. */
51
+ headers: Record<string, string>;
52
+ /** True when a cached token was used (no interactive flow needed). */
53
+ fromCache: boolean;
54
+ }
55
+ /**
56
+ * Resolve auth headers for a server: reuse a valid cached token, else run
57
+ * the interactive Authorization Code + PKCE flow. Returns only the headers;
58
+ * failures surface as `AuthError` so callers (the MCP probe/connect path)
59
+ * can turn them into a clear error instead of a hang.
60
+ */
61
+ export declare function resolveAuth(auth: McpAuthSpec, cacheKey: string): Promise<AuthResolveResult>;
62
+ interface TokenResponse {
63
+ access_token: string;
64
+ token_type?: string;
65
+ expires_in?: number;
66
+ refresh_token?: string;
67
+ scope?: string;
68
+ }
69
+ /**
70
+ * Run the Authorization Code flow with PKCE over a one-shot loopback
71
+ * redirect (RFC 8252). Best-effort and headless-friendly: on any failure it
72
+ * throws `AuthError` rather than leaving a stray listener. The token endpoint
73
+ * defaults to the OAuth metadata `token_endpoint` when the server advertises
74
+ * it, else the spec's `tokenEndpoint`.
75
+ */
76
+ export declare function runCodeFlow(auth: McpAuthSpec, serverUrl?: string): Promise<TokenResponse | null>;
77
+ /**
78
+ * Scrub credential-bearing headers from debug capture so `KLYRO_MCP_DEBUG`
79
+ * never writes a live access token to disk. Non-credential headers pass
80
+ * through untouched.
81
+ */
82
+ export declare function scrubAuthHeaders(headers: Record<string, string>): Record<string, string>;
83
+ /** Refreshes a stale token when a refresh_token is cached (best-effort). */
84
+ export declare function tryRefresh(auth: McpAuthSpec, cached: CachedToken): Promise<CachedToken | null>;
85
+ export {};