klyro 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/anthropic-adapter.d.ts +36 -0
- package/dist/agent/anthropic-adapter.js +68 -16
- package/dist/agent/capabilities.d.ts +23 -0
- package/dist/agent/capabilities.js +46 -5
- package/dist/agent/child-worker.d.ts +104 -0
- package/dist/agent/child-worker.js +250 -0
- package/dist/agent/orchestrator.d.ts +106 -5
- package/dist/agent/orchestrator.js +378 -58
- package/dist/agent/provider-adapter.d.ts +8 -0
- package/dist/agent/provider-adapter.js +12 -3
- package/dist/agent/retry.d.ts +1 -1
- package/dist/agent/retry.js +2 -2
- package/dist/agent/runtime.d.ts +50 -8
- package/dist/agent/runtime.js +196 -33
- package/dist/agent/worktree-manager.d.ts +74 -0
- package/dist/agent/worktree-manager.js +189 -0
- package/dist/checkpoints/store.js +30 -5
- package/dist/cli/auth.js +16 -1
- package/dist/cli/config.d.ts +9 -3
- package/dist/cli/config.js +64 -3
- package/dist/cli/eval.d.ts +6 -1
- package/dist/cli/eval.js +9 -0
- package/dist/cli/repl.js +155 -28
- package/dist/cli/run.d.ts +7 -11
- package/dist/cli/run.js +68 -17
- package/dist/cli/update.d.ts +5 -0
- package/dist/cli/update.js +62 -10
- package/dist/context/import-graph.d.ts +2 -0
- package/dist/context/import-graph.js +31 -3
- package/dist/context/klyro-md.js +4 -1
- package/dist/context/memory.d.ts +8 -0
- package/dist/context/memory.js +50 -2
- package/dist/context/project-map.d.ts +6 -0
- package/dist/context/project-map.js +50 -2
- package/dist/context/repo-map.d.ts +2 -0
- package/dist/context/repo-map.js +31 -1
- package/dist/events/catalog.d.ts +28 -0
- package/dist/index.js +89 -4
- package/dist/mcp/client.d.ts +6 -4
- package/dist/mcp/client.js +83 -14
- package/dist/mcp/config.d.ts +10 -0
- package/dist/mcp/config.js +18 -1
- package/dist/mcp/registry.d.ts +23 -1
- package/dist/mcp/registry.js +78 -6
- package/dist/mcp/schema.d.ts +11 -4
- package/dist/mcp/schema.js +27 -16
- package/dist/mcp/trust.d.ts +20 -0
- package/dist/mcp/trust.js +74 -0
- package/dist/persistence/audit.d.ts +28 -0
- package/dist/persistence/audit.js +101 -1
- package/dist/persistence/store.d.ts +26 -2
- package/dist/persistence/store.js +140 -13
- package/dist/policy/approval.d.ts +14 -0
- package/dist/policy/approval.js +44 -2
- package/dist/policy/engine.d.ts +1 -0
- package/dist/policy/engine.js +88 -8
- package/dist/policy/secret-redactor.js +4 -0
- package/dist/providers/model-info.d.ts +17 -0
- package/dist/providers/model-info.js +35 -2
- package/dist/repl.d.ts +6 -0
- package/dist/repl.js +12 -7
- package/dist/tools/agent/spawn-agent.js +5 -5
- package/dist/tools/agent/task-apply.d.ts +4 -0
- package/dist/tools/agent/task-apply.js +44 -0
- package/dist/tools/agent/task-stop.d.ts +6 -0
- package/dist/tools/agent/task-stop.js +39 -0
- package/dist/tools/agent/task-wait.d.ts +17 -0
- package/dist/tools/agent/task-wait.js +79 -0
- package/dist/tools/fs/apply-patch.js +71 -0
- package/dist/tools/fs/edit-file.js +65 -0
- package/dist/tools/fs/multi-edit.d.ts +4 -0
- package/dist/tools/fs/multi-edit.js +66 -0
- package/dist/tools/fs/write-file.js +67 -0
- package/dist/tools/registry.js +6 -0
- package/dist/tools/shell/background.js +6 -3
- package/dist/tools/shell/sandbox.d.ts +51 -0
- package/dist/tools/shell/sandbox.js +143 -0
- package/dist/tools/shell/shell-exec.d.ts +1 -0
- package/dist/tools/shell/shell-exec.js +83 -11
- package/dist/tools/shell/worker-entry.d.ts +12 -0
- package/dist/tools/shell/worker-entry.js +43 -0
- package/dist/tools/types.d.ts +6 -0
- package/dist/tools/verify/run-verify.js +3 -1
- package/dist/trace/writer.d.ts +13 -0
- package/dist/trace/writer.js +55 -4
- package/dist/tui/app.js +1 -1
- package/dist/tui/approval.js +20 -21
- package/dist/util.d.ts +1 -0
- package/dist/util.js +1 -0
- package/dist/verification/baseline.js +17 -3
- package/dist/verification/classify.js +23 -12
- package/dist/verification/engine.js +3 -1
- package/dist/verification/registry.d.ts +2 -0
- package/dist/verification/registry.js +33 -0
- package/dist/verification/scoped.js +28 -6
- package/package.json +1 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 7.3 — Import graph (cached) — powers L6 scoped tests + imports_of / importers_of
|
|
3
3
|
* Parses TS/JS/Py/Go imports via regex, builds adjacency, caches by mtime.
|
|
4
|
+
*
|
|
5
|
+
* Freshness: the cached graph stores per-file {mtimeMs, size} alongside it.
|
|
6
|
+
* On a cache hit every file in the graph is re-statted (stat-only, bounded
|
|
7
|
+
* to files already in the graph) — any mtime/size mismatch rebuilds.
|
|
4
8
|
*/
|
|
5
9
|
import * as fs from 'node:fs/promises';
|
|
6
10
|
import * as path from 'node:path';
|
|
@@ -19,10 +23,32 @@ async function parseImports(file, content) {
|
|
|
19
23
|
}
|
|
20
24
|
return out;
|
|
21
25
|
}
|
|
26
|
+
/** Re-stat every file in the cached graph; true when all stats still match. */
|
|
27
|
+
async function statsStillMatch(cwd, stats) {
|
|
28
|
+
for (const [rel, prev] of stats) {
|
|
29
|
+
try {
|
|
30
|
+
const s = await fs.stat(path.join(cwd, rel));
|
|
31
|
+
if (s.mtimeMs !== prev.mtimeMs || s.size !== prev.size)
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false; // deleted (or unreadable) — rebuild
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
/** Invalidate the import-graph cache (tests + callers that mutate the tree). */
|
|
41
|
+
export function clearImportGraphCache() {
|
|
42
|
+
cache = null;
|
|
43
|
+
}
|
|
22
44
|
export async function buildImportGraph(cwd) {
|
|
23
|
-
if (cache && cache.cwd === cwd && Date.now() - cache.graph.mtime < 60_000)
|
|
24
|
-
|
|
45
|
+
if (cache && cache.cwd === cwd && Date.now() - cache.graph.mtime < 60_000) {
|
|
46
|
+
if (await statsStillMatch(cwd, cache.stats))
|
|
47
|
+
return cache.graph;
|
|
48
|
+
cache = null;
|
|
49
|
+
}
|
|
25
50
|
const graph = { nodes: new Set(), edges: new Map(), mtime: Date.now() };
|
|
51
|
+
const stats = new Map();
|
|
26
52
|
async function walk(dir, depth = 0) {
|
|
27
53
|
if (depth > 6)
|
|
28
54
|
return;
|
|
@@ -43,6 +69,8 @@ export async function buildImportGraph(cwd) {
|
|
|
43
69
|
const rel = path.relative(cwd, full).replace(/\\/g, '/');
|
|
44
70
|
graph.nodes.add(rel);
|
|
45
71
|
try {
|
|
72
|
+
const st = await fs.stat(full);
|
|
73
|
+
stats.set(rel, { mtimeMs: st.mtimeMs, size: st.size });
|
|
46
74
|
const txt = await fs.readFile(full, 'utf-8');
|
|
47
75
|
const imps = await parseImports(rel, txt);
|
|
48
76
|
for (const imp of imps) {
|
|
@@ -57,7 +85,7 @@ export async function buildImportGraph(cwd) {
|
|
|
57
85
|
}
|
|
58
86
|
}
|
|
59
87
|
await walk(cwd);
|
|
60
|
-
cache = { cwd, graph };
|
|
88
|
+
cache = { cwd, graph, stats };
|
|
61
89
|
return graph;
|
|
62
90
|
}
|
|
63
91
|
export async function importsOf(cwd, file) {
|
package/dist/context/klyro-md.js
CHANGED
|
@@ -27,7 +27,10 @@ export async function loadKlyroMdFiles(cwd) {
|
|
|
27
27
|
catch { /* ignore */ }
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
// Klyro's own instruction file is KLYRO.md. The rest are read-only
|
|
31
|
+
// fallbacks for imported repos that follow other conventions — Klyro
|
|
32
|
+
// never writes them. KLYRO.md wins by load order.
|
|
33
|
+
for (const name of ['KLYRO.md', 'KLYRO.local.md', 'CLAUDE.md', 'CLAUDE.local.md', 'AGENTS.md', '.cursorrules']) {
|
|
31
34
|
const p = path.join(cwd, name);
|
|
32
35
|
try {
|
|
33
36
|
const t = await fs.readFile(p, 'utf-8');
|
package/dist/context/memory.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { PlanStep } from '../agent/runtime.js';
|
|
2
|
+
/** Single-write ceiling — anything larger is rejected, not sliced. */
|
|
3
|
+
export declare const MEMORY_WRITE_LIMIT_CHARS = 8000;
|
|
4
|
+
/** Steady-state cap (~1k tokens ≈ 4k chars) kept via tail slice. */
|
|
5
|
+
export declare const MEMORY_STEADY_STATE_CHARS = 4000;
|
|
2
6
|
export declare function memoryWrite(cwd: string, content: string): Promise<string>;
|
|
3
7
|
export declare function loadMemory(cwd: string): Promise<string>;
|
|
8
|
+
/** Synchronous read for the system-prompt build path (run on every turn). */
|
|
9
|
+
export declare function loadMemorySync(cwd: string): string;
|
|
10
|
+
/** Wrap persisted notes into the injected prompt block; '' when empty. */
|
|
11
|
+
export declare function memoryBlock(cwd: string): string;
|
|
4
12
|
export declare function shouldRemind(turn: number, lastRemindTurn: number): boolean;
|
|
5
13
|
export declare function reminderForTodos(todos: PlanStep[]): string | undefined;
|
package/dist/context/memory.js
CHANGED
|
@@ -1,15 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 8.4 — Working memory & reminders: memory_write → .klyro/memory/session-notes.md (≤1k tokens) + todos re-inject
|
|
3
|
+
*
|
|
4
|
+
* Durability: writes are atomic (tmp + rename in the same dir) so a crash
|
|
5
|
+
* never leaves a half-written note file. Oversized single writes are
|
|
6
|
+
* rejected with an Error (the memory_write tool's safe() wrapper converts
|
|
7
|
+
* that into a ToolResult error) instead of being silently truncated.
|
|
3
8
|
*/
|
|
4
9
|
import * as fs from 'node:fs/promises';
|
|
10
|
+
import { readFileSync } from 'node:fs';
|
|
5
11
|
import * as path from 'node:path';
|
|
12
|
+
import { redact } from '../policy/secret-redactor.js';
|
|
13
|
+
/** Single-write ceiling — anything larger is rejected, not sliced. */
|
|
14
|
+
export const MEMORY_WRITE_LIMIT_CHARS = 8000;
|
|
15
|
+
/** Steady-state cap (~1k tokens ≈ 4k chars) kept via tail slice. */
|
|
16
|
+
export const MEMORY_STEADY_STATE_CHARS = 4000;
|
|
6
17
|
export async function memoryWrite(cwd, content) {
|
|
18
|
+
if (content.length > MEMORY_WRITE_LIMIT_CHARS) {
|
|
19
|
+
throw new Error(`memory budget exceeded: single write is ${content.length} chars (max ${MEMORY_WRITE_LIMIT_CHARS}) — split it into smaller notes`);
|
|
20
|
+
}
|
|
7
21
|
const dir = path.join(cwd, '.klyro', 'memory');
|
|
8
22
|
await fs.mkdir(dir, { recursive: true });
|
|
9
23
|
const p = path.join(dir, 'session-notes.md');
|
|
10
24
|
const prev = await fs.readFile(p, 'utf-8').catch(() => '');
|
|
11
|
-
|
|
12
|
-
|
|
25
|
+
// S4-at-rest: redact before appending — redact() only fires on secret
|
|
26
|
+
// shapes (key/token/password with [:=-]), so normal prose survives.
|
|
27
|
+
const next = (prev + '\n' + redact(content)).slice(-MEMORY_STEADY_STATE_CHARS); // ≤1k tokens ~4k chars
|
|
28
|
+
const tmp = path.join(dir, `.session-notes.md.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
29
|
+
await fs.writeFile(tmp, next, 'utf-8');
|
|
30
|
+
try {
|
|
31
|
+
const fh = await fs.open(tmp, 'r+');
|
|
32
|
+
try {
|
|
33
|
+
await fh.sync();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await fh.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { /* ignore on Windows */ }
|
|
40
|
+
try {
|
|
41
|
+
await fs.rename(tmp, p);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
await fs.unlink(tmp).catch(() => undefined);
|
|
45
|
+
throw new Error('Failed to write memory file');
|
|
46
|
+
}
|
|
13
47
|
return p;
|
|
14
48
|
}
|
|
15
49
|
export async function loadMemory(cwd) {
|
|
@@ -20,6 +54,20 @@ export async function loadMemory(cwd) {
|
|
|
20
54
|
return '';
|
|
21
55
|
}
|
|
22
56
|
}
|
|
57
|
+
/** Synchronous read for the system-prompt build path (run on every turn). */
|
|
58
|
+
export function loadMemorySync(cwd) {
|
|
59
|
+
try {
|
|
60
|
+
return readFileSync(path.join(cwd, '.klyro', 'memory', 'session-notes.md'), 'utf-8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Wrap persisted notes into the injected prompt block; '' when empty. */
|
|
67
|
+
export function memoryBlock(cwd) {
|
|
68
|
+
const notes = loadMemorySync(cwd).trim();
|
|
69
|
+
return notes ? `\n\n<memory>\n${notes.slice(0, 4000)}\n</memory>` : '';
|
|
70
|
+
}
|
|
23
71
|
export function shouldRemind(turn, lastRemindTurn) {
|
|
24
72
|
return turn - lastRemindTurn >= 20;
|
|
25
73
|
}
|
|
@@ -45,6 +45,12 @@ export interface ProjectMap {
|
|
|
45
45
|
hasDocker?: boolean;
|
|
46
46
|
hasEnvExample?: boolean;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Dirty-tree fingerprint: max mtimeMs over a bounded walk (≤200 files,
|
|
50
|
+
* depth ≤4, ignored dirs skipped). Cheap enough to run on every cache
|
|
51
|
+
* lookup; any edit under the tree changes the key and busts the cache.
|
|
52
|
+
*/
|
|
53
|
+
export declare function dirtyMtime(root: string): Promise<number>;
|
|
48
54
|
/** Build a project map for the repo rooted at `root`. */
|
|
49
55
|
export declare function buildProjectMapCached(root: string): Promise<ProjectMap>;
|
|
50
56
|
/** Build a project map for the repo rooted at `root`. */
|
|
@@ -383,7 +383,8 @@ function lockfileHash(root) {
|
|
|
383
383
|
async function getCachedProjectMap(root) {
|
|
384
384
|
const head = await gitHead(root);
|
|
385
385
|
const hash = lockfileHash(root);
|
|
386
|
-
const
|
|
386
|
+
const dirty = await dirtyMtime(root);
|
|
387
|
+
const p = path.join(root, '.klyro', 'cache', `project-map-${head.slice(0, 8)}-${hash}-${dirty.toString(36)}.json`);
|
|
387
388
|
try {
|
|
388
389
|
const raw = await fs.readFile(p, 'utf-8');
|
|
389
390
|
const j = JSON.parse(raw);
|
|
@@ -396,13 +397,60 @@ async function getCachedProjectMap(root) {
|
|
|
396
397
|
async function setCachedProjectMap(root, m) {
|
|
397
398
|
const head = await gitHead(root);
|
|
398
399
|
const hash = lockfileHash(root);
|
|
399
|
-
const
|
|
400
|
+
const dirty = await dirtyMtime(root);
|
|
401
|
+
const p = path.join(root, '.klyro', 'cache', `project-map-${head.slice(0, 8)}-${hash}-${dirty.toString(36)}.json`);
|
|
400
402
|
try {
|
|
401
403
|
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
402
404
|
await fs.writeFile(p, JSON.stringify(m, null, 2), 'utf-8');
|
|
403
405
|
}
|
|
404
406
|
catch { /* ignore */ }
|
|
405
407
|
}
|
|
408
|
+
/**
|
|
409
|
+
* Dirty-tree fingerprint: max mtimeMs over a bounded walk (≤200 files,
|
|
410
|
+
* depth ≤4, ignored dirs skipped). Cheap enough to run on every cache
|
|
411
|
+
* lookup; any edit under the tree changes the key and busts the cache.
|
|
412
|
+
*/
|
|
413
|
+
export async function dirtyMtime(root) {
|
|
414
|
+
let max = 0;
|
|
415
|
+
let seen = 0;
|
|
416
|
+
let stop = false;
|
|
417
|
+
async function walk(dir, depth) {
|
|
418
|
+
if (stop || depth > 4)
|
|
419
|
+
return;
|
|
420
|
+
let entries;
|
|
421
|
+
try {
|
|
422
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
for (const e of entries) {
|
|
428
|
+
if (stop)
|
|
429
|
+
return;
|
|
430
|
+
if (IGNORED_DIRS.has(e.name))
|
|
431
|
+
continue;
|
|
432
|
+
const full = path.join(dir, e.name);
|
|
433
|
+
if (e.isDirectory()) {
|
|
434
|
+
await walk(full, depth + 1);
|
|
435
|
+
}
|
|
436
|
+
else if (e.isFile()) {
|
|
437
|
+
seen++;
|
|
438
|
+
try {
|
|
439
|
+
const s = await fs.stat(full);
|
|
440
|
+
if (s.mtimeMs > max)
|
|
441
|
+
max = s.mtimeMs;
|
|
442
|
+
}
|
|
443
|
+
catch { /* race */ }
|
|
444
|
+
if (seen >= 200) {
|
|
445
|
+
stop = true;
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
await walk(root, 0);
|
|
452
|
+
return Math.floor(max);
|
|
453
|
+
}
|
|
406
454
|
function detectMonorepo(root, rootFiles, rootDirs) {
|
|
407
455
|
if (rootFiles.has('pnpm-workspace.yaml') || rootFiles.has('lerna.json') || rootFiles.has('nx.json'))
|
|
408
456
|
return true;
|
|
@@ -18,6 +18,8 @@ export interface RepoFile {
|
|
|
18
18
|
path: string;
|
|
19
19
|
symbols: Symbol[];
|
|
20
20
|
}
|
|
21
|
+
/** Invalidate the repo-map cache (tests + callers that mutate the tree). */
|
|
22
|
+
export declare function clearRepoMapCache(): void;
|
|
21
23
|
/** Heuristic: walk cwd, return at most `maxFiles` files with symbols. */
|
|
22
24
|
export declare function buildRepoMap(opts: {
|
|
23
25
|
cwd: string;
|
package/dist/context/repo-map.js
CHANGED
|
@@ -12,11 +12,38 @@ import * as fs from 'node:fs/promises';
|
|
|
12
12
|
import * as path from 'node:path';
|
|
13
13
|
const SUPPORTED_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs']);
|
|
14
14
|
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.klyro', 'coverage', 'out']);
|
|
15
|
+
const REPO_MAP_TTL_MS = 60_000;
|
|
16
|
+
let repoMapCache = null;
|
|
17
|
+
/** Invalidate the repo-map cache (tests + callers that mutate the tree). */
|
|
18
|
+
export function clearRepoMapCache() {
|
|
19
|
+
repoMapCache = null;
|
|
20
|
+
}
|
|
21
|
+
/** Re-stat every file in the cached map; true when all stats still match. */
|
|
22
|
+
async function repoMapStatsMatch(cwd, stats) {
|
|
23
|
+
for (const [rel, prev] of stats) {
|
|
24
|
+
try {
|
|
25
|
+
const s = await fs.stat(path.join(cwd, rel));
|
|
26
|
+
if (s.mtimeMs !== prev.mtimeMs || s.size !== prev.size)
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false; // deleted (or unreadable) — rebuild
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
15
35
|
/** Heuristic: walk cwd, return at most `maxFiles` files with symbols. */
|
|
16
36
|
export async function buildRepoMap(opts) {
|
|
17
37
|
const maxFiles = opts.maxFiles ?? 500;
|
|
18
38
|
const maxFileBytes = opts.maxFileBytes ?? 256 * 1024;
|
|
39
|
+
const cached = repoMapCache;
|
|
40
|
+
if (cached && cached.cwd === opts.cwd && cached.maxFiles === maxFiles &&
|
|
41
|
+
cached.maxFileBytes === maxFileBytes && Date.now() - cached.at < REPO_MAP_TTL_MS &&
|
|
42
|
+
(await repoMapStatsMatch(opts.cwd, cached.stats))) {
|
|
43
|
+
return cached.files;
|
|
44
|
+
}
|
|
19
45
|
const out = [];
|
|
46
|
+
const stats = new Map();
|
|
20
47
|
await walk(opts.cwd, async (file) => {
|
|
21
48
|
if (out.length >= maxFiles)
|
|
22
49
|
return false;
|
|
@@ -39,9 +66,12 @@ export async function buildRepoMap(opts) {
|
|
|
39
66
|
catch {
|
|
40
67
|
return true;
|
|
41
68
|
}
|
|
42
|
-
|
|
69
|
+
const rel = path.relative(opts.cwd, file);
|
|
70
|
+
stats.set(rel, { mtimeMs: stat.mtimeMs, size: stat.size });
|
|
71
|
+
out.push({ path: rel, symbols: extractSymbols(content, ext) });
|
|
43
72
|
return true;
|
|
44
73
|
});
|
|
74
|
+
repoMapCache = { cwd: opts.cwd, maxFiles, maxFileBytes, files: out, stats, at: Date.now() };
|
|
45
75
|
return out;
|
|
46
76
|
}
|
|
47
77
|
async function walk(dir, visit) {
|
package/dist/events/catalog.d.ts
CHANGED
|
@@ -167,6 +167,28 @@ export type KlyroEvent = {
|
|
|
167
167
|
code: string;
|
|
168
168
|
message: string;
|
|
169
169
|
};
|
|
170
|
+
} | {
|
|
171
|
+
type: 'subtask.cancelled';
|
|
172
|
+
ts: number;
|
|
173
|
+
sessionId: string;
|
|
174
|
+
taskId: string;
|
|
175
|
+
status: 'cancelled';
|
|
176
|
+
durationMs: number;
|
|
177
|
+
error?: {
|
|
178
|
+
code: string;
|
|
179
|
+
message: string;
|
|
180
|
+
};
|
|
181
|
+
} | {
|
|
182
|
+
type: 'subtask.timed_out';
|
|
183
|
+
ts: number;
|
|
184
|
+
sessionId: string;
|
|
185
|
+
taskId: string;
|
|
186
|
+
status: 'timed_out';
|
|
187
|
+
durationMs: number;
|
|
188
|
+
error?: {
|
|
189
|
+
code: string;
|
|
190
|
+
message: string;
|
|
191
|
+
};
|
|
170
192
|
} | {
|
|
171
193
|
type: 'subtask.tool_call';
|
|
172
194
|
ts: number;
|
|
@@ -189,6 +211,7 @@ export type KlyroEvent = {
|
|
|
189
211
|
sessionId: string;
|
|
190
212
|
taskId: string;
|
|
191
213
|
changedFiles: string[];
|
|
214
|
+
merged?: boolean;
|
|
192
215
|
} | {
|
|
193
216
|
type: 'provider.retry';
|
|
194
217
|
ts: number;
|
|
@@ -203,5 +226,10 @@ export type KlyroEvent = {
|
|
|
203
226
|
path: string;
|
|
204
227
|
reason: 'unknown' | 'changed';
|
|
205
228
|
trusted: boolean;
|
|
229
|
+
} | {
|
|
230
|
+
type: 'context.compacted';
|
|
231
|
+
ts: number;
|
|
232
|
+
sessionId: string;
|
|
233
|
+
dropped: number;
|
|
206
234
|
};
|
|
207
235
|
export type KlyroEventType = KlyroEvent['type'];
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { runCompletion } from './cli/completion.js';
|
|
|
23
23
|
import { runUpdate } from './cli/update.js';
|
|
24
24
|
import { runLogin, runLogout } from './cli/auth.js';
|
|
25
25
|
import { readVersion } from './version.js';
|
|
26
|
+
import { verifyAuditChain } from './persistence/audit.js';
|
|
26
27
|
const VERSION = readVersion();
|
|
27
28
|
function parsePositiveInt(name, v) {
|
|
28
29
|
const n = Number(v);
|
|
@@ -61,8 +62,21 @@ async function main() {
|
|
|
61
62
|
.option('--output-format <fmt>', 'Headless output format: text|json|stream-json (default text)')
|
|
62
63
|
.option('--no-stream', 'Disable streaming (buffer full response)')
|
|
63
64
|
.option('--show-thinking', 'Show thinking blocks');
|
|
65
|
+
// Unknown commands / unknown options are usage errors (exit 2 per PRD).
|
|
66
|
+
// Help and version still exit 0; anything else rethrows to the last-resort
|
|
67
|
+
// handler (exit 1).
|
|
68
|
+
program.exitOverride((err) => {
|
|
69
|
+
if (err.code === 'commander.unknownCommand' || err.code === 'commander.unknownOption') {
|
|
70
|
+
process.stderr.write(`klyro: ${err.message}\n`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
if (err.code === 'commander.helpDisplayed' || err.code === 'commander.version') {
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
});
|
|
64
78
|
// Handle global flags before any command runs
|
|
65
|
-
program.hook('preAction', (thisCommand) => {
|
|
79
|
+
program.hook('preAction', async (thisCommand) => {
|
|
66
80
|
const opts = thisCommand.optsWithGlobals();
|
|
67
81
|
if (opts.cwd) {
|
|
68
82
|
try {
|
|
@@ -73,6 +87,13 @@ async function main() {
|
|
|
73
87
|
process.exit(2);
|
|
74
88
|
}
|
|
75
89
|
}
|
|
90
|
+
// Load <cwd>/.env globally (after chdir) so KLYRO_* vars resolve for
|
|
91
|
+
// every command. Never throws; explicit env wins (no-clobber).
|
|
92
|
+
try {
|
|
93
|
+
const { loadDotenv } = await import('./cli/dotenv.js');
|
|
94
|
+
loadDotenv(process.cwd());
|
|
95
|
+
}
|
|
96
|
+
catch { /* ignore */ }
|
|
76
97
|
if (opts.config)
|
|
77
98
|
process.env.KLYRO_CONFIG = opts.config;
|
|
78
99
|
if (opts.debug)
|
|
@@ -271,6 +292,7 @@ async function main() {
|
|
|
271
292
|
.option('--resume-session <id>', 'Resume from a persisted session (Level 9) by id prefix')
|
|
272
293
|
.option('--verify', 'Enable verification after edits (Level 8, default: enabled)')
|
|
273
294
|
.option('--verify-command <cmd>', 'Custom verification command (default: auto-detected)')
|
|
295
|
+
.option('--verify-mode <mode>', 'Verification mode: strict (default), advisory, off')
|
|
274
296
|
.option('--max-repairs <n>', 'Max autonomous repair attempts (default 3)', (v) => parsePositiveInt('--max-repairs', v))
|
|
275
297
|
.option('--persist', 'Enable session persistence (Level 9, default: enabled)')
|
|
276
298
|
.option('--require-verify', 'Fail with exit 8 if no verification passed after edits (6.5)')
|
|
@@ -287,6 +309,11 @@ async function main() {
|
|
|
287
309
|
process.stderr.write(`klyro: invalid --output: ${output} (expected human|json|silent)\n`);
|
|
288
310
|
process.exit(2);
|
|
289
311
|
}
|
|
312
|
+
const verifyMode = opts.verifyMode ?? 'strict';
|
|
313
|
+
if (verifyMode !== 'strict' && verifyMode !== 'advisory' && verifyMode !== 'off') {
|
|
314
|
+
process.stderr.write(`klyro: invalid --verify-mode: ${opts.verifyMode} (expected strict|advisory|off)\n`);
|
|
315
|
+
process.exit(2);
|
|
316
|
+
}
|
|
290
317
|
// Provider validation is handled inside runOnce (single source of truth)
|
|
291
318
|
try {
|
|
292
319
|
const code = await runOnce({
|
|
@@ -306,6 +333,7 @@ async function main() {
|
|
|
306
333
|
sessionId: opts.resumeSession,
|
|
307
334
|
verify: opts.verify,
|
|
308
335
|
verifyCommand: opts.verifyCommand,
|
|
336
|
+
verifyMode: verifyMode,
|
|
309
337
|
maxRepairAttempts: opts.maxRepairs,
|
|
310
338
|
persist: opts.persist,
|
|
311
339
|
requireVerify: !!opts.requireVerify,
|
|
@@ -570,15 +598,72 @@ async function main() {
|
|
|
570
598
|
});
|
|
571
599
|
// 10.1 — MCP
|
|
572
600
|
const mcp = program.command('mcp').description('MCP client/server (10.1)');
|
|
573
|
-
mcp.command('list').description('List MCP servers').action(async () => {
|
|
601
|
+
mcp.command('list').description('List MCP servers').action(async () => {
|
|
602
|
+
const { loadMcpServers } = await import('./mcp/config.js');
|
|
603
|
+
const cfg = loadMcpServers(process.cwd());
|
|
604
|
+
const names = Object.keys(cfg.servers);
|
|
605
|
+
if (names.length === 0) {
|
|
606
|
+
process.stdout.write('mcp servers: none configured (use .mcp.json)\n');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
for (const name of names) {
|
|
610
|
+
const spec = cfg.servers[name];
|
|
611
|
+
const source = cfg.sources[name] ?? 'unknown';
|
|
612
|
+
process.stdout.write(`${name} source=${source}${spec?.disabled ? ' disabled' : ''}\n`);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
574
615
|
mcp.command('add <name> <url>').description('Add MCP server').action(async (name) => { process.stdout.write(`added mcp ${name} (stub)\n`); });
|
|
575
616
|
mcp.command('serve').description('Serve as MCP server').action(async () => { process.stdout.write('klyro mcp serve — exposing tools (stub)\n'); });
|
|
576
617
|
// 10.2 — Hooks / agents
|
|
577
618
|
program.command('hooks').description('List hooks (10.2)').action(async () => { process.stdout.write('hooks: SessionStart UserPromptSubmit PreToolUse PostToolUse (stub)\n'); });
|
|
578
|
-
program.command('agents').description('List agents (10.2)').action(async () => {
|
|
619
|
+
program.command('agents [name]').description('List agents (10.2) or show one agent definition').action(async (name) => {
|
|
620
|
+
const { BUILTIN_AGENTS } = await import('./agent/orchestrator.js');
|
|
621
|
+
if (!name) {
|
|
622
|
+
for (const a of BUILTIN_AGENTS)
|
|
623
|
+
process.stdout.write(`${a.id} — ${a.description}\n`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const def = BUILTIN_AGENTS.find((a) => a.id === name);
|
|
627
|
+
if (!def) {
|
|
628
|
+
process.stderr.write(`klyro: unknown agent: ${name} (known: ${BUILTIN_AGENTS.map((a) => a.id).join(', ')})\n`);
|
|
629
|
+
process.exit(2);
|
|
630
|
+
}
|
|
631
|
+
const tools = def.allowedTools ?? ['<inherited: all parent tools>'];
|
|
632
|
+
const lines = [
|
|
633
|
+
`agent: ${def.id}`,
|
|
634
|
+
`description: ${def.description}`,
|
|
635
|
+
`tools (${tools.length}): ${tools.join(', ')}`,
|
|
636
|
+
`readonly: ${def.readonly ?? false}`,
|
|
637
|
+
`canSpawn: ${def.canSpawn ?? false}`,
|
|
638
|
+
`model: ${def.model ?? '<session default>'}`,
|
|
639
|
+
`maxSteps: ${def.maxSteps ?? '<default>'}`,
|
|
640
|
+
`maxTokens: ${def.maxTokens ?? '<default>'}`,
|
|
641
|
+
];
|
|
642
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
643
|
+
});
|
|
579
644
|
// 10.3 — Web / git workflows / SDK
|
|
580
645
|
program.command('commit').description('Create commit (10.3)').action(async () => { process.stdout.write('commit — conventional message (stub, use /commit)\n'); });
|
|
581
|
-
program.command('audit').description('
|
|
646
|
+
program.command('audit [session]').description('Verify audit chain (13.4)').action(async (session) => {
|
|
647
|
+
if (!session) {
|
|
648
|
+
process.stderr.write('klyro: audit requires a session id (usage: klyro audit <session>)\n');
|
|
649
|
+
process.exit(2);
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
const { getDefaultSessionsDir } = await import('./persistence/session.js');
|
|
653
|
+
const res = await verifyAuditChain(getDefaultSessionsDir(), session);
|
|
654
|
+
if (res.ok) {
|
|
655
|
+
process.stdout.write(`audit ok: ${res.events} events verified\n`);
|
|
656
|
+
process.exit(0);
|
|
657
|
+
}
|
|
658
|
+
process.stderr.write(`klyro: audit failed: ${res.error ?? 'chain broken'}\n`);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
663
|
+
process.stderr.write(`klyro: audit failed: ${msg}\n`);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
});
|
|
582
667
|
// 10.4 — Benchmark parity (10.5)
|
|
583
668
|
program.command('benchmark').description('Run benchmark (10.5)').action(async () => {
|
|
584
669
|
const { runHarness } = await import('./eval/harness.js');
|
package/dist/mcp/client.d.ts
CHANGED
|
@@ -16,9 +16,9 @@ export interface McpCallResult {
|
|
|
16
16
|
raw: unknown;
|
|
17
17
|
}
|
|
18
18
|
export interface McpClientLike {
|
|
19
|
-
listTools(): Promise<McpToolDef[]>;
|
|
19
|
+
listTools(signal?: AbortSignal): Promise<McpToolDef[]>;
|
|
20
20
|
callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult>;
|
|
21
|
-
listResources(): Promise<McpResource[]>;
|
|
21
|
+
listResources(signal?: AbortSignal): Promise<McpResource[]>;
|
|
22
22
|
close(): Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
export declare class McpError extends Error {
|
|
@@ -39,11 +39,13 @@ export declare class McpClient implements McpClientLike {
|
|
|
39
39
|
constructor(name: string, spec: McpServerSpec);
|
|
40
40
|
connect(): Promise<void>;
|
|
41
41
|
private connected;
|
|
42
|
-
listTools(): Promise<McpToolDef[]>;
|
|
42
|
+
listTools(signal?: AbortSignal): Promise<McpToolDef[]>;
|
|
43
43
|
callTool(name: string, args: unknown, signal?: AbortSignal): Promise<McpCallResult>;
|
|
44
|
-
listResources(): Promise<McpResource[]>;
|
|
44
|
+
listResources(signal?: AbortSignal): Promise<McpResource[]>;
|
|
45
45
|
readResource(uri: string, signal?: AbortSignal): Promise<string>;
|
|
46
46
|
close(): Promise<void>;
|
|
47
|
+
/** Test hook: number of in-flight JSON-RPC requests. */
|
|
48
|
+
pendingCount(): number;
|
|
47
49
|
private timeout;
|
|
48
50
|
private assertLive;
|
|
49
51
|
private onStdout;
|