klyro 0.1.63 → 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 +73 -16
- package/dist/agent/capabilities.d.ts +145 -0
- package/dist/agent/capabilities.js +191 -0
- package/dist/agent/child-worker.d.ts +104 -0
- package/dist/agent/child-worker.js +250 -0
- package/dist/agent/orchestrator.d.ts +232 -0
- package/dist/agent/orchestrator.js +589 -0
- package/dist/agent/provider-adapter.d.ts +17 -0
- package/dist/agent/provider-adapter.js +35 -3
- package/dist/agent/registry.d.ts +1 -0
- package/dist/agent/registry.js +1 -0
- package/dist/agent/retry.d.ts +13 -2
- package/dist/agent/retry.js +21 -3
- package/dist/agent/runtime.d.ts +70 -8
- package/dist/agent/runtime.js +205 -34
- package/dist/agent/scoped-registry.d.ts +22 -0
- package/dist/agent/scoped-registry.js +42 -0
- package/dist/agent/task-manager.d.ts +115 -0
- package/dist/agent/task-manager.js +250 -0
- package/dist/agent/worker-spawner.d.ts +17 -12
- package/dist/agent/worker-spawner.js +26 -20
- 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/dotenv.d.ts +3 -0
- package/dist/cli/dotenv.js +57 -0
- package/dist/cli/eval.d.ts +6 -1
- package/dist/cli/eval.js +9 -0
- package/dist/cli/repl.js +183 -17
- package/dist/cli/run.d.ts +10 -11
- package/dist/cli/run.js +176 -14
- 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.d.ts +6 -0
- package/dist/context/klyro-md.js +25 -16
- 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/context/trust.d.ts +42 -0
- package/dist/context/trust.js +111 -0
- package/dist/events/catalog.d.ts +99 -0
- package/dist/index.js +93 -4
- package/dist/mcp/client.d.ts +55 -0
- package/dist/mcp/client.js +294 -0
- package/dist/mcp/config.d.ts +40 -0
- package/dist/mcp/config.js +99 -0
- package/dist/mcp/policy.d.ts +13 -0
- package/dist/mcp/policy.js +12 -0
- package/dist/mcp/registry.d.ts +72 -0
- package/dist/mcp/registry.js +244 -0
- package/dist/mcp/schema.d.ts +18 -0
- package/dist/mcp/schema.js +57 -0
- 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 +91 -10
- 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.d.ts +9 -0
- package/dist/tools/agent/spawn-agent.js +50 -0
- package/dist/tools/agent/task-apply.d.ts +4 -0
- package/dist/tools/agent/task-apply.js +44 -0
- package/dist/tools/agent/task-get.d.ts +8 -0
- package/dist/tools/agent/task-get.js +40 -0
- package/dist/tools/agent/task-list.d.ts +4 -0
- package/dist/tools/agent/task-list.js +41 -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/plan/todo-write.d.ts +1 -1
- package/dist/tools/registry.js +12 -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 +18 -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
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) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { KlyroEvent } from '../events/catalog.js';
|
|
2
|
+
import { type KlyroMdFile } from './klyro-md.js';
|
|
3
|
+
export interface TrustRecord {
|
|
4
|
+
sha256: string;
|
|
5
|
+
trustedAt: number;
|
|
6
|
+
}
|
|
7
|
+
export type TrustStore = Record<string, TrustRecord>;
|
|
8
|
+
export type TrustReason = 'unknown' | 'changed';
|
|
9
|
+
export declare function hashContent(content: string): string;
|
|
10
|
+
/** Pure evaluation: split files into trusted vs needs-approval. */
|
|
11
|
+
export declare function evaluateTrust(stored: TrustStore, files: KlyroMdFile[]): {
|
|
12
|
+
trusted: KlyroMdFile[];
|
|
13
|
+
untrusted: {
|
|
14
|
+
file: KlyroMdFile;
|
|
15
|
+
reason: TrustReason;
|
|
16
|
+
}[];
|
|
17
|
+
};
|
|
18
|
+
export declare function defaultTrustStorePath(): string;
|
|
19
|
+
export declare class ContextTrust {
|
|
20
|
+
private readonly storePath;
|
|
21
|
+
private store;
|
|
22
|
+
constructor(storePath?: string);
|
|
23
|
+
private load;
|
|
24
|
+
private save;
|
|
25
|
+
check(files: KlyroMdFile[]): ReturnType<typeof evaluateTrust>;
|
|
26
|
+
/** Record approval for the file's current content. */
|
|
27
|
+
approve(file: KlyroMdFile): void;
|
|
28
|
+
isTrusted(file: KlyroMdFile): boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface TrustedLoadOpts {
|
|
31
|
+
trust: ContextTrust;
|
|
32
|
+
/** Return true to include an untrusted file (and persist the approval). */
|
|
33
|
+
approve: (file: KlyroMdFile, reason: TrustReason) => Promise<boolean>;
|
|
34
|
+
sessionId?: string;
|
|
35
|
+
emit?: (ev: KlyroEvent) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load KLYRO.md hierarchy with the trust gate applied. Untrusted files are
|
|
39
|
+
* offered to `approve`; declined files are excluded from the returned text.
|
|
40
|
+
* Every gate decision emits `context.trust_prompt` on the bus.
|
|
41
|
+
*/
|
|
42
|
+
export declare function loadTrustedKlyroMd(cwd: string, opts: TrustedLoadOpts): Promise<string>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* P0.3 — context-file trust gate (r-11-17.md §5.1).
|
|
3
|
+
*
|
|
4
|
+
* Closes the shared HIGH flaw: `KLYRO.md` / `AGENTS.md` are auto-loaded into
|
|
5
|
+
* the prompt, so a malicious commit can smuggle instructions that read as
|
|
6
|
+
* authenticated system guidance. Every auto-loaded file is hashed; the first
|
|
7
|
+
* sighting (or any change) requires explicit approval before the content may
|
|
8
|
+
* enter context. Decisions persist in `~/.klyro/context-trust.json`.
|
|
9
|
+
*
|
|
10
|
+
* Headless runs approve nothing (secure default): untrusted files are
|
|
11
|
+
* excluded and the exclusion is visible on the event bus.
|
|
12
|
+
*/
|
|
13
|
+
import * as crypto from 'node:crypto';
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { loadKlyroMdFiles } from './klyro-md.js';
|
|
18
|
+
export function hashContent(content) {
|
|
19
|
+
return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
20
|
+
}
|
|
21
|
+
/** Pure evaluation: split files into trusted vs needs-approval. */
|
|
22
|
+
export function evaluateTrust(stored, files) {
|
|
23
|
+
const trusted = [];
|
|
24
|
+
const untrusted = [];
|
|
25
|
+
for (const f of files) {
|
|
26
|
+
const rec = stored[f.path];
|
|
27
|
+
if (!rec) {
|
|
28
|
+
untrusted.push({ file: f, reason: 'unknown' });
|
|
29
|
+
}
|
|
30
|
+
else if (rec.sha256 !== hashContent(f.content)) {
|
|
31
|
+
untrusted.push({ file: f, reason: 'changed' });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
trusted.push(f);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { trusted, untrusted };
|
|
38
|
+
}
|
|
39
|
+
export function defaultTrustStorePath() {
|
|
40
|
+
return path.join(os.homedir() || process.cwd(), '.klyro', 'context-trust.json');
|
|
41
|
+
}
|
|
42
|
+
export class ContextTrust {
|
|
43
|
+
storePath;
|
|
44
|
+
store = {};
|
|
45
|
+
constructor(storePath = defaultTrustStorePath()) {
|
|
46
|
+
this.storePath = storePath;
|
|
47
|
+
this.load();
|
|
48
|
+
}
|
|
49
|
+
load() {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs.readFileSync(this.storePath, 'utf-8');
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
54
|
+
this.store = parsed;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
this.store = {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
save() {
|
|
62
|
+
try {
|
|
63
|
+
fs.mkdirSync(path.dirname(this.storePath), { recursive: true });
|
|
64
|
+
fs.writeFileSync(this.storePath, JSON.stringify(this.store, null, 2), 'utf-8');
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* best-effort — trust stays in memory for the session */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
check(files) {
|
|
71
|
+
return evaluateTrust(this.store, files);
|
|
72
|
+
}
|
|
73
|
+
/** Record approval for the file's current content. */
|
|
74
|
+
approve(file) {
|
|
75
|
+
this.store[file.path] = { sha256: hashContent(file.content), trustedAt: Date.now() };
|
|
76
|
+
this.save();
|
|
77
|
+
}
|
|
78
|
+
isTrusted(file) {
|
|
79
|
+
const rec = this.store[file.path];
|
|
80
|
+
return !!rec && rec.sha256 === hashContent(file.content);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Load KLYRO.md hierarchy with the trust gate applied. Untrusted files are
|
|
85
|
+
* offered to `approve`; declined files are excluded from the returned text.
|
|
86
|
+
* Every gate decision emits `context.trust_prompt` on the bus.
|
|
87
|
+
*/
|
|
88
|
+
export async function loadTrustedKlyroMd(cwd, opts) {
|
|
89
|
+
const sessionId = opts.sessionId ?? 'ephemeral';
|
|
90
|
+
const files = await loadKlyroMdFiles(cwd);
|
|
91
|
+
const { trusted, untrusted } = opts.trust.check(files);
|
|
92
|
+
const kept = [...trusted];
|
|
93
|
+
for (const { file, reason } of untrusted) {
|
|
94
|
+
let ok = false;
|
|
95
|
+
try {
|
|
96
|
+
ok = await opts.approve(file, reason);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
ok = false;
|
|
100
|
+
}
|
|
101
|
+
opts.emit?.({ type: 'context.trust_prompt', ts: Date.now(), sessionId, path: file.path, reason, trusted: ok });
|
|
102
|
+
if (ok) {
|
|
103
|
+
opts.trust.approve(file);
|
|
104
|
+
kept.push(file);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Preserve loader order.
|
|
108
|
+
const order = new Map(files.map((f, i) => [f.path, i]));
|
|
109
|
+
kept.sort((a, b) => (order.get(a.path) ?? 0) - (order.get(b.path) ?? 0));
|
|
110
|
+
return kept.map((f) => `# ${f.path}\n${f.content}`).join('\n\n---\n\n');
|
|
111
|
+
}
|
package/dist/events/catalog.d.ts
CHANGED
|
@@ -132,5 +132,104 @@ export type KlyroEvent = {
|
|
|
132
132
|
ts: number;
|
|
133
133
|
sessionId: string;
|
|
134
134
|
reason: string;
|
|
135
|
+
} | {
|
|
136
|
+
type: 'subtask.started';
|
|
137
|
+
ts: number;
|
|
138
|
+
sessionId: string;
|
|
139
|
+
taskId: string;
|
|
140
|
+
parentTaskId?: string;
|
|
141
|
+
agentName: string;
|
|
142
|
+
depth: number;
|
|
143
|
+
model?: string;
|
|
144
|
+
} | {
|
|
145
|
+
type: 'subtask.progress';
|
|
146
|
+
ts: number;
|
|
147
|
+
sessionId: string;
|
|
148
|
+
taskId: string;
|
|
149
|
+
note: string;
|
|
150
|
+
} | {
|
|
151
|
+
type: 'subtask.completed';
|
|
152
|
+
ts: number;
|
|
153
|
+
sessionId: string;
|
|
154
|
+
taskId: string;
|
|
155
|
+
status: 'succeeded';
|
|
156
|
+
durationMs: number;
|
|
157
|
+
steps: number;
|
|
158
|
+
toolCalls: number;
|
|
159
|
+
} | {
|
|
160
|
+
type: 'subtask.failed';
|
|
161
|
+
ts: number;
|
|
162
|
+
sessionId: string;
|
|
163
|
+
taskId: string;
|
|
164
|
+
status: 'failed' | 'cancelled' | 'timed_out' | 'blocked';
|
|
165
|
+
durationMs: number;
|
|
166
|
+
error?: {
|
|
167
|
+
code: string;
|
|
168
|
+
message: string;
|
|
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
|
+
};
|
|
192
|
+
} | {
|
|
193
|
+
type: 'subtask.tool_call';
|
|
194
|
+
ts: number;
|
|
195
|
+
sessionId: string;
|
|
196
|
+
taskId: string;
|
|
197
|
+
callId: string;
|
|
198
|
+
name: string;
|
|
199
|
+
} | {
|
|
200
|
+
type: 'subtask.tool_result';
|
|
201
|
+
ts: number;
|
|
202
|
+
sessionId: string;
|
|
203
|
+
taskId: string;
|
|
204
|
+
callId: string;
|
|
205
|
+
name: string;
|
|
206
|
+
isError: boolean;
|
|
207
|
+
latencyMs: number;
|
|
208
|
+
} | {
|
|
209
|
+
type: 'subtask.merged';
|
|
210
|
+
ts: number;
|
|
211
|
+
sessionId: string;
|
|
212
|
+
taskId: string;
|
|
213
|
+
changedFiles: string[];
|
|
214
|
+
merged?: boolean;
|
|
215
|
+
} | {
|
|
216
|
+
type: 'provider.retry';
|
|
217
|
+
ts: number;
|
|
218
|
+
sessionId: string;
|
|
219
|
+
attempt: number;
|
|
220
|
+
status: string;
|
|
221
|
+
retryAfterMs?: number;
|
|
222
|
+
} | {
|
|
223
|
+
type: 'context.trust_prompt';
|
|
224
|
+
ts: number;
|
|
225
|
+
sessionId: string;
|
|
226
|
+
path: string;
|
|
227
|
+
reason: 'unknown' | 'changed';
|
|
228
|
+
trusted: boolean;
|
|
229
|
+
} | {
|
|
230
|
+
type: 'context.compacted';
|
|
231
|
+
ts: number;
|
|
232
|
+
sessionId: string;
|
|
233
|
+
dropped: number;
|
|
135
234
|
};
|
|
136
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,9 +292,12 @@ 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)')
|
|
299
|
+
.option('--agent <name>', 'Run under an orchestrator context enabling spawn_agent/task_list/task_get (explorer|implementer|tester|reviewer)')
|
|
300
|
+
.option('--max-depth <n>', 'Max spawn depth for child agents (default 1)', (v) => parsePositiveInt('--max-depth', v))
|
|
277
301
|
.action(async (prompt, opts) => {
|
|
278
302
|
const model = opts.model ?? process.env.KLYRO_MODEL;
|
|
279
303
|
if (!model) {
|
|
@@ -285,6 +309,11 @@ async function main() {
|
|
|
285
309
|
process.stderr.write(`klyro: invalid --output: ${output} (expected human|json|silent)\n`);
|
|
286
310
|
process.exit(2);
|
|
287
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
|
+
}
|
|
288
317
|
// Provider validation is handled inside runOnce (single source of truth)
|
|
289
318
|
try {
|
|
290
319
|
const code = await runOnce({
|
|
@@ -304,9 +333,12 @@ async function main() {
|
|
|
304
333
|
sessionId: opts.resumeSession,
|
|
305
334
|
verify: opts.verify,
|
|
306
335
|
verifyCommand: opts.verifyCommand,
|
|
336
|
+
verifyMode: verifyMode,
|
|
307
337
|
maxRepairAttempts: opts.maxRepairs,
|
|
308
338
|
persist: opts.persist,
|
|
309
339
|
requireVerify: !!opts.requireVerify,
|
|
340
|
+
agent: opts.agent,
|
|
341
|
+
maxDepth: opts.maxDepth,
|
|
310
342
|
});
|
|
311
343
|
process.exit(code);
|
|
312
344
|
}
|
|
@@ -566,15 +598,72 @@ async function main() {
|
|
|
566
598
|
});
|
|
567
599
|
// 10.1 — MCP
|
|
568
600
|
const mcp = program.command('mcp').description('MCP client/server (10.1)');
|
|
569
|
-
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
|
+
});
|
|
570
615
|
mcp.command('add <name> <url>').description('Add MCP server').action(async (name) => { process.stdout.write(`added mcp ${name} (stub)\n`); });
|
|
571
616
|
mcp.command('serve').description('Serve as MCP server').action(async () => { process.stdout.write('klyro mcp serve — exposing tools (stub)\n'); });
|
|
572
617
|
// 10.2 — Hooks / agents
|
|
573
618
|
program.command('hooks').description('List hooks (10.2)').action(async () => { process.stdout.write('hooks: SessionStart UserPromptSubmit PreToolUse PostToolUse (stub)\n'); });
|
|
574
|
-
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
|
+
});
|
|
575
644
|
// 10.3 — Web / git workflows / SDK
|
|
576
645
|
program.command('commit').description('Create commit (10.3)').action(async () => { process.stdout.write('commit — conventional message (stub, use /commit)\n'); });
|
|
577
|
-
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
|
+
});
|
|
578
667
|
// 10.4 — Benchmark parity (10.5)
|
|
579
668
|
program.command('benchmark').description('Run benchmark (10.5)').action(async () => {
|
|
580
669
|
const { runHarness } = await import('./eval/harness.js');
|