klyro 1.0.16 → 1.0.18
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 +14 -0
- package/dist/agent/runtime.js +59 -22
- package/dist/chat.js +16 -56
- package/dist/checkpoints/store.d.ts +15 -1
- package/dist/checkpoints/store.js +7 -2
- package/dist/cli/args.d.ts +7 -0
- package/dist/cli/args.js +10 -0
- 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.d.ts +32 -0
- package/dist/cli/eval.js +83 -6
- package/dist/cli/markdown.js +1 -1
- package/dist/cli/repl.js +13 -24
- package/dist/cli/run.d.ts +37 -0
- package/dist/cli/run.js +81 -9
- package/dist/cli/update.js +7 -1
- package/dist/context/accounting.js +1 -1
- package/dist/context/klyro-md.d.ts +11 -0
- package/dist/context/klyro-md.js +59 -4
- 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/baseline.d.ts +47 -0
- package/dist/eval/baseline.js +55 -0
- package/dist/eval/harness.d.ts +14 -2
- package/dist/eval/harness.js +42 -4
- package/dist/index.js +57 -3
- 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/git/git-blame.d.ts +5 -0
- package/dist/tools/git/git-blame.js +22 -0
- package/dist/tools/git/git-diff.js +3 -20
- package/dist/tools/git/git-log.js +5 -19
- package/dist/tools/git/git-show.d.ts +5 -0
- package/dist/tools/git/git-show.js +22 -0
- package/dist/tools/git/git-status.js +4 -21
- package/dist/tools/git/run-git.d.ts +5 -0
- package/dist/tools/git/run-git.js +18 -0
- package/dist/tools/lsp/diagnostics.js +2 -3
- package/dist/tools/plan/todo-write.d.ts +1 -1
- package/dist/tools/registry.js +4 -0
- package/dist/tools/search/dependencies.d.ts +4 -4
- package/dist/tools/search/dependencies.js +1 -1
- package/dist/tools/search/glob.js +9 -4
- package/dist/tools/search/grep.js +11 -4
- package/dist/tools/search/ignore.d.ts +8 -0
- package/dist/tools/search/ignore.js +49 -0
- package/dist/tools/search/recent-files.js +1 -1
- package/dist/tools/search/search-files.js +11 -4
- package/dist/tools/web/web-fetch.js +2 -1
- package/dist/tools/web/web-search.js +3 -1
- package/dist/tui/app.js +117 -10
- package/dist/tui/app.test.js +65 -4
- 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/tui/transcript-commands.d.ts +14 -0
- package/dist/tui/transcript-commands.js +31 -0
- 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/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as fs from 'node:fs/promises';
|
|
7
7
|
import * as fsSync from 'node:fs';
|
|
8
8
|
import * as path from 'node:path';
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
9
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
10
10
|
import { getConfigPath, loadConfig } from './config.js';
|
|
11
11
|
import { resolveProvider, providerHelp } from '../providers.js';
|
|
12
12
|
import { getDefaultSessionsDir } from '../persistence/session.js';
|
|
@@ -120,6 +120,45 @@ async function checkSandbox() {
|
|
|
120
120
|
return { name: 'Sandbox', ok: true, detail: 'none — policy+path guards only' };
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
function checkPackageManager() {
|
|
124
|
+
try {
|
|
125
|
+
const npm = execFileSync('npm', ['--version'], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
126
|
+
return { name: 'npm', ok: true, detail: `npm ${npm} ✓` };
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return { name: 'npm', ok: false, detail: 'npm not on PATH — install Node ≥20' };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function checkPath() {
|
|
133
|
+
const pathVar = process.env.PATH ?? process.env.Path ?? '';
|
|
134
|
+
const nodeDir = process.execPath.includes('\\') || process.execPath.includes('/')
|
|
135
|
+
? process.execPath.slice(0, Math.max(process.execPath.lastIndexOf('\\'), process.execPath.lastIndexOf('/')))
|
|
136
|
+
: '';
|
|
137
|
+
const onPath = !!pathVar && !!nodeDir && pathVar.toLowerCase().split(path.delimiter).some((p) => p.toLowerCase() === nodeDir.toLowerCase());
|
|
138
|
+
return { name: 'PATH', ok: true, detail: onPath ? `node dir on PATH ✓` : `node dir not on PATH (${nodeDir || 'unknown'}) — child shells may miss node` };
|
|
139
|
+
}
|
|
140
|
+
function checkRipgrep() {
|
|
141
|
+
try {
|
|
142
|
+
const v = execFileSync('rg', ['--version'], { encoding: 'utf-8', timeout: 5000 }).trim().split('\n')[0] ?? '';
|
|
143
|
+
return { name: 'ripgrep', ok: true, detail: `${v} ✓` };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { name: 'ripgrep', ok: true, detail: 'not found — built-in grep tool is used instead' };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function checkTerminal() {
|
|
150
|
+
const isTTY = !!process.stdout.isTTY;
|
|
151
|
+
const term = process.env.TERM ?? '(unset)';
|
|
152
|
+
const cols = process.stdout.columns ?? 0;
|
|
153
|
+
return { name: 'Terminal', ok: true, detail: `${isTTY ? 'TTY' : 'non-TTY (headless/pipe mode)'}, TERM=${term}, cols=${cols}` };
|
|
154
|
+
}
|
|
155
|
+
function checkProxy() {
|
|
156
|
+
const vars = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy']
|
|
157
|
+
.filter((k) => process.env[k] && process.env[k].length > 0);
|
|
158
|
+
if (vars.length === 0)
|
|
159
|
+
return { name: 'Proxy', ok: true, detail: 'no proxy env — direct connections' };
|
|
160
|
+
return { name: 'Proxy', ok: true, detail: `${vars.join(', ')} set — honored for provider/web/update fetches` };
|
|
161
|
+
}
|
|
123
162
|
async function checkMcp(cwd) {
|
|
124
163
|
try {
|
|
125
164
|
const { loadMcpServers } = await import('../mcp/config.js');
|
|
@@ -169,14 +208,107 @@ async function checkTrust() {
|
|
|
169
208
|
return { name: 'Trust stores', ok: true, detail: 'none' };
|
|
170
209
|
}
|
|
171
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Pure byte interpreter for `doctor --keys` (unit-tested): names what a
|
|
213
|
+
* terminal delivered so broken arrow/paste/scroll input becomes provable
|
|
214
|
+
* fact instead of guesswork. Covers arrows, paging/home/end, Ctrl codes,
|
|
215
|
+
* bracketed-paste markers, and SGR/X10 mouse sequences.
|
|
216
|
+
*/
|
|
217
|
+
export function describeKeyBytes(chunk) {
|
|
218
|
+
const s = chunk.toString('latin1');
|
|
219
|
+
switch (s) {
|
|
220
|
+
case '\x1b[A': return 'Up arrow (history prev)';
|
|
221
|
+
case '\x1b[B': return 'Down arrow (history next)';
|
|
222
|
+
case '\x1b[C': return 'Right arrow';
|
|
223
|
+
case '\x1b[D': return 'Left arrow';
|
|
224
|
+
case '\x1b[5~': return 'PageUp (scroll half-page up)';
|
|
225
|
+
case '\x1b[6~': return 'PageDown (scroll half-page down)';
|
|
226
|
+
case '\x1b[H':
|
|
227
|
+
case '\x1b[1~': return 'Home (jump top)';
|
|
228
|
+
case '\x1b[F':
|
|
229
|
+
case '\x1b[4~': return 'End (jump bottom)';
|
|
230
|
+
case '\x1b[Z': return 'Shift+Tab';
|
|
231
|
+
case '\r':
|
|
232
|
+
case '\n': return 'Enter';
|
|
233
|
+
case '\x7f': return 'Backspace';
|
|
234
|
+
case '\x1b': return 'Escape (lone — arrow/paste sequences arrive joined; lone ESC means the terminal split them or you pressed Esc)';
|
|
235
|
+
case '\x1b[200~': return 'Bracketed-paste START (terminal supports bulk paste)';
|
|
236
|
+
case '\x1b[201~': return 'Bracketed-paste END';
|
|
237
|
+
case '\x03': return 'Ctrl+C';
|
|
238
|
+
case '\x10': return 'Ctrl+P (history prev, escape-free)';
|
|
239
|
+
case '\x0e': return 'Ctrl+N (history next, escape-free)';
|
|
240
|
+
case '\x15': return 'Ctrl+U (scroll half-page up)';
|
|
241
|
+
case '\x04': return 'Ctrl+D (scroll half-page down)';
|
|
242
|
+
case '\x09': return 'Tab';
|
|
243
|
+
}
|
|
244
|
+
// eslint-disable-next-line no-control-regex -- intentional: parsing SGR mouse sequences requires ESC matching
|
|
245
|
+
if (/^\x1b\[<\d+;\d+;\d+[Mm]$/.test(s)) {
|
|
246
|
+
const cb = Number(s.slice(3).split(';')[0]);
|
|
247
|
+
return (cb & 64) !== 0
|
|
248
|
+
? `Mouse wheel ${(cb & 1) === 0 ? 'up' : 'down'} (reporting active)`
|
|
249
|
+
: 'Mouse click/drag (reporting active — plain selection needs Shift+drag)';
|
|
250
|
+
}
|
|
251
|
+
// eslint-disable-next-line no-control-regex -- intentional: parsing X10 mouse sequences requires ESC matching
|
|
252
|
+
if (/^\x1b\[M...$/.test(s))
|
|
253
|
+
return 'Mouse event, X10 encoding (reporting active)';
|
|
254
|
+
if (/^[\x20-\x7e]+$/.test(s))
|
|
255
|
+
return `Printable text (${s.length} chars — typing/paste-as-typing works)`;
|
|
256
|
+
return 'Unrecognized sequence (terminal-specific — paste the bytes line into a bug report)';
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Interactive key probe: raw-mode stdin echo of every chunk with its
|
|
260
|
+
* meaning. Proves what THIS terminal delivers for arrows, Ctrl+P/N,
|
|
261
|
+
* paste, and wheel — run it when TUI input misbehaves.
|
|
262
|
+
*/
|
|
263
|
+
export async function runKeysProbe() {
|
|
264
|
+
const stdin = process.stdin;
|
|
265
|
+
const stdout = process.stdout;
|
|
266
|
+
if (!stdin.isTTY || typeof stdin.setRawMode !== 'function') {
|
|
267
|
+
process.stderr.write('klyro: doctor --keys needs an interactive terminal (stdin is not a TTY)\n');
|
|
268
|
+
return 2;
|
|
269
|
+
}
|
|
270
|
+
stdout.write('klyro doctor --keys — press keys, see exactly what your terminal sends.\n');
|
|
271
|
+
stdout.write('Try: ↑ ↓ PgUp PgDn Home End Ctrl+P Ctrl+N, then paste text, then scroll the wheel. Ctrl+C quits.\n');
|
|
272
|
+
stdin.setRawMode(true);
|
|
273
|
+
stdin.resume();
|
|
274
|
+
try {
|
|
275
|
+
await new Promise((resolve) => {
|
|
276
|
+
const onData = (chunk) => {
|
|
277
|
+
const bytes = [...chunk].map((b) => b.toString(16).padStart(2, '0')).join(' ');
|
|
278
|
+
stdout.write(`bytes: ${bytes} → ${describeKeyBytes(chunk)}\n`);
|
|
279
|
+
if (chunk.length === 1 && chunk[0] === 0x03) {
|
|
280
|
+
stdin.off('data', onData);
|
|
281
|
+
resolve();
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
stdin.on('data', onData);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
try {
|
|
289
|
+
stdin.setRawMode(false);
|
|
290
|
+
}
|
|
291
|
+
catch { /* ignore */ }
|
|
292
|
+
try {
|
|
293
|
+
stdin.pause();
|
|
294
|
+
}
|
|
295
|
+
catch { /* ignore */ }
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
172
299
|
export async function runDoctor(opts = {}) {
|
|
173
300
|
const cwd = opts.cwd ?? process.cwd();
|
|
174
301
|
const checks = [];
|
|
175
302
|
checks.push(checkNode());
|
|
303
|
+
checks.push(checkPackageManager());
|
|
304
|
+
checks.push(checkPath());
|
|
176
305
|
checks.push(await checkConfig());
|
|
177
306
|
checks.push(await checkProvider());
|
|
178
307
|
checks.push(await checkSessions());
|
|
179
308
|
checks.push(await checkGit());
|
|
309
|
+
checks.push(checkRipgrep());
|
|
310
|
+
checks.push(checkTerminal());
|
|
311
|
+
checks.push(checkProxy());
|
|
180
312
|
checks.push(await checkTools());
|
|
181
313
|
checks.push(checkPlatform());
|
|
182
314
|
checks.push(await checkSandbox());
|
package/dist/cli/errors.js
CHANGED
|
@@ -5,7 +5,7 @@ import { KlyroError } from '../shared/errors.js';
|
|
|
5
5
|
export function handleFatal(err) {
|
|
6
6
|
const isDebug = !!process.env.KLYRO_LOG_LEVEL || !!process.env.DEBUG || process.argv.includes('--debug');
|
|
7
7
|
let code = 1;
|
|
8
|
-
|
|
8
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
9
9
|
let hint;
|
|
10
10
|
if (err instanceof KlyroError) {
|
|
11
11
|
code = err.exitCode;
|
package/dist/cli/eval.d.ts
CHANGED
|
@@ -81,6 +81,22 @@ export interface EvalResult {
|
|
|
81
81
|
};
|
|
82
82
|
/** Isolated workdir the scenario ran in (tmp unless --cwd). Debugging aid. */
|
|
83
83
|
workDir?: string;
|
|
84
|
+
/** 5.4a — provider token usage for the scenario run. */
|
|
85
|
+
tokens?: {
|
|
86
|
+
input: number;
|
|
87
|
+
output: number;
|
|
88
|
+
};
|
|
89
|
+
/** 5.4a — estimated USD cost (runtime pricing helper, same rates as budgets). */
|
|
90
|
+
costUsd?: number;
|
|
91
|
+
/** 5.4a — distinct file paths referenced by tool-call inputs (best-effort). */
|
|
92
|
+
filesTouched?: string[];
|
|
93
|
+
/** 5.4a — verification outcome passthrough. */
|
|
94
|
+
verification?: {
|
|
95
|
+
ok: boolean;
|
|
96
|
+
attempts: number;
|
|
97
|
+
};
|
|
98
|
+
/** 5.4a — repair count passthrough. */
|
|
99
|
+
repairs?: number;
|
|
84
100
|
}
|
|
85
101
|
export interface RunEvalOptions {
|
|
86
102
|
inputPath: string;
|
|
@@ -98,10 +114,26 @@ export interface RunEvalOptions {
|
|
|
98
114
|
* touch the caller's directory. Pass explicitly to inspect artifacts.
|
|
99
115
|
*/
|
|
100
116
|
cwd?: string;
|
|
117
|
+
/**
|
|
118
|
+
* 5.3 — `--auto-answer <text>`: sets `KLYRO_AUTO_ANSWER` before the run
|
|
119
|
+
* starts so headless `ask_user` calls resolve without prompting.
|
|
120
|
+
*/
|
|
121
|
+
autoAnswer?: string;
|
|
101
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* 5.5a — curated stable subset of `evals/fixtures` for `--suite core`.
|
|
125
|
+
* Every entry has task.md + check.sh (verified 2026-09-24).
|
|
126
|
+
*/
|
|
127
|
+
export declare const CORE_SUITE_FIXTURES: string[];
|
|
128
|
+
/**
|
|
129
|
+
* Filter directory names down to the core suite (in curated order).
|
|
130
|
+
* Exported for tests.
|
|
131
|
+
*/
|
|
132
|
+
export declare function selectCoreFixtures(entries: string[]): string[];
|
|
102
133
|
export declare function runEval(opts: RunEvalOptions): Promise<number>;
|
|
103
134
|
export declare function scriptedAdapterFromSpec(spec: Array<Array<unknown[]>> | undefined): ProviderAdapter;
|
|
104
135
|
export declare function runScenario(sc: EvalScenario, judgeOpts?: {
|
|
105
136
|
adapter: ProviderAdapter;
|
|
106
137
|
model: string;
|
|
107
138
|
}, workDir?: string): Promise<EvalResult>;
|
|
139
|
+
export declare function collectFilesTouched(transcript: RunResult['transcript']): string[];
|
package/dist/cli/eval.js
CHANGED
|
@@ -45,11 +45,26 @@ import * as os from 'node:os';
|
|
|
45
45
|
import * as path from 'node:path';
|
|
46
46
|
import * as readline from 'node:readline/promises';
|
|
47
47
|
import { stdin as input, stdout, stderr } from 'node:process';
|
|
48
|
-
import { run } from '../agent/runtime.js';
|
|
48
|
+
import { run, estimateCost } from '../agent/runtime.js';
|
|
49
|
+
import { applyAutoAnswer } from './args.js';
|
|
49
50
|
import { builtinRegistry } from '../tools/registry.js';
|
|
50
51
|
import { builtinRules, DEFAULT_POLICY_CONFIG, PolicyEngine } from '../policy/engine.js';
|
|
51
52
|
import { DenyAllApprovalPrompt } from '../policy/approval.js';
|
|
53
|
+
/**
|
|
54
|
+
* 5.5a — curated stable subset of `evals/fixtures` for `--suite core`.
|
|
55
|
+
* Every entry has task.md + check.sh (verified 2026-09-24).
|
|
56
|
+
*/
|
|
57
|
+
export const CORE_SUITE_FIXTURES = ['read-answer', 'add-fn-test', 'fix-failing-test', 'rename', 'cli-flag'];
|
|
58
|
+
/**
|
|
59
|
+
* Filter directory names down to the core suite (in curated order).
|
|
60
|
+
* Exported for tests.
|
|
61
|
+
*/
|
|
62
|
+
export function selectCoreFixtures(entries) {
|
|
63
|
+
return CORE_SUITE_FIXTURES.filter((id) => entries.includes(id));
|
|
64
|
+
}
|
|
52
65
|
export async function runEval(opts) {
|
|
66
|
+
// 5.3 — --auto-answer sets KLYRO_AUTO_ANSWER before anything runs.
|
|
67
|
+
applyAutoAnswer(opts.autoAnswer);
|
|
53
68
|
// Live judge adapter (shared by suite + JSONL paths) for `judge.rubric`.
|
|
54
69
|
let judgeAdapter;
|
|
55
70
|
if (opts.judgeModel) {
|
|
@@ -65,20 +80,24 @@ export async function runEval(opts) {
|
|
|
65
80
|
}
|
|
66
81
|
// 5.4 — suite mode: load from evals/fixtures
|
|
67
82
|
if (opts.suite) {
|
|
68
|
-
const
|
|
83
|
+
const suiteStart = Date.now();
|
|
69
84
|
const fs = await import('node:fs/promises');
|
|
70
85
|
const path = await import('node:path');
|
|
71
|
-
const suiteDir = path.join(process.cwd(), 'evals', 'fixtures', opts.suite === 'smoke' ? '' : opts.suite);
|
|
72
86
|
// For smoke, use the 10 fixtures directly
|
|
73
87
|
const fixturesDir = path.join(process.cwd(), 'evals', 'fixtures');
|
|
74
|
-
|
|
88
|
+
const tasks = [];
|
|
75
89
|
try {
|
|
76
90
|
const entries = await fs.readdir(fixturesDir);
|
|
77
91
|
for (const e of entries) {
|
|
78
92
|
if (opts.filter && !e.includes(opts.filter))
|
|
79
93
|
continue;
|
|
80
|
-
//
|
|
81
|
-
if (opts.suite
|
|
94
|
+
// 5.5a — core = curated stable subset (see CORE_SUITE_FIXTURES).
|
|
95
|
+
if (opts.suite === 'core') {
|
|
96
|
+
if (!CORE_SUITE_FIXTURES.includes(e))
|
|
97
|
+
continue;
|
|
98
|
+
// 6.5 — suite filter: smoke = type smoke, l6 = prefix l6-introduce
|
|
99
|
+
}
|
|
100
|
+
else if (opts.suite && opts.suite !== 'smoke') {
|
|
82
101
|
if (!e.startsWith(opts.suite) && !e.includes(opts.suite))
|
|
83
102
|
continue;
|
|
84
103
|
}
|
|
@@ -143,6 +162,16 @@ export async function runEval(opts) {
|
|
|
143
162
|
await fs.mkdir(outDir, { recursive: true });
|
|
144
163
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
145
164
|
await fs.writeFile(path.join(outDir, `${ts}.json`), JSON.stringify({ suite: opts.suite, results }, null, 2));
|
|
165
|
+
// 5.5b — refresh the tracked baseline with environment metadata.
|
|
166
|
+
const { collectBaselineMetadata, buildEvalBaseline, writeEvalBaseline } = await import('../eval/baseline.js');
|
|
167
|
+
const meta = collectBaselineMetadata({ model: opts.model });
|
|
168
|
+
await writeEvalBaseline(outDir, buildEvalBaseline(meta, {
|
|
169
|
+
suite: opts.suite ?? '',
|
|
170
|
+
total: results.length,
|
|
171
|
+
passed,
|
|
172
|
+
failed: results.length - passed,
|
|
173
|
+
durationMs: Date.now() - suiteStart,
|
|
174
|
+
}));
|
|
146
175
|
}
|
|
147
176
|
catch { /* ignore */ }
|
|
148
177
|
return passed === results.length ? 0 : 1;
|
|
@@ -167,6 +196,18 @@ export async function runEval(opts) {
|
|
|
167
196
|
stdout.write(`[${tag}] ${r.name} (${r.durationMs}ms, ${r.steps} steps, ${r.toolCalls} tools)\n`);
|
|
168
197
|
for (const f of r.failures)
|
|
169
198
|
stdout.write(` - ${f}\n`);
|
|
199
|
+
// 5.4a — rich record extras (tokens/cost/files/verification).
|
|
200
|
+
const extras = [];
|
|
201
|
+
if (r.tokens)
|
|
202
|
+
extras.push(`${r.tokens.input} in/${r.tokens.output} out`);
|
|
203
|
+
if (r.costUsd !== undefined)
|
|
204
|
+
extras.push(`$${r.costUsd.toFixed(4)}`);
|
|
205
|
+
if (r.filesTouched && r.filesTouched.length > 0)
|
|
206
|
+
extras.push(`${r.filesTouched.length} files`);
|
|
207
|
+
if (r.verification)
|
|
208
|
+
extras.push(`verify:${r.verification.ok ? 'ok' : 'fail'}`);
|
|
209
|
+
if (extras.length > 0)
|
|
210
|
+
stdout.write(` · ${extras.join(' · ')}\n`);
|
|
170
211
|
}
|
|
171
212
|
}
|
|
172
213
|
const passed = results.filter((r) => r.passed).length;
|
|
@@ -334,5 +375,41 @@ export async function runScenario(sc, judgeOpts, workDir) {
|
|
|
334
375
|
durationMs: 0,
|
|
335
376
|
workDir: cwd,
|
|
336
377
|
...(judge ? { judge } : {}),
|
|
378
|
+
// 5.4a — rich records mapped from RunResult (cost reuses the runtime
|
|
379
|
+
// pricing helper, the same rates the budget code uses).
|
|
380
|
+
tokens: { input: result.usage.input, output: result.usage.output },
|
|
381
|
+
costUsd: estimateCost(model, result.usage),
|
|
382
|
+
filesTouched: collectFilesTouched(result.transcript),
|
|
383
|
+
...(result.verification
|
|
384
|
+
? { verification: { ok: result.verification.ok, attempts: result.verification.attempts } }
|
|
385
|
+
: {}),
|
|
386
|
+
...(result.repairs !== undefined ? { repairs: result.repairs } : {}),
|
|
337
387
|
};
|
|
338
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* 5.4a — best-effort distinct file paths from tool-call inputs in a run
|
|
391
|
+
* transcript. Looks for common path-ish keys on `tool_use` blocks.
|
|
392
|
+
* Exported for tests.
|
|
393
|
+
*/
|
|
394
|
+
const FILE_PATH_KEYS = ['path', 'file', 'filePath', 'filename', 'target'];
|
|
395
|
+
export function collectFilesTouched(transcript) {
|
|
396
|
+
const seen = new Set();
|
|
397
|
+
for (const m of transcript) {
|
|
398
|
+
for (const b of m.content) {
|
|
399
|
+
if (b.kind !== 'tool_use')
|
|
400
|
+
continue;
|
|
401
|
+
const input = b.input;
|
|
402
|
+
for (const k of FILE_PATH_KEYS) {
|
|
403
|
+
const v = input[k];
|
|
404
|
+
if (typeof v === 'string' && v.length > 0)
|
|
405
|
+
seen.add(v);
|
|
406
|
+
else if (Array.isArray(v)) {
|
|
407
|
+
for (const e of v)
|
|
408
|
+
if (typeof e === 'string' && e.length > 0)
|
|
409
|
+
seen.add(e);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return [...seen].sort();
|
|
415
|
+
}
|
package/dist/cli/markdown.js
CHANGED
|
@@ -38,7 +38,7 @@ export function renderMarkdown(md, opts = {}) {
|
|
|
38
38
|
const lines = md.split('\n');
|
|
39
39
|
let inCodeBlock = false;
|
|
40
40
|
let fenceLang = '';
|
|
41
|
-
for (
|
|
41
|
+
for (const line of lines) {
|
|
42
42
|
if (line.startsWith('```')) {
|
|
43
43
|
if (!inCodeBlock)
|
|
44
44
|
fenceLang = line.replace(/^```/, '').trim().toLowerCase();
|
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;
|
|
@@ -75,7 +91,28 @@ export interface RunCliOptions {
|
|
|
75
91
|
/** P1.4 — run the task under a named child-capable orchestrator context. */
|
|
76
92
|
agent?: string;
|
|
77
93
|
maxDepth?: number;
|
|
94
|
+
/**
|
|
95
|
+
* 5.3 — `--auto-answer <text>`: answer `ask_user` prompts headlessly.
|
|
96
|
+
* Applied to `process.env.KLYRO_AUTO_ANSWER` at the top of `runOnce`.
|
|
97
|
+
*/
|
|
98
|
+
autoAnswer?: string;
|
|
78
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Split a comma-separated tool/dir list (string or string[]) into names.
|
|
102
|
+
* Exported for tests.
|
|
103
|
+
*/
|
|
104
|
+
export declare function splitToolList(v: string[] | string | undefined): string[];
|
|
105
|
+
/** Smaller of two optional caps (--max-turns aliases --max-steps). Exported for tests. */
|
|
106
|
+
export declare function minDefined(a: number | undefined, b: number | undefined): number | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* Layer --system-prompt/--append-system-prompt over the assembled prompt
|
|
109
|
+
* (2.3): replace wins wholesale, append rides after the assembled system
|
|
110
|
+
* (preserving any suffix the base carries). Exported for tests.
|
|
111
|
+
*/
|
|
112
|
+
export declare function withSystemPromptOverrides(base: SystemPromptFn, opts: {
|
|
113
|
+
replace?: string;
|
|
114
|
+
append?: string;
|
|
115
|
+
}): SystemPromptFn;
|
|
79
116
|
/**
|
|
80
117
|
* Double-Ctrl+C detector (pure, exported for tests): the second SIGINT
|
|
81
118
|
* within 1500ms of the first forces `process.exit(130)`. The live handler
|