klyro 1.0.5 → 1.0.6
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/agent/custom-agents.d.ts +3 -0
- package/dist/agent/custom-agents.js +96 -0
- package/dist/agent/orchestrator.d.ts +22 -2
- package/dist/agent/orchestrator.js +30 -4
- package/dist/agent/runtime.d.ts +5 -0
- package/dist/agent/runtime.js +164 -50
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +20 -0
- package/dist/cli/completion.js +2 -2
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/eval.d.ts +15 -1
- package/dist/cli/eval.js +34 -2
- package/dist/cli/hooks.d.ts +54 -5
- package/dist/cli/hooks.js +85 -6
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/repl.js +146 -25
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +92 -50
- package/dist/cli/slash/custom.d.ts +25 -0
- package/dist/cli/slash/custom.js +166 -0
- package/dist/cli/slash/parser.d.ts +9 -1
- package/dist/cli/slash/parser.js +31 -9
- package/dist/context/memory.js +18 -1
- package/dist/eval/harness.d.ts +21 -3
- package/dist/eval/harness.js +31 -3
- package/dist/eval/judge.d.ts +32 -0
- package/dist/eval/judge.js +63 -0
- package/dist/eval/tasks.js +134 -0
- package/dist/index.js +150 -127
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +3 -1
- package/dist/mcp/config.js +19 -1
- package/dist/mcp/registry.d.ts +13 -0
- package/dist/mcp/registry.js +41 -1
- package/dist/mcp/remote.d.ts +29 -0
- package/dist/mcp/remote.js +153 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -0
- package/dist/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +13 -10
- package/dist/tui/app.js +140 -13
- package/dist/tui/app.test.js +24 -0
- package/dist/tui/approval.js +53 -1
- 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('--
|
|
64
|
-
.option('--
|
|
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).
|
|
@@ -197,12 +199,30 @@ async function main() {
|
|
|
197
199
|
const code = await runDoctor({ json: !!opts.json });
|
|
198
200
|
process.exit(code);
|
|
199
201
|
});
|
|
202
|
+
program
|
|
203
|
+
.command('init')
|
|
204
|
+
.description('Bootstrap this project: scan + KLYRO.md draft + .mcp.json skeleton (never overwrites)')
|
|
205
|
+
.action(async () => {
|
|
206
|
+
const { initProject, nextStepsText } = await import('./cli/init.js');
|
|
207
|
+
try {
|
|
208
|
+
const { created, skipped } = await initProject(process.cwd());
|
|
209
|
+
for (const f of created)
|
|
210
|
+
process.stdout.write(`created ${f}\n`);
|
|
211
|
+
for (const f of skipped)
|
|
212
|
+
process.stdout.write(`kept ${f}\n`);
|
|
213
|
+
process.stdout.write(nextStepsText() + '\n');
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
process.stderr.write(`klyro: init failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
200
221
|
// Support `klyro "prompt"` positional headless (2.5)
|
|
201
222
|
program.argument('[prompt]', 'Headless one-shot prompt (same as -p)');
|
|
202
223
|
program
|
|
203
224
|
.action(async (promptArg) => {
|
|
204
|
-
const opts = program.opts();
|
|
205
|
-
// 9.2 — --continue / --resume handling
|
|
225
|
+
const opts = program.opts(); // 9.2 — --continue / --resume handling
|
|
206
226
|
if (opts.continue || typeof opts.resume === 'string') {
|
|
207
227
|
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
208
228
|
const store = getDefaultSessionStore();
|
|
@@ -243,6 +263,7 @@ async function main() {
|
|
|
243
263
|
cwd: process.cwd(),
|
|
244
264
|
model: process.env.KLYRO_MODEL ?? 'gpt-4o-mini',
|
|
245
265
|
maxTokens: opts.maxTokens,
|
|
266
|
+
temperature: opts.temperature,
|
|
246
267
|
output: output,
|
|
247
268
|
provider: process.env.KLYRO_PROVIDER,
|
|
248
269
|
});
|
|
@@ -298,6 +319,7 @@ async function main() {
|
|
|
298
319
|
.option('--require-verify', 'Fail with exit 8 if no verification passed after edits (6.5)')
|
|
299
320
|
.option('--agent <name>', 'Run under an orchestrator context enabling spawn_agent/task_list/task_get (explorer|implementer|tester|reviewer)')
|
|
300
321
|
.option('--max-depth <n>', 'Max spawn depth for child agents (default 1)', (v) => parsePositiveInt('--max-depth', v))
|
|
322
|
+
.option('--bare', 'Deterministic runs: skip MCP, hooks, memory/KLYRO.md/context, persistence')
|
|
301
323
|
.action(async (prompt, opts) => {
|
|
302
324
|
const model = opts.model ?? process.env.KLYRO_MODEL;
|
|
303
325
|
if (!model) {
|
|
@@ -339,6 +361,7 @@ async function main() {
|
|
|
339
361
|
requireVerify: !!opts.requireVerify,
|
|
340
362
|
agent: opts.agent,
|
|
341
363
|
maxDepth: opts.maxDepth,
|
|
364
|
+
bare: !!opts.bare,
|
|
342
365
|
});
|
|
343
366
|
process.exit(code);
|
|
344
367
|
}
|
|
@@ -370,17 +393,18 @@ async function main() {
|
|
|
370
393
|
.option('--runs <n>', 'Runs per fixture (default 1)', (v) => parsePositiveInt('--runs', v))
|
|
371
394
|
.option('--parallel <n>', 'Parallelism (default 1)', (v) => parsePositiveInt('--parallel', v))
|
|
372
395
|
.option('--model <id>', 'Model for eval')
|
|
396
|
+
.option('--judge-model <id>', 'Live model id for grading judge.rubric (needs endpoint + key)')
|
|
373
397
|
.action(async (input, opts) => {
|
|
374
398
|
const output = (opts.output ?? 'human');
|
|
375
399
|
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 });
|
|
400
|
+
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
401
|
process.exit(code);
|
|
378
402
|
}
|
|
379
403
|
if (!input) {
|
|
380
404
|
process.stderr.write('klyro eval: missing input (provide <input> or --suite)\n');
|
|
381
405
|
process.exit(2);
|
|
382
406
|
}
|
|
383
|
-
const code = await runEval({ inputPath: input, output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model });
|
|
407
|
+
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
408
|
process.exit(code);
|
|
385
409
|
});
|
|
386
410
|
program
|
|
@@ -395,14 +419,11 @@ async function main() {
|
|
|
395
419
|
process.stdout.write(out + '\n');
|
|
396
420
|
process.exit(0);
|
|
397
421
|
});
|
|
398
|
-
// Level 9 — Session management
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
.option('--status <s>', 'Filter by status: open|complete|verify_failed|aborted|max_steps')
|
|
404
|
-
.option('--json', 'Output JSON')
|
|
405
|
-
.action(async (opts) => {
|
|
422
|
+
// Level 9 — Session management.
|
|
423
|
+
// One namespace: `session` and `sessions` accept the SAME subcommands
|
|
424
|
+
// (list/show/resume/export/import/fork/delete). Handlers live here once
|
|
425
|
+
// and both command groups delegate to them.
|
|
426
|
+
async function sessionList(opts) {
|
|
406
427
|
const { getDefaultSessionStore, formatSession } = await import('./persistence/session.js');
|
|
407
428
|
const store = getDefaultSessionStore();
|
|
408
429
|
const all = await store.list(opts.status ? { status: opts.status } : undefined);
|
|
@@ -419,25 +440,26 @@ async function main() {
|
|
|
419
440
|
}
|
|
420
441
|
}
|
|
421
442
|
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
.command('show <id>')
|
|
425
|
-
.description('Show session transcript and observations')
|
|
426
|
-
.option('--json', 'Output JSON')
|
|
427
|
-
.action(async (id, opts) => {
|
|
443
|
+
}
|
|
444
|
+
async function resolveOrExit(id) {
|
|
428
445
|
const { getDefaultSessionStore, resolveSessionId, matchSessionIds } = await import('./persistence/session.js');
|
|
429
446
|
const store = getDefaultSessionStore();
|
|
430
447
|
const full = await resolveSessionId(store, id);
|
|
431
|
-
if (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
process.exit(2);
|
|
448
|
+
if (full)
|
|
449
|
+
return full;
|
|
450
|
+
const matches = await matchSessionIds(store, id);
|
|
451
|
+
if (matches.length > 1) {
|
|
452
|
+
process.stderr.write(`ambiguous id "${id}" matches:\n${matches.map((r) => ` ${r.id.slice(0, 8)} ${r.task.slice(0, 50)}`).join('\n')}\n`);
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
process.stderr.write(`session not found: ${id}\n`);
|
|
440
456
|
}
|
|
457
|
+
process.exit(2);
|
|
458
|
+
}
|
|
459
|
+
async function sessionShow(id, opts) {
|
|
460
|
+
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
461
|
+
const store = getDefaultSessionStore();
|
|
462
|
+
const full = await resolveOrExit(id);
|
|
441
463
|
const rec = await store.get(full);
|
|
442
464
|
const msgs = await store.loadMessages(full);
|
|
443
465
|
const obs = await store.loadObservations(full);
|
|
@@ -453,28 +475,11 @@ async function main() {
|
|
|
453
475
|
for (const o of obs)
|
|
454
476
|
process.stdout.write(` ${o.toolName} -> ${o.isError ? 'ERR' : 'ok'} ${JSON.stringify(o.output).slice(0, 120)}\n`);
|
|
455
477
|
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
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');
|
|
478
|
+
}
|
|
479
|
+
async function sessionResume(id, opts) {
|
|
480
|
+
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
466
481
|
const store = getDefaultSessionStore();
|
|
467
|
-
const full = await
|
|
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
|
-
}
|
|
482
|
+
const full = await resolveOrExit(id);
|
|
478
483
|
const rec = await store.get(full);
|
|
479
484
|
if (!rec) {
|
|
480
485
|
process.stderr.write(`session not found: ${id}\n`);
|
|
@@ -495,69 +500,19 @@ async function main() {
|
|
|
495
500
|
verifyCommand: opts.verifyCommand,
|
|
496
501
|
});
|
|
497
502
|
process.exit(code);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
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');
|
|
503
|
+
}
|
|
504
|
+
async function sessionExport(id, file) {
|
|
505
|
+
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
547
506
|
const store = getDefaultSessionStore();
|
|
548
|
-
const full = await
|
|
549
|
-
if (!full) {
|
|
550
|
-
process.stderr.write(`session not found: ${id}\n`);
|
|
551
|
-
process.exit(2);
|
|
552
|
-
}
|
|
507
|
+
const full = await resolveOrExit(id);
|
|
553
508
|
const rec = await store.get(full);
|
|
554
509
|
const msgs = await store.loadMessages(full);
|
|
555
510
|
const obs = await store.loadObservations(full);
|
|
556
511
|
const out = file ?? `${full}.export.json`;
|
|
557
512
|
await (await import('node:fs/promises')).writeFile(out, JSON.stringify({ record: rec, messages: msgs, observations: obs }, null, 2));
|
|
558
513
|
process.stdout.write(`exported ${full} → ${out}\n`);
|
|
559
|
-
}
|
|
560
|
-
|
|
514
|
+
}
|
|
515
|
+
async function sessionImport(file) {
|
|
561
516
|
let data;
|
|
562
517
|
try {
|
|
563
518
|
data = JSON.parse(await (await import('node:fs/promises')).readFile(file, 'utf-8'));
|
|
@@ -604,8 +559,8 @@ async function main() {
|
|
|
604
559
|
}
|
|
605
560
|
}
|
|
606
561
|
process.stdout.write(`imported → ${created.id} (${restored} messages restored)\n`);
|
|
607
|
-
}
|
|
608
|
-
|
|
562
|
+
}
|
|
563
|
+
async function sessionFork(id) {
|
|
609
564
|
const { getDefaultSessionStore, matchSessionIds } = await import('./persistence/session.js');
|
|
610
565
|
const store = getDefaultSessionStore();
|
|
611
566
|
const matches = await matchSessionIds(store, id);
|
|
@@ -621,8 +576,8 @@ async function main() {
|
|
|
621
576
|
const forked = await store.fork(full);
|
|
622
577
|
const msgs = await store.loadMessages(forked.id);
|
|
623
578
|
process.stdout.write(`forked ${full.slice(0, 8)} → ${forked.id.slice(0, 8)} (${msgs.length} messages carried over)\n`);
|
|
624
|
-
}
|
|
625
|
-
|
|
579
|
+
}
|
|
580
|
+
async function sessionDelete(id) {
|
|
626
581
|
const { getDefaultSessionStore, matchSessionIds } = await import('./persistence/session.js');
|
|
627
582
|
const store = getDefaultSessionStore();
|
|
628
583
|
const matches = await matchSessionIds(store, id);
|
|
@@ -637,7 +592,64 @@ async function main() {
|
|
|
637
592
|
const full = matches[0].id;
|
|
638
593
|
await store.delete(full);
|
|
639
594
|
process.stdout.write(`deleted ${full.slice(0, 8)}\n`);
|
|
640
|
-
}
|
|
595
|
+
}
|
|
596
|
+
const session = program.command('session').description('Session persistence (Level 9)');
|
|
597
|
+
session
|
|
598
|
+
.command('list')
|
|
599
|
+
.description('List persisted sessions')
|
|
600
|
+
.option('--status <s>', 'Filter by status: open|complete|verify_failed|aborted|max_steps')
|
|
601
|
+
.option('--json', 'Output JSON')
|
|
602
|
+
.action(async (opts) => { await sessionList(opts); });
|
|
603
|
+
session
|
|
604
|
+
.command('show <id>')
|
|
605
|
+
.description('Show session transcript and observations')
|
|
606
|
+
.option('--json', 'Output JSON')
|
|
607
|
+
.action(async (id, opts) => { await sessionShow(id, opts); });
|
|
608
|
+
session
|
|
609
|
+
.command('resume <id>')
|
|
610
|
+
.description('Resume a persisted session (requires KLYRO_MODEL etc.)')
|
|
611
|
+
.option('-m, --model <id>', 'Model (default: from session or env)')
|
|
612
|
+
.option('--max-steps <n>', 'Max steps (default 30)', (v) => parsePositiveInt('--max-steps', v))
|
|
613
|
+
.option('--verify-command <cmd>', 'Override verification command')
|
|
614
|
+
.option('--verify', 'Enable verification (default: enabled)')
|
|
615
|
+
.action(async (id, opts) => { await sessionResume(id, opts); });
|
|
616
|
+
session
|
|
617
|
+
.command('export <id> [file]')
|
|
618
|
+
.description('Export session to file (9.4)')
|
|
619
|
+
.action(async (id, file) => { await sessionExport(id, file); });
|
|
620
|
+
session
|
|
621
|
+
.command('import <file>')
|
|
622
|
+
.description('Import session from file (restores record + messages + observations)')
|
|
623
|
+
.action(async (file) => { await sessionImport(file); });
|
|
624
|
+
session
|
|
625
|
+
.command('fork <id>')
|
|
626
|
+
.description('Fork session with full context (9.4)')
|
|
627
|
+
.action(async (id) => { await sessionFork(id); });
|
|
628
|
+
session
|
|
629
|
+
.command('delete <id>')
|
|
630
|
+
.description('Delete a session and its artifacts')
|
|
631
|
+
.action(async (id) => { await sessionDelete(id); });
|
|
632
|
+
// Alias: klyro resume <id> → klyro session resume <id>
|
|
633
|
+
program
|
|
634
|
+
.command('resume <id>')
|
|
635
|
+
.description('Alias for `klyro session resume <id>`')
|
|
636
|
+
.option('-m, --model <id>', 'Model')
|
|
637
|
+
.option('--max-steps <n>', 'Max steps', (v) => parsePositiveInt('--max-steps', v))
|
|
638
|
+
.action(async (id, opts) => { await sessionResume(id, opts); });
|
|
639
|
+
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 })); });
|
|
640
|
+
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 })); });
|
|
641
|
+
// 9.2 — Continue / resume top-level flags (also handled via session resume)
|
|
642
|
+
program.option('-c, --continue', 'Continue most recent session in cwd (9.2)');
|
|
643
|
+
program.option('-r, --resume [id]', 'Resume session by id or pick most recent');
|
|
644
|
+
// 9.4 — same namespace as `session`: every subcommand works under both.
|
|
645
|
+
const sessions = program.command('sessions').description('Alias for session (same subcommands)');
|
|
646
|
+
sessions.command('list').description('List persisted sessions').option('--status <s>', 'Filter by status').option('--json', 'Output JSON').action(async (opts) => { await sessionList(opts); });
|
|
647
|
+
sessions.command('show <id>').description('Show session transcript and observations').option('--json', 'Output JSON').action(async (id, opts) => { await sessionShow(id, opts); });
|
|
648
|
+
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)).action(async (id, opts) => { await sessionResume(id, opts); });
|
|
649
|
+
sessions.command('export <id> [file]').description('Export session to file (9.4)').action(async (id, file) => { await sessionExport(id, file); });
|
|
650
|
+
sessions.command('import <file>').description('Import session from file (restores record + messages + observations)').action(async (file) => { await sessionImport(file); });
|
|
651
|
+
sessions.command('fork <id>').description('Fork session with full context (9.4)').action(async (id) => { await sessionFork(id); });
|
|
652
|
+
sessions.command('delete <id>').description('Delete a session and its artifacts').action(async (id) => { await sessionDelete(id); });
|
|
641
653
|
// 10.1 — MCP
|
|
642
654
|
const mcp = program.command('mcp').description('MCP client/server (10.1)');
|
|
643
655
|
mcp.command('list').description('List MCP servers').action(async () => {
|
|
@@ -654,10 +666,14 @@ async function main() {
|
|
|
654
666
|
process.stdout.write(`${name} source=${source}${spec?.disabled ? ' disabled' : ''}\n`);
|
|
655
667
|
}
|
|
656
668
|
});
|
|
657
|
-
mcp.command('add <name> <command> [args...]').description('Add a project MCP server to .mcp.json (stdio command)').action(async (name, command, args) => {
|
|
669
|
+
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
670
|
const { addProjectServer, projectMcpPath } = await import('./mcp/config.js');
|
|
659
671
|
try {
|
|
660
|
-
|
|
672
|
+
// URL first arg → remote Streamable-HTTP server; else stdio command.
|
|
673
|
+
const spec = /^https?:\/\//i.test(command)
|
|
674
|
+
? { url: command }
|
|
675
|
+
: { command, ...(args && args.length > 0 ? { args } : {}) };
|
|
676
|
+
addProjectServer(process.cwd(), name, spec);
|
|
661
677
|
process.stdout.write(`added mcp server "${name}" → ${projectMcpPath(process.cwd())}\n`);
|
|
662
678
|
}
|
|
663
679
|
catch (err) {
|
|
@@ -687,18 +703,21 @@ async function main() {
|
|
|
687
703
|
process.stderr.write(`klyro: mcp server not found: ${name}\n`);
|
|
688
704
|
process.exit(2);
|
|
689
705
|
}
|
|
690
|
-
const {
|
|
691
|
-
const client =
|
|
692
|
-
// 15s overall probe budget (connect has its own internal timeout too).
|
|
706
|
+
const { makeMcpClient } = await import('./mcp/registry.js');
|
|
707
|
+
const client = makeMcpClient(name, spec); // 15s overall probe budget (connect has its own internal timeout too).
|
|
693
708
|
const timer = setTimeout(() => {
|
|
694
709
|
process.stderr.write(`klyro: mcp probe ${name} timed out after 15s\n`);
|
|
695
710
|
process.exit(2);
|
|
696
711
|
}, 15_000);
|
|
697
712
|
try {
|
|
698
|
-
|
|
713
|
+
const withConnect = client;
|
|
714
|
+
if (typeof withConnect.connect === 'function')
|
|
715
|
+
await withConnect.connect();
|
|
699
716
|
const tools = await client.listTools();
|
|
717
|
+
const prompts = typeof client.promptsList === 'function' ? await client.promptsList().catch(() => []) : [];
|
|
700
718
|
const names = tools.map((t) => t.name);
|
|
701
|
-
|
|
719
|
+
const extra = prompts.length > 0 ? `, ${prompts.length} prompt(s): ${prompts.map((p) => p.name).join(', ')}` : '';
|
|
720
|
+
process.stdout.write(`${name}: ${tools.length} tool(s)${names.length > 0 ? `: ${names.join(', ')}` : ''}${extra}\n`);
|
|
702
721
|
process.exit(0);
|
|
703
722
|
}
|
|
704
723
|
catch (err) {
|
|
@@ -731,15 +750,16 @@ async function main() {
|
|
|
731
750
|
return;
|
|
732
751
|
}
|
|
733
752
|
for (const h of hooks)
|
|
734
|
-
process.stdout.write(`${h.name} ${h.event} ${h.command}\n`);
|
|
753
|
+
process.stdout.write(`${h.name} ${h.event}${h.matcher ? ` (${h.matcher})` : ''} ${h.command}\n`);
|
|
735
754
|
});
|
|
736
|
-
program.command('agents [name] [extra...]').description('List agents (
|
|
737
|
-
const {
|
|
755
|
+
program.command('agents [name] [extra...]').description('List agents (builtins + .klyro/agents/*.md), show one, or run: agents run <name> <task...>').action(async (name, extra) => {
|
|
756
|
+
const { listAllAgents } = await import('./agent/orchestrator.js');
|
|
757
|
+
const ALL = listAllAgents(process.cwd());
|
|
738
758
|
// `klyro agents run <name> <task...>`: one-shot run under a named agent.
|
|
739
759
|
if (name === 'run') {
|
|
740
760
|
const [agentName, ...taskParts] = extra ?? [];
|
|
741
|
-
if (!agentName || !
|
|
742
|
-
process.stderr.write(`klyro: unknown agent: ${agentName ?? '(missing)'} (known: ${
|
|
761
|
+
if (!agentName || !ALL.some((a) => a.id === agentName)) {
|
|
762
|
+
process.stderr.write(`klyro: unknown agent: ${agentName ?? '(missing)'} (known: ${ALL.map((a) => a.id).join(', ')})\n`);
|
|
743
763
|
process.exit(2);
|
|
744
764
|
}
|
|
745
765
|
const task = (taskParts ?? []).join(' ').trim();
|
|
@@ -756,19 +776,20 @@ async function main() {
|
|
|
756
776
|
process.exit(code);
|
|
757
777
|
}
|
|
758
778
|
if (!name) {
|
|
759
|
-
for (const a of
|
|
760
|
-
process.stdout.write(`${a.id} — ${a.description}\n`);
|
|
779
|
+
for (const a of ALL)
|
|
780
|
+
process.stdout.write(`${a.id}${a.source && a.source !== 'builtin' ? ` (${a.source})` : ''} — ${a.description}\n`);
|
|
761
781
|
return;
|
|
762
782
|
}
|
|
763
|
-
const def =
|
|
783
|
+
const def = ALL.find((a) => a.id === name);
|
|
764
784
|
if (!def) {
|
|
765
|
-
process.stderr.write(`klyro: unknown agent: ${name} (known: ${
|
|
785
|
+
process.stderr.write(`klyro: unknown agent: ${name} (known: ${ALL.map((a) => a.id).join(', ')})\n`);
|
|
766
786
|
process.exit(2);
|
|
767
787
|
}
|
|
768
788
|
const tools = def.allowedTools ?? ['<inherited: all parent tools>'];
|
|
769
789
|
const lines = [
|
|
770
790
|
`agent: ${def.id}`,
|
|
771
791
|
`description: ${def.description}`,
|
|
792
|
+
`source: ${def.source ?? 'builtin'}`,
|
|
772
793
|
`tools (${tools.length}): ${tools.join(', ')}`,
|
|
773
794
|
`readonly: ${def.readonly ?? false}`,
|
|
774
795
|
`canSpawn: ${def.canSpawn ?? false}`,
|
|
@@ -776,6 +797,8 @@ async function main() {
|
|
|
776
797
|
`maxSteps: ${def.maxSteps ?? '<default>'}`,
|
|
777
798
|
`maxTokens: ${def.maxTokens ?? '<default>'}`,
|
|
778
799
|
];
|
|
800
|
+
if (def.prompt)
|
|
801
|
+
lines.push(`prompt: ${def.prompt.slice(0, 200)}${def.prompt.length > 200 ? '…' : ''}`);
|
|
779
802
|
process.stdout.write(lines.join('\n') + '\n');
|
|
780
803
|
});
|
|
781
804
|
// 10.3 — Web / git workflows / SDK
|
package/dist/mcp/client.d.ts
CHANGED
|
@@ -9,6 +9,15 @@ export interface McpResource {
|
|
|
9
9
|
name?: string;
|
|
10
10
|
mimeType?: string;
|
|
11
11
|
}
|
|
12
|
+
export interface McpPromptDef {
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
arguments?: Array<{
|
|
16
|
+
name: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
12
21
|
export interface McpCallResult {
|
|
13
22
|
/** Normalized text payload (content blocks joined). */
|
|
14
23
|
text: string;
|
|
@@ -19,6 +28,8 @@ export interface McpClientLike {
|
|
|
19
28
|
listTools(signal?: AbortSignal): Promise<McpToolDef[]>;
|
|
20
29
|
callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult>;
|
|
21
30
|
listResources(signal?: AbortSignal): Promise<McpResource[]>;
|
|
31
|
+
promptsList?(signal?: AbortSignal): Promise<McpPromptDef[]>;
|
|
32
|
+
promptsGet?(name: string, args?: Record<string, string>, signal?: AbortSignal): Promise<string>;
|
|
22
33
|
close(): Promise<void>;
|
|
23
34
|
}
|
|
24
35
|
export declare class McpError extends Error {
|
|
@@ -43,6 +54,10 @@ export declare class McpClient implements McpClientLike {
|
|
|
43
54
|
callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult>;
|
|
44
55
|
listResources(signal?: AbortSignal): Promise<McpResource[]>;
|
|
45
56
|
readResource(uri: string, signal?: AbortSignal): Promise<string>;
|
|
57
|
+
/** List prompts; servers without prompts support yield [] (not an error). */
|
|
58
|
+
promptsList(signal?: AbortSignal): Promise<McpPromptDef[]>;
|
|
59
|
+
/** Get a prompt's messages as text. Throws McpError when unsupported/unknown. */
|
|
60
|
+
promptsGet(name: string, args?: Record<string, string>, signal?: AbortSignal): Promise<string>;
|
|
46
61
|
close(): Promise<void>;
|
|
47
62
|
/** Test hook: number of in-flight JSON-RPC requests. */
|
|
48
63
|
pendingCount(): number;
|
package/dist/mcp/client.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* P1 — MCP stdio client (r-11-17.md §3.1).
|
|
3
3
|
*
|
|
4
4
|
* Minimal JSON-RPC 2.0 over newline-delimited stdio: `initialize`,
|
|
5
|
-
* `tools/list`, `tools/call`, `resources/list`, `resources/read`,
|
|
5
|
+
* `tools/list`, `tools/call`, `resources/list`, `resources/read`,
|
|
6
|
+
* `prompts/list`, `prompts/get`, `ping`.
|
|
6
7
|
* Every call is bound to an `AbortSignal` and a per-call timeout; spawn
|
|
7
8
|
* failures and server errors surface as typed `McpError`s — never throws
|
|
8
9
|
* raw across the boundary.
|
|
@@ -38,15 +39,22 @@ export class McpClient {
|
|
|
38
39
|
constructor(name, spec) {
|
|
39
40
|
this.name = name;
|
|
40
41
|
this.spec = spec;
|
|
42
|
+
// URL specs route to RemoteMcpClient via makeMcpClient — direct
|
|
43
|
+
// construction without a command is a programming error, fail fast.
|
|
44
|
+
if (!spec.command)
|
|
45
|
+
throw new McpError(`mcp server "${name}" has no command (use url for remote servers)`, 'INVALID_SPEC');
|
|
41
46
|
}
|
|
42
47
|
async connect() {
|
|
43
48
|
if (this.child)
|
|
44
49
|
return;
|
|
50
|
+
const command = this.spec.command;
|
|
51
|
+
if (!command)
|
|
52
|
+
throw new McpError(`mcp server "${this.name}" has no command (use url for remote servers)`, 'INVALID_SPEC');
|
|
45
53
|
const timeoutMs = this.spec.policy?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
46
54
|
await new Promise((resolve, reject) => {
|
|
47
55
|
let child;
|
|
48
56
|
try {
|
|
49
|
-
child = spawn(
|
|
57
|
+
child = spawn(command, this.spec.args ?? [], {
|
|
50
58
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
51
59
|
env: { ...process.env, ...(this.spec.env ?? {}) },
|
|
52
60
|
shell: false,
|
|
@@ -136,6 +144,38 @@ export class McpClient {
|
|
|
136
144
|
const contents = Array.isArray(res.contents) ? res.contents : [];
|
|
137
145
|
return contents.map((c) => (typeof c.text === 'string' ? c.text : JSON.stringify(c))).join('\n');
|
|
138
146
|
}
|
|
147
|
+
/** List prompts; servers without prompts support yield [] (not an error). */
|
|
148
|
+
async promptsList(signal) {
|
|
149
|
+
this.assertLive();
|
|
150
|
+
let res;
|
|
151
|
+
try {
|
|
152
|
+
res = (await this.request('prompts/list', {}, this.timeout(), signal));
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
if (err instanceof McpError && err.code === 'SERVER_ERROR' && err.details?.code === -32601) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
return Array.isArray(res.prompts) ? res.prompts : [];
|
|
161
|
+
}
|
|
162
|
+
/** Get a prompt's messages as text. Throws McpError when unsupported/unknown. */
|
|
163
|
+
async promptsGet(name, args, signal) {
|
|
164
|
+
this.assertLive();
|
|
165
|
+
const res = (await this.request('prompts/get', { name, arguments: args ?? {} }, this.timeout(), signal));
|
|
166
|
+
const messages = Array.isArray(res.messages) ? res.messages : [];
|
|
167
|
+
const parts = [];
|
|
168
|
+
if (typeof res.description === 'string' && res.description)
|
|
169
|
+
parts.push(res.description);
|
|
170
|
+
for (const m of messages) {
|
|
171
|
+
const c = m.content;
|
|
172
|
+
if (c && typeof c.text === 'string')
|
|
173
|
+
parts.push(c.text);
|
|
174
|
+
else if (c !== undefined)
|
|
175
|
+
parts.push(JSON.stringify(c));
|
|
176
|
+
}
|
|
177
|
+
return parts.join('\n\n');
|
|
178
|
+
}
|
|
139
179
|
async close() {
|
|
140
180
|
// Idempotent vs the exit handler: closed is set FIRST (the on('exit')
|
|
141
181
|
// callback checks it before failAll), and a second close() with no child
|
package/dist/mcp/config.d.ts
CHANGED
|
@@ -7,9 +7,11 @@ export declare const McpServerPolicySchema: z.ZodObject<{
|
|
|
7
7
|
}, z.core.$strip>;
|
|
8
8
|
export type McpServerPolicy = z.infer<typeof McpServerPolicySchema>;
|
|
9
9
|
export declare const McpServerSpecSchema: z.ZodObject<{
|
|
10
|
-
command: z.ZodString
|
|
10
|
+
command: z.ZodOptional<z.ZodString>;
|
|
11
11
|
args: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
12
12
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
13
|
+
url: z.ZodOptional<z.ZodString>;
|
|
14
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
13
15
|
disabled: z.ZodOptional<z.ZodBoolean>;
|
|
14
16
|
policy: z.ZodOptional<z.ZodObject<{
|
|
15
17
|
allowTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
package/dist/mcp/config.js
CHANGED
|
@@ -19,11 +19,23 @@ export const McpServerPolicySchema = z.object({
|
|
|
19
19
|
timeoutMs: z.number().int().positive().optional(),
|
|
20
20
|
});
|
|
21
21
|
export const McpServerSpecSchema = z.object({
|
|
22
|
-
|
|
22
|
+
/** stdio transport: command to spawn. Required unless `url` is set. */
|
|
23
|
+
command: z.string().min(1).optional(),
|
|
23
24
|
args: z.array(z.string()).optional(),
|
|
24
25
|
env: z.record(z.string(), z.string()).optional(),
|
|
26
|
+
/** Remote transport: Streamable-HTTP JSON-RPC endpoint. Required unless `command` is set. */
|
|
27
|
+
url: z.string().url().optional(),
|
|
28
|
+
/** Extra HTTP headers for remote transport (`${env:VAR}` expanded). */
|
|
29
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
25
30
|
disabled: z.boolean().optional(),
|
|
26
31
|
policy: McpServerPolicySchema.optional(),
|
|
32
|
+
}).superRefine((s, ctx) => {
|
|
33
|
+
if (!s.command && !s.url) {
|
|
34
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'MCP server needs either "command" (stdio) or "url" (remote HTTP)' });
|
|
35
|
+
}
|
|
36
|
+
if (s.command && s.url) {
|
|
37
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'MCP server takes either "command" or "url", not both' });
|
|
38
|
+
}
|
|
27
39
|
});
|
|
28
40
|
/** Upper bound for per-server timeouts — larger values are clamped, not rejected. */
|
|
29
41
|
export const MAX_MCP_TIMEOUT_MS = 600_000;
|
|
@@ -82,6 +94,12 @@ export function loadMcpServers(cwd) {
|
|
|
82
94
|
env[k] = expandEnv(v);
|
|
83
95
|
spec.env = env;
|
|
84
96
|
}
|
|
97
|
+
if (spec.headers) {
|
|
98
|
+
const headers = {};
|
|
99
|
+
for (const [k, v] of Object.entries(spec.headers))
|
|
100
|
+
headers[k] = expandEnv(v);
|
|
101
|
+
spec.headers = headers;
|
|
102
|
+
}
|
|
85
103
|
servers[name] = spec;
|
|
86
104
|
sources[name] = label;
|
|
87
105
|
}
|
package/dist/mcp/registry.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export declare const MCP_SUCCESS_MAX_CHARS = 12000;
|
|
|
30
30
|
* behind a plausible-looking name.
|
|
31
31
|
*/
|
|
32
32
|
export declare function sanitizeMcpName(server: string, tool: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Construct the right client for a spec: remote HTTP when `url` is set,
|
|
35
|
+
* else stdio. Used by registration, probe, and prompt runs.
|
|
36
|
+
*/
|
|
37
|
+
export declare function makeMcpClient(name: string, spec: McpServerSpec): McpClientLike;
|
|
33
38
|
export declare function registerMcpServers(cfg: {
|
|
34
39
|
servers: Record<string, McpServerSpec>;
|
|
35
40
|
}, opts: RegisterMcpOpts): Promise<McpRegisterResult>;
|
|
@@ -52,3 +57,11 @@ export declare function loadAndRegisterMcp(opts: {
|
|
|
52
57
|
source: 'global' | 'project';
|
|
53
58
|
}) => Promise<boolean>;
|
|
54
59
|
}): Promise<McpRegisterResult>;
|
|
60
|
+
/**
|
|
61
|
+
* Run one MCP prompt (`/mcp__<server>__<prompt>`) on demand: fresh stdio
|
|
62
|
+
* connection per invocation (no client lifecycle to manage), redacted
|
|
63
|
+
* output. Typing the prompt name is explicit consent, so project servers
|
|
64
|
+
* connect without the registration-time approval gate. Throws McpError /
|
|
65
|
+
* Error on unknown server, disabled server, or prompt failure.
|
|
66
|
+
*/
|
|
67
|
+
export declare function runMcpPrompt(cwd: string, server: string, prompt: string, args: Record<string, string>): Promise<string>;
|