klyro 1.0.3 → 1.0.5

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 (48) hide show
  1. package/README.md +29 -0
  2. package/dist/agent/runtime.js +11 -2
  3. package/dist/cli/auth.js +11 -3
  4. package/dist/cli/completion.js +63 -10
  5. package/dist/cli/doctor.js +13 -0
  6. package/dist/cli/repl.js +116 -10
  7. package/dist/cli/run.js +14 -0
  8. package/dist/cli/slash/parser.d.ts +7 -1
  9. package/dist/cli/slash/parser.js +37 -10
  10. package/dist/cli/update.d.ts +8 -4
  11. package/dist/cli/update.js +50 -7
  12. package/dist/context/accounting.d.ts +8 -0
  13. package/dist/context/accounting.js +18 -1
  14. package/dist/context/compaction.d.ts +1 -0
  15. package/dist/context/compaction.js +2 -1
  16. package/dist/index.js +78 -8
  17. package/dist/mcp/config.d.ts +7 -0
  18. package/dist/mcp/config.js +45 -0
  19. package/dist/mcp/registry.js +6 -4
  20. package/dist/mcp/serve.d.ts +23 -0
  21. package/dist/mcp/serve.js +111 -0
  22. package/dist/policy/engine.d.ts +11 -1
  23. package/dist/policy/engine.js +14 -1
  24. package/dist/policy/secret-redactor.js +4 -1
  25. package/dist/shared/error-map.d.ts +19 -0
  26. package/dist/shared/error-map.js +58 -0
  27. package/dist/tools/lsp/diagnostics.d.ts +35 -4
  28. package/dist/tools/lsp/diagnostics.js +88 -9
  29. package/dist/tools/normalize.d.ts +3 -0
  30. package/dist/tools/normalize.js +8 -5
  31. package/dist/tools/search/dependencies.d.ts +2 -2
  32. package/dist/tools/shell/background.d.ts +6 -0
  33. package/dist/tools/shell/background.js +17 -0
  34. package/dist/tools/symbols/find-symbol.d.ts +1 -1
  35. package/dist/tools/symbols/find-symbol.js +8 -6
  36. package/dist/tools/types.d.ts +8 -1
  37. package/dist/tui/app.d.ts +2 -0
  38. package/dist/tui/app.js +330 -52
  39. package/dist/tui/app.test.js +45 -6
  40. package/dist/tui/markdown.js +9 -0
  41. package/dist/tui/mouse.d.ts +26 -1
  42. package/dist/tui/mouse.js +104 -6
  43. package/dist/tui/scroll-flow.test.js +4 -2
  44. package/dist/tui/scroll-model.d.ts +3 -1
  45. package/dist/tui/scroll-model.js +6 -5
  46. package/dist/tui/tokens.d.ts +6 -6
  47. package/dist/tui/tokens.js +9 -6
  48. 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,34 @@ 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 normal mode swallows typing, shows mode, i returns to insert', 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('ok');
550
+ await waitForMatch(lastFrame, /ok/);
551
+ hooks.setVimMode('normal');
552
+ await waitForMatch(lastFrame, /--NORMAL--/);
553
+ stdin.write('zzz');
554
+ await new Promise((r) => setTimeout(r, 200));
555
+ expect(lastFrame() ?? '').not.toMatch(/zzz/);
556
+ stdin.write('i');
557
+ await waitForAbsent(lastFrame, /--NORMAL--/);
558
+ stdin.write('!');
559
+ await waitForMatch(lastFrame, /ok!/);
560
+ });
522
561
  it('aborted run leaves no thinking behind (only response may remain)', async () => {
523
562
  let hooks = null;
524
563
  const { lastFrame } = render(_jsx(App, { initialModel: "m", maxSteps: 10, cwd: "/test", onPrompt: async () => { }, onSlash: async () => { }, isFullscreen: true, initialStatus: { status: 'running' }, onMounted: (h) => {
@@ -541,13 +580,13 @@ describe('App', () => {
541
580
  await waitForMatch(lastFrame, /⇅ 0\/\d+/);
542
581
  hooks.append({ id: 'late-1', kind: 'text', text: 'LATE-1-tag', role: 'assistant' });
543
582
  // Badge counts lines grown while pinned.
544
- await waitForMatch(lastFrame, /↓ \d+ new/);
583
+ await waitForMatch(lastFrame, /↓ \d+ unread/);
545
584
  hooks.clearTranscript();
546
585
  // Wait for transcript content to actually clear (no stale MSG-* items) —
547
586
  // this is the "no stale badge" invariant: after a clear, the view must
548
587
  // drop back to follow-tail with the pin/count reset.
549
588
  await waitForAbsent(lastFrame, /MSG-\d{2}-tag/);
550
- await waitForAbsent(lastFrame, /↓ \d+ new/);
589
+ await waitForAbsent(lastFrame, /↓ \d+ unread/);
551
590
  // Fresh session: seed 25 more, let them settle to a bottom-anchored view.
552
591
  for (let i = 0; i < 25; i++) {
553
592
  hooks.append({ id: `n-${i}`, kind: 'text', text: `NEW-${i.toString().padStart(2, '0')}-tag`, role: 'user' });
@@ -558,7 +597,7 @@ describe('App', () => {
558
597
  await waitForMatch(lastFrame, /NEWLATE-tag/);
559
598
  await new Promise((r) => setTimeout(r, 100));
560
599
  const afterFollow = lastFrame() ?? '';
561
- expect(afterFollow).not.toMatch(/↓ \d+ new/);
600
+ expect(afterFollow).not.toMatch(/↓ \d+ unread/);
562
601
  expect(afterFollow).not.toMatch(/MSG-\d{2}-tag/);
563
602
  });
564
603
  it('status bar shows scroll position when content overflows', async () => {
@@ -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)
@@ -66,7 +68,7 @@ describe('scroll flow diagnostics', () => {
66
68
  }
67
69
  const frozen = lastFrame() ?? '';
68
70
  expect(frozen).toContain('MSG-00-tag'); // viewport did not yank down
69
- expect(frozen).toMatch(/↓ \d+ new/); // badge visible
71
+ expect(frozen).toMatch(/↓ \d+ unread/); // badge visible
70
72
  expect(rowsOf(frozen)).toBeLessThanOrEqual(32);
71
73
  stdin.write('\x1b[F'); // End → follow
72
74
  await tick(50);
@@ -63,5 +63,7 @@ export interface Resolved {
63
63
  }
64
64
  /** Anchor → topRow, run once per frame after the line index rebuild (§5.3). */
65
65
  export declare function resolveTopRow(s: ScrollState, ctx: ScrollCtx): Resolved;
66
- /** Badge label per §7.2. Empty string = hidden. */
66
+ /** Badge label per §7.2 (Claude Code style: "N unread" / "jump to end").
67
+ * Empty string = hidden. `idle` = agent done streaming, the only new lines
68
+ * are the ones already counted — the affordance flips to "jump to end". */
67
69
  export declare function badgeLabel(atBottom: boolean, newSinceUnstick: number, idle: boolean): string;
@@ -87,13 +87,14 @@ export function resolveTopRow(s, ctx) {
87
87
  const topRow = clampN(raw, 0, maxTop);
88
88
  return { topRow, atBottom: topRow >= maxTop - FOLLOW_EPSILON };
89
89
  }
90
- /** Badge label per §7.2. Empty string = hidden. */
90
+ /** Badge label per §7.2 (Claude Code style: "N unread" / "jump to end").
91
+ * Empty string = hidden. `idle` = agent done streaming, the only new lines
92
+ * are the ones already counted — the affordance flips to "jump to end". */
91
93
  export function badgeLabel(atBottom, newSinceUnstick, idle) {
92
94
  if (atBottom || newSinceUnstick <= 0)
93
95
  return '';
94
- if (idle)
95
- return '↓ jump to end';
96
+ void idle; // Ink can't capture the idle click; count is shown either way (§7.2)
96
97
  if (newSinceUnstick >= 1000)
97
- return '↓ 999+ new';
98
- return `↓ ${newSinceUnstick} new`;
98
+ return '↓ 999+ unread';
99
+ return `↓ ${newSinceUnstick} unread`;
99
100
  }
@@ -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.3",
3
+ "version": "1.0.5",
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",