klyro 1.0.13 → 1.0.16
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/README.md +14 -5
- package/dist/agent/child-worker.js +15 -4
- package/dist/checkpoints/store.js +7 -9
- package/dist/cli/config.js +87 -2
- package/dist/cli/dotenv.d.ts +2 -0
- package/dist/cli/dotenv.js +74 -0
- package/dist/cli/hooks.d.ts +31 -0
- package/dist/cli/hooks.js +126 -7
- package/dist/cli/repl.js +1 -1
- package/dist/cli/run.js +17 -9
- package/dist/cli/session-commands.js +2 -1
- package/dist/cli/update.js +16 -6
- package/dist/eval/harness.d.ts +3 -2
- package/dist/eval/harness.js +14 -10
- package/dist/index.js +24 -4
- package/dist/mcp/client.js +12 -0
- package/dist/mcp/config.js +2 -1
- package/dist/mcp/serve.d.ts +24 -0
- package/dist/mcp/serve.js +41 -4
- package/dist/persistence/audit.js +8 -1
- package/dist/persistence/store.d.ts +2 -0
- package/dist/persistence/store.js +26 -0
- package/dist/policy/secret-redactor.js +2 -2
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/json.d.ts +7 -0
- package/dist/shared/json.js +9 -0
- package/dist/shared/output-cap.d.ts +30 -0
- package/dist/shared/output-cap.js +28 -0
- package/dist/tools/web/web-fetch.d.ts +9 -0
- package/dist/tools/web/web-fetch.js +42 -6
- package/dist/tui/app.js +11 -24
- package/dist/tui/app.test.js +35 -6
- package/dist/verification/baseline.js +10 -11
- package/dist/verification/classify.js +17 -8
- package/dist/verification/engine.js +9 -10
- package/dist/verification/scoped.js +10 -11
- package/package.json +1 -1
package/dist/tui/app.test.js
CHANGED
|
@@ -246,7 +246,7 @@ describe('App', () => {
|
|
|
246
246
|
await new Promise((r) => setTimeout(r, 30));
|
|
247
247
|
expect(lastFrame() ?? '').toContain('first recallable prompt');
|
|
248
248
|
});
|
|
249
|
-
it('↑ on empty input
|
|
249
|
+
it('↑ on empty input with no history is a no-op (arrows never scroll)', async () => {
|
|
250
250
|
// No prompts submitted yet → history is empty → viewport scrolls.
|
|
251
251
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
252
252
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -257,7 +257,7 @@ describe('App', () => {
|
|
|
257
257
|
await new Promise((r) => setTimeout(r, 30));
|
|
258
258
|
expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
|
|
259
259
|
});
|
|
260
|
-
it('↑ at
|
|
260
|
+
it('↑ recalls history at the tail and while scrolled up (never scrolls)', async () => {
|
|
261
261
|
const onPrompt = vi.fn(async () => { });
|
|
262
262
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: onPrompt, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
263
263
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -274,7 +274,7 @@ describe('App', () => {
|
|
|
274
274
|
await new Promise((r) => setTimeout(r, 30));
|
|
275
275
|
expect(lastFrame() ?? '').toContain('zzrecallme!');
|
|
276
276
|
});
|
|
277
|
-
it('↑ while scrolled up
|
|
277
|
+
it('↑ while scrolled up recalls history instead of scrolling', async () => {
|
|
278
278
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialTranscript: makeInitialTranscript(25) }));
|
|
279
279
|
await new Promise((r) => setTimeout(r, 50));
|
|
280
280
|
stdin.write('zznoscroll');
|
|
@@ -283,14 +283,43 @@ describe('App', () => {
|
|
|
283
283
|
await new Promise((r) => setTimeout(r, 50));
|
|
284
284
|
stdin.write(KEY_HOME);
|
|
285
285
|
await new Promise((r) => setTimeout(r, 30));
|
|
286
|
-
// Scrolled up + empty input + ↑ →
|
|
287
|
-
// typing '!' must
|
|
286
|
+
// Scrolled up + empty input + ↑ → recalls into the input line (viewport
|
|
287
|
+
// untouched, MSG-00 stays): typing '!' must extend the recalled prompt.
|
|
288
288
|
stdin.write('\x1b[A');
|
|
289
289
|
await new Promise((r) => setTimeout(r, 30));
|
|
290
290
|
expect(lastFrame() ?? '').toMatch(/MSG-00-tag/);
|
|
291
291
|
stdin.write('!');
|
|
292
292
|
await new Promise((r) => setTimeout(r, 30));
|
|
293
|
-
expect(lastFrame() ?? '').
|
|
293
|
+
expect(lastFrame() ?? '').toContain('zznoscroll!');
|
|
294
|
+
});
|
|
295
|
+
it('Ctrl+P / Ctrl+N browse history without arrow keys', async () => {
|
|
296
|
+
const onPrompt = vi.fn(async () => { });
|
|
297
|
+
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: onPrompt, onSlash: async () => { } }));
|
|
298
|
+
stdin.write('zzfirst');
|
|
299
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
300
|
+
stdin.write('\x0d');
|
|
301
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
302
|
+
stdin.write('zzsecond');
|
|
303
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
304
|
+
stdin.write('\x0d');
|
|
305
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
306
|
+
expect(onPrompt).toHaveBeenCalledTimes(2);
|
|
307
|
+
// Ctrl+P recalls newest-first with zero escape sequences involved.
|
|
308
|
+
stdin.write('\x10');
|
|
309
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
310
|
+
stdin.write('!');
|
|
311
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
312
|
+
expect(lastFrame() ?? '').toContain('zzsecond!');
|
|
313
|
+
// Ctrl+P twice walks older with no typing between, Ctrl+N walks newer.
|
|
314
|
+
stdin.write('\x10');
|
|
315
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
316
|
+
stdin.write('\x10');
|
|
317
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
318
|
+
stdin.write('\x0e');
|
|
319
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
320
|
+
stdin.write('#');
|
|
321
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
322
|
+
expect(lastFrame() ?? '').toContain('zzsecond#');
|
|
294
323
|
});
|
|
295
324
|
it('/c shows top-6 suggestions and Tab completes', async () => {
|
|
296
325
|
const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { } }));
|
|
@@ -8,6 +8,7 @@ import * as path from 'node:path';
|
|
|
8
8
|
import * as crypto from 'node:crypto';
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
10
|
import { primaryVerifyCommand, filteredVerifyEnv } from './registry.js';
|
|
11
|
+
import { cappedOutput } from '../shared/output-cap.js';
|
|
11
12
|
async function gitHead(cwd) {
|
|
12
13
|
return new Promise((resolve) => {
|
|
13
14
|
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
@@ -83,8 +84,8 @@ function runCmd(cwd, command, timeoutMs) {
|
|
|
83
84
|
return new Promise((resolve) => {
|
|
84
85
|
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
85
86
|
const child = spawn(command, { cwd, shell: true, env: filteredVerifyEnv() });
|
|
86
|
-
const
|
|
87
|
-
const
|
|
87
|
+
const outCap = cappedOutput(MAX_BASELINE_BYTES);
|
|
88
|
+
const errCap = cappedOutput(MAX_BASELINE_BYTES);
|
|
88
89
|
let done = false;
|
|
89
90
|
const timer = setTimeout(() => {
|
|
90
91
|
if (done)
|
|
@@ -94,22 +95,20 @@ function runCmd(cwd, command, timeoutMs) {
|
|
|
94
95
|
child.kill();
|
|
95
96
|
}
|
|
96
97
|
catch { /* ignore */ }
|
|
97
|
-
const so =
|
|
98
|
-
const se =
|
|
98
|
+
const so = outCap.text();
|
|
99
|
+
const se = errCap.text();
|
|
99
100
|
resolve({ ok: false, exitCode: -1, stdout: so, stderr: se + '\n[baseline timeout]' });
|
|
100
101
|
}, timeoutMs);
|
|
101
|
-
child.stdout.on('data', (b) =>
|
|
102
|
-
|
|
103
|
-
child.stderr.on('data', (b) => { if (Buffer.concat(errChunks).length < MAX_BASELINE_BYTES)
|
|
104
|
-
errChunks.push(b); });
|
|
102
|
+
child.stdout.on('data', (b) => outCap.push(b));
|
|
103
|
+
child.stderr.on('data', (b) => errCap.push(b));
|
|
105
104
|
child.on('close', (code) => {
|
|
106
105
|
if (done)
|
|
107
106
|
return;
|
|
108
107
|
done = true;
|
|
109
108
|
clearTimeout(timer);
|
|
110
109
|
const exit = typeof code === 'number' ? code : -1;
|
|
111
|
-
const so =
|
|
112
|
-
const se =
|
|
110
|
+
const so = outCap.text();
|
|
111
|
+
const se = errCap.text();
|
|
113
112
|
resolve({ ok: exit === 0, exitCode: exit, stdout: so, stderr: se });
|
|
114
113
|
});
|
|
115
114
|
child.on('error', (err) => {
|
|
@@ -117,7 +116,7 @@ function runCmd(cwd, command, timeoutMs) {
|
|
|
117
116
|
return;
|
|
118
117
|
done = true;
|
|
119
118
|
clearTimeout(timer);
|
|
120
|
-
const so =
|
|
119
|
+
const so = outCap.text();
|
|
121
120
|
resolve({ ok: false, exitCode: -1, stdout: so, stderr: String(err) });
|
|
122
121
|
});
|
|
123
122
|
});
|
|
@@ -5,6 +5,7 @@ import * as fs from 'node:fs';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { filteredVerifyEnv } from './registry.js';
|
|
8
|
+
import { cappedOutput } from '../shared/output-cap.js';
|
|
8
9
|
import { detect } from './detect.js';
|
|
9
10
|
function signatureOf(failure, fallbackExit) {
|
|
10
11
|
return {
|
|
@@ -91,23 +92,31 @@ export async function gatherRepairContext(cwd, failure) {
|
|
|
91
92
|
: '';
|
|
92
93
|
return { failingTests, hunks, blame };
|
|
93
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* `git diff -U2 | head -n 300`: the pipe bounds a well-behaved shell, this
|
|
97
|
+
* bounds one that ignores it (subshell, `;` chain, verbose stderr). Without a
|
|
98
|
+
* cap a chatty command grew the heap without limit before the 4000-char slice.
|
|
99
|
+
*/
|
|
100
|
+
const MAX_PIPE_BYTES = 64 * 1024;
|
|
94
101
|
function execCapturePipe(cwd, cmd) {
|
|
95
102
|
return new Promise((resolve) => {
|
|
96
103
|
const child = spawn(cmd, { cwd, shell: true, env: filteredVerifyEnv() });
|
|
97
|
-
const
|
|
98
|
-
child.stdout.on('data', (b) =>
|
|
99
|
-
child.stderr.on('data', (b) =>
|
|
100
|
-
child.on('close', () => resolve(
|
|
104
|
+
const sink = cappedOutput(MAX_PIPE_BYTES);
|
|
105
|
+
child.stdout.on('data', (b) => sink.push(b));
|
|
106
|
+
child.stderr.on('data', (b) => sink.push(b));
|
|
107
|
+
child.on('close', () => resolve(sink.text().slice(0, 4000)));
|
|
101
108
|
child.on('error', () => resolve(''));
|
|
102
109
|
});
|
|
103
110
|
}
|
|
111
|
+
/** `git blame` for one file — 20 output lines kept, byte-capped collection. */
|
|
112
|
+
const MAX_BLAME_BYTES = 16 * 1024;
|
|
104
113
|
function execCaptureArgv(cwd, file, args) {
|
|
105
114
|
return new Promise((resolve) => {
|
|
106
115
|
const child = spawn('git', [...args, file], { cwd, shell: false, env: filteredVerifyEnv() });
|
|
107
|
-
const
|
|
108
|
-
child.stdout.on('data', (b) =>
|
|
109
|
-
child.stderr.on('data', (b) =>
|
|
110
|
-
child.on('close', () => resolve(
|
|
116
|
+
const sink = cappedOutput(MAX_BLAME_BYTES);
|
|
117
|
+
child.stdout.on('data', (b) => sink.push(b));
|
|
118
|
+
child.stderr.on('data', (b) => sink.push(b));
|
|
119
|
+
child.on('close', () => resolve(sink.text().split('\n').slice(0, 20).join('\n')));
|
|
111
120
|
child.on('error', () => resolve(''));
|
|
112
121
|
});
|
|
113
122
|
}
|
|
@@ -11,6 +11,7 @@ import { spawn } from 'node:child_process';
|
|
|
11
11
|
import { detect, summarize } from './detect.js';
|
|
12
12
|
import { filteredVerifyEnv } from './registry.js';
|
|
13
13
|
import { redact } from '../policy/secret-redactor.js';
|
|
14
|
+
import { cappedOutput } from '../shared/output-cap.js';
|
|
14
15
|
import { globalBus } from '../events/bus.js';
|
|
15
16
|
const MAX_VERIFY_BYTES = 256 * 1024;
|
|
16
17
|
function appendCapped(current, chunk) {
|
|
@@ -86,16 +87,16 @@ export async function verify(opts) {
|
|
|
86
87
|
return new Promise((resolve) => {
|
|
87
88
|
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
88
89
|
const child = spawn(opts.command, { cwd: opts.cwd, shell: true, env: filteredVerifyEnv() });
|
|
89
|
-
const
|
|
90
|
-
const
|
|
90
|
+
const outCap = cappedOutput(MAX_VERIFY_BYTES);
|
|
91
|
+
const errCap = cappedOutput(MAX_VERIFY_BYTES);
|
|
91
92
|
let done = false;
|
|
92
93
|
const timer = setTimeout(() => {
|
|
93
94
|
if (done)
|
|
94
95
|
return;
|
|
95
96
|
child.kill();
|
|
96
97
|
done = true;
|
|
97
|
-
const so =
|
|
98
|
-
const se =
|
|
98
|
+
const so = outCap.text();
|
|
99
|
+
const se = errCap.text();
|
|
99
100
|
const raw = se + '\n' + so;
|
|
100
101
|
emitFailed('[verify timeout]');
|
|
101
102
|
resolve({
|
|
@@ -106,17 +107,15 @@ export async function verify(opts) {
|
|
|
106
107
|
failure: { type: 'runtime', files: [], raw, exitCode: -1 },
|
|
107
108
|
});
|
|
108
109
|
}, timeout);
|
|
109
|
-
child.stdout.on('data', (b) =>
|
|
110
|
-
|
|
111
|
-
child.stderr.on('data', (b) => { if (Buffer.concat(errChunks).length < MAX_VERIFY_BYTES)
|
|
112
|
-
errChunks.push(b); });
|
|
110
|
+
child.stdout.on('data', (b) => outCap.push(b));
|
|
111
|
+
child.stderr.on('data', (b) => errCap.push(b));
|
|
113
112
|
child.on('close', (code) => {
|
|
114
113
|
if (done)
|
|
115
114
|
return;
|
|
116
115
|
done = true;
|
|
117
116
|
clearTimeout(timer);
|
|
118
|
-
const so =
|
|
119
|
-
const se =
|
|
117
|
+
const so = outCap.text();
|
|
118
|
+
const se = errCap.text();
|
|
120
119
|
const exit = typeof code === 'number' ? code : -1;
|
|
121
120
|
if (exit === 0) {
|
|
122
121
|
emitSucceeded();
|
|
@@ -6,6 +6,7 @@ import * as fs from 'node:fs';
|
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { filteredVerifyEnv } from './registry.js';
|
|
9
|
+
import { cappedOutput } from '../shared/output-cap.js';
|
|
9
10
|
export function findRelatedTests(cwd, editedFiles) {
|
|
10
11
|
if (editedFiles.length === 0)
|
|
11
12
|
return [];
|
|
@@ -124,8 +125,8 @@ export async function runScopedVerify(cwd, command, timeoutMs = 45_000) {
|
|
|
124
125
|
return new Promise((resolve) => {
|
|
125
126
|
// S2: verify commands run with filtered env; servers needing keys must use explicit config.
|
|
126
127
|
const child = spawn(command, { cwd, shell: true, env: filteredVerifyEnv() });
|
|
127
|
-
const
|
|
128
|
-
const
|
|
128
|
+
const outCap = cappedOutput(MAX_SCOPED_BYTES);
|
|
129
|
+
const errCap = cappedOutput(MAX_SCOPED_BYTES);
|
|
129
130
|
let done = false;
|
|
130
131
|
const timer = setTimeout(() => {
|
|
131
132
|
if (done)
|
|
@@ -135,22 +136,20 @@ export async function runScopedVerify(cwd, command, timeoutMs = 45_000) {
|
|
|
135
136
|
child.kill();
|
|
136
137
|
}
|
|
137
138
|
catch { /* ignore */ }
|
|
138
|
-
const so =
|
|
139
|
-
const se =
|
|
139
|
+
const so = outCap.text();
|
|
140
|
+
const se = errCap.text();
|
|
140
141
|
resolve({ ok: false, exitCode: -1, stdout: so, stderr: se + '\n[scoped timeout]' });
|
|
141
142
|
}, timeoutMs);
|
|
142
|
-
child.stdout.on('data', (b) =>
|
|
143
|
-
|
|
144
|
-
child.stderr.on('data', (b) => { if (Buffer.concat(errChunks).length < MAX_SCOPED_BYTES)
|
|
145
|
-
errChunks.push(b); });
|
|
143
|
+
child.stdout.on('data', (b) => outCap.push(b));
|
|
144
|
+
child.stderr.on('data', (b) => errCap.push(b));
|
|
146
145
|
child.on('close', (code) => {
|
|
147
146
|
if (done)
|
|
148
147
|
return;
|
|
149
148
|
done = true;
|
|
150
149
|
clearTimeout(timer);
|
|
151
150
|
const exit = typeof code === 'number' ? code : -1;
|
|
152
|
-
const so =
|
|
153
|
-
const se =
|
|
151
|
+
const so = outCap.text();
|
|
152
|
+
const se = errCap.text();
|
|
154
153
|
resolve({ ok: exit === 0, exitCode: exit, stdout: so, stderr: se });
|
|
155
154
|
});
|
|
156
155
|
child.on('error', (err) => {
|
|
@@ -158,7 +157,7 @@ export async function runScopedVerify(cwd, command, timeoutMs = 45_000) {
|
|
|
158
157
|
return;
|
|
159
158
|
done = true;
|
|
160
159
|
clearTimeout(timer);
|
|
161
|
-
const so =
|
|
160
|
+
const so = outCap.text();
|
|
162
161
|
resolve({ ok: false, exitCode: -1, stdout: so, stderr: String(err) });
|
|
163
162
|
});
|
|
164
163
|
});
|