klyro 1.0.4 → 1.0.6

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 (80) hide show
  1. package/README.md +29 -0
  2. package/dist/agent/custom-agents.d.ts +3 -0
  3. package/dist/agent/custom-agents.js +96 -0
  4. package/dist/agent/orchestrator.d.ts +22 -2
  5. package/dist/agent/orchestrator.js +30 -4
  6. package/dist/agent/runtime.d.ts +5 -0
  7. package/dist/agent/runtime.js +174 -51
  8. package/dist/checkpoints/store.d.ts +9 -0
  9. package/dist/checkpoints/store.js +20 -0
  10. package/dist/cli/auth.js +11 -3
  11. package/dist/cli/completion.js +63 -10
  12. package/dist/cli/config.d.ts +4 -4
  13. package/dist/cli/doctor.js +13 -0
  14. package/dist/cli/eval.d.ts +15 -1
  15. package/dist/cli/eval.js +34 -2
  16. package/dist/cli/hooks.d.ts +54 -5
  17. package/dist/cli/hooks.js +85 -6
  18. package/dist/cli/init.d.ts +6 -0
  19. package/dist/cli/init.js +60 -0
  20. package/dist/cli/repl.js +261 -34
  21. package/dist/cli/run.d.ts +7 -1
  22. package/dist/cli/run.js +97 -41
  23. package/dist/cli/slash/custom.d.ts +25 -0
  24. package/dist/cli/slash/custom.js +166 -0
  25. package/dist/cli/slash/parser.d.ts +16 -2
  26. package/dist/cli/slash/parser.js +67 -18
  27. package/dist/cli/update.d.ts +8 -4
  28. package/dist/cli/update.js +50 -7
  29. package/dist/context/accounting.d.ts +8 -0
  30. package/dist/context/accounting.js +18 -1
  31. package/dist/context/compaction.d.ts +1 -0
  32. package/dist/context/compaction.js +2 -1
  33. package/dist/context/memory.js +18 -1
  34. package/dist/eval/harness.d.ts +21 -3
  35. package/dist/eval/harness.js +31 -3
  36. package/dist/eval/judge.d.ts +32 -0
  37. package/dist/eval/judge.js +63 -0
  38. package/dist/eval/tasks.js +134 -0
  39. package/dist/index.js +225 -132
  40. package/dist/mcp/client.d.ts +15 -0
  41. package/dist/mcp/client.js +42 -2
  42. package/dist/mcp/config.d.ts +10 -1
  43. package/dist/mcp/config.js +64 -1
  44. package/dist/mcp/registry.d.ts +13 -0
  45. package/dist/mcp/registry.js +47 -5
  46. package/dist/mcp/remote.d.ts +29 -0
  47. package/dist/mcp/remote.js +153 -0
  48. package/dist/mcp/serve.d.ts +23 -0
  49. package/dist/mcp/serve.js +111 -0
  50. package/dist/policy/approval.d.ts +15 -1
  51. package/dist/policy/approval.js +8 -0
  52. package/dist/policy/engine.d.ts +11 -1
  53. package/dist/policy/engine.js +14 -1
  54. package/dist/policy/secret-redactor.js +4 -1
  55. package/dist/providers/endpoints.d.ts +43 -0
  56. package/dist/providers/endpoints.js +104 -0
  57. package/dist/providers.js +13 -10
  58. package/dist/shared/error-map.d.ts +19 -0
  59. package/dist/shared/error-map.js +58 -0
  60. package/dist/tools/lsp/diagnostics.d.ts +35 -4
  61. package/dist/tools/lsp/diagnostics.js +88 -9
  62. package/dist/tools/normalize.d.ts +3 -0
  63. package/dist/tools/normalize.js +8 -5
  64. package/dist/tools/search/dependencies.d.ts +2 -2
  65. package/dist/tools/shell/background.d.ts +6 -0
  66. package/dist/tools/shell/background.js +17 -0
  67. package/dist/tools/symbols/find-symbol.d.ts +1 -1
  68. package/dist/tools/symbols/find-symbol.js +8 -6
  69. package/dist/tools/types.d.ts +8 -1
  70. package/dist/tui/app.d.ts +2 -0
  71. package/dist/tui/app.js +454 -53
  72. package/dist/tui/app.test.js +66 -3
  73. package/dist/tui/approval.js +53 -1
  74. package/dist/tui/markdown.js +9 -0
  75. package/dist/tui/mouse.d.ts +26 -1
  76. package/dist/tui/mouse.js +104 -6
  77. package/dist/tui/scroll-flow.test.js +3 -1
  78. package/dist/tui/tokens.d.ts +6 -6
  79. package/dist/tui/tokens.js +9 -6
  80. package/package.json +1 -1
@@ -261,8 +261,13 @@ describe('App', () => {
261
261
  stdin.write('/c');
262
262
  await new Promise((r) => setTimeout(r, 30));
263
263
  const withSuggest = lastFrame() ?? '';
264
- expect(withSuggest).toMatch(/\/clear/);
265
264
  expect(withSuggest).toMatch(/tab to complete/i);
265
+ // Prefix dominance: every suggestion for '/c' still starts with c.
266
+ const sugLines = withSuggest.split('\n').filter((l) => l.includes('/') && /clear|compact|cost|config|context|commands|copy|cancel/.test(l));
267
+ expect(sugLines.length).toBeGreaterThan(0);
268
+ // Unambiguous fuzzy prefix completes deterministically.
269
+ stdin.write('le');
270
+ await new Promise((r) => setTimeout(r, 30));
266
271
  stdin.write('\t');
267
272
  await new Promise((r) => setTimeout(r, 30));
268
273
  expect(lastFrame() ?? '').toContain('/clear ');
@@ -315,9 +320,15 @@ describe('App', () => {
315
320
  const kinds = onSlash.mock.calls.map((c) => c[0].kind);
316
321
  expect(kinds).toEqual(['cancel', 'quit']);
317
322
  });
318
- it('running Esc with empty queue cancels streaming (design.md §18)', async () => {
323
+ it('running Esc×2 with empty queue cancels streaming (double-Esc convention)', async () => {
319
324
  const onSlash = vi.fn(async () => { });
320
- const { stdin } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: onSlash, initialStatus: { status: 'running' } }));
325
+ const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: onSlash, initialStatus: { status: 'running' } }));
326
+ // First Esc only arms (hint shows, no cancel yet).
327
+ stdin.write('\x1b');
328
+ await new Promise((r) => setTimeout(r, 50));
329
+ expect(onSlash).not.toHaveBeenCalled();
330
+ await waitForMatch(lastFrame, /press esc again to cancel/);
331
+ // Second Esc within the window cancels.
321
332
  stdin.write('\x1b');
322
333
  await new Promise((r) => setTimeout(r, 50));
323
334
  expect(onSlash).toHaveBeenCalled();
@@ -519,6 +530,58 @@ describe('App', () => {
519
530
  expect(frame).not.toContain('{"path"');
520
531
  expect(frame).not.toContain('queued:');
521
532
  });
533
+ it('scrubs secret shapes at the render boundary (never paints raw)', async () => {
534
+ let hooks = null;
535
+ const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: false, onMounted: (h) => {
536
+ hooks = { append: h.append };
537
+ } }));
538
+ await new Promise((r) => setTimeout(r, 50));
539
+ hooks.append({ id: 'sec-1', kind: 'text', text: 'deploy with password=hunter2-hunter2', role: 'assistant' });
540
+ const frame = await waitForMatch(lastFrame, /REDACTED/);
541
+ expect(frame).not.toMatch(/hunter2-hunter2/);
542
+ });
543
+ it('vim counts, word motions, and dd edit the buffer', async () => {
544
+ let hooks = null;
545
+ const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: false, onMounted: (h) => {
546
+ hooks = { setVimMode: h.setVimMode };
547
+ } }));
548
+ await new Promise((r) => setTimeout(r, 50));
549
+ stdin.write('abcdef');
550
+ await waitForMatch(lastFrame, /abcdef/);
551
+ hooks.setVimMode('normal');
552
+ await waitForMatch(lastFrame, /--NORMAL--/);
553
+ // 0 then 3x deletes three chars → 'def'
554
+ stdin.write('0');
555
+ await new Promise((r) => setTimeout(r, 30));
556
+ stdin.write('3');
557
+ await new Promise((r) => setTimeout(r, 30));
558
+ stdin.write('x');
559
+ await waitForMatch(lastFrame, /def/);
560
+ expect(lastFrame() ?? '').not.toMatch(/abcdef/);
561
+ // dd clears the single-line buffer
562
+ stdin.write('d');
563
+ await new Promise((r) => setTimeout(r, 30));
564
+ stdin.write('d');
565
+ await waitForMatch(lastFrame, /Message Klyro/);
566
+ });
567
+ it('vim normal mode swallows typing, shows mode, i returns to insert', async () => {
568
+ let hooks = null;
569
+ const { stdin, lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: false, onMounted: (h) => {
570
+ hooks = { setVimMode: h.setVimMode };
571
+ } }));
572
+ await new Promise((r) => setTimeout(r, 50));
573
+ stdin.write('ok');
574
+ await waitForMatch(lastFrame, /ok/);
575
+ hooks.setVimMode('normal');
576
+ await waitForMatch(lastFrame, /--NORMAL--/);
577
+ stdin.write('zzz');
578
+ await new Promise((r) => setTimeout(r, 200));
579
+ expect(lastFrame() ?? '').not.toMatch(/zzz/);
580
+ stdin.write('i');
581
+ await waitForAbsent(lastFrame, /--NORMAL--/);
582
+ stdin.write('!');
583
+ await waitForMatch(lastFrame, /ok!/);
584
+ });
522
585
  it('aborted run leaves no thinking behind (only response may remain)', async () => {
523
586
  let hooks = null;
524
587
  const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialStatus: { status: 'running' }, onMounted: (h) => {
@@ -74,13 +74,65 @@ export function ApprovalModal({ bridge }) {
74
74
  }, [bridge]);
75
75
  const [explain, setExplain] = useState(false);
76
76
  const [full, setFull] = useState(false);
77
+ // Inline edit mode (`e`): buffer holds the JSON input being edited.
78
+ // Enter submits (re-validated + re-prompted by the runtime), Esc cancels
79
+ // back to the choice keys. Reset whenever a new prompt arrives.
80
+ const [editing, setEditing] = useState(null);
81
+ const [editError, setEditError] = useState(null);
82
+ useEffect(() => {
83
+ setEditing(null);
84
+ setEditError(null);
85
+ }, [pending]);
77
86
  useInput((inputStr, key) => {
78
87
  if (!pending)
79
88
  return;
89
+ if (editing !== null) {
90
+ if (key.return) {
91
+ let parsed;
92
+ try {
93
+ parsed = JSON.parse(editing);
94
+ }
95
+ catch (err) {
96
+ setEditError(`invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
97
+ return;
98
+ }
99
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
100
+ setEditError('edited input must be a JSON object');
101
+ return;
102
+ }
103
+ bridge.resolve({ kind: 'edit', editedInput: parsed });
104
+ return;
105
+ }
106
+ if (key.escape) {
107
+ setEditing(null);
108
+ setEditError(null);
109
+ return;
110
+ }
111
+ if (key.backspace || key.delete) {
112
+ setEditing((v) => (v ?? '').slice(0, -1));
113
+ return;
114
+ }
115
+ if (!key.ctrl && !key.meta)
116
+ setEditing((v) => (v ?? '') + inputStr.replace(/\r/g, '\n'));
117
+ return;
118
+ }
80
119
  if (key.return) {
81
120
  bridge.resolve('deny');
82
121
  return;
83
122
  }
123
+ // `e` enters inline edit mode (real edit-and-retry); the stdin prompt
124
+ // keeps `e` as deny since it has no editor. Case-sensitive mapping for
125
+ // the rest lives in approvalChoiceForKey.
126
+ if (inputStr === 'e') {
127
+ try {
128
+ setEditing(JSON.stringify(pending.input ?? {}, null, 2));
129
+ }
130
+ catch {
131
+ setEditing('{}');
132
+ }
133
+ setEditError(null);
134
+ return;
135
+ }
84
136
  // Case-sensitive mapping lives in approvalChoiceForKey: 'A' persists,
85
137
  // 'a' is session-only (the old inline toLowerCase-first code collapsed
86
138
  // them, silently downgrading persistence).
@@ -107,5 +159,5 @@ export function ApprovalModal({ bridge }) {
107
159
  const clean = sanitizeForPrompt(s);
108
160
  return !full && clean.length > n ? clean.slice(0, n) + '…' : clean;
109
161
  };
110
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 1, marginY: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u26A0 approval needed \u2014 ", pending.toolName] }), _jsxs(Text, { color: "gray", children: [" reason: \"", trunc(pending.reason), "\""] }), pending.summary ? _jsxs(Text, { children: [" \"", trunc(pending.summary), "\""] }) : null, explain ? _jsxs(Text, { color: "cyan", children: [" Explain: This tool will ", pending.toolName, " with the shown args. [y] once, [a] session, [A] always\u2192settings, [n] deny, [e] edit, [f] full text, [?] toggle help."] }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "[y] once" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[a] session" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[A] always" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "red", children: "[n] deny" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "yellow", children: "[e] edit" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[f] full" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[?] explain" }), _jsx(Text, { color: "gray", children: " (Enter = deny)" })] })] }));
162
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: "yellow", paddingX: 1, marginY: 1, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["\u26A0 approval needed \u2014 ", pending.toolName] }), _jsxs(Text, { color: "gray", children: [" reason: \"", trunc(pending.reason), "\""] }), pending.summary ? _jsxs(Text, { children: [" \"", trunc(pending.summary), "\""] }) : null, editing !== null ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "yellow", bold: true, children: " editing input JSON (Enter = submit, Esc = back):" }), _jsxs(Text, { wrap: "wrap", children: [" ", (editing.length > 1500 ? editing.slice(0, 1500) + '…' : editing) + '▌'] }), editError ? _jsxs(Text, { color: "red", children: [" ", editError] }) : null] })) : null, explain ? _jsxs(Text, { color: "cyan", children: [" Explain: This tool will ", pending.toolName, " with the shown args. [y] once, [a] session, [A] always\u2192settings, [n] deny, [e] edit, [f] full text, [?] toggle help."] }) : null, _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "green", children: "[y] once" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[a] session" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "green", children: "[A] always" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "red", children: "[n] deny" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "yellow", children: "[e] edit" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[f] full" }), _jsx(Text, { color: "gray", children: " " }), _jsx(Text, { color: "cyan", children: "[?] explain" }), _jsx(Text, { color: "gray", children: " (Enter = deny)" })] })] }));
111
163
  }
@@ -103,6 +103,14 @@ export function highlightCodeLine(line, lang) {
103
103
  }
104
104
  }
105
105
  if (STRING_DELIMS.includes(ch)) {
106
+ // Contraction guard: a single-quote mid-word (it's, don't, users')
107
+ // is prose punctuation, not a string delimiter — only open on `'`
108
+ // after start-of-line or a non-word char. Other delims open anywhere.
109
+ if (ch === "'" && i > 0 && /[A-Za-z0-9_)}\]]/.test(line[i - 1])) {
110
+ buf += ch;
111
+ i++;
112
+ continue;
113
+ }
106
114
  flush();
107
115
  inString = true;
108
116
  strDelim = ch;
@@ -115,6 +123,7 @@ export function highlightCodeLine(line, lang) {
115
123
  while (j < line.length && /[A-Za-z0-9_$]/.test(line[j]))
116
124
  j++;
117
125
  const tok = line.slice(i, j);
126
+ flush(); // pending separators (spaces, punctuation) precede the token
118
127
  flushKw(tok);
119
128
  i = j;
120
129
  continue;
@@ -34,6 +34,28 @@ 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
+ export declare const PASTE_START = "\u001B[200~";
38
+ export declare const PASTE_END = "\u001B[201~";
39
+ export declare const PASTE_ENABLE = "\u001B[?2004h";
40
+ export declare const PASTE_DISABLE = "\u001B[?2004l";
41
+ /**
42
+ * Bracketed-paste splitter. Strips the `ESC[200~` / `ESC[201~` markers and
43
+ * delivers pasted content as one atomic string so the TUI can bulk-insert
44
+ * it (no per-char autocomplete/history side effects). Content split across
45
+ * stdin chunks is reassembled; an unterminated paste is held (up to 1 MiB,
46
+ * then force-flushed) so a partial escape never leaks into the input.
47
+ */
48
+ export declare class PasteFilter {
49
+ private pending;
50
+ private inPaste;
51
+ private chunks;
52
+ private size;
53
+ push(chunk: Buffer): {
54
+ kept: Buffer;
55
+ pastes: string[];
56
+ };
57
+ reset(): void;
58
+ }
37
59
  /**
38
60
  * stdin.read() wrapper implementing the tap (see repl.ts installMouseTap).
39
61
  * Ink 7 consumes stdin via paused-mode read() calls, so filtering happens
@@ -43,4 +65,7 @@ export declare const MOUSE_DISABLE = "\u001B[?1000l\u001B[?1006l";
43
65
  * mouse sequences are swallowed (wheels dispatched); everything else is
44
66
  * returned byte-identical in its original string/Buffer shape.
45
67
  */
46
- export declare function createReadWrapper(origRead: (size?: number) => unknown, filter: MouseFilter, onWheel: (delta: number) => void): (size?: number) => unknown;
68
+ export declare function createReadWrapper(origRead: (size?: number) => unknown, filter: MouseFilter, onWheel: (delta: number) => void, opts?: {
69
+ onPaste?: (text: string) => void;
70
+ pasteFilter?: PasteFilter;
71
+ }): (size?: number) => unknown;
package/dist/tui/mouse.js CHANGED
@@ -92,6 +92,92 @@ 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
+ export const PASTE_START = '\x1b[200~';
96
+ export const PASTE_END = '\x1b[201~';
97
+ export const PASTE_ENABLE = '\x1b[?2004h'; // bracketed paste: terminal wraps pastes
98
+ export const PASTE_DISABLE = '\x1b[?2004l';
99
+ const PASTE_START_BUF = Buffer.from(PASTE_START, 'latin1');
100
+ const PASTE_END_BUF = Buffer.from(PASTE_END, 'latin1');
101
+ const MAX_PASTE_BYTES = 1024 * 1024;
102
+ /**
103
+ * Bracketed-paste splitter. Strips the `ESC[200~` / `ESC[201~` markers and
104
+ * delivers pasted content as one atomic string so the TUI can bulk-insert
105
+ * it (no per-char autocomplete/history side effects). Content split across
106
+ * stdin chunks is reassembled; an unterminated paste is held (up to 1 MiB,
107
+ * then force-flushed) so a partial escape never leaks into the input.
108
+ */
109
+ export class PasteFilter {
110
+ pending = Buffer.alloc(0);
111
+ inPaste = false;
112
+ chunks = [];
113
+ size = 0;
114
+ push(chunk) {
115
+ const data = Buffer.concat([this.pending, chunk]);
116
+ this.pending = Buffer.alloc(0);
117
+ const kept = [];
118
+ const pastes = [];
119
+ let pos = 0;
120
+ const tailPartial = (from) => {
121
+ // Length of the trailing run that could be a split marker prefix.
122
+ const tail = data.subarray(from);
123
+ for (let len = Math.min(tail.length, 5); len >= 1; len--) {
124
+ const piece = tail.subarray(tail.length - len);
125
+ if (PASTE_START_BUF.subarray(0, len).equals(piece) || PASTE_END_BUF.subarray(0, len).equals(piece))
126
+ return len;
127
+ }
128
+ return 0;
129
+ };
130
+ while (pos < data.length) {
131
+ if (!this.inPaste) {
132
+ const idx = data.indexOf(PASTE_START_BUF, pos);
133
+ if (idx === -1) {
134
+ const hold = tailPartial(pos);
135
+ kept.push(data.subarray(pos, hold > 0 ? data.length - hold : data.length));
136
+ if (hold > 0)
137
+ this.pending = data.subarray(data.length - hold);
138
+ break;
139
+ }
140
+ kept.push(data.subarray(pos, idx));
141
+ pos = idx + PASTE_START_BUF.length;
142
+ this.inPaste = true;
143
+ this.chunks = [];
144
+ this.size = 0;
145
+ continue;
146
+ }
147
+ const idx = data.indexOf(PASTE_END_BUF, pos);
148
+ if (idx === -1) {
149
+ const hold = tailPartial(pos);
150
+ const end = hold > 0 ? data.length - hold : data.length;
151
+ if (end > pos) {
152
+ this.chunks.push(data.subarray(pos, end));
153
+ this.size += end - pos;
154
+ }
155
+ if (hold > 0)
156
+ this.pending = data.subarray(data.length - hold);
157
+ if (this.size >= MAX_PASTE_BYTES) {
158
+ pastes.push(Buffer.concat(this.chunks).toString('utf8'));
159
+ this.chunks = [];
160
+ this.size = 0;
161
+ this.inPaste = false; // force-flush: don't hold unbounded input
162
+ }
163
+ break;
164
+ }
165
+ this.chunks.push(data.subarray(pos, idx));
166
+ pastes.push(Buffer.concat(this.chunks).toString('utf8'));
167
+ this.chunks = [];
168
+ this.size = 0;
169
+ this.inPaste = false;
170
+ pos = idx + PASTE_END_BUF.length;
171
+ }
172
+ return { kept: Buffer.concat(kept), pastes };
173
+ }
174
+ reset() {
175
+ this.pending = Buffer.alloc(0);
176
+ this.inPaste = false;
177
+ this.chunks = [];
178
+ this.size = 0;
179
+ }
180
+ }
95
181
  /**
96
182
  * stdin.read() wrapper implementing the tap (see repl.ts installMouseTap).
97
183
  * Ink 7 consumes stdin via paused-mode read() calls, so filtering happens
@@ -101,23 +187,35 @@ export const MOUSE_DISABLE = '\x1b[?1000l\x1b[?1006l';
101
187
  * mouse sequences are swallowed (wheels dispatched); everything else is
102
188
  * returned byte-identical in its original string/Buffer shape.
103
189
  */
104
- export function createReadWrapper(origRead, filter, onWheel) {
190
+ export function createReadWrapper(origRead, filter, onWheel, opts) {
105
191
  return (size) => {
106
192
  if (size !== undefined)
107
193
  return origRead(size);
108
194
  const chunk = origRead();
109
195
  if (chunk == null)
110
196
  return chunk;
111
- const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
112
- const split = filter.push(buf);
113
- for (const w of split.wheels) {
197
+ let buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
198
+ if (opts?.pasteFilter) {
199
+ const split = opts.pasteFilter.push(buf);
200
+ for (const p of split.pastes) {
201
+ try {
202
+ opts.onPaste?.(p);
203
+ }
204
+ catch { /* ignore */ }
205
+ }
206
+ buf = split.kept;
207
+ if (buf.length === 0 && split.pastes.length > 0)
208
+ return null;
209
+ }
210
+ const msplit = filter.push(buf);
211
+ for (const w of msplit.wheels) {
114
212
  try {
115
213
  onWheel(w);
116
214
  }
117
215
  catch { /* ignore */ }
118
216
  }
119
- if (split.kept.length === 0)
217
+ if (msplit.kept.length === 0)
120
218
  return null;
121
- return typeof chunk === 'string' ? split.kept.toString('utf8') : split.kept;
219
+ return typeof chunk === 'string' ? msplit.kept.toString('utf8') : msplit.kept;
122
220
  };
123
221
  }
@@ -45,7 +45,9 @@ describe('scroll flow diagnostics', () => {
45
45
  const chunk = 'STREAMCHUNK lorem ipsum dolor sit amet. ';
46
46
  for (let i = 0; i < 12; i++) {
47
47
  g.__klyroAppendDelta(`${chunk}#${i} `);
48
- await tick(40);
48
+ // Tick longer than the 64ms streaming render throttle so the flush
49
+ // has landed before asserting visibility (follow-tail, not latency).
50
+ await tick(90);
49
51
  const frame = lastFrame() ?? '';
50
52
  expect(rowsOf(frame)).toBeLessThanOrEqual(32);
51
53
  // latest streamed chunk must be visible (follow-tail)
@@ -12,9 +12,9 @@ export declare const tokens: {
12
12
  readonly guide: "#3A3A3A";
13
13
  readonly accent: "#FF6B1A";
14
14
  readonly accentSoft: "#FFF1E6";
15
- readonly ok: "#FF6B1A";
15
+ readonly ok: "#2FA36B";
16
16
  readonly err: "#E06C6C";
17
- readonly warn: "#FF6B1A";
17
+ readonly warn: "#D9A021";
18
18
  readonly info: "#6FA8DC";
19
19
  readonly diffAddBg: "#12250F";
20
20
  readonly diffDelBg: "#2A1212";
@@ -26,15 +26,15 @@ export declare const tokens: {
26
26
  readonly soft: "whiteBright";
27
27
  readonly dim: "gray";
28
28
  readonly guide: "gray";
29
- readonly ok: "yellowBright";
29
+ readonly ok: "green";
30
30
  readonly err: "red";
31
- readonly warn: "yellowBright";
31
+ readonly warn: "yellow";
32
32
  readonly info: "blue";
33
33
  readonly border: "gray";
34
34
  readonly muted: "gray";
35
- readonly success: "yellowBright";
35
+ readonly success: "green";
36
36
  readonly error: "red";
37
- readonly warning: "yellowBright";
37
+ readonly warning: "yellow";
38
38
  };
39
39
  };
40
40
  export declare const glyphs: {
@@ -12,9 +12,12 @@ export const tokens = {
12
12
  guide: '#3A3A3A',
13
13
  accent: '#FF6B1A',
14
14
  accentSoft: '#FFF1E6',
15
- ok: '#FF6B1A',
15
+ // Status hues are deliberately distinct from the orange brand: ok reads
16
+ // green, warn reads amber, err reads red. (Previously ok/warn reused the
17
+ // accent hex and were indistinguishable in the transcript.)
18
+ ok: '#2FA36B',
16
19
  err: '#E06C6C',
17
- warn: '#FF6B1A',
20
+ warn: '#D9A021',
18
21
  info: '#6FA8DC',
19
22
  diffAddBg: '#12250F',
20
23
  diffDelBg: '#2A1212',
@@ -26,15 +29,15 @@ export const tokens = {
26
29
  soft: 'whiteBright',
27
30
  dim: 'gray',
28
31
  guide: 'gray',
29
- ok: 'yellowBright',
32
+ ok: 'green',
30
33
  err: 'red',
31
- warn: 'yellowBright',
34
+ warn: 'yellow',
32
35
  info: 'blue',
33
36
  border: 'gray',
34
37
  muted: 'gray',
35
- success: 'yellowBright',
38
+ success: 'green',
36
39
  error: 'red',
37
- warning: 'yellowBright',
40
+ warning: 'yellow',
38
41
  },
39
42
  };
40
43
  export const glyphs = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "klyro",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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",