@vincemakes/kiso-code 0.1.49 → 0.2.1

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
@@ -32,6 +32,46 @@ export declare function contextWindowTokens(): number;
32
32
  * the model window. Marked ~ everywhere it is shown; no counting API.
33
33
  */
34
34
  export declare function estimateCtxRatio(session: AgentSession): number;
35
+ /** E2 (1.3.0) — the CLI's usage consumer: one event in, the RunUsage the
36
+ * status line and recap render plus the miss estimate. `total` is the
37
+ * carrier for the NEXT turn's miss estimate — the overlap of consecutive
38
+ * prompts is a total-side quantity, never a fresh delta. */
39
+ export interface UsageDelta {
40
+ readonly usage: RunUsage;
41
+ /** The canonical total (fresh + cache) — the miss estimate's carrier.
42
+ * null when the event carried no usage (the canonical total of an
43
+ * unknown event is 0, and a 0 carrier keeps the estimate below the
44
+ * floor — the old consumer's carrier stayed null forever; this one
45
+ * recovers on the next known event). */
46
+ readonly total: number | null;
47
+ readonly missed: number | null;
48
+ }
49
+ /**
50
+ * The CLI's usage consumer (E2 1.3.0, the R2a-1 ruling 2026-08-13) — the
51
+ * HEAL: the mixed-convention consumer is now CANONICAL at the route (the
52
+ * accounting boundary), the same derivation the trace block carries.
53
+ *
54
+ * EXISTING-BEHAVIOR CHANGE — declared, never a silent side-fix:
55
+ * - openai-compat: `in` was the provider-raw TOTAL (fresh + cache); it is
56
+ * now the canonical FRESH count. The >100% cache-ratio disease: raw
57
+ * {input 111, cacheRead 1024} previously rendered "in 111" and the
58
+ * recap's cache % (then cache/in) read 923%; now "in 0" and the recap
59
+ * divides cache by the TOTAL (in + cache — T5) — never > 100%.
60
+ * - the miss estimate is numerically IDENTICAL on openai-compat: its old
61
+ * `in` WAS the total, and the carrier is the canonical total
62
+ * (input + cacheRead), which equals the raw total by construction.
63
+ * - anthropic: `in` was already fresh — unchanged; the miss estimate was
64
+ * min-of-fresh-deltas − cacheRead (always below the floor — silent);
65
+ * it is now min-of-totals, the semantics the openai-compat side always
66
+ * had (a fix, and it can fire).
67
+ * - the unknown-usage carrier: the old consumer's null carrier killed the
68
+ * miss signal forever; the canonical total of an unknown event is 0, so
69
+ * the signal recovers on the next known event.
70
+ * The route key mirrors the trace path's fallback by construction: an
71
+ * absent provider resolves like the tracer's "adapter" identity — the
72
+ * total convention (INPUT_CONVENTIONS), never a crash.
73
+ */
74
+ export declare function usageFromEvent(route: string | undefined, ev: import("@vincemakes/kiso-core").Usage, prevTotal: number | null): UsageDelta;
35
75
  /** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
36
76
  * is gone) — docked only, 200ms rotation between the request and the
37
77
  * first event. */
package/dist/chat.js CHANGED
@@ -8,6 +8,7 @@ import { readFileSync } from "node:fs";
8
8
  import { escapeTerminal, kUnit, palette, renderEvent, renderRecap, toolTarget, } from "@vincemakes/kiso-tui";
9
9
  import { editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
10
10
  import { canonicalTargetPath } from "@vincemakes/kiso-tools-node";
11
+ import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
11
12
  import { dispatch } from "./dispatch.js";
12
13
  import { agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
13
14
  import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
@@ -47,6 +48,49 @@ export function estimateCtxRatio(session) {
47
48
  const chars = JSON.stringify(projected).length;
48
49
  return chars / 4 / contextWindowTokens();
49
50
  }
51
+ /** R-C item 4: the per-turn cache miss — the overlap with the previous
52
+ * prompt that SHOULD have been cached but was re-sent uncached:
53
+ * missed = min(prevIn, in) − cacheRead. Below the 1024-token floor
54
+ * (Anthropic's minimum cacheable block) it is noise — not surfaced. */
55
+ const CACHE_MISS_FLOOR = 1024;
56
+ /**
57
+ * The CLI's usage consumer (E2 1.3.0, the R2a-1 ruling 2026-08-13) — the
58
+ * HEAL: the mixed-convention consumer is now CANONICAL at the route (the
59
+ * accounting boundary), the same derivation the trace block carries.
60
+ *
61
+ * EXISTING-BEHAVIOR CHANGE — declared, never a silent side-fix:
62
+ * - openai-compat: `in` was the provider-raw TOTAL (fresh + cache); it is
63
+ * now the canonical FRESH count. The >100% cache-ratio disease: raw
64
+ * {input 111, cacheRead 1024} previously rendered "in 111" and the
65
+ * recap's cache % (then cache/in) read 923%; now "in 0" and the recap
66
+ * divides cache by the TOTAL (in + cache — T5) — never > 100%.
67
+ * - the miss estimate is numerically IDENTICAL on openai-compat: its old
68
+ * `in` WAS the total, and the carrier is the canonical total
69
+ * (input + cacheRead), which equals the raw total by construction.
70
+ * - anthropic: `in` was already fresh — unchanged; the miss estimate was
71
+ * min-of-fresh-deltas − cacheRead (always below the floor — silent);
72
+ * it is now min-of-totals, the semantics the openai-compat side always
73
+ * had (a fix, and it can fire).
74
+ * - the unknown-usage carrier: the old consumer's null carrier killed the
75
+ * miss signal forever; the canonical total of an unknown event is 0, so
76
+ * the signal recovers on the next known event.
77
+ * The route key mirrors the trace path's fallback by construction: an
78
+ * absent provider resolves like the tracer's "adapter" identity — the
79
+ * total convention (INPUT_CONVENTIONS), never a crash.
80
+ */
81
+ export function usageFromEvent(route, ev, prevTotal) {
82
+ const c = canonicalizeUsage(route ?? "adapter", ev);
83
+ const total = c.input + c.cacheRead + (c.cacheWrite ?? 0);
84
+ let missed = null;
85
+ // R-C item 4: min(prevTotal, total) is the part that could have been
86
+ // cached; what cacheRead did NOT cover is the miss. A below-floor or
87
+ // non-positive difference is noise — not surfaced.
88
+ if (prevTotal !== null) {
89
+ const m = Math.min(prevTotal, total) - c.cacheRead;
90
+ missed = m > CACHE_MISS_FLOOR ? m : null;
91
+ }
92
+ return { usage: { in: c.input, out: c.output, cache: c.cacheRead, known: ev.known }, total, missed };
93
+ }
50
94
  /** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
51
95
  * is gone) — docked only, 200ms rotation between the request and the
52
96
  * first event. */
@@ -212,12 +256,7 @@ export async function consumeRun(session, run, input, turnNo, faux, statusCb,
212
256
  submitTurn) {
213
257
  let last;
214
258
  let usage = { in: null, out: null, cache: null, known: false };
215
- // R-C item 4: the per-turn cache miss — the overlap with the previous
216
- // prompt that SHOULD have been cached but was re-sent uncached:
217
- // missed = min(prevIn, in) − cacheRead. Below the 1024-token floor
218
- // (Anthropic's minimum cacheable block) it is noise — not surfaced.
219
- const CACHE_MISS_FLOOR = 1024;
220
- let prevIn = null;
259
+ let prevTotal = null;
221
260
  let missed = null;
222
261
  // v3 §02: the recap line derives ENTIRELY from the local event stream
223
262
  // (zero tokens) — wall seconds, tool/edit counts, usage, ctx left.
@@ -297,14 +336,10 @@ submitTurn) {
297
336
  body.textEnd();
298
337
  break;
299
338
  case "usage": {
300
- usage = { in: ev.inputTokens, out: ev.outputTokens, cache: ev.cacheRead, known: ev.known };
301
- // R-C item 4: min(prevIn, in) is the part that could have been
302
- // cached; what cacheRead did NOT cover is the miss.
303
- if (usage.in !== null && usage.cache !== null && prevIn !== null) {
304
- const m = Math.min(prevIn, usage.in) - usage.cache;
305
- missed = m > CACHE_MISS_FLOOR ? m : null;
306
- }
307
- prevIn = usage.in;
339
+ const delta = usageFromEvent(session.provider, ev, prevTotal);
340
+ usage = delta.usage;
341
+ prevTotal = delta.total;
342
+ missed = delta.missed;
308
343
  statusCb?.(usage, estimateCtxRatio(session));
309
344
  break;
310
345
  }
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
+ }
@@ -3,7 +3,7 @@
3
3
  * turns. The bodies moved verbatim from chat()'s closure; chat provides
4
4
  * the context (the chain, the run state, the prompt arming).
5
5
  */
6
- import { type AgentSession } from "@vincemakes/kiso-runtime";
6
+ import type { AgentSession } from "@vincemakes/kiso-runtime";
7
7
  import { type LineInput } from "./state.js";
8
8
  /** Everything dispatch touches that chat() owns. */
9
9
  export interface DispatchCtx {
package/dist/dispatch.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * the context (the chain, the run state, the prompt arming).
5
5
  */
6
6
  import { escapeTerminal, kUnit, palette } from "@vincemakes/kiso-tui";
7
- import { buildAdapter } from "@vincemakes/kiso-runtime";
7
+ import { buildAdapter } from "@vincemakes/kiso-runtime/internal";
8
8
  import { MODES, getMode, setMode } from "./mode.js";
9
9
  import { agentModel, body, bodyLog, configModels, dock, setAgentModel, setCurrentModelName } from "./state.js";
10
10
  import { directWriteProfile, profileAvailable } from "./config.js";
package/dist/index.js CHANGED
@@ -502,7 +502,15 @@ async function main() {
502
502
  break;
503
503
  }
504
504
  case "sessions": {
505
- agent = await makeAgent(undefined, undefined, modelFlag);
505
+ // R-I-p2 audit (the argument-consistency mandate): the
506
+ // read-only listing NEVER writes through the input, but the
507
+ // trust gate lives inside makeAgent and ASKS through it — on
508
+ // a TTY with a first-discovery .kiso, the undefined input
509
+ // crashed identically to the bare command (finding R-I-p-2,
510
+ // "reading 'question'" on the dock-less branch). The input
511
+ // exists so the gate's ask can be answered; the listing
512
+ // itself never touches it.
513
+ agent = await makeAgent(undefined, input, modelFlag);
506
514
  for (const meta of agent.sessions()) {
507
515
  console.log(renderSessionLine(meta));
508
516
  }
@@ -525,7 +533,13 @@ async function main() {
525
533
  // chat — the first argument is the session id.
526
534
  const id = command ?? new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
527
535
  dock.enter();
528
- agent = await makeAgent(id);
536
+ // R-I-p2 (finding R-I-p-2): the bare command passes the SAME
537
+ // input source and model flag as chat/resume — the pre-patch
538
+ // call dropped both, and the first-run trust gate read
539
+ // through the undefined input: "Cannot read properties of
540
+ // undefined" at the ask (panelAsk with the dock, question on
541
+ // the dock-less fallback).
542
+ agent = await makeAgent(id, input, modelFlag);
529
543
  const session = await agent.session({ id });
530
544
  bodyLog(`session ${id}\n`);
531
545
  extensionsBanner(recentSessions(id, agent));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.1.49",
4
- "description": "kiso CLI \u2014 the coding-agent reference product: kiso chat / kiso resume / kiso sessions.",
3
+ "version": "0.2.1",
4
+ "description": "kiso CLI \u2014 the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -18,18 +18,18 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-core": "0.1.35",
22
- "@vincemakes/kiso-evals": "0.1.36",
23
- "@vincemakes/kiso-mcp-ext": "0.1.49",
24
- "@vincemakes/kiso-provider-anthropic": "0.1.36",
25
- "@vincemakes/kiso-provider-openai": "0.1.36",
26
- "@vincemakes/kiso-runtime": "0.1.38",
27
- "@vincemakes/kiso-skills-ext": "0.1.49",
28
- "@vincemakes/kiso-subagent-ext": "0.1.49",
29
- "@vincemakes/kiso-task-ext": "0.1.49",
30
- "@vincemakes/kiso-tools-node": "0.1.36",
31
- "@vincemakes/kiso-tui": "0.1.42",
32
- "@vincemakes/kiso-tui-cells": "0.1.42"
21
+ "@vincemakes/kiso-core": "0.2.0",
22
+ "@vincemakes/kiso-evals": "0.2.0",
23
+ "@vincemakes/kiso-mcp-ext": "0.2.0",
24
+ "@vincemakes/kiso-provider-anthropic": "0.2.0",
25
+ "@vincemakes/kiso-provider-openai": "0.2.0",
26
+ "@vincemakes/kiso-runtime": "0.2.1",
27
+ "@vincemakes/kiso-skills-ext": "0.2.0",
28
+ "@vincemakes/kiso-subagent-ext": "0.2.0",
29
+ "@vincemakes/kiso-task-ext": "0.2.0",
30
+ "@vincemakes/kiso-tools-node": "0.2.0",
31
+ "@vincemakes/kiso-tui": "0.2.0",
32
+ "@vincemakes/kiso-tui-cells": "0.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^26.1.2",