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/cli/run.js
CHANGED
|
@@ -19,6 +19,7 @@ import { DenyAllApprovalPrompt } from '../policy/approval.js';
|
|
|
19
19
|
import { redact } from '../policy/secret-redactor.js';
|
|
20
20
|
import { buildLevel6Context } from '../context/level6.js';
|
|
21
21
|
import { memoryBlock } from '../context/memory.js';
|
|
22
|
+
import { estimateCost } from '../providers/model-info.js';
|
|
22
23
|
import { resolveSessionId } from '../persistence/session.js';
|
|
23
24
|
import * as fs from 'node:fs';
|
|
24
25
|
function readEnv(name, fallback) {
|
|
@@ -53,7 +54,8 @@ export async function runOnce(opts) {
|
|
|
53
54
|
catch { /* ignore */ }
|
|
54
55
|
// P1.4 — validate --agent early so typos fail fast (exit 2) even without API keys.
|
|
55
56
|
if (opts.agent) {
|
|
56
|
-
const {
|
|
57
|
+
const { listAllAgents } = await import('../agent/orchestrator.js');
|
|
58
|
+
const _known = listAllAgents(opts.cwd);
|
|
57
59
|
if (!_known.some((a) => a.id === opts.agent)) {
|
|
58
60
|
stderr.write(`klyro: unknown agent: ${opts.agent} (known: ${_known.map((a) => a.id).join(', ')})\n`);
|
|
59
61
|
return 2;
|
|
@@ -141,36 +143,39 @@ export async function runOnce(opts) {
|
|
|
141
143
|
// approve). Project-sourced servers auto-connect ONLY when their exact
|
|
142
144
|
// spec hash is already in the McpTrust store (e.g. approved in a prior
|
|
143
145
|
// REPL session); unknown specs are skipped with a warning.
|
|
146
|
+
// --bare skips MCP entirely (deterministic, no subprocesses).
|
|
144
147
|
let closeMcp;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
148
|
+
if (!opts.bare) {
|
|
149
|
+
try {
|
|
150
|
+
const { loadAndRegisterMcp } = await import('../mcp/registry.js');
|
|
151
|
+
const { McpTrust, hashSpec } = await import('../mcp/trust.js');
|
|
152
|
+
const { loadMcpServers } = await import('../mcp/config.js');
|
|
153
|
+
const mcpTrust = new McpTrust();
|
|
154
|
+
const mcpSpecs = loadMcpServers(opts.cwd).servers;
|
|
155
|
+
const mcp = await loadAndRegisterMcp({
|
|
156
|
+
cwd: opts.cwd,
|
|
157
|
+
registry,
|
|
158
|
+
policy,
|
|
159
|
+
approveProjectServer: async ({ name }) => {
|
|
160
|
+
const spec = mcpSpecs[name];
|
|
161
|
+
if (spec && mcpTrust.isTrusted(name, hashSpec(spec)))
|
|
162
|
+
return true;
|
|
163
|
+
stderr.write(`klyro: mcp project server "${name}" not in trust store (skipped — headless cannot prompt)\n`);
|
|
164
|
+
return false;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
for (const e of mcp.errors)
|
|
168
|
+
stderr.write(`klyro: mcp ${e.server}: ${e.message}\n`);
|
|
169
|
+
if (mcp.registered.length > 0 && output === 'human') {
|
|
170
|
+
stderr.write(`klyro: mcp tools: ${mcp.registered.join(', ')}\n`);
|
|
171
|
+
}
|
|
172
|
+
closeMcp = mcp.closeAll;
|
|
167
173
|
}
|
|
168
|
-
|
|
174
|
+
catch { /* ignore — MCP is optional */ }
|
|
169
175
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const persistEnabled = opts.persist !== false;
|
|
176
|
+
const systemPrompt = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt, opts.bare);
|
|
177
|
+
// Level 9 — session setup (create or resume). --bare skips persistence.
|
|
178
|
+
const persistEnabled = opts.persist !== false && !opts.bare;
|
|
174
179
|
let store;
|
|
175
180
|
let sessionId;
|
|
176
181
|
let initialTranscript;
|
|
@@ -256,17 +261,18 @@ export async function runOnce(opts) {
|
|
|
256
261
|
...(opts.verifyMode ? { mode: opts.verifyMode } : {}),
|
|
257
262
|
};
|
|
258
263
|
let result;
|
|
264
|
+
let endStatus = 'error';
|
|
259
265
|
// P1.4 — if --agent is requested, stand up a parent orchestrator so the
|
|
260
266
|
// model can call spawn_agent / task_list / task_get. The root run keeps
|
|
261
267
|
// depth 0; children are capped at maxDepth (default 1 per r-6-10.fix.md).
|
|
262
268
|
let agentBridge;
|
|
263
269
|
let parentContext;
|
|
264
270
|
if (opts.agent) {
|
|
265
|
-
const { AgentOrchestrator,
|
|
266
|
-
const def =
|
|
271
|
+
const { AgentOrchestrator, findAgent } = await import('../agent/orchestrator.js');
|
|
272
|
+
const def = findAgent(opts.agent, opts.cwd); // validated above
|
|
267
273
|
const maxDepth = opts.maxDepth ?? 1;
|
|
268
274
|
const rootDeps = { adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt };
|
|
269
|
-
const orchestrator = new AgentOrchestrator({ sessionId: sessionId ?? 'ephemeral', deps: rootDeps });
|
|
275
|
+
const orchestrator = new AgentOrchestrator({ sessionId: sessionId ?? 'ephemeral', deps: rootDeps, cwd: opts.cwd });
|
|
270
276
|
const allowedTools = new Set(registry.list().map((t) => t.name));
|
|
271
277
|
parentContext = {
|
|
272
278
|
sessionId: sessionId ?? 'ephemeral',
|
|
@@ -304,6 +310,7 @@ export async function runOnce(opts) {
|
|
|
304
310
|
task: opts.task,
|
|
305
311
|
cwd: opts.cwd,
|
|
306
312
|
model: opts.model,
|
|
313
|
+
...(opts.bare ? { bare: true } : {}),
|
|
307
314
|
maxSteps: opts.maxSteps,
|
|
308
315
|
maxTokens: opts.maxTokens,
|
|
309
316
|
temperature: opts.temperature,
|
|
@@ -371,6 +378,7 @@ export async function runOnce(opts) {
|
|
|
371
378
|
adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt,
|
|
372
379
|
...(failoverAdapters ? { failoverAdapters } : {}),
|
|
373
380
|
});
|
|
381
|
+
endStatus = result.status;
|
|
374
382
|
}
|
|
375
383
|
finally {
|
|
376
384
|
doneSigint();
|
|
@@ -378,6 +386,19 @@ export async function runOnce(opts) {
|
|
|
378
386
|
await closeMcp?.();
|
|
379
387
|
}
|
|
380
388
|
catch { /* ignore */ }
|
|
389
|
+
// sessionEnd hooks: best-effort end-of-run side effects (logging,
|
|
390
|
+
// notifications, cleanup). Skipped in --bare. Never affects exit code.
|
|
391
|
+
if (!opts.bare) {
|
|
392
|
+
try {
|
|
393
|
+
const { runSessionEndHooks } = await import('./hooks.js');
|
|
394
|
+
const outs = await runSessionEndHooks(opts.cwd, sessionId, endStatus);
|
|
395
|
+
for (const o of outs) {
|
|
396
|
+
if (output !== 'silent' && o.output)
|
|
397
|
+
stderr.write(`[hook ${o.name}] ${o.output.slice(0, 300)}\n`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch { /* ignore */ }
|
|
401
|
+
}
|
|
381
402
|
}
|
|
382
403
|
if (store && sessionId) {
|
|
383
404
|
// Finalize session status
|
|
@@ -403,41 +424,73 @@ export async function runOnce(opts) {
|
|
|
403
424
|
}
|
|
404
425
|
if (output === 'human')
|
|
405
426
|
stdout.write('\n');
|
|
427
|
+
// Final cost line: headless runs previously surfaced cost only through
|
|
428
|
+
// [budget] warnings. Human/silent → stderr one-liner; json → additive
|
|
429
|
+
// cost_usd/usage fields on the final object (purely additive, no shape break).
|
|
430
|
+
const finalCost = estimateCost(opts.model, result.usage.input, result.usage.output);
|
|
431
|
+
const costLine = `$${finalCost.toFixed(4)} · ${result.usage.input} in / ${result.usage.output} out${result.usage.estimated ? ' (estimated)' : ''}`;
|
|
432
|
+
if (output === 'json') {
|
|
433
|
+
stdout.write(JSON.stringify({ kind: 'cost', cost_usd: finalCost, usage: result.usage }) + '\n');
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
stderr.write(`klyro: cost ${costLine}\n`);
|
|
437
|
+
}
|
|
438
|
+
// Stable result envelope (machine contract): exactly one `kind:result`
|
|
439
|
+
// line per run in json mode, after all legacy per-status lines. Parsers
|
|
440
|
+
// should read the LAST line; legacy `kind:final` lines are kept for compat.
|
|
441
|
+
const emitEnvelope = (exitCode, extra = {}) => {
|
|
442
|
+
if (output === 'json') {
|
|
443
|
+
stdout.write(JSON.stringify({
|
|
444
|
+
kind: 'result',
|
|
445
|
+
status: result.status,
|
|
446
|
+
exit_code: exitCode,
|
|
447
|
+
text: result.finalText,
|
|
448
|
+
steps: result.steps,
|
|
449
|
+
toolCalls: result.toolCalls,
|
|
450
|
+
cost_usd: finalCost,
|
|
451
|
+
usage: result.usage,
|
|
452
|
+
...(result.verification ? { verification: result.verification } : {}),
|
|
453
|
+
...(sessionId ? { session_id: sessionId } : {}),
|
|
454
|
+
...extra,
|
|
455
|
+
}) + '\n');
|
|
456
|
+
}
|
|
457
|
+
return exitCode;
|
|
458
|
+
};
|
|
406
459
|
if (result.status === 'max_steps') {
|
|
407
460
|
if (output === 'json')
|
|
408
461
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
409
462
|
else
|
|
410
463
|
stderr.write(`klyro: hit max steps (${result.steps}); consider raising --max-steps\n`);
|
|
411
|
-
return 7;
|
|
464
|
+
return emitEnvelope(7);
|
|
412
465
|
}
|
|
413
466
|
if (result.status === 'limit') {
|
|
414
467
|
if (output === 'json')
|
|
415
468
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps, text: result.finalText }) + '\n');
|
|
416
469
|
else
|
|
417
470
|
stderr.write(`klyro: stopped early: ${result.finalText || result.status} (after ${result.steps} steps)\n`);
|
|
418
|
-
return 7;
|
|
471
|
+
return emitEnvelope(7);
|
|
419
472
|
}
|
|
420
473
|
if (result.status === 'stuck') {
|
|
421
474
|
if (output === 'json')
|
|
422
475
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
423
476
|
else
|
|
424
477
|
stderr.write(`klyro: stuck — repeated the same action with no progress; aborting after ${result.steps} steps\n`);
|
|
425
|
-
return 7;
|
|
478
|
+
return emitEnvelope(7);
|
|
426
479
|
}
|
|
427
480
|
if (result.status === 'aborted') {
|
|
428
|
-
return 130;
|
|
481
|
+
return emitEnvelope(130);
|
|
429
482
|
}
|
|
430
483
|
if (result.status === 'no_final') {
|
|
431
484
|
if (output !== 'json')
|
|
432
485
|
stderr.write('klyro: provider error — no final answer\n');
|
|
433
|
-
return 5;
|
|
486
|
+
return emitEnvelope(5);
|
|
434
487
|
}
|
|
435
488
|
if (result.status === 'verify_failed') {
|
|
436
489
|
if (output === 'json')
|
|
437
490
|
stdout.write(JSON.stringify({ kind: 'final', status: 'verify_failed', failureType: result.verification?.failureType }) + '\n');
|
|
438
491
|
else
|
|
439
492
|
stderr.write(`klyro: verification failed after ${result.verification?.attempts ?? 3} repairs — see output above\n`);
|
|
440
|
-
return 8;
|
|
493
|
+
return emitEnvelope(8);
|
|
441
494
|
}
|
|
442
495
|
// 6.5 — --require-verify: if edits were made but verification never passed, exit 8
|
|
443
496
|
if (opts.requireVerify && result.verification && !result.verification.ok) {
|
|
@@ -445,7 +498,7 @@ export async function runOnce(opts) {
|
|
|
445
498
|
stdout.write(JSON.stringify({ kind: 'final', status: 'require_verify_failed' }) + '\n');
|
|
446
499
|
else
|
|
447
500
|
stderr.write('klyro: --require-verify: verification required but not passed\n');
|
|
448
|
-
return 8;
|
|
501
|
+
return emitEnvelope(8);
|
|
449
502
|
}
|
|
450
503
|
if (opts.requireVerify && !result.verification && result.hasEdits) {
|
|
451
504
|
// Edits were made but no verification command found
|
|
@@ -453,11 +506,11 @@ export async function runOnce(opts) {
|
|
|
453
506
|
stdout.write(JSON.stringify({ kind: 'final', status: 'require_verify_missing' }) + '\n');
|
|
454
507
|
else
|
|
455
508
|
stderr.write('klyro: --require-verify: no verification command found and edits were made\n');
|
|
456
|
-
return 8;
|
|
509
|
+
return emitEnvelope(8);
|
|
457
510
|
}
|
|
458
511
|
if (output === 'json')
|
|
459
512
|
stdout.write(JSON.stringify({ kind: 'final', status: 'ok', text: result.finalText }) + '\n');
|
|
460
|
-
return 0;
|
|
513
|
+
return emitEnvelope(0);
|
|
461
514
|
}
|
|
462
515
|
async function dryRunReport(opts) {
|
|
463
516
|
// Assemble the REAL prompt (Level-6 context + KLYRO.md), not the bare base —
|
|
@@ -495,7 +548,10 @@ function defaultRunSystemPrompt(_ctx) {
|
|
|
495
548
|
return _ctx.telemetry ? { system: base, suffix: _ctx.telemetry } : { system: base };
|
|
496
549
|
}
|
|
497
550
|
/** Wrap a system-prompt fn to inject Level-6 context (project map etc.) + KLYRO.md (4.4). */
|
|
498
|
-
export async function makeRunSystemPrompt(cwd, base) {
|
|
551
|
+
export async function makeRunSystemPrompt(cwd, base, bare = false) {
|
|
552
|
+
// --bare: base prompt only — no L6 scan, memory, or KLYRO.md (fast + deterministic).
|
|
553
|
+
if (bare)
|
|
554
|
+
return base;
|
|
499
555
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
500
556
|
const prefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
501
557
|
// Session memory: .klyro/memory/session-notes.md injected so memory_write
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface FrontmatterResult {
|
|
2
|
+
data: Record<string, string>;
|
|
3
|
+
body: string;
|
|
4
|
+
}
|
|
5
|
+
/** Minimal frontmatter parser: `---` fences + `key: value` lines only. */
|
|
6
|
+
export declare function parseFrontmatter(text: string): FrontmatterResult;
|
|
7
|
+
/** Parse a scalar list: `[a, b]`, `a, b`, or newline/`- ` items. */
|
|
8
|
+
export declare function parseList(value: string | undefined): string[];
|
|
9
|
+
export declare function parseBool(value: string | undefined, fallback: boolean): boolean;
|
|
10
|
+
export declare function parseInt_(value: string | undefined): number | undefined;
|
|
11
|
+
export interface CustomCommand {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
body: string;
|
|
15
|
+
source: 'project' | 'global';
|
|
16
|
+
}
|
|
17
|
+
/** Expand `$1..$9` and `$@` (all args joined by space). Missing args → ''. */
|
|
18
|
+
export declare function expandArgs(body: string, args: string[]): string;
|
|
19
|
+
/** Load custom commands: global first, project wins on name clash. Never throws. */
|
|
20
|
+
export declare function loadCustomCommands(cwd: string): CustomCommand[];
|
|
21
|
+
/**
|
|
22
|
+
* Relative file paths for `@`-completion: recursive walk, ignored dirs
|
|
23
|
+
* skipped, capped (directories sort first via trailing `/`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function listCompletableFiles(cwd: string): string[];
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based extensibility: `.klyro/commands/*.md` (project) +
|
|
3
|
+
* `~/.klyro/commands/*.md` (global). Project wins on name clash.
|
|
4
|
+
*
|
|
5
|
+
* Format: YAML-ish frontmatter (`---` fences) with `name` (default:
|
|
6
|
+
* filename), `description`/`hint`, then a body supporting `$1..$9` and
|
|
7
|
+
* `$@` (all args). A custom command expands to prompt text and runs
|
|
8
|
+
* through the normal prompt path (with recursion depth guard).
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
/** Minimal frontmatter parser: `---` fences + `key: value` lines only. */
|
|
14
|
+
export function parseFrontmatter(text) {
|
|
15
|
+
const data = {};
|
|
16
|
+
if (!text.startsWith('---'))
|
|
17
|
+
return { data, body: text };
|
|
18
|
+
const end = text.indexOf('\n---', 3);
|
|
19
|
+
if (end === -1)
|
|
20
|
+
return { data, body: text };
|
|
21
|
+
const head = text.slice(3, end);
|
|
22
|
+
for (const line of head.split('\n')) {
|
|
23
|
+
const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trimEnd());
|
|
24
|
+
if (m)
|
|
25
|
+
data[m[1].toLowerCase()] = (m[2] ?? '').trim();
|
|
26
|
+
}
|
|
27
|
+
return { data, body: text.slice(end + 4).replace(/^\r?\n/, '') };
|
|
28
|
+
}
|
|
29
|
+
/** Parse a scalar list: `[a, b]`, `a, b`, or newline/`- ` items. */
|
|
30
|
+
export function parseList(value) {
|
|
31
|
+
if (!value)
|
|
32
|
+
return [];
|
|
33
|
+
let v = value.trim();
|
|
34
|
+
if (v.startsWith('[') && v.endsWith(']'))
|
|
35
|
+
v = v.slice(1, -1);
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const chunk of v.split(/[\n,]/)) {
|
|
38
|
+
const t = chunk.trim().replace(/^-+\s*/, '').replace(/^['"]|['"]$/g, '');
|
|
39
|
+
if (t)
|
|
40
|
+
out.push(t);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
export function parseBool(value, fallback) {
|
|
45
|
+
if (value === undefined || value === '')
|
|
46
|
+
return fallback;
|
|
47
|
+
return /^(true|yes|1|on)$/i.test(value.trim());
|
|
48
|
+
}
|
|
49
|
+
export function parseInt_(value) {
|
|
50
|
+
if (!value)
|
|
51
|
+
return undefined;
|
|
52
|
+
const n = Number(value.trim());
|
|
53
|
+
return Number.isInteger(n) && n > 0 ? n : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Expand `$1..$9` and `$@` (all args joined by space). Missing args → ''. */
|
|
56
|
+
export function expandArgs(body, args) {
|
|
57
|
+
const all = args.join(' ');
|
|
58
|
+
return body
|
|
59
|
+
.replace(/\$@/g, () => all)
|
|
60
|
+
.replace(/\$([1-9])/g, (_m, d) => args[Number(d) - 1] ?? '');
|
|
61
|
+
}
|
|
62
|
+
function commandsDir(home) {
|
|
63
|
+
return [path.join(home, '.klyro', 'commands')];
|
|
64
|
+
}
|
|
65
|
+
function readCommandFile(file, source) {
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const { data, body } = parseFrontmatter(raw);
|
|
74
|
+
const fallback = path.basename(file, path.extname(file)).toLowerCase();
|
|
75
|
+
const name = (data['name'] || fallback).toLowerCase();
|
|
76
|
+
if (!/^[a-z0-9_-]{1,32}$/.test(name))
|
|
77
|
+
return null;
|
|
78
|
+
if (!body.trim())
|
|
79
|
+
return null;
|
|
80
|
+
return { name, description: data['description'] || data['hint'] || '', body: body.trim(), source };
|
|
81
|
+
}
|
|
82
|
+
function listCommandFiles(dir) {
|
|
83
|
+
let entries;
|
|
84
|
+
try {
|
|
85
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return entries
|
|
91
|
+
.filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.md'))
|
|
92
|
+
.map((e) => path.join(dir, e.name))
|
|
93
|
+
.sort();
|
|
94
|
+
}
|
|
95
|
+
/** Load custom commands: global first, project wins on name clash. Never throws. */
|
|
96
|
+
export function loadCustomCommands(cwd) {
|
|
97
|
+
const byName = new Map();
|
|
98
|
+
try {
|
|
99
|
+
const home = os.homedir() || process.cwd();
|
|
100
|
+
for (const dir of commandsDir(home)) {
|
|
101
|
+
for (const f of listCommandFiles(dir)) {
|
|
102
|
+
const c = readCommandFile(f, 'global');
|
|
103
|
+
if (c)
|
|
104
|
+
byName.set(c.name, c);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const projectDir = path.join(cwd, '.klyro', 'commands');
|
|
108
|
+
for (const f of listCommandFiles(projectDir)) {
|
|
109
|
+
const c = readCommandFile(f, 'project');
|
|
110
|
+
if (c)
|
|
111
|
+
byName.set(c.name, c);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return [...byName.values()];
|
|
116
|
+
}
|
|
117
|
+
return [...byName.values()];
|
|
118
|
+
}
|
|
119
|
+
const COMPLETABLE_IGNORED = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.klyro', 'out', '.next', 'target', 'vendor']);
|
|
120
|
+
const MAX_COMPLETABLE_FILES = 1000;
|
|
121
|
+
/**
|
|
122
|
+
* Relative file paths for `@`-completion: recursive walk, ignored dirs
|
|
123
|
+
* skipped, capped (directories sort first via trailing `/`).
|
|
124
|
+
*/
|
|
125
|
+
export function listCompletableFiles(cwd) {
|
|
126
|
+
const out = [];
|
|
127
|
+
const walk = (dir, rel) => {
|
|
128
|
+
if (out.length >= MAX_COMPLETABLE_FILES)
|
|
129
|
+
return;
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const sorted = entries.sort((a, b) => (a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1));
|
|
138
|
+
for (const e of sorted) {
|
|
139
|
+
if (out.length >= MAX_COMPLETABLE_FILES)
|
|
140
|
+
return;
|
|
141
|
+
if (rel === '' && COMPLETABLE_IGNORED.has(e.name))
|
|
142
|
+
continue;
|
|
143
|
+
if (e.name.startsWith('.') && rel === '') {
|
|
144
|
+
// Top-level dotfiles are completable (e.g. .env.example) but never traversed.
|
|
145
|
+
if (e.isFile())
|
|
146
|
+
out.push(e.name);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const r = rel === '' ? e.name : `${rel}/${e.name}`;
|
|
150
|
+
if (e.isDirectory()) {
|
|
151
|
+
out.push(`${r}/`);
|
|
152
|
+
walk(path.join(dir, e.name), r);
|
|
153
|
+
}
|
|
154
|
+
else if (e.isFile()) {
|
|
155
|
+
out.push(r);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
walk(cwd, '');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
@@ -39,8 +39,13 @@ export type SlashCommand = {
|
|
|
39
39
|
kind: 'diff';
|
|
40
40
|
} | {
|
|
41
41
|
kind: 'undo';
|
|
42
|
+
n?: number;
|
|
42
43
|
} | {
|
|
43
44
|
kind: 'rewind';
|
|
45
|
+
n?: number;
|
|
46
|
+
summary?: boolean;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'checkpoints';
|
|
44
49
|
} | {
|
|
45
50
|
kind: 'plan';
|
|
46
51
|
task?: string;
|
|
@@ -68,6 +73,9 @@ export type SlashCommand = {
|
|
|
68
73
|
kind: 'thinking';
|
|
69
74
|
} | {
|
|
70
75
|
kind: 'memory';
|
|
76
|
+
} | {
|
|
77
|
+
kind: 'memory-append';
|
|
78
|
+
text: string;
|
|
71
79
|
} | {
|
|
72
80
|
kind: 'jobs';
|
|
73
81
|
} | {
|
|
@@ -316,5 +324,11 @@ export interface CommandDef {
|
|
|
316
324
|
}
|
|
317
325
|
/** Curated canonical commands with one-line hints — drives /help, /commands and TUI autocomplete. */
|
|
318
326
|
export declare const COMMAND_DEFS: CommandDef[];
|
|
319
|
-
/**
|
|
320
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Fuzzy score for TUI autocomplete: subsequence match with bonuses for
|
|
329
|
+
* prefix (+100), word-boundary (+30), and consecutive (+15) matches, minus
|
|
330
|
+
* a gap penalty. Returns -Infinity when `query` is not a subsequence.
|
|
331
|
+
*/
|
|
332
|
+
export declare function fuzzyScore(name: string, query: string): number;
|
|
333
|
+
/** Fuzzy-match command names for TUI autocomplete — top `limit` (default 6). */
|
|
334
|
+
export declare function suggestCommands(prefix: string, limit?: number, extra?: CommandDef[]): CommandDef[];
|
package/dist/cli/slash/parser.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Anything not starting with "/" is a regular prompt and yields
|
|
15
15
|
* { kind: 'prompt', text }.
|
|
16
16
|
*/
|
|
17
|
-
const KNOWN = ['help', 'clear', 'new', 'exit', 'quit', 'q', 'compact', 'resume', 'sessions', 'rename', 'fork', 'branch', 'export', 'copy', 'model', 'm', 'models', 'provider', 'p', 'effort', 'e', 'fast', 'init', 'status', 'context', 'diff', 'plan', 'todos', 'memory', 'permissions', 'mode', 'sandbox', 'approve', 'deny', 'login', 'logout', 'auth', 'version', 'update', 'cancel', 'shell', 'mention', 'tools', 'config', 'settings', 'doctor', 'cost', 'thinking', 'jobs', 'verify', 'project', 'undo', 'rewind', 'review', 'code-review', 'security-review', 'simplify', 'test', 'lint', 'build', 'run', 'fix', 'explain', 'format', 'ask', 'redo', 'checkpoint', 'accept', 'reject', 'details', 'verbose', 'raw', 'activity', 'tasks', 'ps', 'stop', 'queue', 'retry', 'kill', 'mcp', 'agents', 'agent', 'subagents', 'subtask', 'background', 'add-dir', 'cd', 'attach', 'drop', 'image', 'paste', 'files', 'ls', 'tree', 'search', 'web', 'read', 'map', 'tokens', 'commit', 'push', 'pull', 'pr', 'issue', 'editor', 'keymap', 'vim', 'theme', 'statusline', 'output-style', 'debug', 'whoami', 'reload', 'reset', 'bug', 'changelog', 'prompt', 'alias', 'commands', 'env', 'deps', 'install'];
|
|
17
|
+
const KNOWN = ['help', 'clear', 'new', 'exit', 'quit', 'q', 'compact', 'resume', 'sessions', 'rename', 'fork', 'branch', 'export', 'copy', 'model', 'm', 'models', 'provider', 'p', 'effort', 'e', 'fast', 'init', 'status', 'context', 'diff', 'plan', 'todos', 'memory', 'permissions', 'mode', 'sandbox', 'approve', 'deny', 'login', 'logout', 'auth', 'version', 'update', 'cancel', 'shell', 'mention', 'tools', 'config', 'settings', 'doctor', 'cost', 'thinking', 'jobs', 'verify', 'project', 'undo', 'rewind', 'checkpoints', 'review', 'code-review', 'security-review', 'simplify', 'test', 'lint', 'build', 'run', 'fix', 'explain', 'format', 'ask', 'redo', 'checkpoint', 'accept', 'reject', 'details', 'verbose', 'raw', 'activity', 'tasks', 'ps', 'stop', 'queue', 'retry', 'kill', 'mcp', 'agents', 'agent', 'subagents', 'subtask', 'background', 'add-dir', 'cd', 'attach', 'drop', 'image', 'paste', 'files', 'ls', 'tree', 'search', 'web', 'read', 'map', 'tokens', 'commit', 'push', 'pull', 'pr', 'issue', 'editor', 'keymap', 'vim', 'theme', 'statusline', 'output-style', 'debug', 'whoami', 'reload', 'reset', 'bug', 'changelog', 'prompt', 'alias', 'commands', 'env', 'deps', 'install'];
|
|
18
18
|
export function parse(input) {
|
|
19
19
|
const trimmed = input.trim();
|
|
20
20
|
// `!cmd` alias for /shell, `@path` alias for /mention (commands.md P1)
|
|
@@ -35,14 +35,35 @@ export function parse(input) {
|
|
|
35
35
|
case 'new': return { kind: 'new' };
|
|
36
36
|
case 'compact': return { kind: 'compact', focus: rest || undefined };
|
|
37
37
|
case 'diff': return { kind: 'diff' };
|
|
38
|
-
case 'undo':
|
|
39
|
-
|
|
38
|
+
case 'undo': {
|
|
39
|
+
const un = Number(rest.split(/\s+/).filter(Boolean)[0] ?? '1');
|
|
40
|
+
return { kind: 'undo', n: Number.isInteger(un) && un > 0 ? un : 1 };
|
|
41
|
+
}
|
|
42
|
+
case 'rewind': {
|
|
43
|
+
// /rewind [n] [summary] — nth-back snapshot (1 = latest), optional
|
|
44
|
+
// post-restore summary of the reverted changes.
|
|
45
|
+
const parts = rest.split(/\s+/).filter(Boolean);
|
|
46
|
+
const n = parts.length > 0 ? Number(parts[0]) : 1;
|
|
47
|
+
return {
|
|
48
|
+
kind: 'rewind',
|
|
49
|
+
n: Number.isInteger(n) && n > 0 ? n : 1,
|
|
50
|
+
...(parts.slice(1).join(' ').toLowerCase() === 'summary' ? { summary: true } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
case 'checkpoints': return { kind: 'checkpoints' };
|
|
40
54
|
case 'plan': return { kind: 'plan', task: rest || undefined };
|
|
41
55
|
case 'todos': return { kind: 'todos' };
|
|
42
56
|
case 'status': return { kind: 'status' };
|
|
43
57
|
case 'cost': return { kind: 'cost' };
|
|
44
58
|
case 'thinking': return { kind: 'thinking' };
|
|
45
|
-
case 'memory':
|
|
59
|
+
case 'memory': {
|
|
60
|
+
// /memory shows notes; /memory append <text> writes one (human path,
|
|
61
|
+
// same redaction + atomicity as the memory_write tool).
|
|
62
|
+
const mm = /^append\s+([\s\S]+)$/.exec(rest);
|
|
63
|
+
if (mm)
|
|
64
|
+
return { kind: 'memory-append', text: mm[1].trim() };
|
|
65
|
+
return { kind: 'memory' };
|
|
66
|
+
}
|
|
46
67
|
case 'jobs': return { kind: 'jobs' };
|
|
47
68
|
case 'verify': return { kind: 'verify' };
|
|
48
69
|
case 'project': return { kind: 'project' };
|
|
@@ -233,9 +254,9 @@ export const COMMAND_DEFS = [
|
|
|
233
254
|
{ name: 'explain', hint: 'explain code' },
|
|
234
255
|
{ name: 'format', hint: 'format code' },
|
|
235
256
|
{ name: 'ask', hint: 'read-only Q&A' },
|
|
236
|
-
{ name: 'undo', hint: 'undo change' },
|
|
237
|
-
{ name: 'redo', hint: 'redo change' },
|
|
257
|
+
{ name: 'undo', hint: 'undo change' }, { name: 'redo', hint: 'redo change' },
|
|
238
258
|
{ name: 'rewind', hint: 'rewind code' },
|
|
259
|
+
{ name: 'checkpoints', hint: 'list snapshots' },
|
|
239
260
|
{ name: 'checkpoint', hint: 'create checkpoint' },
|
|
240
261
|
{ name: 'accept', hint: 'accept edits' },
|
|
241
262
|
{ name: 'reject', hint: 'reject edits' },
|
|
@@ -293,18 +314,46 @@ export const COMMAND_DEFS = [
|
|
|
293
314
|
{ name: 'deps', hint: 'dependencies' },
|
|
294
315
|
{ name: 'install', hint: 'install deps' },
|
|
295
316
|
];
|
|
296
|
-
/**
|
|
297
|
-
|
|
317
|
+
/**
|
|
318
|
+
* Fuzzy score for TUI autocomplete: subsequence match with bonuses for
|
|
319
|
+
* prefix (+100), word-boundary (+30), and consecutive (+15) matches, minus
|
|
320
|
+
* a gap penalty. Returns -Infinity when `query` is not a subsequence.
|
|
321
|
+
*/
|
|
322
|
+
export function fuzzyScore(name, query) {
|
|
323
|
+
const n = name.toLowerCase();
|
|
324
|
+
const q = query.toLowerCase();
|
|
325
|
+
if (!q)
|
|
326
|
+
return 0;
|
|
327
|
+
let score = 0;
|
|
328
|
+
let ni = 0;
|
|
329
|
+
let lastHit = -2;
|
|
330
|
+
for (let qi = 0; qi < q.length; qi++) {
|
|
331
|
+
const found = n.indexOf(q[qi], ni);
|
|
332
|
+
if (found === -1)
|
|
333
|
+
return -Infinity;
|
|
334
|
+
if (qi === 0 && found === 0)
|
|
335
|
+
score += 100; // prefix
|
|
336
|
+
if (found > 0 && (n[found - 1] === '-' || n[found - 1] === '_'))
|
|
337
|
+
score += 30; // word boundary
|
|
338
|
+
if (found === lastHit + 1)
|
|
339
|
+
score += 15; // consecutive
|
|
340
|
+
else
|
|
341
|
+
score -= (found - ni); // gap penalty
|
|
342
|
+
lastHit = found;
|
|
343
|
+
ni = found + 1;
|
|
344
|
+
}
|
|
345
|
+
score -= (n.length - q.length); // prefer shorter names
|
|
346
|
+
return score;
|
|
347
|
+
}
|
|
348
|
+
/** Fuzzy-match command names for TUI autocomplete — top `limit` (default 6). */
|
|
349
|
+
export function suggestCommands(prefix, limit = 6, extra = []) {
|
|
298
350
|
const p = prefix.toLowerCase().replace(/^\//, '');
|
|
351
|
+
const pool = extra.length > 0 ? [...extra, ...COMMAND_DEFS] : COMMAND_DEFS;
|
|
299
352
|
if (!p)
|
|
300
|
-
return
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
else if (d.name.includes(p))
|
|
307
|
-
contains.push(d);
|
|
308
|
-
}
|
|
309
|
-
return [...starts, ...contains].slice(0, limit);
|
|
353
|
+
return pool.slice(0, limit);
|
|
354
|
+
return pool.map((d) => ({ d, s: fuzzyScore(d.name, p) + fuzzyScore(d.hint, p) * 0.25 }))
|
|
355
|
+
.filter((x) => x.s > -Infinity)
|
|
356
|
+
.sort((a, b) => b.s - a.s)
|
|
357
|
+
.slice(0, limit)
|
|
358
|
+
.map((x) => x.d);
|
|
310
359
|
}
|
package/dist/cli/update.d.ts
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* klyro update — check registry for newer version, cached 24h.
|
|
3
3
|
* Env KLYRO_NO_UPDATE_CHECK=1 disables.
|
|
4
4
|
*
|
|
5
|
-
* Integrity: before recommending `npm i`, we verify the tarball
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Integrity: before recommending `npm i`, we verify the tarball against
|
|
6
|
+
* BOTH the registry's SRI digest (sha512/sha256) AND the legacy sha1
|
|
7
|
+
* `dist.shasum` when present — a tampered CDN or MITM registry response
|
|
8
|
+
* must forge two independent digests to push a malicious binary.
|
|
9
|
+
* Downgrade protection: a registry `latest` that is not strictly newer
|
|
10
|
+
* than the running version (semver) is never recommended.
|
|
9
11
|
*/
|
|
12
|
+
/** Minimal semver compare for `x.y.z[-prerelease]`; null when unparseable. */
|
|
13
|
+
export declare function compareSemver(a: string, b: string): number | null;
|
|
10
14
|
export declare function checkForUpdate(current: string): Promise<string | null>;
|
|
11
15
|
export declare function runUpdate(): Promise<number>;
|