klyro 1.0.4 → 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/README.md +29 -0
- 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 +174 -51
- package/dist/checkpoints/store.d.ts +9 -0
- package/dist/checkpoints/store.js +20 -0
- package/dist/cli/auth.js +11 -3
- package/dist/cli/completion.js +63 -10
- package/dist/cli/config.d.ts +4 -4
- package/dist/cli/doctor.js +13 -0
- 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 +261 -34
- package/dist/cli/run.d.ts +7 -1
- package/dist/cli/run.js +97 -41
- package/dist/cli/slash/custom.d.ts +25 -0
- package/dist/cli/slash/custom.js +166 -0
- package/dist/cli/slash/parser.d.ts +16 -2
- package/dist/cli/slash/parser.js +67 -18
- package/dist/cli/update.d.ts +8 -4
- package/dist/cli/update.js +50 -7
- package/dist/context/accounting.d.ts +8 -0
- package/dist/context/accounting.js +18 -1
- package/dist/context/compaction.d.ts +1 -0
- package/dist/context/compaction.js +2 -1
- 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 +225 -132
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +42 -2
- package/dist/mcp/config.d.ts +10 -1
- package/dist/mcp/config.js +64 -1
- package/dist/mcp/registry.d.ts +13 -0
- package/dist/mcp/registry.js +47 -5
- package/dist/mcp/remote.d.ts +29 -0
- package/dist/mcp/remote.js +153 -0
- package/dist/mcp/serve.d.ts +23 -0
- package/dist/mcp/serve.js +111 -0
- package/dist/policy/approval.d.ts +15 -1
- package/dist/policy/approval.js +8 -0
- package/dist/policy/engine.d.ts +11 -1
- package/dist/policy/engine.js +14 -1
- package/dist/policy/secret-redactor.js +4 -1
- package/dist/providers/endpoints.d.ts +43 -0
- package/dist/providers/endpoints.js +104 -0
- package/dist/providers.js +13 -10
- package/dist/shared/error-map.d.ts +19 -0
- package/dist/shared/error-map.js +58 -0
- package/dist/tools/lsp/diagnostics.d.ts +35 -4
- package/dist/tools/lsp/diagnostics.js +88 -9
- package/dist/tools/normalize.d.ts +3 -0
- package/dist/tools/normalize.js +8 -5
- package/dist/tools/search/dependencies.d.ts +2 -2
- package/dist/tools/shell/background.d.ts +6 -0
- package/dist/tools/shell/background.js +17 -0
- package/dist/tools/symbols/find-symbol.d.ts +1 -1
- package/dist/tools/symbols/find-symbol.js +8 -6
- package/dist/tools/types.d.ts +8 -1
- package/dist/tui/app.d.ts +2 -0
- package/dist/tui/app.js +454 -53
- package/dist/tui/app.test.js +66 -3
- package/dist/tui/approval.js +53 -1
- package/dist/tui/markdown.js +9 -0
- package/dist/tui/mouse.d.ts +26 -1
- package/dist/tui/mouse.js +104 -6
- package/dist/tui/scroll-flow.test.js +3 -1
- package/dist/tui/tokens.d.ts +6 -6
- package/dist/tui/tokens.js +9 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,12 +56,14 @@ async function main() {
|
|
|
56
56
|
.option('--verbose', 'Verbose output')
|
|
57
57
|
.option('--quiet', 'Suppress non-essential output')
|
|
58
58
|
.option('--json', 'Force JSON output where supported')
|
|
59
|
-
.option('--yes', 'Auto-approve prompts
|
|
59
|
+
.option('--yes', 'Auto-approve commit prompts (scope: `klyro commit` only)')
|
|
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,75 +500,67 @@ 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);
|
|
510
|
+
const obs = await store.loadObservations(full);
|
|
555
511
|
const out = file ?? `${full}.export.json`;
|
|
556
|
-
await (await import('node:fs/promises')).writeFile(out, JSON.stringify({ record: rec, messages: msgs }, null, 2));
|
|
512
|
+
await (await import('node:fs/promises')).writeFile(out, JSON.stringify({ record: rec, messages: msgs, observations: obs }, null, 2));
|
|
557
513
|
process.stdout.write(`exported ${full} → ${out}\n`);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
|
|
514
|
+
}
|
|
515
|
+
async function sessionImport(file) {
|
|
516
|
+
let data;
|
|
517
|
+
try {
|
|
518
|
+
data = JSON.parse(await (await import('node:fs/promises')).readFile(file, 'utf-8'));
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
process.stderr.write(`klyro: cannot import ${file}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
522
|
+
process.exit(2);
|
|
523
|
+
}
|
|
524
|
+
const rec = data.record ?? {};
|
|
561
525
|
const { getDefaultSessionStore } = await import('./persistence/session.js');
|
|
562
526
|
const store = getDefaultSessionStore();
|
|
563
|
-
const
|
|
564
|
-
process.
|
|
565
|
-
|
|
566
|
-
|
|
527
|
+
const cfg = (rec.config && typeof rec.config === 'object' ? rec.config : { model: 'imported', maxSteps: 30 });
|
|
528
|
+
const created = await store.create({ cwd: typeof rec.cwd === 'string' ? rec.cwd : process.cwd(), task: typeof rec.task === 'string' ? rec.task : 'imported', config: cfg });
|
|
529
|
+
// Restore the transcript — previously this was silently dropped (lossy
|
|
530
|
+
// import). Messages/observations go through append* so at-rest redaction
|
|
531
|
+
// still applies. Malformed entries fail loudly instead of half-importing.
|
|
532
|
+
const d = data;
|
|
533
|
+
let restored = 0;
|
|
534
|
+
if (d.messages !== undefined) {
|
|
535
|
+
if (!Array.isArray(d.messages)) {
|
|
536
|
+
process.stderr.write(`klyro: import failed: "messages" is not an array in ${file}\n`);
|
|
537
|
+
process.exit(2);
|
|
538
|
+
}
|
|
539
|
+
for (const m of d.messages) {
|
|
540
|
+
if (!m || typeof m !== 'object' || typeof m.role !== 'string' || !('content' in m)) {
|
|
541
|
+
process.stderr.write(`klyro: import failed: malformed message entry in ${file}\n`);
|
|
542
|
+
process.exit(2);
|
|
543
|
+
}
|
|
544
|
+
await store.appendMessage(created.id, m);
|
|
545
|
+
restored++;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (d.observations !== undefined) {
|
|
549
|
+
if (!Array.isArray(d.observations)) {
|
|
550
|
+
process.stderr.write(`klyro: import failed: "observations" is not an array in ${file}\n`);
|
|
551
|
+
process.exit(2);
|
|
552
|
+
}
|
|
553
|
+
for (const o of d.observations) {
|
|
554
|
+
if (!o || typeof o !== 'object') {
|
|
555
|
+
process.stderr.write(`klyro: import failed: malformed observation entry in ${file}\n`);
|
|
556
|
+
process.exit(2);
|
|
557
|
+
}
|
|
558
|
+
await store.appendObservation(created.id, o);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
process.stdout.write(`imported → ${created.id} (${restored} messages restored)\n`);
|
|
562
|
+
}
|
|
563
|
+
async function sessionFork(id) {
|
|
567
564
|
const { getDefaultSessionStore, matchSessionIds } = await import('./persistence/session.js');
|
|
568
565
|
const store = getDefaultSessionStore();
|
|
569
566
|
const matches = await matchSessionIds(store, id);
|
|
@@ -579,8 +576,8 @@ async function main() {
|
|
|
579
576
|
const forked = await store.fork(full);
|
|
580
577
|
const msgs = await store.loadMessages(forked.id);
|
|
581
578
|
process.stdout.write(`forked ${full.slice(0, 8)} → ${forked.id.slice(0, 8)} (${msgs.length} messages carried over)\n`);
|
|
582
|
-
}
|
|
583
|
-
|
|
579
|
+
}
|
|
580
|
+
async function sessionDelete(id) {
|
|
584
581
|
const { getDefaultSessionStore, matchSessionIds } = await import('./persistence/session.js');
|
|
585
582
|
const store = getDefaultSessionStore();
|
|
586
583
|
const matches = await matchSessionIds(store, id);
|
|
@@ -595,7 +592,64 @@ async function main() {
|
|
|
595
592
|
const full = matches[0].id;
|
|
596
593
|
await store.delete(full);
|
|
597
594
|
process.stdout.write(`deleted ${full.slice(0, 8)}\n`);
|
|
598
|
-
}
|
|
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); });
|
|
599
653
|
// 10.1 — MCP
|
|
600
654
|
const mcp = program.command('mcp').description('MCP client/server (10.1)');
|
|
601
655
|
mcp.command('list').description('List MCP servers').action(async () => {
|
|
@@ -612,7 +666,35 @@ async function main() {
|
|
|
612
666
|
process.stdout.write(`${name} source=${source}${spec?.disabled ? ' disabled' : ''}\n`);
|
|
613
667
|
}
|
|
614
668
|
});
|
|
615
|
-
mcp.command('add <name> <
|
|
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) => {
|
|
670
|
+
const { addProjectServer, projectMcpPath } = await import('./mcp/config.js');
|
|
671
|
+
try {
|
|
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);
|
|
677
|
+
process.stdout.write(`added mcp server "${name}" → ${projectMcpPath(process.cwd())}\n`);
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
process.stderr.write(`klyro: mcp add failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
681
|
+
process.exit(2);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
mcp.command('remove <name>').description('Remove a project MCP server from .mcp.json').action(async (name) => {
|
|
685
|
+
const { removeProjectServer, projectMcpPath } = await import('./mcp/config.js');
|
|
686
|
+
try {
|
|
687
|
+
if (!removeProjectServer(process.cwd(), name)) {
|
|
688
|
+
process.stderr.write(`klyro: mcp server not found in ${projectMcpPath(process.cwd())}: ${name}\n`);
|
|
689
|
+
process.exit(2);
|
|
690
|
+
}
|
|
691
|
+
process.stdout.write(`removed mcp server "${name}"\n`);
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
process.stderr.write(`klyro: mcp remove failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
695
|
+
process.exit(2);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
616
698
|
mcp.command('probe <name>').description('Connect to an MCP server (15s timeout), list its tools, print count+names').action(async (name) => {
|
|
617
699
|
const { loadMcpServers } = await import('./mcp/config.js');
|
|
618
700
|
const cfg = loadMcpServers(process.cwd());
|
|
@@ -621,18 +703,21 @@ async function main() {
|
|
|
621
703
|
process.stderr.write(`klyro: mcp server not found: ${name}\n`);
|
|
622
704
|
process.exit(2);
|
|
623
705
|
}
|
|
624
|
-
const {
|
|
625
|
-
const client =
|
|
626
|
-
// 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).
|
|
627
708
|
const timer = setTimeout(() => {
|
|
628
709
|
process.stderr.write(`klyro: mcp probe ${name} timed out after 15s\n`);
|
|
629
710
|
process.exit(2);
|
|
630
711
|
}, 15_000);
|
|
631
712
|
try {
|
|
632
|
-
|
|
713
|
+
const withConnect = client;
|
|
714
|
+
if (typeof withConnect.connect === 'function')
|
|
715
|
+
await withConnect.connect();
|
|
633
716
|
const tools = await client.listTools();
|
|
717
|
+
const prompts = typeof client.promptsList === 'function' ? await client.promptsList().catch(() => []) : [];
|
|
634
718
|
const names = tools.map((t) => t.name);
|
|
635
|
-
|
|
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`);
|
|
636
721
|
process.exit(0);
|
|
637
722
|
}
|
|
638
723
|
catch (err) {
|
|
@@ -647,7 +732,11 @@ async function main() {
|
|
|
647
732
|
catch { /* ignore */ }
|
|
648
733
|
}
|
|
649
734
|
});
|
|
650
|
-
mcp.command('serve').description('Serve as MCP server').action(async () => {
|
|
735
|
+
mcp.command('serve').description('Serve builtin tools as an MCP server over stdio (policy-gated)').action(async () => {
|
|
736
|
+
const { serveStdio } = await import('./mcp/serve.js');
|
|
737
|
+
const code = await serveStdio(process.cwd());
|
|
738
|
+
process.exit(code);
|
|
739
|
+
});
|
|
651
740
|
// 10.2 — Hooks: list configured preToolUse/postToolUse hooks.
|
|
652
741
|
program.command('hooks [cmd]').description('Hooks (10.2): `klyro hooks` or `klyro hooks list` prints configured hooks').action(async (cmd) => {
|
|
653
742
|
if (cmd && cmd !== 'list') {
|
|
@@ -661,15 +750,16 @@ async function main() {
|
|
|
661
750
|
return;
|
|
662
751
|
}
|
|
663
752
|
for (const h of hooks)
|
|
664
|
-
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`);
|
|
665
754
|
});
|
|
666
|
-
program.command('agents [name] [extra...]').description('List agents (
|
|
667
|
-
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());
|
|
668
758
|
// `klyro agents run <name> <task...>`: one-shot run under a named agent.
|
|
669
759
|
if (name === 'run') {
|
|
670
760
|
const [agentName, ...taskParts] = extra ?? [];
|
|
671
|
-
if (!agentName || !
|
|
672
|
-
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`);
|
|
673
763
|
process.exit(2);
|
|
674
764
|
}
|
|
675
765
|
const task = (taskParts ?? []).join(' ').trim();
|
|
@@ -686,19 +776,20 @@ async function main() {
|
|
|
686
776
|
process.exit(code);
|
|
687
777
|
}
|
|
688
778
|
if (!name) {
|
|
689
|
-
for (const a of
|
|
690
|
-
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`);
|
|
691
781
|
return;
|
|
692
782
|
}
|
|
693
|
-
const def =
|
|
783
|
+
const def = ALL.find((a) => a.id === name);
|
|
694
784
|
if (!def) {
|
|
695
|
-
process.stderr.write(`klyro: unknown agent: ${name} (known: ${
|
|
785
|
+
process.stderr.write(`klyro: unknown agent: ${name} (known: ${ALL.map((a) => a.id).join(', ')})\n`);
|
|
696
786
|
process.exit(2);
|
|
697
787
|
}
|
|
698
788
|
const tools = def.allowedTools ?? ['<inherited: all parent tools>'];
|
|
699
789
|
const lines = [
|
|
700
790
|
`agent: ${def.id}`,
|
|
701
791
|
`description: ${def.description}`,
|
|
792
|
+
`source: ${def.source ?? 'builtin'}`,
|
|
702
793
|
`tools (${tools.length}): ${tools.join(', ')}`,
|
|
703
794
|
`readonly: ${def.readonly ?? false}`,
|
|
704
795
|
`canSpawn: ${def.canSpawn ?? false}`,
|
|
@@ -706,6 +797,8 @@ async function main() {
|
|
|
706
797
|
`maxSteps: ${def.maxSteps ?? '<default>'}`,
|
|
707
798
|
`maxTokens: ${def.maxTokens ?? '<default>'}`,
|
|
708
799
|
];
|
|
800
|
+
if (def.prompt)
|
|
801
|
+
lines.push(`prompt: ${def.prompt.slice(0, 200)}${def.prompt.length > 200 ? '…' : ''}`);
|
|
709
802
|
process.stdout.write(lines.join('\n') + '\n');
|
|
710
803
|
});
|
|
711
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
|