@vincemakes/kiso-code 0.11.0 → 0.13.0

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/chat.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * estimates. All bodies moved verbatim from index.ts.
6
6
  */
7
7
  import { type RunUsage } from "@vincemakes/kiso-tui";
8
+ import { type SaferFailure, type SaferOption } from "@vincemakes/kiso-tui";
8
9
  import type { AgentSession, Run } from "@vincemakes/kiso-runtime";
9
10
  import { type LineInput } from "./state.js";
10
11
  /**
@@ -81,6 +82,68 @@ export declare function usageFromEvent(route: string | undefined, ev: import("@v
81
82
  * first event. */
82
83
  export declare function startStatusSpinner(onTick: (glyph: string) => void): () => void;
83
84
  export declare function startShellTail(sessionId: string, callId: string, command: string, startedAt: number): () => void;
85
+ /**
86
+ * R3v2-F1: the format contract, stated FIRMLY. The first cut asked for
87
+ * "JSON ONLY" and left it there, which a verbose model reads as a
88
+ * preference — it wrote three sentences of preamble, opened a fence, and
89
+ * the cap ended the reply mid-string. Forbidding prose, naming the exact
90
+ * schema, and giving the nothing-is-safer case its own literal answer
91
+ * are all the same instruction: there is one thing to emit and no room
92
+ * to be helpful in the margins.
93
+ *
94
+ * The schema is an ENVELOPE rather than a bare array because a single
95
+ * top-level object leaves the model nowhere to put a preamble.
96
+ */
97
+ export declare const SAFER_SYSTEM_PROMPT: string;
98
+ /**
99
+ * R3v2-F1: the side query's output ceiling — raised from 500, which was
100
+ * the cap the live failures hit EXACTLY.
101
+ *
102
+ * The JSON-only reply the prompt now asks for is about 200 tokens for
103
+ * three alternatives, so this ceiling is a runaway guard and not a
104
+ * budget the answer is expected to approach: it exists so a model that
105
+ * ignores the contract and writes an essay still stops, not so the
106
+ * answer has room. Output is billed only when generated, and the query
107
+ * fires only on a press, so the raise costs nothing on the path that
108
+ * works and removes the one that could not.
109
+ */
110
+ export declare const SAFER_MAX_TOKENS = 1500;
111
+ /**
112
+ * R3v2-F1: WHY the ask failed, when the reply's own text can prove it.
113
+ *
114
+ * This side reports the cause and never the copy — the sentences live in
115
+ * the panel package, next to each other, so there is one place where the
116
+ * words are chosen and one place they can drift from.
117
+ *
118
+ * "Cut short" is a DIAGNOSIS, so it is only claimed when the text shows
119
+ * it: a reply that closed its JSON and then failed our SHAPE returns
120
+ * null and gets the unqualified line, because telling that human their
121
+ * reply was truncated would be a confident wrong answer.
122
+ */
123
+ export declare function saferFailure(text: string): SaferFailure | null;
124
+ /**
125
+ * Parse the model's answer DEFENSIVELY — anything unexpected is a
126
+ * failure, and a failure degrades honestly.
127
+ *
128
+ * The temptation here is to be clever: salvage a half-parse, coerce a
129
+ * string into a command, accept an object where an array was asked for.
130
+ * All of that produces a list of alternatives the model did not propose,
131
+ * shown to a human deciding whether to run a destructive command. The
132
+ * only honest failure mode is the dim line, so anything that is not
133
+ * exactly the requested shape returns null.
134
+ *
135
+ * A fenced code block is the one accommodation, because models emit it
136
+ * constantly and it changes no content.
137
+ *
138
+ * R3v2-F1 widens that accommodation and NOTHING else. Unwrapping the
139
+ * named `alternatives` envelope, and reading `reason` as the spelling of
140
+ * `why` the prompt now asks for, are transport details: the entries that
141
+ * come out are verbatim the entries the model put in. That is the line
142
+ * between an accommodation and the salvage this parser refuses — a
143
+ * salvage changes WHICH alternatives are shown, and every rule that does
144
+ * that is still here. One bad entry still poisons the batch.
145
+ */
146
+ export declare function parseSaferOptions(text: string): SaferOption[] | null;
84
147
  /**
85
148
  * Consume a run, answering approval pauses as they arrive. `resumeMode`
86
149
  * marks a session.resume() continuation. `faux` picks the status line's
package/dist/chat.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { readFileSync, statSync } from "node:fs";
8
8
  import { escapeTerminal, cacheHitPct, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, STATUS_GLYPHS, } from "@vincemakes/kiso-tui";
9
- import { editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
9
+ import { deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
10
10
  import { canonicalTargetPath, shellProgressPath } from "@vincemakes/kiso-tools-node";
11
11
  import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
12
12
  import { dispatch } from "./dispatch.js";
@@ -178,24 +178,198 @@ function approvalDiff(name, input) {
178
178
  return null; // never let the diff break the approval
179
179
  }
180
180
  }
181
+ /**
182
+ * TUI2-R3v2 ③ — the safer-options request: its prompt, and its parser.
183
+ *
184
+ * The prompt is deliberately small. It carries the pending call and
185
+ * nothing else — no conversation, no tools, no project context — because
186
+ * everything it does not send is rent the human pays for pressing a
187
+ * button, and because "propose a safer version of THIS command" is a
188
+ * question that needs no history to answer.
189
+ */
190
+ /** TUI2-R3v2 ③: tools whose NEXT approval is the model's answer to a
191
+ * refusal — the "(amended)" marker's source. Per process, cleared as
192
+ * soon as it is shown: the marker describes ONE call, not a mode. */
193
+ const amendedCalls = new Set();
194
+ /**
195
+ * R3v2-F1: the format contract, stated FIRMLY. The first cut asked for
196
+ * "JSON ONLY" and left it there, which a verbose model reads as a
197
+ * preference — it wrote three sentences of preamble, opened a fence, and
198
+ * the cap ended the reply mid-string. Forbidding prose, naming the exact
199
+ * schema, and giving the nothing-is-safer case its own literal answer
200
+ * are all the same instruction: there is one thing to emit and no room
201
+ * to be helpful in the margins.
202
+ *
203
+ * The schema is an ENVELOPE rather than a bare array because a single
204
+ * top-level object leaves the model nowhere to put a preamble.
205
+ */
206
+ export const SAFER_SYSTEM_PROMPT = [
207
+ "You propose safer alternatives to a single shell/tool call a human is being asked to approve.",
208
+ "Reply with JSON ONLY — no prose, no preamble, no code fence, nothing before or after the JSON.",
209
+ 'The exact schema is {"alternatives":[{"command":"...","reason":"..."}]}, with 2-3 entries.',
210
+ '"command" is the full replacement call. "reason" is ONE line of plain language saying what it does differently.',
211
+ 'Prefer alternatives that avoid irreversible deletion. If you cannot improve on it, reply {"alternatives":[]}.',
212
+ ].join(" ");
213
+ /**
214
+ * R3v2-F1: the side query's output ceiling — raised from 500, which was
215
+ * the cap the live failures hit EXACTLY.
216
+ *
217
+ * The JSON-only reply the prompt now asks for is about 200 tokens for
218
+ * three alternatives, so this ceiling is a runaway guard and not a
219
+ * budget the answer is expected to approach: it exists so a model that
220
+ * ignores the contract and writes an essay still stops, not so the
221
+ * answer has room. Output is billed only when generated, and the query
222
+ * fires only on a press, so the raise costs nothing on the path that
223
+ * works and removes the one that could not.
224
+ */
225
+ export const SAFER_MAX_TOKENS = 1500;
226
+ /**
227
+ * R3v2-F1: WHY the ask failed, when the reply's own text can prove it.
228
+ *
229
+ * This side reports the cause and never the copy — the sentences live in
230
+ * the panel package, next to each other, so there is one place where the
231
+ * words are chosen and one place they can drift from.
232
+ *
233
+ * "Cut short" is a DIAGNOSIS, so it is only claimed when the text shows
234
+ * it: a reply that closed its JSON and then failed our SHAPE returns
235
+ * null and gets the unqualified line, because telling that human their
236
+ * reply was truncated would be a confident wrong answer.
237
+ */
238
+ export function saferFailure(text) {
239
+ return jsonBody(text) === "truncated" ? { reason: "truncated" } : null;
240
+ }
241
+ /**
242
+ * R3v2-F1: find the reply's JSON body by BALANCING brackets rather than
243
+ * by first-and-last.
244
+ *
245
+ * `indexOf("[")` / `lastIndexOf("]")` had two failure modes a verbose
246
+ * model hits constantly: a bracket in the trailing prose moved the end
247
+ * past the array, and a reply the cap cut in half had no end at all.
248
+ * Both returned null, and null could not say which — which is why the
249
+ * degradation line could not either.
250
+ *
251
+ * Returns the balanced slice, `"truncated"` when a value opens and the
252
+ * text ends before it closes, or null when there is no JSON value at
253
+ * all.
254
+ */
255
+ function jsonBody(text) {
256
+ // a CLOSED fence is content-preserving to strip. An OPEN one means
257
+ // the reply ended inside the block — drop the opener and let the scan
258
+ // below reach the same verdict from the content.
259
+ const closed = text.match(/```(?:json)?\s*([\s\S]*?)```/);
260
+ const body = closed?.[1] ?? text.replace(/^[\s\S]*?```(?:json)?[ \t]*\r?\n/, "");
261
+ const start = body.search(/[[{]/);
262
+ if (start < 0)
263
+ return null;
264
+ let depth = 0;
265
+ let inString = false;
266
+ let escaped = false;
267
+ for (let i = start; i < body.length; i += 1) {
268
+ const c = body[i];
269
+ if (escaped) {
270
+ escaped = false;
271
+ }
272
+ else if (inString) {
273
+ if (c === "\\")
274
+ escaped = true;
275
+ else if (c === '"')
276
+ inString = false;
277
+ }
278
+ else if (c === '"') {
279
+ inString = true;
280
+ }
281
+ else if (c === "[" || c === "{") {
282
+ depth += 1;
283
+ }
284
+ else if (c === "]" || c === "}") {
285
+ depth -= 1;
286
+ if (depth === 0)
287
+ return body.slice(start, i + 1);
288
+ if (depth < 0)
289
+ return null;
290
+ }
291
+ }
292
+ return "truncated";
293
+ }
294
+ /**
295
+ * Parse the model's answer DEFENSIVELY — anything unexpected is a
296
+ * failure, and a failure degrades honestly.
297
+ *
298
+ * The temptation here is to be clever: salvage a half-parse, coerce a
299
+ * string into a command, accept an object where an array was asked for.
300
+ * All of that produces a list of alternatives the model did not propose,
301
+ * shown to a human deciding whether to run a destructive command. The
302
+ * only honest failure mode is the dim line, so anything that is not
303
+ * exactly the requested shape returns null.
304
+ *
305
+ * A fenced code block is the one accommodation, because models emit it
306
+ * constantly and it changes no content.
307
+ *
308
+ * R3v2-F1 widens that accommodation and NOTHING else. Unwrapping the
309
+ * named `alternatives` envelope, and reading `reason` as the spelling of
310
+ * `why` the prompt now asks for, are transport details: the entries that
311
+ * come out are verbatim the entries the model put in. That is the line
312
+ * between an accommodation and the salvage this parser refuses — a
313
+ * salvage changes WHICH alternatives are shown, and every rule that does
314
+ * that is still here. One bad entry still poisons the batch.
315
+ */
316
+ export function parseSaferOptions(text) {
317
+ const body = jsonBody(text);
318
+ // truncation and absence part ways in saferFailureNote(), which reads
319
+ // the same scan; for the list itself both are the same nothing.
320
+ if (body === null || body === "truncated")
321
+ return null;
322
+ let parsed;
323
+ try {
324
+ parsed = JSON.parse(body);
325
+ }
326
+ catch {
327
+ return null;
328
+ }
329
+ const envelope = typeof parsed === "object" && parsed !== null ? parsed.alternatives : undefined;
330
+ const list = Array.isArray(parsed) ? parsed : Array.isArray(envelope) ? envelope : null;
331
+ if (list === null || list.length === 0)
332
+ return null;
333
+ const out = [];
334
+ for (const item of list.slice(0, 3)) {
335
+ if (typeof item !== "object" || item === null)
336
+ return null;
337
+ const { command, reason, why } = item;
338
+ if (typeof command !== "string" || command.trim() === "")
339
+ return null;
340
+ const line = typeof reason === "string" ? reason : typeof why === "string" ? why : "";
341
+ out.push({ command: command.trim(), why: line.trim() });
342
+ }
343
+ return out.length === 0 ? null : out;
344
+ }
181
345
  /** W21 — the panel view for a permission_requested: the rule line (the
182
346
  * why-asked speaker + the §3.5 fix hint), the toolTarget title, the
183
347
  * "▸ run paused" status, and the ALWAYS-verbose args. */
184
- function approvalView(name, ev) {
348
+ function approvalView(name, ev, amended = false) {
185
349
  const speaker = ev.speaker ?? "kiso";
186
350
  const input = ev.input ?? {};
187
351
  // exactOptionalPropertyTypes: the hint is OMITTED when the speaker has
188
352
  // no fix (mode:accept-edits, shell in default) — never `hint: undefined`.
189
353
  const hint = fixHintFor(speaker, name);
354
+ // TUI2-R3v2 ④: the deletion-risk line, for shell calls whose command
355
+ // matches one of the four irreversible patterns. Local rules, no
356
+ // request, and absent for every other command — which is most of them.
357
+ const risk = name === "shell" ? deletionRiskHint(String(input.command ?? "")) : null;
190
358
  return {
191
359
  flavor: "approval",
192
360
  name,
193
361
  title: toolTarget(name, input),
194
362
  speaker,
195
363
  ...(hint !== undefined ? { hint } : {}),
364
+ ...(risk !== null ? { riskHint: risk } : {}),
196
365
  statusText: "▸ run paused",
197
366
  args: approvalArgs(name, input),
198
367
  fallbackQuestion: `approve ${escapeTerminal(name)}? (y/n) `,
368
+ // TUI2-R3v2 ③: the v4 frame's "(amended)" marker. It says WHY this
369
+ // call looks different from the one just refused — without it, a
370
+ // second approval for the same tool reads as the product asking
371
+ // twice rather than as the model answering.
372
+ ...(amended ? { amended: true } : {}),
199
373
  };
200
374
  }
201
375
  /** The panel's ALWAYS-verbose args: edit_file/write_file → the full ±
@@ -433,7 +607,25 @@ submitTurn) {
433
607
  const name = ev.name;
434
608
  body.toolApproval(ev.callId, approvalDiff(name, ev.input ?? {}));
435
609
  const decisionId = ev.decisionId;
436
- const verdict = await askPanel(input, approvalView(name, ev));
610
+ // TUI2-R3v2 ③: the on-demand alternatives provider. It is built
611
+ // per approval and captured by the panel; it fires ONLY if the
612
+ // human presses option 3, which is the whole zero-ambient-rent
613
+ // mechanism — no press, no request, nothing in the trace.
614
+ const safer = async () => {
615
+ const answer = await session.sideQuery({
616
+ purpose: "safer-options",
617
+ systemPrompt: SAFER_SYSTEM_PROMPT,
618
+ prompt: `the pending call is: ${name} ${JSON.stringify(ev.input ?? {})}`,
619
+ maxTokens: SAFER_MAX_TOKENS,
620
+ });
621
+ // R3v2-F1: a failure reports its CAUSE when the reply can
622
+ // prove one, so the panel can say which failure this was.
623
+ // saferFailure() returns null for every cause we cannot
624
+ // demonstrate, which is the unqualified line — unchanged.
625
+ return parseSaferOptions(answer) ?? saferFailure(answer);
626
+ };
627
+ const verdict = await askPanel(input, approvalView(name, ev, amendedCalls.has(name)), { safer });
628
+ amendedCalls.delete(name);
437
629
  switch (verdict.action) {
438
630
  case "cancel": {
439
631
  // round 10: a cancellation is a CONSERVATIVE denial,
@@ -465,6 +657,9 @@ submitTurn) {
465
657
  if (verdict.reason.trim() !== "") {
466
658
  // No+words — the words become the tool_result; the
467
659
  // run continues with the model seeing the denial.
660
+ // TUI2-R3v2 ③: whatever the model proposes next for
661
+ // this tool IS the amended call, and the panel says so.
662
+ amendedCalls.add(name);
468
663
  await session.approve(decisionId, false, verdict.reason);
469
664
  }
470
665
  else {
package/dist/index.js CHANGED
@@ -154,8 +154,8 @@ function editorInput(editor) {
154
154
  },
155
155
  // W21: the panel — the editor's own state machine takes the
156
156
  // keys; the compositor renders it via the bound state.
157
- panelAsk(view, onCommit) {
158
- editor.beginPanel(view, onCommit);
157
+ panelAsk(view, onCommit, opts) {
158
+ editor.beginPanel(view, onCommit, opts);
159
159
  },
160
160
  panelCancel() {
161
161
  editor.cancelPanel();
@@ -202,6 +202,10 @@ function makeLineInput() {
202
202
  // "input lives here"; the line-mode path keeps the brick ▌, so
203
203
  // pipe bytes do not change)
204
204
  dock.bindInput(() => editor.dockState(), "› ");
205
+ // TUI2-R3v2 ②: the click hit-test's wiring — the compositor places
206
+ // the panel's option rows, so the compositor is what the editor asks
207
+ // where they are. Neither side computes the other's geometry.
208
+ editor.bindPanelRows(() => dock.panelOptionRows());
205
209
  dock.bindMenu(() => editor.menuState()); // v3 §04: the slash-command menu
206
210
  editor.bindAtItems(atFiles); // KC3 §5: the file source — listed per OPEN
207
211
  dock.bindAt(() => editor.atState()); // KC3 §4: the picker's band
package/dist/state.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * creates the mutable ones (setBody / setAgentModel / setExtensionLists);
6
6
  * the moved modules read and mutate at call time.
7
7
  */
8
- import { Dock, type AtItem, type Body, type PanelVerdict, type PanelView, type SessionCardView } from "@vincemakes/kiso-tui";
8
+ import { Dock, type AtItem, type Body, type PanelVerdict, type PanelView, type SaferAnswer, type SessionCardView } from "@vincemakes/kiso-tui";
9
9
  import type { KisoExtension, StoreRecord } from "@vincemakes/kiso-runtime";
10
10
  /** finding #11: KISO_HOME is the ONE root — every default path derives from
11
11
  * it (sessions, trust, extensions, mcp config, skills). The dedicated
@@ -80,7 +80,14 @@ export interface LineInput {
80
80
  /** W21: open the approval panel — the editor's state machine takes
81
81
  * the keys (the digits/y/n/tab/esc/enter routing, the rule input,
82
82
  * the tab-amend), the compositor renders the block + the leads. */
83
- panelAsk(view: PanelView, onCommit: (v: PanelVerdict) => void): void;
83
+ /** TUI2-R3v2 ③: `opts.safer` is the on-demand alternatives provider —
84
+ * absent for every panel that has no such option (the ask, the pick,
85
+ * the trust gate), so those buttons cannot exist to be pressed.
86
+ * R3v2-F1: it resolves a SaferAnswer, so a failure that can name its
87
+ * cause does — `null` still means a failure with nothing to add. */
88
+ panelAsk(view: PanelView, onCommit: (v: PanelVerdict) => void, opts?: {
89
+ safer?: () => Promise<SaferAnswer>;
90
+ }): void;
84
91
  /** W21: cancel the panel — the SIGINT pair to panelAsk. */
85
92
  panelCancel(): void;
86
93
  /** TUI2-R2 ②: open the session picker — the editor takes the keys
@@ -5,7 +5,7 @@
5
5
  * chat.ts), the don't-ask-again rule writer, and the uncertain-execution
6
6
  * decisions. All bodies moved verbatim from index.ts.
7
7
  */
8
- import { type PanelVerdict, type PanelView } from "@vincemakes/kiso-tui";
8
+ import { type PanelVerdict, type PanelView, type SaferAnswer } from "@vincemakes/kiso-tui";
9
9
  import type { AskUI } from "@vincemakes/kiso-ask-ext";
10
10
  import { type ProjectArtifacts } from "@vincemakes/kiso-runtime";
11
11
  import type { AgentSession } from "@vincemakes/kiso-runtime";
@@ -34,7 +34,9 @@ import { type LineInput } from "./state.js";
34
34
  * the dead question.
35
35
  */
36
36
  export declare let pendingAsk: (() => void) | null;
37
- export declare function askPanel(input: LineInput, view: PanelView): Promise<PanelVerdict>;
37
+ export declare function askPanel(input: LineInput, view: PanelView, opts?: {
38
+ safer?: () => Promise<SaferAnswer>;
39
+ }): Promise<PanelVerdict>;
38
40
  /**
39
41
  * KC3.5 — the AskUI bridge: the panel the ask extension asks through.
40
42
  *
package/dist/trust-ui.js CHANGED
@@ -38,7 +38,12 @@ import { getMode } from "./mode.js";
38
38
  * the dead question.
39
39
  */
40
40
  export let pendingAsk = null;
41
- export function askPanel(input, view) {
41
+ export function askPanel(input, view,
42
+ // TUI2-R3v2 ③: the safer-options provider, when the caller has one.
43
+ // Only the approval site passes it; every other panel (the ask, the
44
+ // pick, the trust gate, the uncertain resolutions) omits it, so their
45
+ // lists cannot grow a button they have no answer for.
46
+ opts) {
42
47
  if (!process.stdin.isTTY) {
43
48
  console.log(`[non-interactive — no human to ask: ${view.fallbackQuestion}]`);
44
49
  return Promise.resolve({ action: "deny", reason: "no human to ask" });
@@ -63,7 +68,7 @@ export function askPanel(input, view) {
63
68
  settled = true;
64
69
  pendingAsk = null;
65
70
  resolve(verdict);
66
- });
71
+ }, opts);
67
72
  }
68
73
  else {
69
74
  // v2c: a TTY without a dock (rows < 4) — the fallback question
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "kiso CLI — the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,19 +18,19 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-ask-ext": "0.11.0",
22
- "@vincemakes/kiso-core": "0.11.0",
23
- "@vincemakes/kiso-evals": "0.11.0",
24
- "@vincemakes/kiso-mcp-ext": "0.11.0",
25
- "@vincemakes/kiso-provider-anthropic": "0.11.0",
26
- "@vincemakes/kiso-provider-openai": "0.11.0",
27
- "@vincemakes/kiso-runtime": "0.11.0",
28
- "@vincemakes/kiso-skills-ext": "0.11.0",
29
- "@vincemakes/kiso-subagent-ext": "0.11.0",
30
- "@vincemakes/kiso-task-ext": "0.11.0",
31
- "@vincemakes/kiso-tools-node": "0.11.0",
32
- "@vincemakes/kiso-tui": "0.11.0",
33
- "@vincemakes/kiso-tui-cells": "0.11.0"
21
+ "@vincemakes/kiso-ask-ext": "0.13.0",
22
+ "@vincemakes/kiso-core": "0.13.0",
23
+ "@vincemakes/kiso-evals": "0.13.0",
24
+ "@vincemakes/kiso-mcp-ext": "0.13.0",
25
+ "@vincemakes/kiso-provider-anthropic": "0.13.0",
26
+ "@vincemakes/kiso-provider-openai": "0.13.0",
27
+ "@vincemakes/kiso-runtime": "0.13.0",
28
+ "@vincemakes/kiso-skills-ext": "0.13.0",
29
+ "@vincemakes/kiso-subagent-ext": "0.13.0",
30
+ "@vincemakes/kiso-task-ext": "0.13.0",
31
+ "@vincemakes/kiso-tools-node": "0.13.0",
32
+ "@vincemakes/kiso-tui": "0.13.0",
33
+ "@vincemakes/kiso-tui-cells": "0.13.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^26.1.2",