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.
- package/README.md +13 -0
- package/dist/agent/custom-agents.d.ts +3 -0
- package/dist/agent/custom-agents.js +96 -0
- package/dist/agent/orchestrator.d.ts +26 -0
- package/dist/agent/orchestrator.js +41 -4
- package/dist/agent/runtime.d.ts +15 -0
- package/dist/agent/runtime.js +232 -61
- package/dist/chat.d.ts +10 -0
- package/dist/chat.js +39 -7
- package/dist/checkpoints/store.d.ts +11 -0
- package/dist/checkpoints/store.js +32 -0
- package/dist/cli/auth.d.ts +10 -3
- package/dist/cli/auth.js +43 -5
- package/dist/cli/completion.js +2 -2
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/doctor.js +0 -1
- package/dist/cli/eval.d.ts +15 -1
- package/dist/cli/eval.js +43 -5
- package/dist/cli/hooks.d.ts +74 -5
- package/dist/cli/hooks.js +118 -7
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.js +60 -0
- package/dist/cli/keychain.d.ts +10 -0
- package/dist/cli/keychain.js +86 -0
- package/dist/cli/repl.js +188 -30
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +92 -50
- package/dist/cli/setup.js +3 -2
- 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 +34 -9
- package/dist/cli/update.d.ts +3 -1
- package/dist/cli/update.js +16 -1
- package/dist/context/accounting.d.ts +6 -0
- package/dist/context/accounting.js +8 -2
- package/dist/context/compaction.d.ts +2 -1
- package/dist/context/compaction.js +39 -12
- package/dist/context/memory.d.ts +11 -0
- package/dist/context/memory.js +59 -4
- package/dist/eval/harness.d.ts +40 -5
- package/dist/eval/harness.js +103 -10
- 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 +239 -130
- package/dist/mcp/auth.d.ts +85 -0
- package/dist/mcp/auth.js +249 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +31 -1
- package/dist/mcp/config.js +84 -1
- package/dist/mcp/registry.d.ts +19 -0
- package/dist/mcp/registry.js +118 -2
- package/dist/mcp/remote.d.ts +36 -0
- package/dist/mcp/remote.js +207 -0
- package/dist/mcp/sse.d.ts +42 -0
- package/dist/mcp/sse.js +310 -0
- package/dist/persistence/audit.d.ts +15 -3
- package/dist/persistence/audit.js +84 -13
- package/dist/persistence/store.d.ts +9 -0
- package/dist/persistence/store.js +17 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -0
- package/dist/policy/engine.js +9 -0
- package/dist/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +17 -14
- package/dist/tools/shell/shell-exec.d.ts +13 -0
- package/dist/tools/shell/shell-exec.js +64 -2
- package/dist/tui/app.js +172 -15
- package/dist/tui/app.test.js +27 -2
- package/dist/tui/approval.js +55 -1
- package/dist/tui/scroll-model.d.ts +2 -2
- package/dist/tui/scroll-model.js +9 -3
- package/dist/tui/tokens.d.ts +8 -11
- package/dist/tui/tokens.js +18 -11
- 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).
|
|
@@ -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
|
-
.
|
|
147
|
-
|
|
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
|
-
|
|
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) => {
|
|
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
|
-
|
|
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 (
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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');
|
|
479
|
+
}
|
|
480
|
+
async function sessionResume(id, opts) {
|
|
481
|
+
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
466
482
|
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
|
-
}
|
|
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
|
-
|
|
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');
|
|
504
|
+
}
|
|
505
|
+
async function sessionExport(id, file) {
|
|
506
|
+
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
547
507
|
const store = getDefaultSessionStore();
|
|
548
|
-
const full = await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
691
|
-
const client =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
737
|
-
const {
|
|
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 || !
|
|
742
|
-
process.stderr.write(`klyro: unknown agent: ${agentName ?? '(missing)'} (known: ${
|
|
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
|
|
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 =
|
|
869
|
+
const def = ALL.find((a) => a.id === name);
|
|
764
870
|
if (!def) {
|
|
765
|
-
process.stderr.write(`klyro: unknown agent: ${name} (known: ${
|
|
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 {};
|