@vincemakes/kiso-code 0.31.1 → 0.32.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/state.d.ts CHANGED
@@ -12,6 +12,14 @@ import type { KisoExtension, StoreRecord } from "@vincemakes/kiso-runtime";
12
12
  * env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
13
13
  * still override their own path; nothing hard-codes ~/.kiso anymore. */
14
14
  export declare function kisoHome(): string;
15
+ /** DC-49 — the workspace the coding tools may touch, and the one place it
16
+ * is decided. The session's own tool set and the `!` command's runner both
17
+ * need it, and two copies of a ruling-bearing literal is how a later DC-49
18
+ * amendment reaches one caller and not the other. */
19
+ export declare function codingToolOptions(): {
20
+ readonly workspaceRoot: string;
21
+ readonly excludeRoots: readonly string[];
22
+ };
15
23
  export declare function sessionsDir(): string;
16
24
  /** E1: the extension scan directory — KISO_EXTENSIONS_DIR overrides. */
17
25
  export declare function extensionsDir(): string;
@@ -77,6 +85,15 @@ export interface LineInput {
77
85
  /** W15: the expand key (ctrl+o) — the chain-level action, never the
78
86
  * editor's own interpretation. */
79
87
  onExpand(cb: () => void): void;
88
+ /** §2.3: the thinking key (ctrl+t) — the same chain-level shape as
89
+ * onExpand. Optional: the readline input has no such key, and a
90
+ * dock-less session has nothing to reprint. */
91
+ onThink?(cb: () => void): void;
92
+ /** §2.4: the external-editor key (ctrl+g). */
93
+ onEditor?(cb: () => void): void;
94
+ /** §2.4: hand the terminal over, run `edit`, take it back. The editor
95
+ * owns the handover; the CLI owns the spawn. */
96
+ externalEdit?(edit: (text: string) => string | null): void;
80
97
  /** E1 §3: the copy key (ctrl+x) — the chain-level action, never the
81
98
  * composer's own. The readline input ignores it; `/copy` is the
82
99
  * route that works everywhere. */
@@ -178,6 +195,24 @@ export declare let configModels: Readonly<Record<string, import("./config.js").M
178
195
  export declare function setConfigModels(models: Readonly<Record<string, import("./config.js").ModelProfile>>): void;
179
196
  /** The name of the model currently driving the session ("faux" or the
180
197
  * profile name / provider/model write / env model). */
198
+ /**
199
+ * §2.5 — the profile the human CHOSE by name, and the only source a
200
+ * rebuild may read for the model.
201
+ *
202
+ * Not `currentModelName`: that holds whatever resolution produced, and
203
+ * an env-resolved session's name ("gpt-4o", "claude-sonnet-5") is not a
204
+ * key in `models`, so handing it back to `resolveModel` throws. Not the
205
+ * startup flag either, or a `/model` switch would silently revert on the
206
+ * first reload — the effort-axis round produced that bug once already
207
+ * from having two sources for the model.
208
+ *
209
+ * One variable, seeded by `makeAgent` from the startup flag and
210
+ * overwritten by `/model`. Undefined means "resolve exactly as this
211
+ * process did at startup", which is the right answer for a session that
212
+ * never named a profile.
213
+ */
214
+ export declare let modelChoice: string | undefined;
215
+ export declare function setModelChoice(value: string | undefined): void;
181
216
  export declare let currentModelName: string;
182
217
  export declare function setCurrentModelName(value: string): void;
183
218
  /** E1: the extensions loaded by makeAgent — their names feed the banner. */
package/dist/state.js CHANGED
@@ -18,6 +18,13 @@ import { AT_CAP, AT_SKIP, Dock } from "@vincemakes/kiso-tui";
18
18
  export function kisoHome() {
19
19
  return process.env.KISO_HOME ?? join(homedir(), ".kiso");
20
20
  }
21
+ /** DC-49 — the workspace the coding tools may touch, and the one place it
22
+ * is decided. The session's own tool set and the `!` command's runner both
23
+ * need it, and two copies of a ruling-bearing literal is how a later DC-49
24
+ * amendment reaches one caller and not the other. */
25
+ export function codingToolOptions() {
26
+ return { workspaceRoot: process.cwd(), excludeRoots: [kisoHome()] };
27
+ }
21
28
  export function sessionsDir() {
22
29
  return join(kisoHome(), "sessions");
23
30
  }
@@ -246,6 +253,26 @@ export function setConfigModels(models) {
246
253
  }
247
254
  /** The name of the model currently driving the session ("faux" or the
248
255
  * profile name / provider/model write / env model). */
256
+ /**
257
+ * §2.5 — the profile the human CHOSE by name, and the only source a
258
+ * rebuild may read for the model.
259
+ *
260
+ * Not `currentModelName`: that holds whatever resolution produced, and
261
+ * an env-resolved session's name ("gpt-4o", "claude-sonnet-5") is not a
262
+ * key in `models`, so handing it back to `resolveModel` throws. Not the
263
+ * startup flag either, or a `/model` switch would silently revert on the
264
+ * first reload — the effort-axis round produced that bug once already
265
+ * from having two sources for the model.
266
+ *
267
+ * One variable, seeded by `makeAgent` from the startup flag and
268
+ * overwritten by `/model`. Undefined means "resolve exactly as this
269
+ * process did at startup", which is the right answer for a session that
270
+ * never named a profile.
271
+ */
272
+ export let modelChoice;
273
+ export function setModelChoice(value) {
274
+ modelChoice = value;
275
+ }
249
276
  export let currentModelName = "faux";
250
277
  export function setCurrentModelName(value) {
251
278
  currentModelName = value;
package/dist/trust-ui.js CHANGED
@@ -162,17 +162,38 @@ export function askUi(input) {
162
162
  export async function addDontAskAgainRule(rule) {
163
163
  const dir = extensionsDir();
164
164
  const file = join(dir, "dont-ask-again.mjs");
165
- const existed = existsSync(file);
165
+ // §2.5: the LIVE extension, found by name in the array the chain reads.
166
+ // This is the only handle on the rules that is guaranteed to be the one
167
+ // in force, and finding it also tells us whether this is a first rule.
168
+ const at = currentAgentExtensions.findIndex((e) => e.name === "dont-ask-again");
169
+ const live = at >= 0 ? currentAgentExtensions[at] : undefined;
170
+ // Every rule the session already grants, plus this one (RL-F2: the file
171
+ // used to be rewritten with ONLY the newest rule, so a second grant
172
+ // erased the first from disk — invisible in-session, because the
173
+ // in-memory Set kept both, and only discovered at the next start).
174
+ //
175
+ // RL-F5: a file generated BEFORE this change exports RULES and puts
176
+ // nothing on the extension, so `live.rules` is undefined for it and a
177
+ // union starting from nothing would rewrite the file with this grant
178
+ // alone — every rule the human gave under 0.31.x gone, in exactly the
179
+ // way RL-F2 was invisible. The rules come out of the file instead, and
180
+ // the migration costs one grant.
181
+ const existing = live?.rules !== undefined ? [...live.rules] : rulesInFile(file);
182
+ const rules = [...new Set([...existing, rule])].sort();
166
183
  const module = [
167
184
  "// generated by kiso — the \"2 Yes, don't ask again for <tool>\" rule.",
168
185
  "// HUMAN-EDITABLE and HUMAN-DELETABLE — this file IS the revocation path:",
169
186
  "// delete a rule from the set (or the whole file) to be asked again.",
170
187
  "// ALLOW-ONLY by design (R3): never emits deny or ask — the mode moat",
171
188
  "// and the safe-defaults moat keep their teeth.",
172
- `export const RULES = new Set(${JSON.stringify([rule])});`,
189
+ `const RULES = new Set(${JSON.stringify(rules)});`,
173
190
  "",
174
191
  "export default {",
175
192
  ' name: "dont-ask-again",',
193
+ " // the live handle: kiso mutates THIS set to make a new rule take",
194
+ " // effect without a reload. It is the same object the decide below",
195
+ " // closes over, which is the whole point.",
196
+ " rules: RULES,",
176
197
  " approvals: [",
177
198
  " {",
178
199
  " decide(call) {",
@@ -185,21 +206,57 @@ export async function addDontAskAgainRule(rule) {
185
206
  ].join("\n");
186
207
  mkdirSync(dir, { recursive: true });
187
208
  writeFileSync(file, module, "utf8");
188
- // The live in-session effect: a file that existed at startup was
189
- // imported by the loader — ONE namespace — so mutating the exported
190
- // RULES Set is live NOW (the current run's chain reads the same Set).
191
- const mod = (await import(pathToFileURL(file).href));
192
- if (mod.RULES !== undefined && !mod.RULES.has(rule))
193
- mod.RULES.add(rule);
194
- // A FIRST-time rule (a file that did not exist at startup) is not yet
195
- // in the current run's chain — the chain is fixed at a run's start —
196
- // so the generated extension joins the NEXT run's policies (run.ts
197
- // re-reads the session config's extensions array, shared by
198
- // reference). The honest story: a new rule applies from the next run.
199
- if (!existed && mod.default !== undefined) {
209
+ // The live in-session effect. This used to re-import the plain file URL
210
+ // and mutate the module's exported Set, on the stated assumption that a
211
+ // file the loader imported is ONE namespace. §2.5's loader appends a
212
+ // per-load nonce so `/reload` can pick up edited code, and that
213
+ // assumption died with it: the loader holds `file?load=N` and a plain
214
+ // import is the startup namespace. A rule granted after any reload
215
+ // would mutate a Set nothing reads — the human clicks don't-ask-again
216
+ // and is asked again on the very next identical call.
217
+ //
218
+ // So there is no second import. The rule is added THROUGH the live
219
+ // extension, which is one source of truth by construction.
220
+ if (live?.rules !== undefined) {
221
+ live.rules.add(rule);
222
+ return;
223
+ }
224
+ // Either a FIRST rule (no such extension loaded) or the RL-F5 migration
225
+ // (a legacy extension is loaded but has no live handle). The freshly
226
+ // written file is imported and joins the chain — REPLACING the legacy
227
+ // entry rather than sitting beside it, because two extensions of the
228
+ // same name in the chain is a second source of truth and the whole point
229
+ // of the handle is that there is one. The chain is fixed at a run's
230
+ // start, so a first rule applies from the NEXT run (run.ts re-reads the
231
+ // session config's extensions array, shared by reference).
232
+ //
233
+ // The nonce is the loader's own lesson applied here: a plain file URL is
234
+ // cached forever, so a second grant in one process would import the
235
+ // bytes of the first.
236
+ const mod = (await import(`${pathToFileURL(file).href}?grant=${(grantSerial += 1)}`));
237
+ if (mod.default === undefined)
238
+ return;
239
+ if (at >= 0)
240
+ currentAgentExtensions[at] = mod.default;
241
+ else
200
242
  currentAgentExtensions.push(mod.default);
243
+ }
244
+ /** RL-F5 — the rules a generated file already holds. The `new Set([...])`
245
+ * literal is JSON, which is what makes reading it back honest rather than
246
+ * a parser. An unreadable or hand-edited-past-recognition file yields
247
+ * nothing, and the new rule stands alone — the file is documented as
248
+ * human-editable, so kiso must not fail on what a human wrote there. */
249
+ function rulesInFile(file) {
250
+ try {
251
+ const m = /new Set\((\[[^\]]*\])\)/.exec(readFileSync(file, "utf8"));
252
+ return m === null ? [] : JSON.parse(m[1]);
253
+ }
254
+ catch {
255
+ return [];
201
256
  }
202
257
  }
258
+ /** RL-F5: distinguishes each grant's import of the generated file. */
259
+ let grantSerial = 0;
203
260
  /**
204
261
  * W21 — the §3.5 fix-hint table: per speaker, the ONE-line path to stop
205
262
  * being asked. The hint names the FIX, never a bypass ("switch to
@@ -309,8 +366,24 @@ function readMcpConfig(path) {
309
366
  }
310
367
  /** Merge user-level + project-level mcp.json into one temp file and point
311
368
  * KISO_MCP_CONFIG at it — the mcp extension reads it at load time. */
369
+ /**
370
+ * RL-F4 — the user mcp.json as it was BEFORE any merge, and RL-F1's exact
371
+ * sibling one function up. `applyMcpMerge` read `KISO_MCP_CONFIG` and then
372
+ * assigned the merged file to it, so a second merge — which is what a
373
+ * reload is — took the first merged file as the user config. That file
374
+ * already holds the project's servers, so the "exists in both" check
375
+ * throws for every one of them and EVERY reload fails in exactly the
376
+ * projects that carry MCP servers.
377
+ *
378
+ * Found by the lead reviewing the commit that fixed RL-F1 in the function
379
+ * below. The lesson is the sibling, not the loop: a defect found in one
380
+ * function is a question to ask of every function beside it, and I fixed
381
+ * one merge without looking at the other.
382
+ */
383
+ let originalUserMcpPath;
312
384
  function applyMcpMerge(root) {
313
- const userPath = process.env.KISO_MCP_CONFIG ?? join(kisoHome(), "mcp.json");
385
+ originalUserMcpPath ??= process.env.KISO_MCP_CONFIG ?? join(kisoHome(), "mcp.json");
386
+ const userPath = originalUserMcpPath;
314
387
  const user = readMcpConfig(userPath);
315
388
  const project = readMcpConfig(join(root, "mcp.json"));
316
389
  const userServers = user.mcpServers ?? {};
@@ -323,17 +396,37 @@ function applyMcpMerge(root) {
323
396
  }
324
397
  }
325
398
  const merged = { mcpServers: { ...userServers, ...projectServers } };
326
- const temp = join(tmpdir(), `kiso-mcp-merged-${process.pid}.json`);
399
+ // RL-F4, second half: the path used to be `kiso-mcp-merged-<pid>.json`,
400
+ // the SAME file on every merge in one process — so a reload's cleanup of
401
+ // the paths the previous set owned deleted the file the new set had just
402
+ // been merged into. One directory per merge, and the directory is what is
403
+ // recorded, so the cleanup removes exactly what that merge created.
404
+ const dir = mkdtempSync(join(tmpdir(), "kiso-mcp-"));
405
+ const temp = join(dir, "mcp.json");
327
406
  writeFileSync(temp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
328
407
  process.env.KISO_MCP_CONFIG = temp;
329
- mergedTempPaths.push(temp);
408
+ mergedTempPaths.push(dir);
330
409
  }
331
410
  /** Merge user-level + project-level skills into one temp scan dir (project
332
411
  * skill dirs symlinked first; a name in both → project wins + a stderr
333
412
  * note) and point KISO_SKILLS_DIR at it — the skills extension's existing
334
413
  * scan reads it at load time and per read_skill call. */
414
+ /**
415
+ * §2.5 — the user skills directory as it was BEFORE any merge.
416
+ *
417
+ * `applySkillsMerge` ends by pointing `KISO_SKILLS_DIR` at the merged
418
+ * directory it just made. Read twice — which is exactly what a reload
419
+ * does — that is a loop: the second merge takes the FIRST merge as its
420
+ * user directory. A skill deleted from the real user directory would
421
+ * survive forever, and each merge would produce a temp directory the
422
+ * previous one's symlinks depend on, so none of them could ever be
423
+ * removed. This tree has already lost an afternoon to 449,956 stale
424
+ * temp directories.
425
+ */
426
+ let originalUserSkillsDir;
335
427
  function applySkillsMerge(root) {
336
- const userDir = process.env.KISO_SKILLS_DIR ?? join(kisoHome(), "skills");
428
+ originalUserSkillsDir ??= process.env.KISO_SKILLS_DIR ?? join(kisoHome(), "skills");
429
+ const userDir = originalUserSkillsDir;
337
430
  const projectDir = join(root, "skills");
338
431
  const merged = mkdtempSync(join(tmpdir(), "kiso-skills-"));
339
432
  mergedTempPaths.push(merged);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.31.1",
4
- "description": "kiso CLI — the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
3
+ "version": "0.32.1",
4
+ "description": "kiso-code — the coding agent on kiso, the durable runtime for AI agents: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -18,20 +18,20 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-ask-ext": "0.31.1",
22
- "@vincemakes/kiso-core": "0.31.1",
23
- "@vincemakes/kiso-evals": "0.31.1",
24
- "@vincemakes/kiso-mcp-ext": "0.31.1",
25
- "@vincemakes/kiso-provider-anthropic": "0.31.1",
26
- "@vincemakes/kiso-provider-openai": "0.31.1",
27
- "@vincemakes/kiso-provider-openai-responses": "0.31.1",
28
- "@vincemakes/kiso-runtime": "0.31.1",
29
- "@vincemakes/kiso-skills-ext": "0.31.1",
30
- "@vincemakes/kiso-subagent-ext": "0.31.1",
31
- "@vincemakes/kiso-task-ext": "0.31.1",
32
- "@vincemakes/kiso-tools-node": "0.31.1",
33
- "@vincemakes/kiso-tui": "0.31.1",
34
- "@vincemakes/kiso-tui-cells": "0.31.1"
21
+ "@vincemakes/kiso-ask-ext": "0.32.1",
22
+ "@vincemakes/kiso-core": "0.32.1",
23
+ "@vincemakes/kiso-evals": "0.32.1",
24
+ "@vincemakes/kiso-mcp-ext": "0.32.1",
25
+ "@vincemakes/kiso-provider-anthropic": "0.32.1",
26
+ "@vincemakes/kiso-provider-openai": "0.32.1",
27
+ "@vincemakes/kiso-provider-openai-responses": "0.32.1",
28
+ "@vincemakes/kiso-runtime": "0.32.1",
29
+ "@vincemakes/kiso-skills-ext": "0.32.1",
30
+ "@vincemakes/kiso-subagent-ext": "0.32.1",
31
+ "@vincemakes/kiso-task-ext": "0.32.1",
32
+ "@vincemakes/kiso-tools-node": "0.32.1",
33
+ "@vincemakes/kiso-tui": "0.32.1",
34
+ "@vincemakes/kiso-tui-cells": "0.32.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^26.1.2",