klyro 1.0.10 → 1.0.12
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/cli/eval.d.ts +9 -1
- package/dist/cli/eval.js +46 -24
- package/dist/cli/repl.js +13 -5
- package/dist/index.js +2 -1
- package/dist/tui/app.js +9 -9
- package/dist/tui/app.test.js +25 -0
- package/dist/tui/mouse.d.ts +10 -0
- package/dist/tui/mouse.js +12 -0
- package/package.json +1 -1
package/dist/cli/eval.d.ts
CHANGED
|
@@ -79,6 +79,8 @@ export interface EvalResult {
|
|
|
79
79
|
notes: string;
|
|
80
80
|
skipped: boolean;
|
|
81
81
|
};
|
|
82
|
+
/** Isolated workdir the scenario ran in (tmp unless --cwd). Debugging aid. */
|
|
83
|
+
workDir?: string;
|
|
82
84
|
}
|
|
83
85
|
export interface RunEvalOptions {
|
|
84
86
|
inputPath: string;
|
|
@@ -90,10 +92,16 @@ export interface RunEvalOptions {
|
|
|
90
92
|
model?: string;
|
|
91
93
|
/** Live model id for grading `judge.rubric` (env endpoint + key required). */
|
|
92
94
|
judgeModel?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Shared workdir for JSONL scenarios. When omitted each scenario runs in
|
|
97
|
+
* a fresh tmp dir (deleted afterwards) so scripted tool calls can never
|
|
98
|
+
* touch the caller's directory. Pass explicitly to inspect artifacts.
|
|
99
|
+
*/
|
|
100
|
+
cwd?: string;
|
|
93
101
|
}
|
|
94
102
|
export declare function runEval(opts: RunEvalOptions): Promise<number>;
|
|
95
103
|
export declare function scriptedAdapterFromSpec(spec: Array<Array<unknown[]>> | undefined): ProviderAdapter;
|
|
96
104
|
export declare function runScenario(sc: EvalScenario, judgeOpts?: {
|
|
97
105
|
adapter: ProviderAdapter;
|
|
98
106
|
model: string;
|
|
99
|
-
}): Promise<EvalResult>;
|
|
107
|
+
}, workDir?: string): Promise<EvalResult>;
|
package/dist/cli/eval.js
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
* otherwise.
|
|
41
41
|
*/
|
|
42
42
|
import * as fs from 'node:fs';
|
|
43
|
+
import * as fsp from 'node:fs/promises';
|
|
44
|
+
import * as os from 'node:os';
|
|
45
|
+
import * as path from 'node:path';
|
|
43
46
|
import * as readline from 'node:readline/promises';
|
|
44
47
|
import { stdin as input, stdout, stderr } from 'node:process';
|
|
45
48
|
import { run } from '../agent/runtime.js';
|
|
@@ -153,7 +156,7 @@ export async function runEval(opts) {
|
|
|
153
156
|
const results = [];
|
|
154
157
|
for (const sc of scenarios) {
|
|
155
158
|
const start = Date.now();
|
|
156
|
-
const r = await runScenario(sc, judgeAdapter && opts.judgeModel ? { adapter: judgeAdapter, model: opts.judgeModel } : undefined);
|
|
159
|
+
const r = await runScenario(sc, judgeAdapter && opts.judgeModel ? { adapter: judgeAdapter, model: opts.judgeModel } : undefined, opts.cwd);
|
|
157
160
|
r.durationMs = Date.now() - start;
|
|
158
161
|
results.push(r);
|
|
159
162
|
if (opts.output === 'json') {
|
|
@@ -242,35 +245,53 @@ function tupleToEvent(tuple) {
|
|
|
242
245
|
throw new Error(`scriptedAdapterFromSpec: unknown event kind: ${kind}`);
|
|
243
246
|
}
|
|
244
247
|
}
|
|
245
|
-
export async function runScenario(sc, judgeOpts) {
|
|
248
|
+
export async function runScenario(sc, judgeOpts, workDir) {
|
|
246
249
|
const failures = [];
|
|
247
250
|
const model = sc.model ?? 'mock';
|
|
248
251
|
const adapter = scriptedAdapterFromSpec(sc.scripted_events);
|
|
249
252
|
const registry = builtinRegistry();
|
|
250
253
|
const policy = new PolicyEngine(builtinRules(), DEFAULT_POLICY_CONFIG);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
254
|
+
// Isolation (fix: scripted tool calls must never run in the caller's
|
|
255
|
+
// directory — a JSONL scenario writing a.txt/b.txt used to pollute it).
|
|
256
|
+
// Explicit workDir is shared as-is (inspect artifacts); otherwise each
|
|
257
|
+
// scenario gets a fresh tmp dir that is removed afterwards.
|
|
258
|
+
const owned = !workDir;
|
|
259
|
+
const cwd = workDir ?? await fsp.mkdtemp(path.join(os.tmpdir(), 'klyro-eval-jsonl-'));
|
|
260
|
+
await fsp.mkdir(cwd, { recursive: true });
|
|
261
|
+
let result;
|
|
262
|
+
try {
|
|
263
|
+
result = await run({
|
|
264
|
+
task: sc.task,
|
|
265
|
+
cwd,
|
|
266
|
+
model,
|
|
267
|
+
maxSteps: sc.maxSteps,
|
|
268
|
+
maxTokens: sc.maxTokens,
|
|
269
|
+
nonInteractive: true,
|
|
270
|
+
...(sc.verify
|
|
271
|
+
? {
|
|
272
|
+
verify: {
|
|
273
|
+
enabled: true,
|
|
274
|
+
...(sc.verify.command !== undefined ? { command: sc.verify.command } : {}),
|
|
275
|
+
...(sc.verify.mode !== undefined ? { mode: sc.verify.mode } : {}),
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
: {}),
|
|
279
|
+
}, {
|
|
280
|
+
adapter,
|
|
281
|
+
registry,
|
|
282
|
+
policy,
|
|
283
|
+
approval: new DenyAllApprovalPrompt(),
|
|
284
|
+
systemPrompt: ({ cwd }) => `You are Klyro. cwd=${cwd}.`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
if (owned) {
|
|
289
|
+
try {
|
|
290
|
+
await fsp.rm(cwd, { recursive: true, force: true });
|
|
265
291
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
registry,
|
|
270
|
-
policy,
|
|
271
|
-
approval: new DenyAllApprovalPrompt(),
|
|
272
|
-
systemPrompt: ({ cwd }) => `You are Klyro. cwd=${cwd}.`,
|
|
273
|
-
});
|
|
292
|
+
catch { /* ignore */ }
|
|
293
|
+
}
|
|
294
|
+
}
|
|
274
295
|
const exp = sc.expect ?? {};
|
|
275
296
|
if (exp.status !== undefined && result.status !== exp.status) {
|
|
276
297
|
failures.push(`status: expected ${exp.status}, got ${result.status}`);
|
|
@@ -311,6 +332,7 @@ export async function runScenario(sc, judgeOpts) {
|
|
|
311
332
|
toolCalls: result.toolCalls,
|
|
312
333
|
text: result.finalText,
|
|
313
334
|
durationMs: 0,
|
|
335
|
+
workDir: cwd,
|
|
314
336
|
...(judge ? { judge } : {}),
|
|
315
337
|
};
|
|
316
338
|
}
|
package/dist/cli/repl.js
CHANGED
|
@@ -25,7 +25,7 @@ import { parseUnifiedDiff } from '../tui/diff-parser.js';
|
|
|
25
25
|
import { parse } from './slash/parser.js';
|
|
26
26
|
import { resolveProvider, providerHelp, lastProviderError } from '../providers.js';
|
|
27
27
|
import { readVersion } from '../version.js';
|
|
28
|
-
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, PASTE_ENABLE, PASTE_DISABLE, PasteFilter, createReadWrapper } from '../tui/mouse.js';
|
|
28
|
+
import { MouseFilter, MOUSE_ENABLE, MOUSE_DISABLE, PASTE_ENABLE, PASTE_DISABLE, PasteFilter, createReadWrapper, isMouseReportingEnabled } from '../tui/mouse.js';
|
|
29
29
|
import { inferProviderFromBaseURL } from '../agent/registry.js';
|
|
30
30
|
import { getDefaultSessionStore } from '../persistence/session.js';
|
|
31
31
|
import { buildSystemPrompt, parseImageInput } from '../context/system-prompt.js';
|
|
@@ -372,13 +372,20 @@ export async function startRepl(opts = {}) {
|
|
|
372
372
|
// ── Full-screen takeover like OpenCode — always when klyro in TTY (user explicitly wants it)
|
|
373
373
|
// Scroll now works correctly via internal viewport, not native terminal scroll
|
|
374
374
|
const isAltScreen = useTui && !!process.stdout.isTTY && process.env.KLYRO_NO_ALT !== '1';
|
|
375
|
+
// Mouse reporting is opt-in (KLYRO_MOUSE=1): when on, the terminal sends
|
|
376
|
+
// clicks/wheel to the app (wheel scrolls ±3 lines) but native text
|
|
377
|
+
// selection and right-click paste stop working. Default off so select to
|
|
378
|
+
// copy and right-click paste work out of the box; bracketed paste
|
|
379
|
+
// (keyboard paste) is unaffected and always enabled below.
|
|
380
|
+
const mouseReporting = isAltScreen && isMouseReportingEnabled();
|
|
375
381
|
const enterAlt = () => {
|
|
376
382
|
if (!isAltScreen)
|
|
377
383
|
return;
|
|
378
384
|
try {
|
|
379
385
|
process.stdout.write('\x1b[?1049h\x1b[?25l'); // alt screen + hide cursor
|
|
380
386
|
process.stdout.write('\x1b[H\x1b[2J'); // home + clear
|
|
381
|
-
|
|
387
|
+
if (mouseReporting)
|
|
388
|
+
process.stdout.write(MOUSE_ENABLE); // wheel events (SGR), see tui/mouse.ts
|
|
382
389
|
}
|
|
383
390
|
catch { /* ignore */ }
|
|
384
391
|
};
|
|
@@ -386,7 +393,8 @@ export async function startRepl(opts = {}) {
|
|
|
386
393
|
if (!isAltScreen)
|
|
387
394
|
return;
|
|
388
395
|
try {
|
|
389
|
-
|
|
396
|
+
if (mouseReporting)
|
|
397
|
+
process.stdout.write(MOUSE_DISABLE);
|
|
390
398
|
process.stdout.write('\x1b[?25h\x1b[?1049l'); // show cursor + leave alt
|
|
391
399
|
}
|
|
392
400
|
catch { /* ignore */ }
|
|
@@ -2681,8 +2689,8 @@ export async function startRepl(opts = {}) {
|
|
|
2681
2689
|
' Enter send · Shift+Enter newline · Tab complete slash · Esc drop queued / Esc×2 cancel run',
|
|
2682
2690
|
' Ctrl+C cancel (1st) / quit (2nd) · Ctrl+O expand last tool group · Ctrl+G jump bottom',
|
|
2683
2691
|
' PgUp/PgDn or Ctrl+U/Ctrl+D half-page · Ctrl+Home/End top/bottom · Home/End jump · Space jump to unread',
|
|
2684
|
-
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line · ↑/↓ history
|
|
2685
|
-
' Shift+
|
|
2692
|
+
' Ctrl+B/F page · Shift/Ctrl+↑/↓ line · ↑/↓ input history · PgUp/Dn scroll (KLYRO_MOUSE=1 adds wheel ±3 lines)',
|
|
2693
|
+
' Text selection/copy and right-click paste work natively · Shift+Enter newline · /vim toggles vim input mode · /keymap <note> saves a display note',
|
|
2686
2694
|
].join('\n'),
|
|
2687
2695
|
});
|
|
2688
2696
|
return;
|
package/dist/index.js
CHANGED
|
@@ -390,6 +390,7 @@ async function main() {
|
|
|
390
390
|
.option('--parallel <n>', 'Parallelism (default 1)', (v) => parsePositiveInt('--parallel', v))
|
|
391
391
|
.option('--model <id>', 'Model for eval')
|
|
392
392
|
.option('--judge-model <id>', 'Live model id for grading judge.rubric (needs endpoint + key)')
|
|
393
|
+
.option('--cwd <path>', 'Shared scenario workdir (default: isolated tmp per scenario)')
|
|
393
394
|
.action(async (input, opts) => {
|
|
394
395
|
const output = (opts.output ?? 'human');
|
|
395
396
|
if (opts.suite) {
|
|
@@ -400,7 +401,7 @@ async function main() {
|
|
|
400
401
|
process.stderr.write('klyro eval: missing input (provide <input> or --suite)\n');
|
|
401
402
|
process.exit(2);
|
|
402
403
|
}
|
|
403
|
-
const code = await runEval({ inputPath: input, output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model, judgeModel: opts.judgeModel });
|
|
404
|
+
const code = await runEval({ inputPath: input, output, suite: opts.suite, filter: opts.filter, runs: opts.runs, parallel: opts.parallel, model: opts.model, judgeModel: opts.judgeModel, cwd: opts.cwd });
|
|
404
405
|
process.exit(code);
|
|
405
406
|
});
|
|
406
407
|
program
|
package/dist/tui/app.js
CHANGED
|
@@ -1052,16 +1052,16 @@ export function App(props) {
|
|
|
1052
1052
|
void props.onSlash({ kind: 'quit' });
|
|
1053
1053
|
return;
|
|
1054
1054
|
}
|
|
1055
|
-
// Contextual ↑/↓ (§8.3):
|
|
1056
|
-
// empty
|
|
1055
|
+
// Contextual ↑/↓ (§8.3): history always wins when entries exist —
|
|
1056
|
+
// empty input + ↑ recalls the last prompt (standard REPL behavior),
|
|
1057
|
+
// typing filters by prefix is unnecessary so plain recall applies;
|
|
1058
|
+
// the viewport scrolls only when there is no history to show.
|
|
1057
1059
|
if (key.upArrow && !key.shift && !key.ctrl) {
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
setVimCursor(null);
|
|
1064
|
-
}
|
|
1060
|
+
if (history.length > 0) {
|
|
1061
|
+
const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
|
|
1062
|
+
setHistIdx(next);
|
|
1063
|
+
setInput(history[next] ?? '');
|
|
1064
|
+
setVimCursor(null);
|
|
1065
1065
|
return;
|
|
1066
1066
|
}
|
|
1067
1067
|
if (isFullscreen && maxTop > 0) {
|
package/dist/tui/app.test.js
CHANGED
|
@@ -247,6 +247,7 @@ describe('App', () => {
|
|
|
247
247
|
expect(lastFrame() ?? '').toContain('first recallable prompt');
|
|
248
248
|
});
|
|
249
249
|
it('↑ on empty input scrolls one line instead of history', async () => {
|
|
250
|
+
// No prompts submitted yet → history is empty → viewport scrolls.
|
|
250
251
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
251
252
|
await new Promise((r) => setTimeout(r, 50));
|
|
252
253
|
stdin.write(KEY_HOME);
|
|
@@ -256,6 +257,30 @@ describe('App', () => {
|
|
|
256
257
|
await new Promise((r) => setTimeout(r, 30));
|
|
257
258
|
expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
|
|
258
259
|
});
|
|
260
|
+
it('↑ on empty input recalls history newest-first; ↓ browses back (user report)', async () => {
|
|
261
|
+
const onPrompt = vi.fn(async () => { });
|
|
262
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: onPrompt, onSlash: async () => { } }));
|
|
263
|
+
stdin.write('hi');
|
|
264
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
265
|
+
stdin.write('\x0d');
|
|
266
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
267
|
+
stdin.write('second prompt');
|
|
268
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
269
|
+
stdin.write('\x0d');
|
|
270
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
271
|
+
expect(onPrompt).toHaveBeenCalledTimes(2);
|
|
272
|
+
// Input is empty after each submit; plain ↑ must recall, not scroll.
|
|
273
|
+
stdin.write('\x1b[A');
|
|
274
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
275
|
+
expect(lastFrame() ?? '').toContain('second prompt');
|
|
276
|
+
stdin.write('\x1b[A');
|
|
277
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
278
|
+
expect(lastFrame() ?? '').toContain('hi');
|
|
279
|
+
// ↓ walks back toward newer entries.
|
|
280
|
+
stdin.write('\x1b[B');
|
|
281
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
282
|
+
expect(lastFrame() ?? '').toContain('second prompt');
|
|
283
|
+
});
|
|
259
284
|
it('/c shows top-6 suggestions and Tab completes', async () => {
|
|
260
285
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
|
|
261
286
|
stdin.write('/c');
|
package/dist/tui/mouse.d.ts
CHANGED
|
@@ -34,6 +34,16 @@ export declare class MouseFilter {
|
|
|
34
34
|
}
|
|
35
35
|
export declare const MOUSE_ENABLE = "\u001B[?1000h\u001B[?1006h";
|
|
36
36
|
export declare const MOUSE_DISABLE = "\u001B[?1000l\u001B[?1006l";
|
|
37
|
+
/**
|
|
38
|
+
* Whether the TUI may request terminal mouse reporting (wheel scrolling).
|
|
39
|
+
*
|
|
40
|
+
* Default OFF: with button reporting enabled the terminal routes
|
|
41
|
+
* selection clicks and right-click paste to the app (which swallows them),
|
|
42
|
+
* so native select-to-copy and right-click-paste break. Native selection
|
|
43
|
+
* works out of the box; set `KLYRO_MOUSE=1` to opt into wheel scrolling
|
|
44
|
+
* (Shift+drag still selects natively in most terminals).
|
|
45
|
+
*/
|
|
46
|
+
export declare function isMouseReportingEnabled(env?: Readonly<Record<string, string | undefined>>): boolean;
|
|
37
47
|
export declare const PASTE_START = "\u001B[200~";
|
|
38
48
|
export declare const PASTE_END = "\u001B[201~";
|
|
39
49
|
export declare const PASTE_ENABLE = "\u001B[?2004h";
|
package/dist/tui/mouse.js
CHANGED
|
@@ -92,6 +92,18 @@ export class MouseFilter {
|
|
|
92
92
|
}
|
|
93
93
|
export const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1006h'; // button events + SGR coords
|
|
94
94
|
export const MOUSE_DISABLE = '\x1b[?1000l\x1b[?1006l';
|
|
95
|
+
/**
|
|
96
|
+
* Whether the TUI may request terminal mouse reporting (wheel scrolling).
|
|
97
|
+
*
|
|
98
|
+
* Default OFF: with button reporting enabled the terminal routes
|
|
99
|
+
* selection clicks and right-click paste to the app (which swallows them),
|
|
100
|
+
* so native select-to-copy and right-click-paste break. Native selection
|
|
101
|
+
* works out of the box; set `KLYRO_MOUSE=1` to opt into wheel scrolling
|
|
102
|
+
* (Shift+drag still selects natively in most terminals).
|
|
103
|
+
*/
|
|
104
|
+
export function isMouseReportingEnabled(env = process.env) {
|
|
105
|
+
return env.KLYRO_MOUSE === '1';
|
|
106
|
+
}
|
|
95
107
|
export const PASTE_START = '\x1b[200~';
|
|
96
108
|
export const PASTE_END = '\x1b[201~';
|
|
97
109
|
export const PASTE_ENABLE = '\x1b[?2004h'; // bracketed paste: terminal wraps pastes
|