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.
Files changed (92) hide show
  1. package/dist/agent/anthropic-adapter.js +26 -1
  2. package/dist/agent/provider-adapter.d.ts +7 -0
  3. package/dist/agent/provider-adapter.js +26 -1
  4. package/dist/agent/runtime.d.ts +14 -0
  5. package/dist/agent/runtime.js +59 -22
  6. package/dist/chat.js +16 -56
  7. package/dist/checkpoints/store.d.ts +15 -1
  8. package/dist/checkpoints/store.js +7 -2
  9. package/dist/cli/args.d.ts +7 -0
  10. package/dist/cli/args.js +10 -0
  11. package/dist/cli/completion.js +0 -3
  12. package/dist/cli/doctor.d.ts +13 -0
  13. package/dist/cli/doctor.js +133 -1
  14. package/dist/cli/errors.js +1 -1
  15. package/dist/cli/eval.d.ts +32 -0
  16. package/dist/cli/eval.js +83 -6
  17. package/dist/cli/markdown.js +1 -1
  18. package/dist/cli/repl.js +13 -24
  19. package/dist/cli/run.d.ts +37 -0
  20. package/dist/cli/run.js +81 -9
  21. package/dist/cli/update.js +7 -1
  22. package/dist/context/accounting.js +1 -1
  23. package/dist/context/klyro-md.d.ts +11 -0
  24. package/dist/context/klyro-md.js +59 -4
  25. package/dist/context/level7.d.ts +4 -0
  26. package/dist/context/level7.js +20 -1
  27. package/dist/context/project-map.js +14 -13
  28. package/dist/eval/baseline.d.ts +47 -0
  29. package/dist/eval/baseline.js +55 -0
  30. package/dist/eval/harness.d.ts +14 -2
  31. package/dist/eval/harness.js +42 -4
  32. package/dist/index.js +57 -3
  33. package/dist/persistence/store.js +2 -2
  34. package/dist/policy/approval.js +3 -0
  35. package/dist/policy/path-guard.d.ts +9 -4
  36. package/dist/policy/path-guard.js +47 -23
  37. package/dist/providers/endpoints.js +8 -0
  38. package/dist/providers/model-info.d.ts +1 -0
  39. package/dist/providers/model-info.js +19 -0
  40. package/dist/providers.js +6 -2
  41. package/dist/renderers/terminal.d.ts +7 -0
  42. package/dist/renderers/terminal.js +52 -0
  43. package/dist/repl.d.ts +9 -0
  44. package/dist/repl.js +24 -5
  45. package/dist/shared/index.d.ts +1 -0
  46. package/dist/shared/index.js +1 -0
  47. package/dist/shared/proxy.d.ts +37 -0
  48. package/dist/shared/proxy.js +360 -0
  49. package/dist/tools/agent/task-list.d.ts +1 -1
  50. package/dist/tools/fs/apply-patch.js +4 -4
  51. package/dist/tools/fs/edit-file.js +2 -4
  52. package/dist/tools/fs/list-dir.js +1 -1
  53. package/dist/tools/fs/multi-edit.js +2 -2
  54. package/dist/tools/fs/read-file.js +1 -1
  55. package/dist/tools/fs/write-file.js +2 -2
  56. package/dist/tools/git/git-blame.d.ts +5 -0
  57. package/dist/tools/git/git-blame.js +22 -0
  58. package/dist/tools/git/git-diff.js +3 -20
  59. package/dist/tools/git/git-log.js +5 -19
  60. package/dist/tools/git/git-show.d.ts +5 -0
  61. package/dist/tools/git/git-show.js +22 -0
  62. package/dist/tools/git/git-status.js +4 -21
  63. package/dist/tools/git/run-git.d.ts +5 -0
  64. package/dist/tools/git/run-git.js +18 -0
  65. package/dist/tools/lsp/diagnostics.js +2 -3
  66. package/dist/tools/plan/todo-write.d.ts +1 -1
  67. package/dist/tools/registry.js +4 -0
  68. package/dist/tools/search/dependencies.d.ts +4 -4
  69. package/dist/tools/search/dependencies.js +1 -1
  70. package/dist/tools/search/glob.js +9 -4
  71. package/dist/tools/search/grep.js +11 -4
  72. package/dist/tools/search/ignore.d.ts +8 -0
  73. package/dist/tools/search/ignore.js +49 -0
  74. package/dist/tools/search/recent-files.js +1 -1
  75. package/dist/tools/search/search-files.js +11 -4
  76. package/dist/tools/web/web-fetch.js +2 -1
  77. package/dist/tools/web/web-search.js +3 -1
  78. package/dist/tui/app.js +117 -10
  79. package/dist/tui/app.test.js +65 -4
  80. package/dist/tui/measure.js +1 -0
  81. package/dist/tui/mouse.d.ts +7 -5
  82. package/dist/tui/mouse.js +9 -6
  83. package/dist/tui/scroll-flow.test.js +0 -1
  84. package/dist/tui/transcript-commands.d.ts +14 -0
  85. package/dist/tui/transcript-commands.js +31 -0
  86. package/dist/util/log.d.ts +19 -10
  87. package/dist/util/log.js +87 -52
  88. package/dist/verification/classify.d.ts +1 -1
  89. package/dist/verification/classify.js +2 -2
  90. package/dist/verification/engine.js +0 -6
  91. package/dist/verification/scoped.js +6 -4
  92. package/package.json +6 -1
@@ -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());
@@ -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
- let message = err instanceof Error ? err.message : String(err);
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;
@@ -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 { runHarness, loadFileFixture } = await import('../eval/harness.js');
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
- let tasks = [];
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
- // 6.5 — suite filter: smoke = type smoke, l6 = prefix l6-introduce
81
- if (opts.suite && opts.suite !== 'smoke') {
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
+ }
@@ -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 (let line of lines) {
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
- let replFailoverAdapters = [];
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 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.
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 · wheel scrolls with KLYRO_MOUSE=1',
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