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.
@@ -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 scrolls one line instead of history', async () => {
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 live tail recalls history; scrolled up it scrolls (both kept)', async () => {
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 keeps scrolling and leaves input empty', async () => {
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 + ↑ → scroll (MSG-00 stays), input untouched:
287
- // typing '!' must NOT extend the submitted prompt.
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() ?? '').not.toContain('zznoscroll!');
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 outChunks = [];
87
- const errChunks = [];
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 = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
98
- const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
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) => { if (Buffer.concat(outChunks).length < MAX_BASELINE_BYTES)
102
- outChunks.push(b); });
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 = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
112
- const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
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 = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_BASELINE_BYTES);
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 chunks = [];
98
- child.stdout.on('data', (b) => { chunks.push(b); });
99
- child.stderr.on('data', (b) => { chunks.push(b); });
100
- child.on('close', () => resolve(Buffer.concat(chunks).toString().slice(0, 4000)));
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 chunks = [];
108
- child.stdout.on('data', (b) => { chunks.push(b); });
109
- child.stderr.on('data', (b) => { chunks.push(b); });
110
- child.on('close', () => resolve(Buffer.concat(chunks).toString().split('\n').slice(0, 20).join('\n')));
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 outChunks = [];
90
- const errChunks = [];
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 = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
98
- const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
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) => { if (Buffer.concat(outChunks).length < MAX_VERIFY_BYTES)
110
- outChunks.push(b); });
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 = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
119
- const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_VERIFY_BYTES);
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 outChunks = [];
128
- const errChunks = [];
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 = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
139
- const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
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) => { if (Buffer.concat(outChunks).length < MAX_SCOPED_BYTES)
143
- outChunks.push(b); });
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 = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
153
- const se = Buffer.concat(errChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
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 = Buffer.concat(outChunks).toString('utf-8').slice(0, MAX_SCOPED_BYTES);
160
+ const so = outCap.text();
162
161
  resolve({ ok: false, exitCode: -1, stdout: so, stderr: String(err) });
163
162
  });
164
163
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "1.0.13",
3
+ "version": "1.0.16",
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",