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.
@@ -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
- const result = await run({
252
- task: sc.task,
253
- cwd: process.cwd(),
254
- model,
255
- maxSteps: sc.maxSteps,
256
- maxTokens: sc.maxTokens,
257
- nonInteractive: true,
258
- ...(sc.verify
259
- ? {
260
- verify: {
261
- enabled: true,
262
- ...(sc.verify.command !== undefined ? { command: sc.verify.command } : {}),
263
- ...(sc.verify.mode !== undefined ? { mode: sc.verify.mode } : {}),
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
- adapter,
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
- process.stdout.write(MOUSE_ENABLE); // wheel events (SGR), see tui/mouse.ts
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
- process.stdout.write(MOUSE_DISABLE);
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 (with text) else scroll · wheel ±3 lines',
2685
- ' Shift+drag selects · /vim toggles vim input mode · /keymap <note> saves a display note',
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): text in buffer (or browsing) → history;
1056
- // empty buffer → scroll viewport one line.
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 (input.trim() !== '' || histIdx !== null) {
1059
- if (history.length > 0) {
1060
- const next = histIdx === null ? history.length - 1 : Math.max(0, histIdx - 1);
1061
- setHistIdx(next);
1062
- setInput(history[next] ?? '');
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) {
@@ -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');
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Klyro — autonomous coding harness CLI that streams from any OpenAI-compatible or Anthropic LLM endpoint.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",