@vincemakes/kiso-code 0.1.15 → 0.1.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/dist/body.d.ts CHANGED
@@ -39,6 +39,9 @@ export type BodyCell = {
39
39
  state: "pending" | "approval" | "running" | "done";
40
40
  isError: boolean;
41
41
  resultText: string;
42
+ diff: import("./diff.js").DiffLine[] | null;
43
+ added: number;
44
+ removed: number;
42
45
  startedAt: number | null;
43
46
  doneAt: number | null;
44
47
  done: boolean;
@@ -96,7 +99,7 @@ export declare class Body {
96
99
  thinkingAppend(text: string): void;
97
100
  thinkingEnd(): void;
98
101
  toolStart(name: string, callId: string, input: Record<string, unknown>): void;
99
- toolApproval(callId: string): void;
102
+ toolApproval(callId: string, diff: import("./diff.js").DiffResult | null): void;
100
103
  toolRunning(callId: string): void;
101
104
  toolSucceeded(callId: string): void;
102
105
  toolFailed(callId: string, error: string): void;
package/dist/body.js CHANGED
@@ -23,6 +23,7 @@
23
23
  * NoticeCell / raw block) — deliberately NOT pi's Component interface
24
24
  * shape (ADR-0040).
25
25
  */
26
+ import { truncateDiff } from "./diff.js";
26
27
  import { displayWidth } from "./editor.js";
27
28
  import { escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary, } from "./render.js";
28
29
  /** The spinner glyphs, cycled by the heartbeat. */
@@ -35,7 +36,7 @@ export class Body {
35
36
  #opts;
36
37
  #cells = [];
37
38
  #nextFrozen = 0; // index of the first not-yet-printed cell
38
- #frozenRows = 0; // rows of the region occupied by printed cells
39
+ #frozenRows = 0; // the frozen area's rows filled WITHOUT scrolling (then the real LFs take over)
39
40
  #oldTailTop = 0; // the tail's previous first row — for the clear pass
40
41
  #frameTimer = null;
41
42
  #heartbeat = null;
@@ -90,11 +91,13 @@ export class Body {
90
91
  userLine(text) {
91
92
  if (!this.#isActive()) {
92
93
  this.#closeOpenThinking();
94
+ this.#closeOpenText();
93
95
  const p = palette();
94
96
  this.#write(`${p.blue}you> ${escapeTerminal(text)}${p.reset}\n`);
95
97
  return;
96
98
  }
97
99
  this.#closeOpenThinking();
100
+ this.#closeOpenText();
98
101
  this.#cells.push({ kind: "user", text, done: true });
99
102
  this.#mark();
100
103
  }
@@ -129,19 +132,27 @@ export class Body {
129
132
  this.#pendingCalls.set(callId, { name, input, result: { content: "", isError: false } });
130
133
  if (!this.#isActive()) {
131
134
  this.#closeOpenThinking();
135
+ this.#closeOpenText();
132
136
  process.stdout.write(`→ ${escapeTerminal(name)}(${escapeTerminal(JSON.stringify(input).slice(0, 200))})\n`);
133
137
  return;
134
138
  }
135
139
  this.#toolCells.set(callId, this.#cells.length);
136
- this.#cells.push({ kind: "tool", name, input: summary, state: "pending", isError: false, resultText: "", startedAt: null, doneAt: null, done: false });
140
+ this.#cells.push({ kind: "tool", name, input: summary, state: "pending", isError: false, resultText: "", diff: null, added: 0, removed: 0, startedAt: null, doneAt: null, done: false });
137
141
  this.#mark();
138
142
  }
139
- toolApproval(callId) {
143
+ toolApproval(callId, diff) {
140
144
  if (!this.#isActive())
141
145
  return;
142
146
  const cell = this.#toolCell(callId);
143
- if (cell !== null && cell.kind === "tool" && !cell.done)
147
+ if (cell !== null && cell.kind === "tool" && !cell.done) {
144
148
  cell.state = "approval";
149
+ // v2e: the mini-diff renders BELOW the tool line at the approval
150
+ // moment — the human sees the change before deciding. Auto-allowed
151
+ // tools pass null (nobody is looking — no diff, no cost).
152
+ cell.diff = diff === null ? null : truncateDiff(diff.lines);
153
+ cell.added = diff?.added ?? 0;
154
+ cell.removed = diff?.removed ?? 0;
155
+ }
145
156
  this.#mark();
146
157
  }
147
158
  toolRunning(callId) {
@@ -196,6 +207,7 @@ export class Body {
196
207
  textAppend(text) {
197
208
  if (!this.#isActive()) {
198
209
  this.#closeOpenThinking();
210
+ this.#closeOpenText();
199
211
  process.stdout.write(escapeTerminal(text));
200
212
  return;
201
213
  }
@@ -205,6 +217,7 @@ export class Body {
205
217
  }
206
218
  else {
207
219
  this.#closeOpenThinking();
220
+ this.#closeOpenText();
208
221
  this.#cells.push({ kind: "text", text, done: false });
209
222
  }
210
223
  this.#mark();
@@ -223,21 +236,25 @@ export class Body {
223
236
  terminal(label, statusLine) {
224
237
  if (!this.#isActive()) {
225
238
  this.#closeOpenThinking();
239
+ this.#closeOpenText();
226
240
  // the v2c bytes: the terminal label (\ndone\n) + the status gap.
227
241
  process.stdout.write(label + renderTerminalGap(statusLine));
228
242
  return;
229
243
  }
230
244
  this.#closeOpenThinking();
245
+ this.#closeOpenText();
231
246
  this.#cells.push({ kind: "terminal", label: label.trim(), line: statusLine, done: true });
232
247
  this.#mark();
233
248
  }
234
249
  notice(text) {
235
250
  if (!this.#isActive()) {
236
251
  this.#closeOpenThinking();
252
+ this.#closeOpenText();
237
253
  process.stdout.write(`${text}\n`);
238
254
  return;
239
255
  }
240
256
  this.#closeOpenThinking();
257
+ this.#closeOpenText();
241
258
  this.#cells.push({ kind: "notice", text, done: true });
242
259
  this.#mark();
243
260
  }
@@ -246,11 +263,13 @@ export class Body {
246
263
  raw(lines) {
247
264
  if (!this.#isActive()) {
248
265
  this.#closeOpenThinking();
266
+ this.#closeOpenText();
249
267
  for (const line of lines)
250
268
  process.stdout.write(`${line}\n`);
251
269
  return;
252
270
  }
253
271
  this.#closeOpenThinking();
272
+ this.#closeOpenText();
254
273
  this.#cells.push({ kind: "raw", lines, done: true });
255
274
  this.#mark();
256
275
  }
@@ -280,34 +299,47 @@ export class Body {
280
299
  return;
281
300
  const H = this.#opts.height();
282
301
  const W = this.#opts.width();
283
- const regionBottom = H - 3;
284
- if (regionBottom < 1)
302
+ if (H < 4)
285
303
  return;
286
304
  const out = [];
287
- out.push("\x1b[?2026h");
288
- // 1. freeze completed cells print their final form at the frozen
289
- // area's next rows. The terminal scrolls the region when full.
290
- while (this.#nextFrozen < this.#cells.length && this.#cells[this.#nextFrozen].done) {
291
- const cell = this.#cells[this.#nextFrozen];
292
- const lines = this.#cellLines(cell, W);
293
- for (const line of lines) {
294
- const row = Math.min(this.#frozenRows + 1, regionBottom);
295
- out.push(`\x1b[${row};1H\x1b[0K${line}`);
296
- if (this.#frozenRows + 1 > regionBottom) {
297
- out.push("\n"); // the region scrolls — the frozen rows shift up
305
+ // #13 (P1), v2d-B: NO DECSTBM — overflow scrolls with a REAL LF at
306
+ // the screen's last row, so the frozen lines enter the terminal's
307
+ // NATIVE scrollback deterministically (region-scrolled lines are
308
+ // terminal-dependent; some terminals drop them — the measured v2d-A
309
+ // defect). The body fills from the top without scrolling; once full,
310
+ // every new frozen line scrolls the whole screen (\x1b[H;1H\n — the
311
+ // top line leaves into the scrollback) and lands at the body's
312
+ // bottom row, just above the active tail. The dock is redrawn after.
313
+ // The tail (the remaining ACTIVE cells) and its geometry — computed
314
+ // FIRST from the final nextFrozen, so the frozen cells are NOT in it
315
+ // (a stale tail would re-draw them — the double-render).
316
+ let nextFrozen = this.#nextFrozen;
317
+ while (nextFrozen < this.#cells.length && this.#cells[nextFrozen].done)
318
+ nextFrozen += 1;
319
+ const tail = this.#cells.slice(nextFrozen);
320
+ const tailHeight = tail.reduce((n, c) => n + this.#cellHeight(c, W), 0);
321
+ const tailTop = Math.max(1, H - 2 - tailHeight);
322
+ const writeRow = Math.max(1, tailTop - 1); // the frozen area's bottom row
323
+ let scrolled = 0;
324
+ for (let i = this.#nextFrozen; i < nextFrozen; i += 1) {
325
+ for (const line of this.#cellLines(this.#cells[i], W)) {
326
+ if (this.#frozenRows < writeRow) {
327
+ this.#frozenRows += 1;
328
+ out.push(`\x1b[${this.#frozenRows};1H\x1b[0K${line}`);
298
329
  }
299
330
  else {
300
- this.#frozenRows += 1;
331
+ out.push(`\x1b[${H};1H\n`); // the REAL LF — the whole screen scrolls
332
+ out.push(`\x1b[${writeRow};1H\x1b[0K${line}`);
333
+ scrolled += 1;
301
334
  }
302
335
  }
303
336
  this.#nextFrozen += 1;
304
337
  }
305
- // 2. the active tail — clear its old area, draw the cells.
306
- const tail = this.#cells.slice(this.#nextFrozen);
307
- const tailHeight = tail.reduce((n, c) => n + this.#cellHeight(c, W), 0);
308
- const tailTop = Math.max(1, regionBottom - tailHeight + 1);
309
- const clearFrom = Math.min(this.#oldTailTop === 0 ? tailTop : this.#oldTailTop, tailTop);
310
- for (let row = clearFrom; row <= regionBottom; row += 1) {
338
+ // 2. the active tail — clear its old area (shifted up by the freeze
339
+ // scrolls) and the current area, draw the cells at the body's bottom.
340
+ out.push("\x1b[?2026h");
341
+ const clearFrom = Math.min(this.#oldTailTop === 0 ? tailTop : this.#oldTailTop - scrolled, tailTop);
342
+ for (let row = clearFrom; row <= H - 3; row += 1) {
311
343
  out.push(`\x1b[${row};1H\x1b[0K`);
312
344
  }
313
345
  let row = tailTop;
@@ -317,11 +349,13 @@ export class Body {
317
349
  row += 1;
318
350
  }
319
351
  }
320
- this.#oldTailTop = tailTop;
321
352
  // 3. the cursor home — the input line's edit column.
322
353
  out.push(`\x1b[${H};${this.#opts.editCol()}H`);
323
354
  out.push("\x1b[?2026l");
324
355
  this.#write(out.join(""));
356
+ // 4. the dock rows — the freeze scrolls shifted them; redraw (the
357
+ // dock's own redraw re-pins the cursor at the edit position).
358
+ this.#opts.onDock?.();
325
359
  }
326
360
  // ---- cell → lines ----
327
361
  #cellLines(cell, W) {
@@ -345,10 +379,25 @@ export class Body {
345
379
  const err = escapeTerminal(cell.resultText.split("\n")[0].slice(0, 60));
346
380
  return [`${p.red}✗ ${name} (${err}, ${elapsed}s)${p.reset}`];
347
381
  }
348
- return [`${p.blue}✓ ${name}${p.reset} (${summary}, ${elapsed}s)`];
382
+ const delta = cell.added + cell.removed > 0 ? `, +${cell.added} -${cell.removed}` : "";
383
+ return [`${p.blue}✓ ${name}${p.reset} (${summary}${delta}, ${elapsed}s)`];
384
+ }
385
+ if (cell.state === "approval") {
386
+ const lines = [`→ ${name} ${summary} ${p.blue}⏸${p.reset}`];
387
+ // v2e: the mini-diff — ▎ blue edge (the brick motif), - red /
388
+ // + green / context dim; NO_COLOR keeps the ± prefixes plain.
389
+ if (cell.diff !== null) {
390
+ for (const d of cell.diff) {
391
+ const body = d.kind === "-"
392
+ ? `${p.red}- ${escapeTerminal(d.text)}${p.reset}`
393
+ : d.kind === "+"
394
+ ? `${p.green}+ ${escapeTerminal(d.text)}${p.reset}`
395
+ : `${p.dim} ${escapeTerminal(d.text)}${p.reset}`;
396
+ lines.push(`${p.blue}▎${p.reset}${body}`);
397
+ }
398
+ }
399
+ return lines;
349
400
  }
350
- if (cell.state === "approval")
351
- return [`→ ${name} ${summary} ${p.blue}⏸${p.reset}`];
352
401
  if (cell.state === "running") {
353
402
  const elapsed = cell.startedAt !== null ? Math.max(1, Math.round((Date.now() - cell.startedAt) / 1000)) : 1;
354
403
  return [`→ ${name} ${summary} ${p.blue}${SPINNER[this.#spinnerI % SPINNER.length]}${p.reset} ${elapsed}s`];
@@ -401,6 +450,16 @@ export class Body {
401
450
  const i = this.#toolCells.get(callId);
402
451
  return i === undefined ? null : (this.#cells[i] ?? null);
403
452
  }
453
+ /** Close an open TEXT cell when a new cell starts — the runtime emits
454
+ * no text_end (it is an adapter-level event), so the stream's next
455
+ * cell is the close signal; without it the freeze blocks behind the
456
+ * open text and everything after it re-renders in the tail forever
457
+ * (the #13 flood reproduced the overwrite). */
458
+ #closeOpenText() {
459
+ const last = this.#cells[this.#cells.length - 1];
460
+ if (last !== undefined && last.kind === "text" && !last.done)
461
+ last.done = true;
462
+ }
404
463
  /** Close an open thinking cell when a new cell starts (the block's
405
464
  * fold freezes at the transition). */
406
465
  #closeOpenThinking() {
package/dist/diff.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * v2e — the diff renderer: edit/write changes as inline ± lines, zero
3
+ * dependencies, no syntax highlighting (the spec's scope line). Shown at
4
+ * the approval moment ONLY — the frozen summary stays one line (v2d's
5
+ * anti-leak principle), /last has the full data.
6
+ *
7
+ * edit_file diffs IN PLACE (the search→replace windows are known — no
8
+ * general engine needed); write_file does a row-level LCS over the old
9
+ * file (small files are the target). Context: 2 rows each side. The
10
+ * RENDERER truncates (18 head + 18 tail + "… N lines"); the stats come
11
+ * from the full diff.
12
+ */
13
+ /** The diff block's per-row kind. */
14
+ export type DiffLine = {
15
+ kind: "-" | "+" | " ";
16
+ text: string;
17
+ };
18
+ export interface DiffResult {
19
+ /** The FULL diff (with context, not truncated) — the display truncates. */
20
+ lines: DiffLine[];
21
+ added: number;
22
+ removed: number;
23
+ }
24
+ /** The RENDERER's truncation: head + "… N lines (/last for full)" + tail. */
25
+ export declare function truncateDiff(diff: DiffLine[]): DiffLine[];
26
+ /** edit_file: the search→replace windows replace in place — the changed
27
+ * region is KNOWN, so the diff is the old window vs the new window,
28
+ * context from the surrounding file. */
29
+ export declare function editFileDiff(oldContent: string, search: string, replace: string): DiffResult;
30
+ /** write_file: a new file is all +; an existing file diffs row-level
31
+ * against its old content. */
32
+ export declare function writeFileDiff(oldContent: string | null, newContent: string): DiffResult;
package/dist/diff.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * v2e — the diff renderer: edit/write changes as inline ± lines, zero
3
+ * dependencies, no syntax highlighting (the spec's scope line). Shown at
4
+ * the approval moment ONLY — the frozen summary stays one line (v2d's
5
+ * anti-leak principle), /last has the full data.
6
+ *
7
+ * edit_file diffs IN PLACE (the search→replace windows are known — no
8
+ * general engine needed); write_file does a row-level LCS over the old
9
+ * file (small files are the target). Context: 2 rows each side. The
10
+ * RENDERER truncates (18 head + 18 tail + "… N lines"); the stats come
11
+ * from the full diff.
12
+ */
13
+ /** A line-level LCS diff — the classic two-row DP, ~small inputs. */
14
+ function lcsDiff(oldLines, newLines) {
15
+ const n = oldLines.length;
16
+ const m = newLines.length;
17
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
18
+ for (let i = n - 1; i >= 0; i -= 1) {
19
+ for (let j = m - 1; j >= 0; j -= 1) {
20
+ dp[i][j] = oldLines[i] === newLines[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
21
+ }
22
+ }
23
+ const out = [];
24
+ let i = 0;
25
+ let j = 0;
26
+ while (i < n && j < m) {
27
+ if (oldLines[i] === newLines[j]) {
28
+ out.push({ kind: " ", text: oldLines[i] });
29
+ i += 1;
30
+ j += 1;
31
+ }
32
+ else if (dp[i + 1][j] >= dp[i][j + 1]) {
33
+ out.push({ kind: "-", text: oldLines[i] });
34
+ i += 1;
35
+ }
36
+ else {
37
+ out.push({ kind: "+", text: newLines[j] });
38
+ j += 1;
39
+ }
40
+ }
41
+ while (i < n) {
42
+ out.push({ kind: "-", text: oldLines[i] });
43
+ i += 1;
44
+ }
45
+ while (j < m) {
46
+ out.push({ kind: "+", text: newLines[j] });
47
+ j += 1;
48
+ }
49
+ return out;
50
+ }
51
+ /** Keep 2 context rows around each change — the unified-style window. */
52
+ function withContext(diff) {
53
+ const out = [];
54
+ let lastAdded = -10;
55
+ for (let k = 0; k < diff.length; k += 1) {
56
+ if (diff[k].kind === " ")
57
+ continue;
58
+ const from = Math.max(0, k - 2);
59
+ const to = Math.min(diff.length - 1, k + 2);
60
+ for (let c = from; c <= to; c += 1) {
61
+ if (c > lastAdded) {
62
+ out.push(diff[c]);
63
+ lastAdded = c;
64
+ }
65
+ }
66
+ lastAdded = to;
67
+ }
68
+ return out;
69
+ }
70
+ const MAX_DIFF_LINES = 40; // the RENDERED cap
71
+ const TRUNCATE_KEEP = 18;
72
+ /** The RENDERER's truncation: head + "… N lines (/last for full)" + tail. */
73
+ export function truncateDiff(diff) {
74
+ if (diff.length <= MAX_DIFF_LINES)
75
+ return diff;
76
+ const omitted = diff.length - 2 * TRUNCATE_KEEP;
77
+ return [
78
+ ...diff.slice(0, TRUNCATE_KEEP),
79
+ { kind: " ", text: `… ${omitted} lines (/last for full)` },
80
+ ...diff.slice(diff.length - TRUNCATE_KEEP),
81
+ ];
82
+ }
83
+ function stats(diff) {
84
+ let added = 0;
85
+ let removed = 0;
86
+ for (const d of diff) {
87
+ if (d.kind === "+")
88
+ added += 1;
89
+ else if (d.kind === "-")
90
+ removed += 1;
91
+ }
92
+ return { added, removed };
93
+ }
94
+ /** edit_file: the search→replace windows replace in place — the changed
95
+ * region is KNOWN, so the diff is the old window vs the new window,
96
+ * context from the surrounding file. */
97
+ export function editFileDiff(oldContent, search, replace) {
98
+ const oldLines = oldContent.split("\n");
99
+ const searchLines = search.split("\n");
100
+ const replaceLines = replace.split("\n");
101
+ // Locate the search window (the first occurrence — the edit tool's own
102
+ // semantics); no occurrence → the whole file is the old side.
103
+ let at = -1;
104
+ for (let i = 0; i + searchLines.length <= oldLines.length; i += 1) {
105
+ if (oldLines.slice(i, i + searchLines.length).join("\n") === search) {
106
+ at = i;
107
+ break;
108
+ }
109
+ }
110
+ const lines = at < 0 ? withContext(lcsDiff(oldLines, replaceLines)) : withContext(lcsDiff(oldLines, [...oldLines.slice(0, at), ...replaceLines, ...oldLines.slice(at + searchLines.length)]));
111
+ return { lines, ...stats(lines) };
112
+ }
113
+ /** write_file: a new file is all +; an existing file diffs row-level
114
+ * against its old content. */
115
+ export function writeFileDiff(oldContent, newContent) {
116
+ if (oldContent === null) {
117
+ const lines = newContent.split("\n").map((text) => ({ kind: "+", text }));
118
+ return { lines, added: lines.length, removed: 0 };
119
+ }
120
+ const lines = withContext(lcsDiff(oldContent.split("\n"), newContent.split("\n")));
121
+ return { lines, ...stats(lines) };
122
+ }
package/dist/dock.d.ts CHANGED
@@ -27,7 +27,11 @@ export declare class Dock {
27
27
  line: string;
28
28
  cursor: number;
29
29
  }, prompt: string): void;
30
- /** Enter docked mode: DECSTBM the scroll region, draw the chrome. A
30
+ /** Enter docked mode: draw the chrome. #13 (P1): the DECSTBM scroll
31
+ * region is GONE — v2d-B (ADR-0040): the body uses plain LF scrolling
32
+ * so frozen lines enter the native scrollback deterministically
33
+ * (region-scrolled lines are terminal-dependent — some terminals drop
34
+ * them). The dock rows are redrawn by the body after every scroll. A
31
35
  * TTY without a real window size (rows < 4) stays in the v2a line
32
36
  * mode — the bottom three rows need room to exist. */
33
37
  enter(): void;
@@ -36,21 +40,8 @@ export declare class Dock {
36
40
  * from main's finally on EVERY exit path (kill -9 excepted — README:
37
41
  * `reset` saves it). */
38
42
  exit(): void;
39
- /** SIGWINCH: recompute the region, redraw the chrome. */
43
+ /** SIGWINCH: recompute the size, redraw the chrome. */
40
44
  onResize(): void;
41
- /** Body output: position the cursor inside the scroll region at the
42
- * body's tracked position, write, and hand the cursor back to the
43
- * input line's EDIT position. The row/col tracking is approximate for
44
- * width-wrapped and wide-char lines (documented) — the region clamp
45
- * keeps the bottom rows safe regardless.
46
- *
47
- * The edit-position return is a correctness requirement, not
48
- * cosmetics: readline tracks its cursor internally and NEVER
49
- * re-syncs after an external move — a body write that left the
50
- * cursor at column 1 made the next keystroke overwrite the prompt
51
- * (probe-confirmed; the dock's redraw self-repaired ~200ms later,
52
- * which read the user as cursor drift). */
53
- writeBody(text: string): void;
54
45
  /** The input line's edit column — prompt width + cursor + 1. The
55
46
  * dock's redraw and the body's cursor return both end here, so the
56
47
  * ACTUAL cursor always equals what the editor tracks. The width is
package/dist/dock.js CHANGED
@@ -41,7 +41,11 @@ export class Dock {
41
41
  this.#inputState = state;
42
42
  this.#inputPrompt = prompt;
43
43
  }
44
- /** Enter docked mode: DECSTBM the scroll region, draw the chrome. A
44
+ /** Enter docked mode: draw the chrome. #13 (P1): the DECSTBM scroll
45
+ * region is GONE — v2d-B (ADR-0040): the body uses plain LF scrolling
46
+ * so frozen lines enter the native scrollback deterministically
47
+ * (region-scrolled lines are terminal-dependent — some terminals drop
48
+ * them). The dock rows are redrawn by the body after every scroll. A
45
49
  * TTY without a real window size (rows < 4) stays in the v2a line
46
50
  * mode — the bottom three rows need room to exist. */
47
51
  enter() {
@@ -53,7 +57,6 @@ export class Dock {
53
57
  this.#width = process.stdout.columns ?? 80;
54
58
  this.#bodyRow = 1;
55
59
  this.#bodyCol = 1;
56
- process.stdout.write(`\x1b[1;${this.#height - 3}r`); // scroll region: top .. H-3
57
60
  this.redraw();
58
61
  this.#resizeHandler = () => this.onResize();
59
62
  process.stdout.on("resize", this.#resizeHandler);
@@ -77,47 +80,14 @@ export class Dock {
77
80
  }
78
81
  process.stdout.write(`\x1b[${H};1H`);
79
82
  }
80
- /** SIGWINCH: recompute the region, redraw the chrome. */
83
+ /** SIGWINCH: recompute the size, redraw the chrome. */
81
84
  onResize() {
82
85
  if (!this.#active)
83
86
  return;
84
87
  this.#height = process.stdout.rows ?? this.#height;
85
88
  this.#width = process.stdout.columns ?? this.#width;
86
- process.stdout.write(`\x1b[1;${this.#height - 3}r`);
87
89
  this.redraw();
88
90
  }
89
- /** Body output: position the cursor inside the scroll region at the
90
- * body's tracked position, write, and hand the cursor back to the
91
- * input line's EDIT position. The row/col tracking is approximate for
92
- * width-wrapped and wide-char lines (documented) — the region clamp
93
- * keeps the bottom rows safe regardless.
94
- *
95
- * The edit-position return is a correctness requirement, not
96
- * cosmetics: readline tracks its cursor internally and NEVER
97
- * re-syncs after an external move — a body write that left the
98
- * cursor at column 1 made the next keystroke overwrite the prompt
99
- * (probe-confirmed; the dock's redraw self-repaired ~200ms later,
100
- * which read the user as cursor drift). */
101
- writeBody(text) {
102
- if (!this.#active) {
103
- process.stdout.write(text);
104
- return;
105
- }
106
- const row = Math.min(this.#bodyRow, this.#height - 3);
107
- const col = this.#bodyCol > this.#width ? this.#width : this.#bodyCol;
108
- process.stdout.write(`\x1b[${row};${col}H`);
109
- process.stdout.write(text);
110
- for (const ch of text) {
111
- if (ch === "\n") {
112
- this.#bodyRow += 1;
113
- this.#bodyCol = 1;
114
- }
115
- else {
116
- this.#bodyCol += 1;
117
- }
118
- }
119
- process.stdout.write(`\x1b[${this.#height};${this.#inputCol()}H`); // back to the edit position
120
- }
121
91
  /** The input line's edit column — prompt width + cursor + 1. The
122
92
  * dock's redraw and the body's cursor return both end here, so the
123
93
  * ACTUAL cursor always equals what the editor tracks. The width is
package/dist/index.js CHANGED
@@ -18,6 +18,8 @@
18
18
  import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
19
19
  import { createInterface } from "node:readline";
20
20
  import { Body } from "./body.js";
21
+ import { editFileDiff, writeFileDiff } from "./diff.js";
22
+ import { MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
21
23
  import { Editor, PROMPT as EDITOR_PROMPT } from "./editor.js";
22
24
  import { homedir, tmpdir } from "node:os";
23
25
  import { dirname, join } from "node:path";
@@ -27,17 +29,6 @@ import { createFauxProvider } from "@vincemakes/kiso-evals";
27
29
  import { createCodingTools } from "@vincemakes/kiso-tools-node";
28
30
  import { escapeTerminal, foldResult, foldThinking, palette, renderEvent, renderSessionLine, renderStatusLine, renderTerminalGap, renderToolSummary, } from "./render.js";
29
31
  import { Dock } from "./dock.js";
30
- const PERMISSION_POLICY = {
31
- rules: [
32
- { tool: "read_file", action: "allow" },
33
- { tool: "list_dir", action: "allow" },
34
- { tool: "search_text", action: "allow" },
35
- { tool: "write_file", action: "defer" },
36
- { tool: "edit_file", action: "defer" },
37
- { tool: "shell", action: "defer" },
38
- ],
39
- default: "deny",
40
- };
41
32
  /** 发现#11: KISO_HOME is the ONE root — every default path derives from
42
33
  * it (sessions, trust, extensions, mcp config, skills). The dedicated
43
34
  * env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
@@ -260,6 +251,21 @@ function bannerExtensionText() {
260
251
  parts.push(`project: ${projectExtensions.map((e) => e.name).join(", ")}`);
261
252
  return ` · [${total} extension${total === 1 ? "" : "s"}: ${parts.join(" · ")}]`;
262
253
  }
254
+ /** Modes: the status-bar indicator — the default tier shows nothing; the
255
+ * others show their blue name, the dangerous ones (plan/bypass) with the
256
+ * ⚠ prefix. */
257
+ function modeStatusText() {
258
+ if (getMode() === "default")
259
+ return "";
260
+ const p = palette();
261
+ const danger = getMode() === "plan" || getMode() === "bypass" ? "⚠ " : "";
262
+ return `${p.blue}${danger}${getMode()}${p.reset}`;
263
+ }
264
+ /** Modes: append the mode indicator to a composed status base. */
265
+ function statusWithMode(base) {
266
+ const mode = modeStatusText();
267
+ return mode === "" ? base : `${base} · ${mode}`;
268
+ }
263
269
  /** E1: the startup banner line(s) — TTY: logo + merged extensions; off-TTY:
264
270
  * the historical `[N extensions: ...]` standalone line (zero change). */
265
271
  function extensionsBanner() {
@@ -518,15 +524,26 @@ async function makeAgent(fauxSkipTurns = 0, input) {
518
524
  // Area 5: the coding tools are bound to the workspace — every path
519
525
  // they touch is canonicalized inside cwd, escapes are refused.
520
526
  tools: [...createCodingTools({ workspaceRoot: process.cwd() })],
521
- permissionPolicy: PERMISSION_POLICY,
522
- systemPrompt: composeSystemPrompt(process.cwd()),
527
+ // Modes: the five tiers ride the E1 policy chain (mode:<tier>
528
+ // extensions, current tier first) — the old static PERMISSION_POLICY
529
+ // is gone, its semantics live in the "default" tier. The banner
530
+ // still counts loadedExtensions only — the modes are in-process,
531
+ // never a file extension.
532
+ systemPrompt: (() => {
533
+ const sp = composeSystemPrompt(process.cwd());
534
+ const extra = modeSystemPrompt();
535
+ return extra === undefined ? sp : `${sp}\n\n${extra}`;
536
+ })(),
523
537
  // C 区: microcompact is ON by default in the product — threshold =
524
538
  // half the model window (KISO_CONTEXT_WINDOW override included;
525
539
  // 200k window → 100k tokens). Long sessions compact old read/list/
526
540
  // search/shell outputs instead of silently growing past the window.
527
541
  microcompact: { thresholdTokens: contextWindowTokens() / 2 },
528
542
  maxTurns: 20,
529
- extensions: loadedExtensions,
543
+ // Modes: the five tiers join at the CHAIN HEAD, before the user/
544
+ // project extensions (the deny>ask>allow composition keeps a user
545
+ // deny winning over any mode tier — bypass included).
546
+ extensions: [...modeExtensions(), ...loadedExtensions],
530
547
  ...(provider !== undefined
531
548
  ? {
532
549
  provider,
@@ -699,6 +716,38 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
699
716
  * line's form; `liveInput` (non-null only in interactive chat) carries the
700
717
  * last line THIS process's readline consumed — the double-echo filter.
701
718
  */
719
+ /** v2e: the approval-moment mini-diff — edit_file/write_file changes as
720
+ * ± lines; other tools get null (no diff, no cost). The file read is
721
+ * best-effort: an unreadable file yields NO diff, never a failure —
722
+ * the diff must never break the approval. */
723
+ function approvalDiff(name, input) {
724
+ if (name !== "edit_file" && name !== "write_file")
725
+ return null;
726
+ const path = typeof input.path === "string" ? input.path : "";
727
+ if (path === "")
728
+ return null;
729
+ let oldContent = null;
730
+ try {
731
+ oldContent = readFileSync(path, "utf8");
732
+ }
733
+ catch {
734
+ // a new write_file target (or an unreadable one) — all + degrades
735
+ }
736
+ try {
737
+ if (name === "edit_file") {
738
+ const search = typeof input.search === "string" ? input.search : "";
739
+ const replace = typeof input.replace === "string" ? input.replace : "";
740
+ if (search === "")
741
+ return null;
742
+ return editFileDiff(oldContent ?? "", search, replace);
743
+ }
744
+ const content = typeof input.content === "string" ? input.content : "";
745
+ return writeFileDiff(oldContent, content);
746
+ }
747
+ catch {
748
+ return null; // never let the diff break the approval
749
+ }
750
+ }
702
751
  async function consumeRun(session, run, input, turnNo, faux, liveInput, statusCb) {
703
752
  let last;
704
753
  let usage = { in: null, out: null, cache: null, known: false };
@@ -762,9 +811,12 @@ async function consumeRun(session, run, input, turnNo, faux, liveInput, statusCb
762
811
  case "permission_requested": {
763
812
  // v2d: the ToolCell shows the ⏸ badge; the question takes over
764
813
  // the dock status position; the answer lands at the input line.
765
- body.toolApproval(ev.callId);
766
- const decisionId = ev.decisionId;
814
+ // v2e: the mini-diff for edit/write at the approval moment —
815
+ // the human sees the change BEFORE deciding (auto-allowed tools
816
+ // skip the diff: nobody is looking).
767
817
  const name = ev.name;
818
+ body.toolApproval(ev.callId, approvalDiff(name, ev.input ?? {}));
819
+ const decisionId = ev.decisionId;
768
820
  const answer = await ask(input, `approve ${escapeTerminal(name)}? (y/n) `);
769
821
  if (answer === CANCELLED) {
770
822
  // 十: a cancellation is a CONSERVATIVE denial, explicitly
@@ -777,6 +829,7 @@ async function consumeRun(session, run, input, turnNo, faux, liveInput, statusCb
777
829
  break;
778
830
  }
779
831
  case "terminal":
832
+ statusCb?.(usage, estimateCtxRatio(session));
780
833
  statusCb?.(usage, estimateCtxRatio(session));
781
834
  // v2a rhythm: the honest label (\ndone\n — the completed
782
835
  // marker), the status line hugging it, then EXACTLY one blank
@@ -925,13 +978,20 @@ async function chat(session, faux, input) {
925
978
  // v2c: turns submitted while another runs are QUEUED on the chain — the
926
979
  // live count rides the status bar (+N queued).
927
980
  let queued = 0;
928
- // v2b: the live status bar (docked only).
981
+ // v2b: the live status bar (docked only). Modes: /mode switches repaint
982
+ // it immediately through paintStatus (the last turn stats are kept).
983
+ let statusSt = null;
929
984
  const statusCb = (u, ctx) => {
930
985
  if (!dock.active)
931
986
  return;
932
- const st = renderStatusLine(turnNo, u, ctx, faux);
933
- const base = st === null ? `${session.id} · ${agentModel}` : `${session.id} · ${agentModel} · ${st}`;
934
- dock.setStatus(queued > 0 ? `${base} · +${queued} queued` : base);
987
+ statusSt = renderStatusLine(turnNo, u, ctx, faux);
988
+ paintStatus();
989
+ };
990
+ const paintStatus = () => {
991
+ if (!dock.active)
992
+ return;
993
+ const base = statusSt === null ? `${session.id} · ${agentModel}` : `${session.id} · ${agentModel} · ${statusSt}`;
994
+ dock.setStatus(`${statusWithMode(base)}${queued > 0 ? ` · +${queued} queued` : ""}`);
935
995
  };
936
996
  // The ONE dispatcher: slash commands, exit, and turns. The recovery
937
997
  // replay routes through it too — a queued "/last" must never become a
@@ -949,6 +1009,7 @@ async function chat(session, faux, input) {
949
1009
  bodyLog(cmd("/think", "show the last full thinking block"));
950
1010
  bodyLog(cmd("/last", "show the most recent tool call's input and output"));
951
1011
  bodyLog(cmd("/status", "show session id, event count, and context estimate"));
1012
+ bodyLog(cmd("/mode", "show the approval tier; /mode <name> switches (manual/default/accept-edits/plan/bypass)"));
952
1013
  bodyLog(cmd("exit", "leave the session"));
953
1014
  input.prompt();
954
1015
  });
@@ -1002,6 +1063,29 @@ async function chat(session, faux, input) {
1002
1063
  });
1003
1064
  return;
1004
1065
  }
1066
+ if (trimmed === "/mode" || trimmed.startsWith("/mode ")) {
1067
+ // Modes: /mode alone prints the current tier + the list;
1068
+ // /mode <name> switches — the notice cell leaves the audit
1069
+ // line in the body, the status bar repaints at once.
1070
+ chain = chain.then(async () => {
1071
+ const m = MODES.find((x) => x === trimmed.slice(5).trim());
1072
+ if (trimmed.slice(5).trim() === "") {
1073
+ bodyLog(`mode ${getMode()}`);
1074
+ bodyLog(`tiers: ${MODES.join(" ")}`);
1075
+ }
1076
+ else if (m === undefined) {
1077
+ bodyLog(`no such mode: ${trimmed.slice(5).trim()}`);
1078
+ bodyLog(`tiers: ${MODES.join(" ")}`);
1079
+ }
1080
+ else {
1081
+ setMode(m);
1082
+ body.notice(`mode → ${m}`);
1083
+ paintStatus();
1084
+ }
1085
+ input.prompt();
1086
+ });
1087
+ return;
1088
+ }
1005
1089
  if (trimmed === "exit" || trimmed === "") {
1006
1090
  input.close();
1007
1091
  return;
@@ -1068,7 +1152,8 @@ async function resume(session, prompt, faux, input) {
1068
1152
  if (!dock.active)
1069
1153
  return;
1070
1154
  const st = renderStatusLine(turnNo, u, ctx, faux);
1071
- dock.setStatus(st === null ? `${session.id} · ${agentModel}` : `${session.id} · ${agentModel} · ${st}`);
1155
+ const base = st === null ? `${session.id} · ${agentModel}` : `${session.id} · ${agentModel} · ${st}`;
1156
+ dock.setStatus(statusWithMode(base));
1072
1157
  };
1073
1158
  const withRun = async (run) => {
1074
1159
  currentRun = run;
@@ -1127,7 +1212,24 @@ async function resume(session, prompt, faux, input) {
1127
1212
  }
1128
1213
  }
1129
1214
  async function main() {
1130
- const [command, arg] = process.argv.slice(2);
1215
+ // Modes: --mode <name> wins over KISO_MODE — both applied before the
1216
+ // first makeAgent (the tier extensions read `current` live). The flag
1217
+ // is stripped from the positional args, so it works in any position.
1218
+ const args = process.argv.slice(2);
1219
+ const modeFlag = args.indexOf("--mode");
1220
+ if (modeFlag !== -1) {
1221
+ const m = MODES.find((x) => x === args[modeFlag + 1]);
1222
+ if (m === undefined) {
1223
+ console.error(`unknown mode: ${args[modeFlag + 1]} (tiers: ${MODES.join(", ")})`);
1224
+ process.exit(2);
1225
+ }
1226
+ setMode(m);
1227
+ args.splice(modeFlag, 2);
1228
+ }
1229
+ else {
1230
+ setMode(modeFromEnv());
1231
+ }
1232
+ const [command, arg] = args;
1131
1233
  // 八: faux mode is the keyless demo script — an exhausted script must
1132
1234
  // exit non-zero, never masquerade as a successful provider run.
1133
1235
  const faux = process.env.ANTHROPIC_API_KEY === undefined && process.env.OPENAI_API_KEY === undefined;
@@ -1144,6 +1246,7 @@ async function main() {
1144
1246
  height: () => process.stdout.rows ?? 24,
1145
1247
  width: () => process.stdout.columns ?? 80,
1146
1248
  editCol: () => dock.editCol(),
1249
+ onDock: () => dock.redraw(), // v2d-B: the freeze scrolls the dock up — re-pin it
1147
1250
  });
1148
1251
  try {
1149
1252
  switch (command) {
package/dist/mode.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Modes — the five built-in approval tiers, built ON the E1 policy chain
3
+ * (the kernel is untouched). Each tier is an in-process "mode:<name>"
4
+ * extension whose decide() is live — it only speaks when it is the
5
+ * CURRENT tier (otherwise abstain = no opinion, ADR-0042), so /mode
6
+ * switches take effect immediately. The extension NAME rides the runtime's decidedBy
7
+ * field: an automated denial records decidedBy: "mode:<name>" — the
8
+ * audit sell. User-level extensions stay on the chain AFTER the mode
9
+ * tiers; a user deny always wins (the chain's deny>ask>allow
10
+ * monotonicity — bypass cannot override an extension deny).
11
+ */
12
+ import type { KisoExtension } from "@vincemakes/kiso-runtime";
13
+ export type Mode = "manual" | "default" | "accept-edits" | "plan" | "bypass";
14
+ export declare const MODES: readonly Mode[];
15
+ export declare function getMode(): Mode;
16
+ export declare function setMode(m: Mode): void;
17
+ /** The startup mode: KISO_MODE env (or the --mode flag — the CLI applies
18
+ * it before the first makeAgent). */
19
+ export declare function modeFromEnv(): Mode;
20
+ /** The five built-in mode tiers as chain extensions — named "mode:<tier>"
21
+ * so the runtime's decidedBy records exactly that (the runtime derives
22
+ * approvalPolicies from extensions[].approvals, tagging each with the
23
+ * extension name). The CURRENT tier is first: an all-allow chain records
24
+ * decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
25
+ * names that mode honestly. Order never affects verdicts — the chain is
26
+ * deny>ask>allow over the SPEAKING verdicts (abstain = no opinion), so a
27
+ * user extension's deny wins over any mode tier, bypass included (the
28
+ * monotonicity e2e pins it). */
29
+ export declare function modeExtensions(): readonly KisoExtension[];
30
+ /** The plan tier's system prompt addition — injected at startup when the
31
+ * initial mode is plan (the session prompt is fixed at creation; runtime
32
+ * switches are guided by the deny reason). */
33
+ export declare function modeSystemPrompt(): string | undefined;
package/dist/mode.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Modes — the five built-in approval tiers, built ON the E1 policy chain
3
+ * (the kernel is untouched). Each tier is an in-process "mode:<name>"
4
+ * extension whose decide() is live — it only speaks when it is the
5
+ * CURRENT tier (otherwise abstain = no opinion, ADR-0042), so /mode
6
+ * switches take effect immediately. The extension NAME rides the runtime's decidedBy
7
+ * field: an automated denial records decidedBy: "mode:<name>" — the
8
+ * audit sell. User-level extensions stay on the chain AFTER the mode
9
+ * tiers; a user deny always wins (the chain's deny>ask>allow
10
+ * monotonicity — bypass cannot override an extension deny).
11
+ */
12
+ export const MODES = ["manual", "default", "accept-edits", "plan", "bypass"];
13
+ /** The read-only tool set (plan): reading is allowed, everything else
14
+ * denied with the guiding reason. */
15
+ const READ_TOOLS = new Set(["read_file", "list_dir", "search_text", "read_skill"]);
16
+ let current = "default";
17
+ export function getMode() {
18
+ return current;
19
+ }
20
+ export function setMode(m) {
21
+ current = m;
22
+ }
23
+ /** The startup mode: KISO_MODE env (or the --mode flag — the CLI applies
24
+ * it before the first makeAgent). */
25
+ export function modeFromEnv() {
26
+ const raw = process.env.KISO_MODE;
27
+ const m = MODES.find((x) => x === raw);
28
+ return m ?? "default";
29
+ }
30
+ /** The per-tier verdict for a tool call — only when this tier is current. */
31
+ function tierVerdict(tier, call) {
32
+ if (tier !== current)
33
+ return { action: "abstain" }; // not our tier — no opinion
34
+ switch (tier) {
35
+ case "manual":
36
+ return { action: "ask" }; // every tool asks
37
+ case "default":
38
+ if (READ_TOOLS.has(call.name))
39
+ return { action: "allow" };
40
+ if (call.name === "write_file" || call.name === "edit_file" || call.name === "shell")
41
+ return { action: "ask" };
42
+ // Abstain (ADR-0042): an extension-provided tool is the
43
+ // EXTENSIONS' business — the tier neither allows nor denies.
44
+ // The chain falls to the ask flow when nobody else speaks, so
45
+ // an uncovered external tool STILL meets the human (the P2
46
+ // finding: "allow"-as-no-opinion auto-approved it).
47
+ return { action: "abstain" };
48
+ case "accept-edits":
49
+ if (READ_TOOLS.has(call.name))
50
+ return { action: "allow" };
51
+ if (call.name === "write_file" || call.name === "edit_file")
52
+ return { action: "allow" };
53
+ if (call.name === "shell")
54
+ return { action: "ask" };
55
+ return { action: "abstain" }; // see "default"
56
+ case "plan":
57
+ if (READ_TOOLS.has(call.name))
58
+ return { action: "allow" };
59
+ return { action: "deny", reason: "plan mode: read-only" };
60
+ case "bypass":
61
+ return { action: "allow" }; // everything — a REAL allow, never an abstain
62
+ }
63
+ }
64
+ /** The five built-in mode tiers as chain extensions — named "mode:<tier>"
65
+ * so the runtime's decidedBy records exactly that (the runtime derives
66
+ * approvalPolicies from extensions[].approvals, tagging each with the
67
+ * extension name). The CURRENT tier is first: an all-allow chain records
68
+ * decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
69
+ * names that mode honestly. Order never affects verdicts — the chain is
70
+ * deny>ask>allow over the SPEAKING verdicts (abstain = no opinion), so a
71
+ * user extension's deny wins over any mode tier, bypass included (the
72
+ * monotonicity e2e pins it). */
73
+ export function modeExtensions() {
74
+ return [...MODES.filter((m) => m === current), ...MODES.filter((m) => m !== current)].map((m) => ({
75
+ name: `mode:${m}`,
76
+ approvals: [
77
+ {
78
+ decide: async (payload) => tierVerdict(m, { name: payload.name }),
79
+ },
80
+ ],
81
+ }));
82
+ }
83
+ /** The plan tier's system prompt addition — injected at startup when the
84
+ * initial mode is plan (the session prompt is fixed at creation; runtime
85
+ * switches are guided by the deny reason). */
86
+ export function modeSystemPrompt() {
87
+ if (current !== "plan")
88
+ return undefined;
89
+ return ("plan mode: read-only. You may inspect the workspace (read_file, list_dir, search_text, " +
90
+ "read_skill) but every write/edit/shell call is DENIED with 'plan mode: read-only'. " +
91
+ "Produce a concrete plan (files, searches, proposed edits) as your output; the human " +
92
+ "switches to another mode to execute it.");
93
+ }
package/dist/render.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface Palette {
16
16
  readonly blue: string;
17
17
  readonly dim: string;
18
18
  readonly red: string;
19
+ readonly green: string;
19
20
  readonly reset: string;
20
21
  }
21
22
  export declare const COLOR_ON: Palette;
package/dist/render.js CHANGED
@@ -3,8 +3,8 @@
3
3
  * the lines a human sees. Colors are raw ANSI — no dependencies.
4
4
  */
5
5
  import { canonicalTargetPath } from "@vincemakes/kiso-tools-node";
6
- export const COLOR_ON = { blue: "\x1b[38;5;75m", dim: "\x1b[2m", red: "\x1b[31m", reset: "\x1b[0m" };
7
- export const COLOR_OFF = { blue: "", dim: "", red: "", reset: "" };
6
+ export const COLOR_ON = { blue: "\x1b[38;5;75m", dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", reset: "\x1b[0m" };
7
+ export const COLOR_OFF = { blue: "", dim: "", red: "", green: "", reset: "" };
8
8
  export function palette() {
9
9
  return process.env.NO_COLOR === undefined && process.stdout.isTTY ? COLOR_ON : COLOR_OFF;
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "kiso CLI — the coding-agent reference product: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,12 +18,12 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-core": "0.1.15",
22
- "@vincemakes/kiso-evals": "0.1.15",
23
- "@vincemakes/kiso-provider-anthropic": "0.1.15",
24
- "@vincemakes/kiso-provider-openai": "0.1.15",
25
- "@vincemakes/kiso-runtime": "0.1.15",
26
- "@vincemakes/kiso-tools-node": "0.1.15"
21
+ "@vincemakes/kiso-core": "0.1.16",
22
+ "@vincemakes/kiso-evals": "0.1.16",
23
+ "@vincemakes/kiso-provider-anthropic": "0.1.16",
24
+ "@vincemakes/kiso-provider-openai": "0.1.16",
25
+ "@vincemakes/kiso-runtime": "0.1.16",
26
+ "@vincemakes/kiso-tools-node": "0.1.16"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^26.1.2",