klyro 1.0.16 → 1.0.17
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.js +26 -1
- package/dist/agent/provider-adapter.d.ts +7 -0
- package/dist/agent/provider-adapter.js +26 -1
- package/dist/agent/runtime.d.ts +7 -0
- package/dist/agent/runtime.js +18 -21
- package/dist/chat.js +16 -56
- package/dist/cli/completion.js +0 -3
- package/dist/cli/doctor.d.ts +13 -0
- package/dist/cli/doctor.js +133 -1
- package/dist/cli/errors.js +1 -1
- package/dist/cli/eval.js +1 -3
- package/dist/cli/markdown.js +1 -1
- package/dist/cli/repl.js +13 -24
- package/dist/cli/run.d.ts +32 -0
- package/dist/cli/run.js +77 -9
- package/dist/cli/update.js +7 -1
- package/dist/context/accounting.js +1 -1
- package/dist/context/level7.d.ts +4 -0
- package/dist/context/level7.js +20 -1
- package/dist/context/project-map.js +14 -13
- package/dist/eval/harness.d.ts +1 -1
- package/dist/eval/harness.js +2 -2
- package/dist/index.js +52 -1
- package/dist/persistence/store.js +2 -2
- package/dist/policy/approval.js +3 -0
- package/dist/policy/path-guard.d.ts +9 -4
- package/dist/policy/path-guard.js +47 -23
- package/dist/providers/endpoints.js +8 -0
- package/dist/providers/model-info.d.ts +1 -0
- package/dist/providers/model-info.js +19 -0
- package/dist/providers.js +6 -2
- package/dist/renderers/terminal.d.ts +7 -0
- package/dist/renderers/terminal.js +52 -0
- package/dist/repl.d.ts +9 -0
- package/dist/repl.js +24 -5
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/proxy.d.ts +37 -0
- package/dist/shared/proxy.js +360 -0
- package/dist/tools/agent/task-list.d.ts +1 -1
- package/dist/tools/fs/apply-patch.js +4 -4
- package/dist/tools/fs/edit-file.js +2 -4
- package/dist/tools/fs/list-dir.js +1 -1
- package/dist/tools/fs/multi-edit.js +2 -2
- package/dist/tools/fs/read-file.js +1 -1
- package/dist/tools/fs/write-file.js +2 -2
- package/dist/tools/lsp/diagnostics.js +2 -3
- package/dist/tools/plan/todo-write.d.ts +1 -1
- package/dist/tools/search/dependencies.d.ts +4 -4
- package/dist/tools/search/dependencies.js +1 -1
- package/dist/tools/search/glob.js +1 -1
- package/dist/tools/search/grep.js +1 -1
- package/dist/tools/search/recent-files.js +1 -1
- package/dist/tools/search/search-files.js +1 -1
- package/dist/tools/web/web-fetch.js +2 -1
- package/dist/tools/web/web-search.js +3 -1
- package/dist/tui/app.js +80 -9
- package/dist/tui/app.test.js +48 -1
- package/dist/tui/measure.js +1 -0
- package/dist/tui/mouse.d.ts +7 -5
- package/dist/tui/mouse.js +9 -6
- package/dist/tui/scroll-flow.test.js +0 -1
- package/dist/util/log.d.ts +19 -10
- package/dist/util/log.js +87 -52
- package/dist/verification/classify.d.ts +1 -1
- package/dist/verification/classify.js +2 -2
- package/dist/verification/engine.js +0 -6
- package/dist/verification/scoped.js +6 -4
- package/package.json +6 -1
package/dist/cli/repl.js
CHANGED
|
@@ -103,6 +103,7 @@ export async function startRepl(opts = {}) {
|
|
|
103
103
|
// sanitizeForPrompt in sibling-owned policy/approval.ts — intentionally
|
|
104
104
|
// not imported; kept local to avoid coupling the REPL to that module).
|
|
105
105
|
function sanitizeForPrompt(s) {
|
|
106
|
+
// eslint-disable-next-line no-control-regex -- intentional: stripping C0 controls + CSI sequences
|
|
106
107
|
return s.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]|\x1b\[[0-9;]*[A-Za-z]/g, '').slice(0, 120);
|
|
107
108
|
}
|
|
108
109
|
async function loadMcpTools() {
|
|
@@ -190,7 +191,7 @@ export async function startRepl(opts = {}) {
|
|
|
190
191
|
// become fallback adapters for agent runs in this session. Live
|
|
191
192
|
// /provider switches rebuild a single adapter (buildAdapter) and bypass
|
|
192
193
|
// the chain — documented, not a bug.
|
|
193
|
-
|
|
194
|
+
const replFailoverAdapters = [];
|
|
194
195
|
try {
|
|
195
196
|
const { resolveProviderChain } = await import('./config.js');
|
|
196
197
|
const chain = await resolveProviderChain(cwd);
|
|
@@ -372,11 +373,12 @@ export async function startRepl(opts = {}) {
|
|
|
372
373
|
// ── Full-screen takeover like OpenCode — always when klyro in TTY (user explicitly wants it)
|
|
373
374
|
// Scroll now works correctly via internal viewport, not native terminal scroll
|
|
374
375
|
const isAltScreen = useTui && !!process.stdout.isTTY && process.env.KLYRO_NO_ALT !== '1';
|
|
375
|
-
// Mouse reporting is
|
|
376
|
-
// clicks
|
|
377
|
-
// selection
|
|
378
|
-
//
|
|
379
|
-
//
|
|
376
|
+
// Mouse reporting is ON by default (opencode parity): the wheel scrolls
|
|
377
|
+
// the chat, clicks are swallowed by the app. That means plain mouse
|
|
378
|
+
// selection/right-click go to the app, not the terminal — use Shift+drag
|
|
379
|
+
// to select and Shift+right-click to paste, or set KLYRO_MOUSE=0 for fully
|
|
380
|
+
// native selection (then scroll with PgUp/PgDn, Ctrl+U/D, Shift+↑/↓,
|
|
381
|
+
// Home/End, Space). Bracketed (keyboard) paste is unaffected and always on.
|
|
380
382
|
const mouseReporting = isAltScreen && isMouseReportingEnabled();
|
|
381
383
|
const enterAlt = () => {
|
|
382
384
|
if (!isAltScreen)
|
|
@@ -501,8 +503,9 @@ export async function startRepl(opts = {}) {
|
|
|
501
503
|
if (consoleRing.length > 200)
|
|
502
504
|
consoleRing.splice(0, consoleRing.length - 200);
|
|
503
505
|
try {
|
|
506
|
+
// KLYRO_LOG_DIR relocates the debug log (1.5); default is ~/.klyro.
|
|
504
507
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? cwd;
|
|
505
|
-
const dir = nodePath.join(home, '.klyro');
|
|
508
|
+
const dir = process.env.KLYRO_LOG_DIR ?? nodePath.join(home, '.klyro');
|
|
506
509
|
fsSync.mkdirSync(dir, { recursive: true });
|
|
507
510
|
const logFile = nodePath.join(dir, 'debug.log');
|
|
508
511
|
// Rotation: never let the debug log grow without bound (2 MiB cap).
|
|
@@ -724,6 +727,7 @@ export async function startRepl(opts = {}) {
|
|
|
724
727
|
}
|
|
725
728
|
catch { /* silent */ }
|
|
726
729
|
})();
|
|
730
|
+
// eslint-disable-next-line prefer-const -- single assignment, but `let` is required: declaration precedes this for TDZ-safe handler wiring.
|
|
727
731
|
app = render(React.createElement(App, {
|
|
728
732
|
initialModel: model,
|
|
729
733
|
maxSteps: currentMaxSteps,
|
|
@@ -778,6 +782,7 @@ export async function startRepl(opts = {}) {
|
|
|
778
782
|
}), { patchConsole: false });
|
|
779
783
|
// Install SIGINT handler only after app exists (avoids TDZ) and use once
|
|
780
784
|
// On Ctrl+C in alt-screen, leave alt before exit so shell is restored
|
|
785
|
+
// eslint-disable-next-line prefer-const -- see above: declaration order is load-bearing.
|
|
781
786
|
sigintHandler = () => {
|
|
782
787
|
ac.abort();
|
|
783
788
|
queuedStatus({ status: 'aborted' });
|
|
@@ -861,9 +866,6 @@ export async function startRepl(opts = {}) {
|
|
|
861
866
|
return;
|
|
862
867
|
}
|
|
863
868
|
}
|
|
864
|
-
let activeCallId = null;
|
|
865
|
-
let activeCallName = null;
|
|
866
|
-
let activeCallArgs = '';
|
|
867
869
|
let runEndStatus = 'error';
|
|
868
870
|
try {
|
|
869
871
|
const result = await run({
|
|
@@ -902,14 +904,6 @@ export async function startRepl(opts = {}) {
|
|
|
902
904
|
else if (ev.kind === 'checkpoint_saved') {
|
|
903
905
|
queuedStatus({ status: 'running' });
|
|
904
906
|
}
|
|
905
|
-
else if (ev.kind === 'tool_call_start') {
|
|
906
|
-
activeCallId = ev.id;
|
|
907
|
-
activeCallName = ev.name;
|
|
908
|
-
activeCallArgs = '';
|
|
909
|
-
}
|
|
910
|
-
else if (ev.kind === 'tool_call_delta') {
|
|
911
|
-
activeCallArgs += ev.argsJson;
|
|
912
|
-
}
|
|
913
907
|
else if (ev.kind === 'tool_call_end') {
|
|
914
908
|
pendingToolIds.add(ev.id);
|
|
915
909
|
queuedAppend({
|
|
@@ -920,9 +914,6 @@ export async function startRepl(opts = {}) {
|
|
|
920
914
|
args: JSON.stringify(ev.input, null, 2),
|
|
921
915
|
status: 'running',
|
|
922
916
|
});
|
|
923
|
-
activeCallId = null;
|
|
924
|
-
activeCallName = null;
|
|
925
|
-
activeCallArgs = '';
|
|
926
917
|
}
|
|
927
918
|
else if (ev.kind === 'policy_decision') {
|
|
928
919
|
queuedAppend({
|
|
@@ -1354,8 +1345,6 @@ export async function startRepl(opts = {}) {
|
|
|
1354
1345
|
}
|
|
1355
1346
|
case 'context': {
|
|
1356
1347
|
const { renderContextBreakdown } = await import('./context.js');
|
|
1357
|
-
// use last transcript via closure? approximate with empty
|
|
1358
|
-
const { accounting } = await import('../context/accounting.js');
|
|
1359
1348
|
const sys = ''; // system prompt approx
|
|
1360
1349
|
const breakdown = renderContextBreakdown(sys, []);
|
|
1361
1350
|
queuedAppend({ id: `ctx-${Date.now()}`, kind: 'text', text: breakdown, role: 'assistant' });
|
|
@@ -2689,7 +2678,7 @@ export async function startRepl(opts = {}) {
|
|
|
2689
2678
|
' Enter send · Shift+Enter newline · Tab complete slash · Esc drop queued / Esc×2 cancel run',
|
|
2690
2679
|
' Ctrl+C cancel (1st) / quit (2nd) · Ctrl+O expand last tool group · Ctrl+G jump bottom',
|
|
2691
2680
|
' PgUp/PgDn or Ctrl+U/Ctrl+D half-page · Ctrl+Home/End top/bottom · Home/End jump · Space jump to unread',
|
|
2692
|
-
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line · ↑/↓ and Ctrl+P/N browse input history only · PgUp/Dn or Ctrl+U/D scroll ·
|
|
2681
|
+
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line · ↑/↓ and Ctrl+P/N browse input history only · wheel scrolls · PgUp/Dn or Ctrl+U/D scroll · KLYRO_MOUSE=0 for native selection',
|
|
2693
2682
|
' Text selection/copy and right-click paste work natively · Shift+Enter newline · /vim toggles vim input mode · /keymap <note> saves a display note',
|
|
2694
2683
|
].join('\n'),
|
|
2695
2684
|
});
|
package/dist/cli/run.d.ts
CHANGED
|
@@ -15,9 +15,25 @@ export interface RunCliOptions {
|
|
|
15
15
|
cwd: string;
|
|
16
16
|
model: string;
|
|
17
17
|
maxSteps?: number;
|
|
18
|
+
/** Alias for maxSteps (3.5) — when both are set, the smaller wins. */
|
|
19
|
+
maxTurns?: number;
|
|
18
20
|
maxTokens?: number;
|
|
19
21
|
temperature?: number;
|
|
22
|
+
/** Reasoning effort for reasoning models (OpenAI `reasoning_effort`). */
|
|
23
|
+
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
20
24
|
systemPrompt?: SystemPromptFn;
|
|
25
|
+
/** Replace the assembled system prompt wholesale (2.3). */
|
|
26
|
+
systemPromptText?: string;
|
|
27
|
+
/** Extra instructions appended after the assembled system prompt (2.3). */
|
|
28
|
+
appendSystemPrompt?: string;
|
|
29
|
+
/** Permission mode override (3.4): default|accept-edits|plan|auto. */
|
|
30
|
+
permissionMode?: import('../policy/engine.js').PermissionMode;
|
|
31
|
+
/** Tool names pre-approved for this run (added to allow rules). */
|
|
32
|
+
allowedTools?: string[] | string;
|
|
33
|
+
/** Tool names denied for this run (deny always wins). */
|
|
34
|
+
disallowedTools?: string[] | string;
|
|
35
|
+
/** Extra sandbox directories, as with /add-dir. */
|
|
36
|
+
addDir?: string[] | string;
|
|
21
37
|
baseUrl?: string;
|
|
22
38
|
apiKey?: string;
|
|
23
39
|
timeoutMs?: number;
|
|
@@ -76,6 +92,22 @@ export interface RunCliOptions {
|
|
|
76
92
|
agent?: string;
|
|
77
93
|
maxDepth?: number;
|
|
78
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Split a comma-separated tool/dir list (string or string[]) into names.
|
|
97
|
+
* Exported for tests.
|
|
98
|
+
*/
|
|
99
|
+
export declare function splitToolList(v: string[] | string | undefined): string[];
|
|
100
|
+
/** Smaller of two optional caps (--max-turns aliases --max-steps). Exported for tests. */
|
|
101
|
+
export declare function minDefined(a: number | undefined, b: number | undefined): number | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Layer --system-prompt/--append-system-prompt over the assembled prompt
|
|
104
|
+
* (2.3): replace wins wholesale, append rides after the assembled system
|
|
105
|
+
* (preserving any suffix the base carries). Exported for tests.
|
|
106
|
+
*/
|
|
107
|
+
export declare function withSystemPromptOverrides(base: SystemPromptFn, opts: {
|
|
108
|
+
replace?: string;
|
|
109
|
+
append?: string;
|
|
110
|
+
}): SystemPromptFn;
|
|
79
111
|
/**
|
|
80
112
|
* Double-Ctrl+C detector (pure, exported for tests): the second SIGINT
|
|
81
113
|
* within 1500ms of the first forces `process.exit(130)`. The live handler
|
package/dist/cli/run.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* even on a long task. Tool calls and policy decisions go to stderr so
|
|
8
8
|
* they don't pollute the captured `finalText` if the user pipes.
|
|
9
9
|
*/
|
|
10
|
+
import * as path from 'node:path';
|
|
10
11
|
import { stdout, stderr } from 'node:process';
|
|
11
12
|
import { httpChatAdapter } from '../agent/provider-adapter.js';
|
|
12
13
|
import { anthropicAdapter } from '../agent/anthropic-adapter.js';
|
|
@@ -20,8 +21,47 @@ import { redact } from '../policy/secret-redactor.js';
|
|
|
20
21
|
import { buildLevel6Context } from '../context/level6.js';
|
|
21
22
|
import { memoryBlock } from '../context/memory.js';
|
|
22
23
|
import { estimateCost } from '../providers/model-info.js';
|
|
24
|
+
import { resolveModelAlias } from '../providers/model-info.js';
|
|
25
|
+
import { logDebug, logInfo } from '../util/log.js';
|
|
23
26
|
import { resolveSessionId } from '../persistence/session.js';
|
|
24
27
|
import * as fs from 'node:fs';
|
|
28
|
+
/**
|
|
29
|
+
* Split a comma-separated tool/dir list (string or string[]) into names.
|
|
30
|
+
* Exported for tests.
|
|
31
|
+
*/
|
|
32
|
+
export function splitToolList(v) {
|
|
33
|
+
const arr = v === undefined ? [] : Array.isArray(v) ? v : [v];
|
|
34
|
+
return arr.flatMap((s) => String(s).split(',')).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
35
|
+
}
|
|
36
|
+
/** Smaller of two optional caps (--max-turns aliases --max-steps). Exported for tests. */
|
|
37
|
+
export function minDefined(a, b) {
|
|
38
|
+
if (a === undefined)
|
|
39
|
+
return b;
|
|
40
|
+
if (b === undefined)
|
|
41
|
+
return a;
|
|
42
|
+
return Math.min(a, b);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Layer --system-prompt/--append-system-prompt over the assembled prompt
|
|
46
|
+
* (2.3): replace wins wholesale, append rides after the assembled system
|
|
47
|
+
* (preserving any suffix the base carries). Exported for tests.
|
|
48
|
+
*/
|
|
49
|
+
export function withSystemPromptOverrides(base, opts) {
|
|
50
|
+
if (opts.replace === undefined && opts.append === undefined)
|
|
51
|
+
return base;
|
|
52
|
+
return (ctx) => {
|
|
53
|
+
if (opts.replace !== undefined) {
|
|
54
|
+
return opts.append ? `${opts.replace}\n\n${opts.append}` : opts.replace;
|
|
55
|
+
}
|
|
56
|
+
const r = base(ctx);
|
|
57
|
+
const system = typeof r === 'string' ? r : r.system;
|
|
58
|
+
const suffix = typeof r === 'string' ? undefined : r.suffix;
|
|
59
|
+
const out = { system: `${system}\n\n${opts.append}` };
|
|
60
|
+
if (suffix !== undefined)
|
|
61
|
+
out.suffix = suffix;
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
25
65
|
/**
|
|
26
66
|
* Double-Ctrl+C detector (pure, exported for tests): the second SIGINT
|
|
27
67
|
* within 1500ms of the first forces `process.exit(130)`. The live handler
|
|
@@ -38,6 +78,9 @@ export async function runOnce(opts) {
|
|
|
38
78
|
loadDotenv(opts.cwd);
|
|
39
79
|
}
|
|
40
80
|
catch { /* ignore */ }
|
|
81
|
+
// Model aliases (2.2: sonnet/opus/haiku/gpt/local) resolve once here so
|
|
82
|
+
// the session store, cost math, and every adapter agree on the id.
|
|
83
|
+
opts = { ...opts, model: resolveModelAlias(opts.model) };
|
|
41
84
|
const output = opts.output ?? 'human';
|
|
42
85
|
if (opts.dryRun) {
|
|
43
86
|
return await dryRunReport(opts);
|
|
@@ -138,7 +181,18 @@ export async function runOnce(opts) {
|
|
|
138
181
|
catch { /* best-effort — single-provider run proceeds */ }
|
|
139
182
|
}
|
|
140
183
|
const registry = builtinRegistry();
|
|
141
|
-
const
|
|
184
|
+
const policyConfig = clonePolicyConfig();
|
|
185
|
+
// Explicit CLI overrides (3.4). Persisted rules load next via applyRules;
|
|
186
|
+
// the engine's deny-first precedence keeps any denial terminal.
|
|
187
|
+
if (opts.permissionMode !== undefined)
|
|
188
|
+
policyConfig.mode = opts.permissionMode;
|
|
189
|
+
for (const t of splitToolList(opts.allowedTools))
|
|
190
|
+
policyConfig.allow.push(t);
|
|
191
|
+
for (const t of splitToolList(opts.disallowedTools))
|
|
192
|
+
policyConfig.deny.push(t);
|
|
193
|
+
for (const d of splitToolList(opts.addDir))
|
|
194
|
+
policyConfig.additionalDirs.push(path.resolve(opts.cwd, d));
|
|
195
|
+
const policy = new PolicyEngine(builtinRules(), policyConfig);
|
|
142
196
|
// Persisted "always allow" patterns apply to one-shot runs too.
|
|
143
197
|
try {
|
|
144
198
|
const { loadPermissionRules } = await import('./config.js');
|
|
@@ -181,7 +235,7 @@ export async function runOnce(opts) {
|
|
|
181
235
|
}
|
|
182
236
|
catch { /* ignore — MCP is optional */ }
|
|
183
237
|
}
|
|
184
|
-
const systemPrompt = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt, opts.bare);
|
|
238
|
+
const systemPrompt = withSystemPromptOverrides(await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt, opts.bare), { replace: opts.systemPromptText, append: opts.appendSystemPrompt });
|
|
185
239
|
// Level 9 — session setup (create or resume). --bare skips persistence.
|
|
186
240
|
const persistEnabled = opts.persist !== false && !opts.bare;
|
|
187
241
|
let store;
|
|
@@ -219,9 +273,6 @@ export async function runOnce(opts) {
|
|
|
219
273
|
stderr.write(`klyro: resuming session ${sessionId.slice(0, 8)} — task: "${rec.task}"\n`);
|
|
220
274
|
}
|
|
221
275
|
}
|
|
222
|
-
else if (opts.resumePath) {
|
|
223
|
-
// already handled
|
|
224
|
-
}
|
|
225
276
|
else {
|
|
226
277
|
// Create new session
|
|
227
278
|
const rec = await store.create({ cwd: opts.cwd, task: opts.task, config: { model: opts.model, maxSteps: opts.maxSteps ?? 30 } });
|
|
@@ -313,23 +364,34 @@ export async function runOnce(opts) {
|
|
|
313
364
|
model: opts.model,
|
|
314
365
|
});
|
|
315
366
|
}
|
|
367
|
+
// 2.5 latency tracking: TTFT = first model event after run start.
|
|
368
|
+
const runStartMs = Date.now();
|
|
369
|
+
let ttftMs;
|
|
370
|
+
logDebug('run start', { model: opts.model, maxSteps: opts.maxSteps, bare: !!opts.bare });
|
|
316
371
|
try {
|
|
317
372
|
result = await run({
|
|
318
373
|
task: opts.task,
|
|
319
374
|
cwd: opts.cwd,
|
|
320
375
|
model: opts.model,
|
|
321
376
|
...(opts.bare ? { bare: true } : {}),
|
|
322
|
-
maxSteps: opts.maxSteps,
|
|
377
|
+
maxSteps: minDefined(opts.maxSteps, opts.maxTurns),
|
|
323
378
|
maxTokens: opts.maxTokens,
|
|
324
379
|
temperature: opts.temperature,
|
|
380
|
+
reasoningEffort: opts.reasoningEffort,
|
|
325
381
|
signal: ac.signal,
|
|
326
382
|
nonInteractive: true,
|
|
327
383
|
initialTranscript,
|
|
384
|
+
// Sandbox roots for file tools: cwd plus --add-dir extras (resolved
|
|
385
|
+
// absolute). checkAllowedPaths narrows to exactly this set.
|
|
386
|
+
allowedPaths: [path.resolve(opts.cwd), ...(policyConfig.additionalDirs ?? [])],
|
|
328
387
|
verify: verifyOpts,
|
|
329
388
|
persist: store && sessionId ? { store, sessionId } : undefined,
|
|
330
389
|
...(agentBridge ? { agentBridge } : {}),
|
|
331
390
|
...(parentContext ? { parentContext } : {}),
|
|
332
391
|
onEvent: (ev) => {
|
|
392
|
+
if (ttftMs === undefined && (ev.kind === 'text_delta' || ev.kind === 'tool_call_start')) {
|
|
393
|
+
ttftMs = Date.now() - runStartMs;
|
|
394
|
+
}
|
|
333
395
|
if (output === 'json') {
|
|
334
396
|
stdout.write(JSON.stringify(ev) + '\n');
|
|
335
397
|
return;
|
|
@@ -436,9 +498,12 @@ export async function runOnce(opts) {
|
|
|
436
498
|
// [budget] warnings. Human/silent → stderr one-liner; json → additive
|
|
437
499
|
// cost_usd/usage fields on the final object (purely additive, no shape break).
|
|
438
500
|
const finalCost = estimateCost(opts.model, result.usage.input, result.usage.output);
|
|
439
|
-
const
|
|
501
|
+
const totalMs = Date.now() - runStartMs;
|
|
502
|
+
logInfo('run finish', { status: result.status, steps: result.steps, toolCalls: result.toolCalls, totalMs });
|
|
503
|
+
const ttftPart = ttftMs !== undefined ? ` · ttft ${ttftMs}ms` : '';
|
|
504
|
+
const costLine = `$${finalCost.toFixed(4)} · ${result.usage.input} in / ${result.usage.output} out${result.usage.estimated ? ' (estimated)' : ''}${ttftPart} · total ${totalMs}ms`;
|
|
440
505
|
if (output === 'json') {
|
|
441
|
-
stdout.write(JSON.stringify({ kind: 'cost', cost_usd: finalCost, usage: result.usage }) + '\n');
|
|
506
|
+
stdout.write(JSON.stringify({ kind: 'cost', cost_usd: finalCost, usage: result.usage, ttft_ms: ttftMs ?? null, total_ms: totalMs }) + '\n');
|
|
442
507
|
}
|
|
443
508
|
else {
|
|
444
509
|
stderr.write(`klyro: cost ${costLine}\n`);
|
|
@@ -457,6 +522,8 @@ export async function runOnce(opts) {
|
|
|
457
522
|
toolCalls: result.toolCalls,
|
|
458
523
|
cost_usd: finalCost,
|
|
459
524
|
usage: result.usage,
|
|
525
|
+
ttft_ms: ttftMs ?? null,
|
|
526
|
+
total_ms: totalMs,
|
|
460
527
|
...(result.verification ? { verification: result.verification } : {}),
|
|
461
528
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
462
529
|
...extra,
|
|
@@ -523,7 +590,7 @@ export async function runOnce(opts) {
|
|
|
523
590
|
async function dryRunReport(opts) {
|
|
524
591
|
// Assemble the REAL prompt (Level-6 context + KLYRO.md), not the bare base —
|
|
525
592
|
// otherwise dry-run shows a different prompt than production runs use.
|
|
526
|
-
const systemPromptFn = await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt);
|
|
593
|
+
const systemPromptFn = withSystemPromptOverrides(await makeRunSystemPrompt(opts.cwd, opts.systemPrompt ?? defaultRunSystemPrompt), { replace: opts.systemPromptText, append: opts.appendSystemPrompt });
|
|
527
594
|
const { resolveSystemPrompt } = await import('../agent/runtime.js');
|
|
528
595
|
const { system, suffix } = resolveSystemPrompt(systemPromptFn, { cwd: opts.cwd });
|
|
529
596
|
const systemPrompt = suffix ? `${system}\n\n${suffix}` : system;
|
|
@@ -536,6 +603,7 @@ async function dryRunReport(opts) {
|
|
|
536
603
|
maxSteps: opts.maxSteps,
|
|
537
604
|
maxTokens: opts.maxTokens,
|
|
538
605
|
temperature: opts.temperature,
|
|
606
|
+
reasoningEffort: opts.reasoningEffort,
|
|
539
607
|
// Secrets must never leak into a printable report: redact both the
|
|
540
608
|
// assembled system prompt and the task before printing.
|
|
541
609
|
systemPrompt: redact(systemPrompt),
|
package/dist/cli/update.js
CHANGED
|
@@ -13,6 +13,7 @@ import * as fs from 'node:fs/promises';
|
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import * as os from 'node:os';
|
|
15
15
|
import { createHash } from 'node:crypto';
|
|
16
|
+
import { proxiedFetch } from '../shared/proxy.js';
|
|
16
17
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
17
18
|
const REGISTRY_BASE = 'https://registry.npmjs.org/klyro';
|
|
18
19
|
const FETCH_TIMEOUT_MS = 3000;
|
|
@@ -64,7 +65,7 @@ async function fetchWithTimeout(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
|
64
65
|
const ctrl = new AbortController();
|
|
65
66
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
66
67
|
try {
|
|
67
|
-
const res = await
|
|
68
|
+
const res = await proxiedFetch(url, { signal: ctrl.signal, timeoutMs });
|
|
68
69
|
if (!res.ok)
|
|
69
70
|
throw new Error(`HTTP ${res.status}`);
|
|
70
71
|
return res;
|
|
@@ -95,6 +96,11 @@ async function verifyTarballIntegrity(dist) {
|
|
|
95
96
|
export async function checkForUpdate(current) {
|
|
96
97
|
if (process.env.KLYRO_NO_UPDATE_CHECK === '1')
|
|
97
98
|
return null;
|
|
99
|
+
// Disabled in CI unless explicitly enabled (1.5): automated pipelines
|
|
100
|
+
// must never phone home or suggest interactive updates.
|
|
101
|
+
if ((process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true') && process.env.KLYRO_UPDATE_CHECK !== '1') {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
98
104
|
const cache = cachePath();
|
|
99
105
|
try {
|
|
100
106
|
const raw = await fs.readFile(cache, 'utf-8');
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* 8.1 — Accounting & stable assembly
|
|
3
3
|
* Live token estimate, ctx%, compactAt, reserveOutput, toolResultMax
|
|
4
4
|
*/
|
|
5
|
-
import { totalTokens } from './tokenizer.js';
|
|
6
5
|
import { getModelInfo } from '../providers/model-info.js';
|
|
6
|
+
import { totalTokens } from './tokenizer.js';
|
|
7
7
|
/**
|
|
8
8
|
* Single output-reserve used by every budget in the harness (displayed
|
|
9
9
|
* accounting, runtime enforcement, compaction elide). One constant so the
|
package/dist/context/level7.d.ts
CHANGED
|
@@ -61,6 +61,10 @@ export declare class RuntimeTelemetry {
|
|
|
61
61
|
private errorCount;
|
|
62
62
|
private recent;
|
|
63
63
|
constructor(opts?: Level7Options);
|
|
64
|
+
private muted;
|
|
65
|
+
/** Disable telemetry accumulation + reporting (also via KLYRO_TELEMETRY=0). */
|
|
66
|
+
mute(): void;
|
|
67
|
+
isMuted(): boolean;
|
|
64
68
|
setMaxSteps(n: number): void;
|
|
65
69
|
recordStepStart(step: number): void;
|
|
66
70
|
recordUsage(input: number, output: number): void;
|
package/dist/context/level7.js
CHANGED
|
@@ -40,14 +40,29 @@ export class RuntimeTelemetry {
|
|
|
40
40
|
maxRecentCalls: opts.maxRecentCalls ?? DEFAULT_MAX_RECENT,
|
|
41
41
|
maxChars: opts.maxChars ?? DEFAULT_MAX_CHARS,
|
|
42
42
|
};
|
|
43
|
+
// KLYRO_TELEMETRY=0 disables telemetry accumulation + reporting.
|
|
44
|
+
this.muted = process.env.KLYRO_TELEMETRY === '0';
|
|
45
|
+
}
|
|
46
|
+
muted = false;
|
|
47
|
+
/** Disable telemetry accumulation + reporting (also via KLYRO_TELEMETRY=0). */
|
|
48
|
+
mute() {
|
|
49
|
+
this.muted = true;
|
|
50
|
+
}
|
|
51
|
+
isMuted() {
|
|
52
|
+
return this.muted;
|
|
43
53
|
}
|
|
44
54
|
setMaxSteps(n) { this.maxSteps = n; }
|
|
45
|
-
recordStepStart(step) { this.
|
|
55
|
+
recordStepStart(step) { if (!this.muted)
|
|
56
|
+
this.step = step; }
|
|
46
57
|
recordUsage(input, output) {
|
|
58
|
+
if (this.muted)
|
|
59
|
+
return;
|
|
47
60
|
this.inputTokens += input;
|
|
48
61
|
this.outputTokens += output;
|
|
49
62
|
}
|
|
50
63
|
recordToolCall(call, latencyMs, isError) {
|
|
64
|
+
if (this.muted)
|
|
65
|
+
return;
|
|
51
66
|
this.toolCallCount++;
|
|
52
67
|
if (isError)
|
|
53
68
|
this.errorCount++;
|
|
@@ -63,6 +78,8 @@ export class RuntimeTelemetry {
|
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
recordError(msg) {
|
|
81
|
+
if (this.muted)
|
|
82
|
+
return;
|
|
66
83
|
this.lastError = msg;
|
|
67
84
|
}
|
|
68
85
|
/**
|
|
@@ -89,6 +106,8 @@ export class RuntimeTelemetry {
|
|
|
89
106
|
};
|
|
90
107
|
}
|
|
91
108
|
format() {
|
|
109
|
+
if (this.muted)
|
|
110
|
+
return emptyTelemetryBlock();
|
|
92
111
|
const s = this.snapshot();
|
|
93
112
|
const lines = [];
|
|
94
113
|
lines.push('# Runtime telemetry');
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* what to walk past.
|
|
22
22
|
*/
|
|
23
23
|
import * as fs from 'node:fs/promises';
|
|
24
|
+
import { statSync, readFileSync } from 'node:fs';
|
|
24
25
|
import * as path from 'node:path';
|
|
25
26
|
export const IGNORED_DIRS = new Set([
|
|
26
27
|
'node_modules', '.git', 'dist', 'build', '.next', '.turbo',
|
|
@@ -246,7 +247,7 @@ function detectPackageManager(root, pkg) {
|
|
|
246
247
|
// Avoid the async cost by re-using the root listing we already do.
|
|
247
248
|
// (Caller threads the listing into detectFrameworks; here we re-stat directly.)
|
|
248
249
|
try {
|
|
249
|
-
|
|
250
|
+
statSync(path.join(root, lock));
|
|
250
251
|
return pm;
|
|
251
252
|
}
|
|
252
253
|
catch { /* not present */ }
|
|
@@ -295,9 +296,9 @@ function extractBuildCommands(pkg, root) {
|
|
|
295
296
|
// Makefile: pick the first `target:` block as a hint.
|
|
296
297
|
// (Don't try to parse make fully — just surface the targets as a single hint.)
|
|
297
298
|
try {
|
|
298
|
-
const stat =
|
|
299
|
+
const stat = statSync(path.join(root, 'Makefile'));
|
|
299
300
|
if (stat.isFile() && stat.size < MAX_BYTES) {
|
|
300
|
-
const raw =
|
|
301
|
+
const raw = readFileSync(path.join(root, 'Makefile'), 'utf-8');
|
|
301
302
|
const targets = [...raw.matchAll(/^([a-zA-Z_][\w-]*)\s*:/gm)].map((m) => m[1]).slice(0, 6);
|
|
302
303
|
if (targets.length)
|
|
303
304
|
out.push(`make <target> # targets: ${targets.join(', ')}`);
|
|
@@ -368,11 +369,11 @@ async function gitHead(root) {
|
|
|
368
369
|
}
|
|
369
370
|
function lockfileHash(root) {
|
|
370
371
|
const candidates = ['pnpm-lock.yaml', 'yarn.lock', 'package-lock.json', 'bun.lockb', 'Cargo.lock', 'go.sum', 'poetry.lock', 'uv.lock'];
|
|
371
|
-
|
|
372
|
+
const h = crypto.createHash('sha1');
|
|
372
373
|
let found = false;
|
|
373
374
|
for (const f of candidates) {
|
|
374
375
|
try {
|
|
375
|
-
const d =
|
|
376
|
+
const d = readFileSync(path.join(root, f));
|
|
376
377
|
h.update(d);
|
|
377
378
|
found = true;
|
|
378
379
|
}
|
|
@@ -456,17 +457,17 @@ function detectMonorepo(root, rootFiles, rootDirs) {
|
|
|
456
457
|
return true;
|
|
457
458
|
if (rootDirs.includes('packages') || rootDirs.includes('apps')) {
|
|
458
459
|
try {
|
|
459
|
-
const s =
|
|
460
|
+
const s = statSync(path.join(root, 'packages'));
|
|
460
461
|
if (s.isDirectory())
|
|
461
462
|
return true;
|
|
462
463
|
}
|
|
463
|
-
catch { }
|
|
464
|
+
catch { /* ignore — best-effort probe */ }
|
|
464
465
|
try {
|
|
465
|
-
const s2 =
|
|
466
|
+
const s2 = statSync(path.join(root, 'apps'));
|
|
466
467
|
if (s2.isDirectory())
|
|
467
468
|
return true;
|
|
468
469
|
}
|
|
469
|
-
catch { }
|
|
470
|
+
catch { /* ignore — best-effort probe */ }
|
|
470
471
|
}
|
|
471
472
|
return false;
|
|
472
473
|
}
|
|
@@ -477,20 +478,20 @@ async function detectRuntimeVersions(root) {
|
|
|
477
478
|
if (v)
|
|
478
479
|
out['node'] = v.trim();
|
|
479
480
|
}
|
|
480
|
-
catch { }
|
|
481
|
+
catch { /* ignore — best-effort probe */ }
|
|
481
482
|
try {
|
|
482
483
|
const pkg = await readJsonSafe(path.join(root, 'package.json'));
|
|
483
484
|
const eng = pkg?.engines;
|
|
484
485
|
if (eng?.node)
|
|
485
486
|
out['node-eng'] = eng.node;
|
|
486
487
|
}
|
|
487
|
-
catch { }
|
|
488
|
+
catch { /* ignore — best-effort probe */ }
|
|
488
489
|
try {
|
|
489
490
|
const py = await readTextSafe(path.join(root, '.python-version'));
|
|
490
491
|
if (py)
|
|
491
492
|
out['python'] = py.trim();
|
|
492
493
|
}
|
|
493
|
-
catch { }
|
|
494
|
+
catch { /* ignore — best-effort probe */ }
|
|
494
495
|
try {
|
|
495
496
|
const go = await readTextSafe(path.join(root, 'go.mod'));
|
|
496
497
|
if (go) {
|
|
@@ -499,7 +500,7 @@ async function detectRuntimeVersions(root) {
|
|
|
499
500
|
out['go'] = m[1];
|
|
500
501
|
}
|
|
501
502
|
}
|
|
502
|
-
catch { }
|
|
503
|
+
catch { /* ignore — best-effort probe */ }
|
|
503
504
|
return out;
|
|
504
505
|
}
|
|
505
506
|
async function detectEntryPoints(root) {
|
package/dist/eval/harness.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface FileFixture {
|
|
|
77
77
|
script?: StreamEvent[][];
|
|
78
78
|
}
|
|
79
79
|
export declare function loadFileFixture(dir: string): Promise<FileFixture>;
|
|
80
|
-
export declare function runFileFixture(fixture: FileFixture,
|
|
80
|
+
export declare function runFileFixture(fixture: FileFixture, _opts?: {
|
|
81
81
|
runs?: number;
|
|
82
82
|
parallel?: number;
|
|
83
83
|
}): Promise<TaskResult>;
|
package/dist/eval/harness.js
CHANGED
|
@@ -122,7 +122,7 @@ export async function runTask(t, opts = {}) {
|
|
|
122
122
|
try {
|
|
123
123
|
await fs.rm(cwd, { recursive: true, force: true });
|
|
124
124
|
}
|
|
125
|
-
catch { }
|
|
125
|
+
catch { /* ignore — best-effort tmp cleanup */ }
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
}
|
|
@@ -177,7 +177,7 @@ export async function loadFileFixture(dir) {
|
|
|
177
177
|
catch { /* no script → static fixture */ }
|
|
178
178
|
return { dir, task: task.trim(), checkSh, meta, ...(repo ? { repo } : {}), ...(script ? { script } : {}) };
|
|
179
179
|
}
|
|
180
|
-
export async function runFileFixture(fixture,
|
|
180
|
+
export async function runFileFixture(fixture, _opts = {}) {
|
|
181
181
|
const start = Date.now();
|
|
182
182
|
const tmp = path.join(os.tmpdir(), 'klyro-eval-file-' + Math.random().toString(36).slice(2));
|
|
183
183
|
await fs.mkdir(tmp, { recursive: true });
|