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/cli/run.js
CHANGED
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
import { stdout, stderr } from 'node:process';
|
|
11
11
|
import { httpChatAdapter } from '../agent/provider-adapter.js';
|
|
12
12
|
import { anthropicAdapter } from '../agent/anthropic-adapter.js';
|
|
13
|
+
import { retryingAdapter } from '../agent/retry.js';
|
|
14
|
+
import { globalBus } from '../events/bus.js';
|
|
13
15
|
import { run } from '../agent/runtime.js';
|
|
14
16
|
import { builtinRegistry } from '../tools/registry.js';
|
|
15
17
|
import { builtinRules, clonePolicyConfig, PolicyEngine } from '../policy/engine.js';
|
|
16
18
|
import { DenyAllApprovalPrompt } from '../policy/approval.js';
|
|
17
19
|
import { buildLevel6Context } from '../context/level6.js';
|
|
20
|
+
import { memoryBlock } from '../context/memory.js';
|
|
18
21
|
import { resolveSessionId } from '../persistence/session.js';
|
|
19
22
|
import * as fs from 'node:fs';
|
|
20
23
|
function readEnv(name, fallback) {
|
|
@@ -22,6 +25,13 @@ function readEnv(name, fallback) {
|
|
|
22
25
|
return v && v.length > 0 ? v : fallback;
|
|
23
26
|
}
|
|
24
27
|
export async function runOnce(opts) {
|
|
28
|
+
// P0.5 — load <cwd>/.env first so KLYRO_* vars resolve without `export`.
|
|
29
|
+
// Never throws (missing file is a no-op); explicit env wins (no-clobber).
|
|
30
|
+
try {
|
|
31
|
+
const { loadDotenv } = await import('./dotenv.js');
|
|
32
|
+
loadDotenv(opts.cwd);
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore */ }
|
|
25
35
|
const output = opts.output ?? 'human';
|
|
26
36
|
if (opts.dryRun) {
|
|
27
37
|
return await dryRunReport(opts);
|
|
@@ -32,6 +42,27 @@ export async function runOnce(opts) {
|
|
|
32
42
|
clearReadHistory();
|
|
33
43
|
}
|
|
34
44
|
catch { /* ignore */ }
|
|
45
|
+
// P1.4 — validate --agent early so typos fail fast (exit 2) even without API keys.
|
|
46
|
+
if (opts.agent) {
|
|
47
|
+
const { BUILTIN_AGENTS: _known } = await import('../agent/orchestrator.js');
|
|
48
|
+
if (!_known.some((a) => a.id === opts.agent)) {
|
|
49
|
+
stderr.write(`klyro: unknown agent: ${opts.agent} (known: ${_known.map((a) => a.id).join(', ')})\n`);
|
|
50
|
+
return 2;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Box so retry telemetry (emitted before the session exists) picks up the
|
|
54
|
+
// real sessionId once create/resume assigns it below.
|
|
55
|
+
const sessionIdForRetry = { id: 'ephemeral' };
|
|
56
|
+
const onRetryEmit = (info) => {
|
|
57
|
+
globalBus.emit({
|
|
58
|
+
type: 'provider.retry',
|
|
59
|
+
ts: Date.now(),
|
|
60
|
+
sessionId: sessionIdForRetry.id,
|
|
61
|
+
attempt: info.attempt,
|
|
62
|
+
status: info.status,
|
|
63
|
+
...(info.retryAfterMs !== undefined ? { retryAfterMs: info.retryAfterMs } : {}),
|
|
64
|
+
});
|
|
65
|
+
};
|
|
35
66
|
let adapter = opts.adapter;
|
|
36
67
|
if (!adapter) {
|
|
37
68
|
const provider = opts.provider ?? 'openai';
|
|
@@ -42,19 +73,19 @@ export async function runOnce(opts) {
|
|
|
42
73
|
return 2;
|
|
43
74
|
}
|
|
44
75
|
if (provider === 'anthropic') {
|
|
45
|
-
adapter = anthropicAdapter({
|
|
76
|
+
adapter = retryingAdapter(anthropicAdapter({
|
|
46
77
|
baseURL: baseUrl,
|
|
47
78
|
apiKey,
|
|
48
79
|
timeoutMs: opts.timeoutMs ?? 60_000,
|
|
49
80
|
authHeader: opts.authHeader,
|
|
50
|
-
});
|
|
81
|
+
}), { onRetry: onRetryEmit });
|
|
51
82
|
}
|
|
52
83
|
else {
|
|
53
84
|
if (!baseUrl) {
|
|
54
85
|
stderr.write('klyro: KLYRO_BASE_URL is not set (or pass --base-url)\n');
|
|
55
86
|
return 2;
|
|
56
87
|
}
|
|
57
|
-
adapter = httpChatAdapter({ baseURL: baseUrl, apiKey, timeoutMs: opts.timeoutMs ?? 60_000 });
|
|
88
|
+
adapter = retryingAdapter(httpChatAdapter({ baseURL: baseUrl, apiKey, timeoutMs: opts.timeoutMs ?? 60_000 }), { onRetry: onRetryEmit });
|
|
58
89
|
}
|
|
59
90
|
}
|
|
60
91
|
const registry = builtinRegistry();
|
|
@@ -67,6 +98,37 @@ export async function runOnce(opts) {
|
|
|
67
98
|
catch {
|
|
68
99
|
/* ignore — engine defaults stand */
|
|
69
100
|
}
|
|
101
|
+
// Best-effort MCP tools: never fatal, never prompts (headless cannot
|
|
102
|
+
// approve). Project-sourced servers auto-connect ONLY when their exact
|
|
103
|
+
// spec hash is already in the McpTrust store (e.g. approved in a prior
|
|
104
|
+
// REPL session); unknown specs are skipped with a warning.
|
|
105
|
+
let closeMcp;
|
|
106
|
+
try {
|
|
107
|
+
const { loadAndRegisterMcp } = await import('../mcp/registry.js');
|
|
108
|
+
const { McpTrust, hashSpec } = await import('../mcp/trust.js');
|
|
109
|
+
const { loadMcpServers } = await import('../mcp/config.js');
|
|
110
|
+
const mcpTrust = new McpTrust();
|
|
111
|
+
const mcpSpecs = loadMcpServers(opts.cwd).servers;
|
|
112
|
+
const mcp = await loadAndRegisterMcp({
|
|
113
|
+
cwd: opts.cwd,
|
|
114
|
+
registry,
|
|
115
|
+
policy,
|
|
116
|
+
approveProjectServer: async ({ name }) => {
|
|
117
|
+
const spec = mcpSpecs[name];
|
|
118
|
+
if (spec && mcpTrust.isTrusted(name, hashSpec(spec)))
|
|
119
|
+
return true;
|
|
120
|
+
stderr.write(`klyro: mcp project server "${name}" not in trust store (skipped — headless cannot prompt)\n`);
|
|
121
|
+
return false;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
for (const e of mcp.errors)
|
|
125
|
+
stderr.write(`klyro: mcp ${e.server}: ${e.message}\n`);
|
|
126
|
+
if (mcp.registered.length > 0 && output === 'human') {
|
|
127
|
+
stderr.write(`klyro: mcp tools: ${mcp.registered.join(', ')}\n`);
|
|
128
|
+
}
|
|
129
|
+
closeMcp = mcp.closeAll;
|
|
130
|
+
}
|
|
131
|
+
catch { /* ignore — MCP is optional */ }
|
|
70
132
|
const systemPrompt = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt);
|
|
71
133
|
// Level 9 — session setup (create or resume)
|
|
72
134
|
const persistEnabled = opts.persist !== false;
|
|
@@ -110,6 +172,9 @@ export async function runOnce(opts) {
|
|
|
110
172
|
else if (opts.resumePath) {
|
|
111
173
|
initialTranscript = loadTranscript(opts.resumePath);
|
|
112
174
|
}
|
|
175
|
+
// Publish the real sessionId to retry telemetry (stays 'ephemeral' when
|
|
176
|
+
// persistence is disabled or the session was never created).
|
|
177
|
+
sessionIdForRetry.id = sessionId ?? 'ephemeral';
|
|
113
178
|
const ac = new AbortController();
|
|
114
179
|
const onSigint = () => {
|
|
115
180
|
stderr.write('\nklyro: SIGINT — aborting\n');
|
|
@@ -129,8 +194,52 @@ export async function runOnce(opts) {
|
|
|
129
194
|
maxRepairAttempts: opts.maxRepairAttempts ?? 3,
|
|
130
195
|
timeoutMs: opts.verifyTimeoutMs,
|
|
131
196
|
requireVerify: opts.requireVerify,
|
|
197
|
+
...(opts.verifyMode ? { mode: opts.verifyMode } : {}),
|
|
132
198
|
};
|
|
133
199
|
let result;
|
|
200
|
+
// P1.4 — if --agent is requested, stand up a parent orchestrator so the
|
|
201
|
+
// model can call spawn_agent / task_list / task_get. The root run keeps
|
|
202
|
+
// depth 0; children are capped at maxDepth (default 1 per r-6-10.fix.md).
|
|
203
|
+
let agentBridge;
|
|
204
|
+
let parentContext;
|
|
205
|
+
if (opts.agent) {
|
|
206
|
+
const { AgentOrchestrator, BUILTIN_AGENTS } = await import('../agent/orchestrator.js');
|
|
207
|
+
const def = BUILTIN_AGENTS.find((a) => a.id === opts.agent); // validated above
|
|
208
|
+
const maxDepth = opts.maxDepth ?? 1;
|
|
209
|
+
const rootDeps = { adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt };
|
|
210
|
+
const orchestrator = new AgentOrchestrator({ sessionId: sessionId ?? 'ephemeral', deps: rootDeps });
|
|
211
|
+
const allowedTools = new Set(registry.list().map((t) => t.name));
|
|
212
|
+
parentContext = {
|
|
213
|
+
sessionId: sessionId ?? 'ephemeral',
|
|
214
|
+
depth: 0,
|
|
215
|
+
maxDepth,
|
|
216
|
+
allowedTools,
|
|
217
|
+
model: def.model ?? opts.model,
|
|
218
|
+
};
|
|
219
|
+
agentBridge = orchestrator.bridgeFor({
|
|
220
|
+
sessionId: sessionId ?? 'ephemeral',
|
|
221
|
+
cwd: opts.cwd,
|
|
222
|
+
depth: 0,
|
|
223
|
+
maxDepth,
|
|
224
|
+
allowedTools,
|
|
225
|
+
model: def.model ?? opts.model,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
else if (opts.maxDepth !== undefined) {
|
|
229
|
+
const allowedTools = new Set(registry.list().map((t) => t.name));
|
|
230
|
+
const { AgentOrchestrator } = await import('../agent/orchestrator.js');
|
|
231
|
+
const rootDeps = { adapter, registry, policy, approval: new DenyAllApprovalPrompt(), systemPrompt };
|
|
232
|
+
const orchestrator = new AgentOrchestrator({ sessionId: sessionId ?? 'ephemeral', deps: rootDeps });
|
|
233
|
+
parentContext = { sessionId: sessionId ?? 'ephemeral', depth: 0, maxDepth: opts.maxDepth, allowedTools, model: opts.model };
|
|
234
|
+
agentBridge = orchestrator.bridgeFor({
|
|
235
|
+
sessionId: sessionId ?? 'ephemeral',
|
|
236
|
+
cwd: opts.cwd,
|
|
237
|
+
depth: 0,
|
|
238
|
+
maxDepth: opts.maxDepth,
|
|
239
|
+
allowedTools,
|
|
240
|
+
model: opts.model,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
134
243
|
try {
|
|
135
244
|
result = await run({
|
|
136
245
|
task: opts.task,
|
|
@@ -144,6 +253,8 @@ export async function runOnce(opts) {
|
|
|
144
253
|
initialTranscript,
|
|
145
254
|
verify: verifyOpts,
|
|
146
255
|
persist: store && sessionId ? { store, sessionId } : undefined,
|
|
256
|
+
...(agentBridge ? { agentBridge } : {}),
|
|
257
|
+
...(parentContext ? { parentContext } : {}),
|
|
147
258
|
onEvent: (ev) => {
|
|
148
259
|
if (output === 'json') {
|
|
149
260
|
stdout.write(JSON.stringify(ev) + '\n');
|
|
@@ -192,12 +303,17 @@ export async function runOnce(opts) {
|
|
|
192
303
|
}
|
|
193
304
|
finally {
|
|
194
305
|
doneSigint();
|
|
306
|
+
try {
|
|
307
|
+
await closeMcp?.();
|
|
308
|
+
}
|
|
309
|
+
catch { /* ignore */ }
|
|
195
310
|
}
|
|
196
311
|
if (store && sessionId) {
|
|
197
312
|
// Finalize session status
|
|
198
313
|
const statusMap = {
|
|
199
314
|
complete: 'complete',
|
|
200
315
|
max_steps: 'max_steps',
|
|
316
|
+
limit: 'max_steps',
|
|
201
317
|
aborted: 'aborted',
|
|
202
318
|
no_final: 'aborted',
|
|
203
319
|
verify_failed: 'verify_failed',
|
|
@@ -221,7 +337,21 @@ export async function runOnce(opts) {
|
|
|
221
337
|
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
222
338
|
else
|
|
223
339
|
stderr.write(`klyro: hit max steps (${result.steps}); consider raising --max-steps\n`);
|
|
224
|
-
return
|
|
340
|
+
return 7;
|
|
341
|
+
}
|
|
342
|
+
if (result.status === 'limit') {
|
|
343
|
+
if (output === 'json')
|
|
344
|
+
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps, text: result.finalText }) + '\n');
|
|
345
|
+
else
|
|
346
|
+
stderr.write(`klyro: stopped early: ${result.finalText || result.status} (after ${result.steps} steps)\n`);
|
|
347
|
+
return 7;
|
|
348
|
+
}
|
|
349
|
+
if (result.status === 'stuck') {
|
|
350
|
+
if (output === 'json')
|
|
351
|
+
stdout.write(JSON.stringify({ kind: 'final', status: result.status, steps: result.steps }) + '\n');
|
|
352
|
+
else
|
|
353
|
+
stderr.write(`klyro: stuck — repeated the same action with no progress; aborting after ${result.steps} steps\n`);
|
|
354
|
+
return 7;
|
|
225
355
|
}
|
|
226
356
|
if (result.status === 'aborted') {
|
|
227
357
|
return 130;
|
|
@@ -229,14 +359,14 @@ export async function runOnce(opts) {
|
|
|
229
359
|
if (result.status === 'no_final') {
|
|
230
360
|
if (output !== 'json')
|
|
231
361
|
stderr.write('klyro: provider error — no final answer\n');
|
|
232
|
-
return
|
|
362
|
+
return 5;
|
|
233
363
|
}
|
|
234
364
|
if (result.status === 'verify_failed') {
|
|
235
365
|
if (output === 'json')
|
|
236
366
|
stdout.write(JSON.stringify({ kind: 'final', status: 'verify_failed', failureType: result.verification?.failureType }) + '\n');
|
|
237
367
|
else
|
|
238
368
|
stderr.write(`klyro: verification failed after ${result.verification?.attempts ?? 3} repairs — see output above\n`);
|
|
239
|
-
return
|
|
369
|
+
return 8;
|
|
240
370
|
}
|
|
241
371
|
// 6.5 — --require-verify: if edits were made but verification never passed, exit 8
|
|
242
372
|
if (opts.requireVerify && result.verification && !result.verification.ok) {
|
|
@@ -262,7 +392,9 @@ async function dryRunReport(opts) {
|
|
|
262
392
|
// Assemble the REAL prompt (Level-6 context + KLYRO.md), not the bare base —
|
|
263
393
|
// otherwise dry-run shows a different prompt than production runs use.
|
|
264
394
|
const systemPromptFn = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt);
|
|
265
|
-
const
|
|
395
|
+
const { resolveSystemPrompt } = await import('../agent/runtime.js');
|
|
396
|
+
const { system, suffix } = resolveSystemPrompt(systemPromptFn, { cwd: opts.cwd });
|
|
397
|
+
const systemPrompt = suffix ? `${system}\n\n${suffix}` : system;
|
|
266
398
|
const registry = builtinRegistry();
|
|
267
399
|
const rules = builtinRules();
|
|
268
400
|
const report = {
|
|
@@ -287,23 +419,53 @@ function defaultRunSystemPrompt(_ctx) {
|
|
|
287
419
|
'Solve it by calling tools as needed. When done, produce a short final text answer.',
|
|
288
420
|
'Do not invent file paths. Do not call tools outside the working directory.',
|
|
289
421
|
].join(' ');
|
|
290
|
-
return _ctx.telemetry ?
|
|
422
|
+
return _ctx.telemetry ? { system: base, suffix: _ctx.telemetry } : { system: base };
|
|
291
423
|
}
|
|
292
424
|
/** Wrap a system-prompt fn to inject Level-6 context (project map etc.) + KLYRO.md (4.4). */
|
|
293
425
|
export async function makeRunSystemPrompt(cwd, base) {
|
|
294
426
|
const ctxBlock = await buildLevel6Context({ cwd });
|
|
295
427
|
const prefix = ctxBlock.formatted ? `\n\n<context>\n${ctxBlock.formatted}\n</context>` : '';
|
|
428
|
+
// Session memory: .klyro/memory/session-notes.md injected so memory_write
|
|
429
|
+
// actually takes effect (redacted at write time — see context/memory.ts).
|
|
430
|
+
const memBlock = memoryBlock(cwd);
|
|
296
431
|
let klyroBlock = '';
|
|
297
432
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
433
|
+
// P0.3 trust gate with headless approve-if-store-known: approve(file) is
|
|
434
|
+
// trust.isTrusted(file), so REPL-persisted approvals are honored with no
|
|
435
|
+
// prompting; unknown/changed files stay excluded and each exclusion is
|
|
436
|
+
// bus-visible.
|
|
437
|
+
const { loadKlyroMdFiles } = await import('../context/klyro-md.js');
|
|
438
|
+
const { ContextTrust } = await import('../context/trust.js');
|
|
439
|
+
const { globalBus } = await import('../events/bus.js');
|
|
440
|
+
const trust = new ContextTrust();
|
|
441
|
+
// Headless approve-if-store-known (no prompts): approve(file) is
|
|
442
|
+
// trust.isTrusted(file), so REPL-persisted approvals are honored;
|
|
443
|
+
// unknown/changed files stay excluded.
|
|
444
|
+
const approve = (file) => trust.isTrusted(file);
|
|
445
|
+
const files = await loadKlyroMdFiles(cwd);
|
|
446
|
+
const { trusted, untrusted } = trust.check(files);
|
|
447
|
+
for (const { file, reason } of untrusted) {
|
|
448
|
+
globalBus.emit({ type: 'context.trust_prompt', ts: Date.now(), sessionId: 'ephemeral', path: file.path, reason, trusted: false });
|
|
449
|
+
}
|
|
450
|
+
if (untrusted.length > 0) {
|
|
451
|
+
stderr.write(`klyro: trust gate excluded ${untrusted.length} unapproved context file(s): ${untrusted.map((u) => u.file.path).join(', ')}\n`);
|
|
452
|
+
}
|
|
453
|
+
const keptFiles = trusted.filter(approve);
|
|
454
|
+
const kept = keptFiles.map((f) => `# ${f.path}\n${f.content}`).join('\n\n---\n\n');
|
|
455
|
+
if (kept)
|
|
456
|
+
klyroBlock = `\n\n<KLYRO.md>\n${kept.slice(0, 4000)}\n</KLYRO.md>`;
|
|
302
457
|
}
|
|
303
458
|
catch { /* ignore */ }
|
|
304
459
|
return (ctx) => {
|
|
305
|
-
const
|
|
306
|
-
|
|
460
|
+
const r = base(ctx);
|
|
461
|
+
if (typeof r === 'string') {
|
|
462
|
+
// Legacy string fn: keep the exact legacy concatenation behavior.
|
|
463
|
+
const t = ctx.telemetry ? '\n\n' + ctx.telemetry : '';
|
|
464
|
+
return r + prefix + klyroBlock + memBlock + t;
|
|
465
|
+
}
|
|
466
|
+
// Split shape: Level-6/KLYRO.md join the stable prefix; telemetry stays
|
|
467
|
+
// the volatile suffix for cache-friendly adapters.
|
|
468
|
+
return { system: r.system + prefix + klyroBlock + memBlock, ...(r.suffix !== undefined ? { suffix: r.suffix } : {}) };
|
|
307
469
|
};
|
|
308
470
|
}
|
|
309
471
|
export function loadTranscript(path) {
|
package/dist/cli/update.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* klyro update — check registry for newer version, cached 24h.
|
|
3
3
|
* Env KLYRO_NO_UPDATE_CHECK=1 disables.
|
|
4
|
+
*
|
|
5
|
+
* Integrity: before recommending `npm i`, we verify the tarball's SRI hash
|
|
6
|
+
* (sha512) against the registry's recorded `dist.integrity`. An install is
|
|
7
|
+
* only recommended when the download hash matches, so a tampered CDN or
|
|
8
|
+
* MITM registry response can't push a malicious binary to the operator.
|
|
4
9
|
*/
|
|
5
10
|
export declare function checkForUpdate(current: string): Promise<string | null>;
|
|
6
11
|
export declare function runUpdate(): Promise<number>;
|
package/dist/cli/update.js
CHANGED
|
@@ -1,15 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* klyro update — check registry for newer version, cached 24h.
|
|
3
3
|
* Env KLYRO_NO_UPDATE_CHECK=1 disables.
|
|
4
|
+
*
|
|
5
|
+
* Integrity: before recommending `npm i`, we verify the tarball's SRI hash
|
|
6
|
+
* (sha512) against the registry's recorded `dist.integrity`. An install is
|
|
7
|
+
* only recommended when the download hash matches, so a tampered CDN or
|
|
8
|
+
* MITM registry response can't push a malicious binary to the operator.
|
|
4
9
|
*/
|
|
5
10
|
import * as fs from 'node:fs/promises';
|
|
6
11
|
import * as path from 'node:path';
|
|
7
12
|
import * as os from 'node:os';
|
|
13
|
+
import { createHash } from 'node:crypto';
|
|
8
14
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const REGISTRY_BASE = 'https://registry.npmjs.org/klyro';
|
|
16
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
17
|
+
const TARBALL_TIMEOUT_MS = 10000;
|
|
9
18
|
function cachePath() {
|
|
19
|
+
// KLYRO_UPDATE_CACHE lets tests isolate the on-disk cache (and power users
|
|
20
|
+
// relocate it); default is the home cache like the rest of Klyro.
|
|
21
|
+
const override = process.env.KLYRO_UPDATE_CACHE;
|
|
22
|
+
if (override)
|
|
23
|
+
return override;
|
|
10
24
|
const home = os.homedir() || process.cwd();
|
|
11
25
|
return path.join(home, '.klyro', 'update-cache.json');
|
|
12
26
|
}
|
|
27
|
+
/** SRI string may carry multiple hashes parsable with `pick`; we accept sha512 or sha256. */
|
|
28
|
+
function parseSRI(integrity) {
|
|
29
|
+
if (!integrity)
|
|
30
|
+
return null;
|
|
31
|
+
for (const part of integrity.split(/\s+/)) {
|
|
32
|
+
const m = /^(sha256|sha512)-([A-Za-z0-9+/=]+)$/.exec(part);
|
|
33
|
+
if (m)
|
|
34
|
+
return { algo: m[1], digest: m[2] };
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/** Fetch with a timeout; rejects on non-2xx. */
|
|
39
|
+
async function fetchWithTimeout(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
40
|
+
const ctrl = new AbortController();
|
|
41
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
throw new Error(`HTTP ${res.status}`);
|
|
46
|
+
return res;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(t);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Download the tarball and confirm its hash equals the registry's SRI digest. */
|
|
53
|
+
async function verifyTarballIntegrity(dist) {
|
|
54
|
+
const sri = parseSRI(dist.integrity);
|
|
55
|
+
const tarball = dist.tarball;
|
|
56
|
+
if (!sri || !tarball)
|
|
57
|
+
return false;
|
|
58
|
+
const res = await fetchWithTimeout(tarball, TARBALL_TIMEOUT_MS);
|
|
59
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
60
|
+
const actual = createHash(sri.algo).update(buf).digest('base64');
|
|
61
|
+
return actual === sri.digest;
|
|
62
|
+
}
|
|
13
63
|
export async function checkForUpdate(current) {
|
|
14
64
|
if (process.env.KLYRO_NO_UPDATE_CHECK === '1')
|
|
15
65
|
return null;
|
|
@@ -25,18 +75,20 @@ export async function checkForUpdate(current) {
|
|
|
25
75
|
// ignore
|
|
26
76
|
}
|
|
27
77
|
try {
|
|
28
|
-
const
|
|
29
|
-
const t = setTimeout(() => ctrl.abort(), 3000);
|
|
30
|
-
const res = await fetch('https://registry.npmjs.org/klyro/latest', { signal: ctrl.signal });
|
|
31
|
-
clearTimeout(t);
|
|
32
|
-
if (!res.ok)
|
|
33
|
-
return null;
|
|
78
|
+
const res = await fetchWithTimeout(`${REGISTRY_BASE}/latest`);
|
|
34
79
|
const json = (await res.json());
|
|
35
80
|
const latest = json.version ?? '';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
if (latest && latest !== current) {
|
|
82
|
+
// Verify the tarball's integrity before caching/recommending this version.
|
|
83
|
+
const verRes = await fetchWithTimeout(`${REGISTRY_BASE}/${encodeURIComponent(latest)}`);
|
|
84
|
+
const verJson = (await verRes.json());
|
|
85
|
+
const ok = await verifyTarballIntegrity(verJson.dist ?? {});
|
|
86
|
+
if (!ok)
|
|
87
|
+
return null; // integrity mismatch — treat as no update, never recommend it
|
|
88
|
+
await fs.mkdir(path.dirname(cache), { recursive: true });
|
|
89
|
+
await fs.writeFile(cache, JSON.stringify({ at: Date.now(), latest }), 'utf-8');
|
|
39
90
|
return latest;
|
|
91
|
+
}
|
|
40
92
|
}
|
|
41
93
|
catch {
|
|
42
94
|
// network failure — silent
|
|
@@ -55,7 +107,7 @@ export async function runUpdate() {
|
|
|
55
107
|
const cur = pkg.version ?? '0.0.0';
|
|
56
108
|
const latest = await checkForUpdate(cur);
|
|
57
109
|
if (latest) {
|
|
58
|
-
process.stdout.write(`Update available: ${cur} → ${latest}\n npm i -g klyro@latest\n`);
|
|
110
|
+
process.stdout.write(`Update available: ${cur} → ${latest} (integrity verified)\n npm i -g klyro@latest\n`);
|
|
59
111
|
}
|
|
60
112
|
else {
|
|
61
113
|
process.stdout.write(`klyro ${cur} is latest\n`);
|
|
@@ -3,6 +3,8 @@ export interface ImportGraph {
|
|
|
3
3
|
edges: Map<string, Set<string>>;
|
|
4
4
|
mtime: number;
|
|
5
5
|
}
|
|
6
|
+
/** Invalidate the import-graph cache (tests + callers that mutate the tree). */
|
|
7
|
+
export declare function clearImportGraphCache(): void;
|
|
6
8
|
export declare function buildImportGraph(cwd: string): Promise<ImportGraph>;
|
|
7
9
|
export declare function importsOf(cwd: string, file: string): Promise<string[]>;
|
|
8
10
|
export declare function importersOf(cwd: string, file: string): Promise<string[]>;
|
|
@@ -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) {
|
|
@@ -6,5 +6,11 @@
|
|
|
6
6
|
* secret files into context), each file is capped, and total output is
|
|
7
7
|
* capped so a giant monorepo doc can't blow the context budget.
|
|
8
8
|
*/
|
|
9
|
+
export interface KlyroMdFile {
|
|
10
|
+
path: string;
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
/** Per-file loader (used by the trust gate to approve/hash individual files). */
|
|
14
|
+
export declare function loadKlyroMdFiles(cwd: string): Promise<KlyroMdFile[]>;
|
|
9
15
|
export declare function loadKlyroMd(cwd: string): Promise<string>;
|
|
10
16
|
export declare function handleInit(cwd: string): Promise<string>;
|
package/dist/context/klyro-md.js
CHANGED
|
@@ -14,37 +14,46 @@ const MAX_TOTAL_CHARS = 8000;
|
|
|
14
14
|
function cap(s, n) {
|
|
15
15
|
return s.length > n ? s.slice(0, n) + `\n... [truncated ${s.length - n} chars]` : s;
|
|
16
16
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const push = (s) => {
|
|
21
|
-
if (total >= MAX_TOTAL_CHARS)
|
|
22
|
-
return;
|
|
23
|
-
const room = MAX_TOTAL_CHARS - total;
|
|
24
|
-
const chunk = s.length > room ? s.slice(0, room) + '\n... [truncated]' : s;
|
|
25
|
-
parts.push(chunk);
|
|
26
|
-
total += chunk.length;
|
|
27
|
-
};
|
|
28
|
-
// Global
|
|
17
|
+
/** Per-file loader (used by the trust gate to approve/hash individual files). */
|
|
18
|
+
export async function loadKlyroMdFiles(cwd) {
|
|
19
|
+
const files = [];
|
|
29
20
|
const home = os.homedir();
|
|
30
21
|
if (home) {
|
|
31
22
|
for (const p of [path.join(home, '.klyro', 'KLYRO.md'), path.join(home, '.klyro', 'KLYRO.local.md')]) {
|
|
32
23
|
try {
|
|
33
24
|
const t = await fs.readFile(p, 'utf-8');
|
|
34
|
-
push(
|
|
25
|
+
files.push({ path: p, content: cap(t, MAX_FILE_CHARS) });
|
|
35
26
|
}
|
|
36
27
|
catch { /* ignore */ }
|
|
37
28
|
}
|
|
38
29
|
}
|
|
39
|
-
//
|
|
40
|
-
for
|
|
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']) {
|
|
41
34
|
const p = path.join(cwd, name);
|
|
42
35
|
try {
|
|
43
36
|
const t = await fs.readFile(p, 'utf-8');
|
|
44
|
-
push(
|
|
37
|
+
files.push({ path: p, content: cap(await resolveImports(t, path.dirname(p), cwd), MAX_FILE_CHARS) });
|
|
45
38
|
}
|
|
46
39
|
catch { /* ignore */ }
|
|
47
40
|
}
|
|
41
|
+
return files;
|
|
42
|
+
}
|
|
43
|
+
export async function loadKlyroMd(cwd) {
|
|
44
|
+
const parts = [];
|
|
45
|
+
let total = 0;
|
|
46
|
+
const push = (s) => {
|
|
47
|
+
if (total >= MAX_TOTAL_CHARS)
|
|
48
|
+
return;
|
|
49
|
+
const room = MAX_TOTAL_CHARS - total;
|
|
50
|
+
const chunk = s.length > room ? s.slice(0, room) + '\n... [truncated]' : s;
|
|
51
|
+
parts.push(chunk);
|
|
52
|
+
total += chunk.length;
|
|
53
|
+
};
|
|
54
|
+
for (const f of await loadKlyroMdFiles(cwd)) {
|
|
55
|
+
push(`# ${f.path}\n${f.content}`);
|
|
56
|
+
}
|
|
48
57
|
return parts.join('\n\n---\n\n');
|
|
49
58
|
}
|
|
50
59
|
async function resolveImports(text, base, root, depth = 0) {
|
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;
|