@vincemakes/kiso-code 0.40.5 → 0.40.6

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.
@@ -49,6 +49,18 @@ export declare function effectiveBaseUrl(kind: string, baseUrl?: string): string
49
49
  /** The provider identity a profile's credential is stored under, or null
50
50
  * for an origin nobody recognizes (custom: env var only). */
51
51
  export declare function providerIdOf(kind: string, baseUrl?: string): string | null;
52
+ /**
53
+ * 0.40.6 — the store id for a GATEWAY's own key: `endpoint:<origin>`.
54
+ *
55
+ * A vendor's credential is stored under the vendor and only ever reaches
56
+ * the vendor's origin (R1 above). A gateway has no vendor identity, so its
57
+ * key is stored under the one thing that names where it may go — its
58
+ * ORIGIN (scheme, host, port). `kiso login --endpoint <url>` writes it; a
59
+ * profile whose endpoint has exactly that origin reads it; no other origin
60
+ * ever does, a local forwarder in front of the gateway included. Null for
61
+ * anything that is not an http(s) URL — never a guess.
62
+ */
63
+ export declare function endpointCredentialId(url: string | undefined): string | null;
52
64
  export declare function authPath(): string;
53
65
  export declare function readAuthFile(path?: string): AuthFile;
54
66
  export declare function getCredential(providerId: string, path?: string): Credential | undefined;
@@ -133,6 +133,31 @@ function originOf(baseUrl) {
133
133
  return null;
134
134
  }
135
135
  }
136
+ /**
137
+ * 0.40.6 — the store id for a GATEWAY's own key: `endpoint:<origin>`.
138
+ *
139
+ * A vendor's credential is stored under the vendor and only ever reaches
140
+ * the vendor's origin (R1 above). A gateway has no vendor identity, so its
141
+ * key is stored under the one thing that names where it may go — its
142
+ * ORIGIN (scheme, host, port). `kiso login --endpoint <url>` writes it; a
143
+ * profile whose endpoint has exactly that origin reads it; no other origin
144
+ * ever does, a local forwarder in front of the gateway included. Null for
145
+ * anything that is not an http(s) URL — never a guess.
146
+ */
147
+ export function endpointCredentialId(url) {
148
+ if (url === undefined)
149
+ return null;
150
+ let u;
151
+ try {
152
+ u = new URL(url);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ if (u.protocol !== "https:" && u.protocol !== "http:")
158
+ return null;
159
+ return `endpoint:${u.origin}`;
160
+ }
136
161
  export function authPath() {
137
162
  return join(kisoHome(), "auth.json");
138
163
  }
package/dist/chat.d.ts CHANGED
@@ -87,10 +87,9 @@ export declare function statedContextWindow(of?: {
87
87
  /** CW-1: a window as the displays write it — `1M`, `1.05M`, `272K`,
88
88
  * `1,048,576` (a figure that is not round is shown whole). */
89
89
  export declare function windowLabel(tokens: number): string;
90
- /** CW-1: the source, said. The short form rides a /model row; the long
91
- * form is /status's. Only the model step is marked in the short form —
92
- * it is the one figure no row states for this endpoint. */
93
- export declare function windowSourceNote(w: StatedWindow | null, form: "short" | "long"): string;
90
+ /** CW-1: the window and its source, said — /status's line (0.40.6: the
91
+ * short form that rode each /model row is gone with its caller). */
92
+ export declare function windowSourceNote(w: StatedWindow | null): string;
94
93
  export declare function contextWindowTokens(of?: {
95
94
  readonly model: string;
96
95
  readonly baseUrl?: string;
package/dist/chat.js CHANGED
@@ -122,15 +122,12 @@ export function windowLabel(tokens) {
122
122
  return `${tokens / 1000}K`;
123
123
  return tokens.toLocaleString("en-US");
124
124
  }
125
- /** CW-1: the source, said. The short form rides a /model row; the long
126
- * form is /status's. Only the model step is marked in the short form —
127
- * it is the one figure no row states for this endpoint. */
128
- export function windowSourceNote(w, form) {
125
+ /** CW-1: the window and its source, said — /status's line (0.40.6: the
126
+ * short form that rode each /model row is gone with its caller). */
127
+ export function windowSourceNote(w) {
129
128
  if (w === null)
130
- return form === "short" ? "ctx ?" : `window unknown — compaction assumes ${windowLabel(DEFAULT_CONTEXT_WINDOW)}; set contextWindow on the profile to state it`;
129
+ return `window unknown — compaction assumes ${windowLabel(DEFAULT_CONTEXT_WINDOW)}; set contextWindow on the profile to state it`;
131
130
  const size = windowLabel(w.tokens);
132
- if (form === "short")
133
- return w.source === "model" ? `ctx ${size} inferred` : `ctx ${size}`;
134
131
  switch (w.source) {
135
132
  case "set":
136
133
  return `window ${size}, as you set it`;
package/dist/config.js CHANGED
@@ -32,7 +32,7 @@ import { readFileSync, statSync } from "node:fs";
32
32
  import { homedir } from "node:os";
33
33
  import { join } from "node:path";
34
34
  import { kisoHome } from "./state.js";
35
- import { AuthError, getCredential, providerIdOf } from "./auth/credentials.js";
35
+ import { AuthError, effectiveBaseUrl, endpointCredentialId, getCredential, providerIdOf } from "./auth/credentials.js";
36
36
  import { BUILTIN_MANIFESTS } from "@vincemakes/kiso-runtime/internal";
37
37
  /** Header names a profile may not set: credentials (a key never lives in the
38
38
  * config file) and the framing the adapter owns. */
@@ -294,12 +294,34 @@ export function authForProfile(name, p) {
294
294
  if (providerId !== null && !manifestTakesKey(providerId)) {
295
295
  throw new ConfigError(`model ${name}: unavailable — not signed in: run \`kiso login ${providerId}\` (this provider takes no API key)`);
296
296
  }
297
+ // 0.40.6: a GATEWAY's own key, stored for exactly this profile's origin
298
+ // (`kiso login --endpoint <url>`). The rule a vendor credential follows:
299
+ // stored first, the env var only when nothing is stored — and it goes to
300
+ // that origin alone (credentials.ts endpointCredentialId).
301
+ const endpointId = providerId === null ? endpointCredentialId(effectiveBaseUrl(p.kind, p.baseUrl)) : null;
302
+ if (endpointId !== null) {
303
+ let stored;
304
+ try {
305
+ stored = getCredential(endpointId);
306
+ }
307
+ catch (err) {
308
+ if (err instanceof AuthError)
309
+ throw new ConfigError(`model ${name}: ${err.message}`);
310
+ throw err;
311
+ }
312
+ if (stored?.type === "api-key")
313
+ return { type: "api-key", apiKey: stored.key, source: "store" };
314
+ }
297
315
  if (p.apiKeyEnv === undefined)
298
316
  return { type: "api-key", apiKey: "none", source: "none" };
299
317
  const fromEnv = process.env[p.apiKeyEnv];
300
318
  if (fromEnv !== undefined)
301
319
  return { type: "api-key", apiKey: fromEnv, source: "env" };
302
- const hint = providerId !== null ? `run \`kiso login ${providerId}\` or set the env var ${p.apiKeyEnv}` : `set the env var ${p.apiKeyEnv}`;
320
+ const hint = providerId !== null
321
+ ? `run \`kiso login ${providerId}\` or set the env var ${p.apiKeyEnv}`
322
+ : endpointId !== null
323
+ ? `set the env var ${p.apiKeyEnv}, or run \`kiso login --endpoint ${endpointId.slice("endpoint:".length)}\``
324
+ : `set the env var ${p.apiKeyEnv}`;
303
325
  throw new ConfigError(`model ${name}: unavailable — no credential: ${hint} (configs never store keys, only the env-var name)`);
304
326
  }
305
327
  /** OR-2: does the provider's manifest admit an API key (or no auth at all)?
package/dist/dispatch.js CHANGED
@@ -13,6 +13,10 @@ import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelNa
13
13
  import { adapterOptionsFor } from "./auth/adapter-options.js";
14
14
  import { profileProviderLabel, providerLabel } from "./provider-label.js";
15
15
  import { installedVersion, versionStatusLine } from "./stale-version.js";
16
+ import { preferences, setPreference } from "./preferences.js";
17
+ import { settingsRows } from "./settings.js";
18
+ import { floorOn, settingsLayers } from "./state.js";
19
+ import { currentGround } from "@vincemakes/kiso-tui-cells/render";
16
20
  import { queuedSwitchLines } from "./state.js";
17
21
  import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner, statedContextWindow, windowSourceNote } from "./chat.js";
18
22
  import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
@@ -110,14 +114,6 @@ function signInNote(p) {
110
114
  }
111
115
  return p.apiKeyEnv ?? "no key";
112
116
  }
113
- /** CW-1 (owner, 2026-09-23): a profile's window on its /model row — the
114
- * profile's own stated figure first, then the registry's chain for its
115
- * model, endpoint and upstream; `inferred` marks the one no row states
116
- * for that endpoint. */
117
- function windowNoteOf(p) {
118
- const w = statedContextWindow({ model: p.model, ...(p.baseUrl !== undefined ? { baseUrl: p.baseUrl } : {}), ...(p.upstream !== undefined ? { upstream: p.upstream } : {}) }, { configured: resolveContextWindow(mergedConfig, p) });
119
- return windowSourceNote(w, "short");
120
- }
121
117
  /** The ONE dispatcher — slash commands, exit, and turns. The recovery
122
118
  * replay routes through it too — a queued "/last" must never become a
123
119
  * user turn (v2c: the rl lives in main, so lines arrive earlier and the
@@ -383,18 +379,15 @@ export function dispatch(line, ctx) {
383
379
  return;
384
380
  }
385
381
  if (trimmed === "\x14think") {
386
- // §2.3 — ctrl+t folds the committed thinking blocks, and folds them
387
- // back. DC-50's mechanism, not a second one: one boolean, then the
388
- // session is reprinted, so the blocks already on screen obey the
389
- // switch rather than only the next ones.
390
- //
391
- // The folded row is `foldThinking`'s, which is what the PIPE writes
392
- // — so thinking has two renderings in the product and not three,
393
- // and the pipe's byte-identity gate is this row's gate too.
394
- //
395
- // The live `thinking…` placeholder is NOT touched: it belongs to
396
- // the live region, and this switch is about committed blocks.
382
+ // §2.3 / 0.40.6 — ctrl+t hides thinking (one italic line per block,
383
+ // the open one included) and shows it again. DC-50's mechanism: one
384
+ // boolean, then the session is reprinted, so the blocks already on
385
+ // screen obey the switch. The choice is REMEMBERED (preferences.json)
386
+ // — the owner, 2026-09-23, after the reference implementation's
387
+ // persisted toggle; shown stays the default. The pipe keeps its own
388
+ // one-line fold whatever the choice.
397
389
  body.toggleThinking();
390
+ setPreference("thinking", body.thinkingHidden() ? "hidden" : "shown");
398
391
  ctx.input.prompt();
399
392
  return;
400
393
  }
@@ -487,6 +480,31 @@ export function dispatch(line, ctx) {
487
480
  });
488
481
  return;
489
482
  }
483
+ if (trimmed === "/settings") {
484
+ // 0.40.6 — what kiso runs with, each value's layer, and how to change
485
+ // it (settings.ts). Read-only: the config file stays the human's.
486
+ ctx.chainRef.current = ctx.chainRef.current.then(async () => {
487
+ const w = statedContextWindow();
488
+ const profile = currentProfileName;
489
+ bodyLog(settingsRows({
490
+ user: settingsLayers.user,
491
+ project: settingsLayers.project,
492
+ env: process.env,
493
+ ...(settingsLayers.modeFlag !== undefined ? { modeFlag: settingsLayers.modeFlag } : {}),
494
+ ...(settingsLayers.modelFlag !== undefined ? { modelFlag: settingsLayers.modelFlag } : {}),
495
+ mode: getMode(),
496
+ model: { label: `${agentModel}${providerLabel(agentBaseUrl, upstreamOf(agentBaseUrl))}${profile === null ? "" : ` · profile ${profile}`}`, profile, switched: settingsLayers.modelSwitched },
497
+ ground: currentGround(),
498
+ floorOn,
499
+ window: windowSourceNote(w).replace(/^window /, ""),
500
+ thinkingHidden: body.thinkingHidden(),
501
+ thinkingRemembered: preferences().thinking !== undefined,
502
+ version: versionStatusLine(installedVersion(), VERSION).replace(/^version /, ""),
503
+ }).join("\n\n"), "words");
504
+ ctx.input.prompt();
505
+ });
506
+ return;
507
+ }
490
508
  if (trimmed === "/status") {
491
509
  // B area: session id, durable event count, and the ~ context
492
510
  // estimate — all read straight from the live session, nothing
@@ -499,7 +517,7 @@ export function dispatch(line, ctx) {
499
517
  // CW-1: the percentage names its denominator and who stated it — a
500
518
  // window inferred from the model reads differently from one the
501
519
  // registry states for this endpoint.
502
- bodyLog(`ctx ${ctxPct} · ${windowSourceNote(statedContextWindow(), "long")}`);
520
+ bodyLog(`ctx ${ctxPct} · ${windowSourceNote(statedContextWindow())}`);
503
521
  // The owner, 2026-09-21: /status is where "what am I actually
504
522
  // running on" is answered, so the identity is spelled out HERE —
505
523
  // the model, the host the request goes to, the profile it came
@@ -622,9 +640,11 @@ export function dispatch(line, ctx) {
622
640
  // profile key is what `/model <name>` takes, not what a chooser
623
641
  // reading a list needs. What remains is what the chooser CANNOT
624
642
  // infer: availability, and which one is live.
625
- // CW-1: the window rides LAST — availability and the live mark
626
- // are what a narrow row must keep.
627
- const marks = [...(profileAvailable(profile) ? [] : ["unavailable"]), ...(name === currentProfileName ? ["current"] : []), windowNoteOf(profile)];
643
+ // 0.40.6 (the owner, 2026-09-23: "who told you to write ctx"):
644
+ // the window rode here in 0.40.3–0.40.5 and cut `unavailable`
645
+ // to `unavailabl…` on the owner's rows. It lives on /status and
646
+ // /settings, where there is room to say it and its source.
647
+ const marks = [...(profileAvailable(profile) ? [] : ["unavailable"]), ...(name === currentProfileName ? ["current"] : [])];
628
648
  const host = profileProviderLabel(profile.kind, profile.baseUrl, profile.upstream);
629
649
  return { label: `${profile.kind}/${profile.model}${host === "" ? "" : ` ${host}`}`, note: marks.join(" · "), ...effortAxis(profile) };
630
650
  }),
@@ -676,7 +696,7 @@ export function dispatch(line, ctx) {
676
696
  for (const name of names) {
677
697
  const p = configModels[name];
678
698
  const hostOf = profileProviderLabel(p.kind, p.baseUrl, p.upstream);
679
- bodyLog(` ${name} → ${p.kind}/${p.model}${hostOf === "" ? "" : ` ${hostOf}`} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)} · ${windowNoteOf(p)}`);
699
+ bodyLog(` ${name} → ${p.kind}/${p.model}${hostOf === "" ? "" : ` ${hostOf}`} · ${signInNote(p)} ${profileAvailable(p) ? "(available)" : "(unavailable)"} · ${effortNote(p)}`);
680
700
  }
681
701
  }
682
702
  bodyLog("switch with /model <profile-name|provider/model>");
@@ -773,6 +793,7 @@ export function dispatch(line, ctx) {
773
793
  setAgentModel(profile.model, profile.baseUrl);
774
794
  setCurrentModelName(arg);
775
795
  setCurrentProfileName(direct === null ? null : profName);
796
+ settingsLayers.modelSwitched = true;
776
797
  // §2.5: the ONE source a reload reads for the model — a
777
798
  // switch made here must survive the rebuild.
778
799
  setModelChoice(arg);
package/dist/index.js CHANGED
@@ -53,6 +53,8 @@ import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
53
53
  import { fauxSkip, readFauxScript } from "./faux-glue.js";
54
54
  import { chat, compactionDiscardedNotice, contextWindowTokens, displayCtxRatio, knownContextWindow, microcompactThresholdFor, statusModelLabel, unknownWindowNotice, windowLearnedNotice } from "./chat.js";
55
55
  import { recordLearnedWindow, useLearnedWindows } from "./learned-windows.js";
56
+ import { preferences, usePreferences } from "./preferences.js";
57
+ import { settingsLayers } from "./state.js";
56
58
  import { providerHost } from "./provider-label.js";
57
59
  import { adapterOptionsFor } from "./auth/adapter-options.js";
58
60
  import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
@@ -759,6 +761,10 @@ async function makeAgent(sessionId, input, modelFlag) {
759
761
  const userCfg = loadUserConfig();
760
762
  const projectCfg = loadProjectConfig(process.cwd(), project !== null);
761
763
  const merged = mergeConfigs(userCfg, projectCfg);
764
+ // 0.40.6: /settings names each value's layer from these
765
+ settingsLayers.user = userCfg;
766
+ settingsLayers.project = projectCfg;
767
+ settingsLayers.modelFlag = modelFlag;
762
768
  const secretEnvNames = secretEnvNamesOf(merged.models ?? {});
763
769
  const builtIn = await builtInLayer(user, proj, input !== undefined && process.stdin.isTTY ? askUi(input) : undefined, secretEnvNames);
764
770
  setExtensionLists(builtIn, user, proj, [...builtIn, ...user, ...proj]);
@@ -1643,6 +1649,7 @@ async function main() {
1643
1649
  process.exit(2);
1644
1650
  }
1645
1651
  setMode(m);
1652
+ settingsLayers.modeFlag = m;
1646
1653
  args.splice(modeFlag, 2);
1647
1654
  }
1648
1655
  else {
@@ -1790,6 +1797,10 @@ async function main() {
1790
1797
  editCol: () => dock.editCol(),
1791
1798
  onDock: () => dock.redraw(), // v2d-B: the freeze scrolls the dock up — re-pin it
1792
1799
  }));
1800
+ // 0.40.6: the choices kiso remembers (preferences.json) — today ctrl+t's
1801
+ // thinking display, restored before the first block can be drawn.
1802
+ usePreferences();
1803
+ body.setThinkingHidden(preferences().thinking === "hidden");
1793
1804
  // REL-0152-D11: pasting an image sends no bytes, so an empty paste is
1794
1805
  // the signal to go and look at the clipboard. What comes back is a
1795
1806
  // PATH, which the turn's attachment scan then picks up exactly as it
@@ -2001,8 +2012,33 @@ async function main() {
2001
2012
  // `login <provider>` reads the key from a hidden prompt on a TTY
2002
2013
  // or from stdin otherwise (never from an argument — shell history);
2003
2014
  // `logout <provider>` deletes; `auth` lists, keys masked.
2004
- const { KNOWN_PROVIDERS, OAUTH_PROVIDERS, authPath, deleteCredential, maskSecret, readAuthFile, setCredential } = await import("./auth/credentials.js");
2015
+ const { KNOWN_PROVIDERS, OAUTH_PROVIDERS, authPath, deleteCredential, endpointCredentialId, maskSecret, providerIdOf, readAuthFile, setCredential } = await import("./auth/credentials.js");
2005
2016
  const provider = arg;
2017
+ if (provider === "--endpoint" && command !== "auth") {
2018
+ // 0.40.6: a gateway's own key, stored for its ORIGIN and sent
2019
+ // there alone (credentials.ts endpointCredentialId) — so a
2020
+ // gateway profile starts from plain `kiso`, no env var on the
2021
+ // command line. The key still never comes from an argument.
2022
+ const url = args[2];
2023
+ const id = endpointCredentialId(url);
2024
+ if (id === null)
2025
+ throw new CliUsageError(`kiso ${command} --endpoint <url> — an http(s) URL, e.g. https://gateway.example/v1`);
2026
+ const origin = id.slice("endpoint:".length);
2027
+ const vendor = ["openai-compat", "anthropic", "openai-responses"].map((k) => providerIdOf(k, url)).find((v) => v !== null);
2028
+ if (vendor !== undefined && vendor !== null)
2029
+ throw new CliUsageError(`${origin} is ${vendor}'s own endpoint — run \`kiso ${command} ${vendor}\``);
2030
+ if (command === "logout") {
2031
+ const had = deleteCredential(id);
2032
+ process.stdout.write(had ? `removed the key for ${origin}\n` : `nothing stored for ${origin}\n`);
2033
+ break;
2034
+ }
2035
+ const key = (await readSecret(`API key for ${origin}: `)).trim();
2036
+ if (key === "")
2037
+ throw new CliUsageError(`kiso login --endpoint ${origin}: no key given`);
2038
+ setCredential(id, { type: "api-key", key, savedAt: Date.now() });
2039
+ process.stdout.write(`stored an API key for ${origin} (${maskSecret(key)}) in ${authPath()} — sent to that origin only; every profile whose baseUrl is on it uses this key before its env var\n`);
2040
+ break;
2041
+ }
2006
2042
  if (command === "auth") {
2007
2043
  const file = readAuthFile();
2008
2044
  const rows = Object.entries(file.credentials);
@@ -2019,7 +2055,7 @@ async function main() {
2019
2055
  break;
2020
2056
  }
2021
2057
  if (provider === undefined || !KNOWN_PROVIDERS.includes(provider)) {
2022
- throw new CliUsageError(`kiso ${command} <provider> — one of: ${KNOWN_PROVIDERS.join(", ")}`);
2058
+ throw new CliUsageError(`kiso ${command} <provider> — one of: ${KNOWN_PROVIDERS.join(", ")}; or kiso ${command} --endpoint <url> for a gateway`);
2023
2059
  }
2024
2060
  if (command === "logout") {
2025
2061
  const had = deleteCredential(provider);
@@ -2096,7 +2132,8 @@ async function main() {
2096
2132
  " kiso sessions [--all|--current] list durable sessions (a terminal shows this workspace's by default)\n" +
2097
2133
  " kiso login <provider> anthropic|openai|deepseek|zai: store an API key (hidden prompt, or stdin when piped);\n" +
2098
2134
  " chatgpt: sign in with a ChatGPT subscription (browser; unofficial third-party flow)\n" +
2099
- " kiso logout <provider> remove the stored credential\n" +
2135
+ " kiso login --endpoint <url> a gateway: store its API key for that URL's origin only\n" +
2136
+ " kiso logout <provider> remove the stored credential (or --endpoint <url>)\n" +
2100
2137
  " kiso auth list stored credentials (keys masked)\n" +
2101
2138
  " kiso update install the latest release (npm i -g @vincemakes/kiso-code@latest)\n" +
2102
2139
  " kiso help this help\n\n" +
@@ -2109,7 +2146,7 @@ async function main() {
2109
2146
  " OPENAI_API_KEY OpenAI-compatible (OPENAI_MODEL, default gpt-4o;\n" +
2110
2147
  " OPENAI_BASE_URL for DeepSeek/compat endpoints) — checked first\n" +
2111
2148
  " ANTHROPIC_API_KEY Anthropic (ANTHROPIC_MODEL, default claude-sonnet-5)\n" +
2112
- " ~/.kiso/config.json named model profiles (keys stay in env vars; see the README)\n");
2149
+ " ~/.kiso/config.json named model profiles (keys stay in env vars or kiso login; see the README)\n");
2113
2150
  break;
2114
2151
  }
2115
2152
  case undefined:
@@ -0,0 +1,14 @@
1
+ export interface Preferences {
2
+ /** ctrl+t: the thinking blocks as one italic line each. Absent = shown. */
3
+ readonly thinking?: "shown" | "hidden";
4
+ }
5
+ export declare function preferencesPath(home?: string): string;
6
+ /** Startup: read the file, and keep choices made from here on in it. */
7
+ export declare function usePreferences(path?: string): void;
8
+ /** The remembered choices; empty before startup names the file. */
9
+ export declare function preferences(): Preferences;
10
+ /** Remember a choice. Returns false when no file was named. The file is
11
+ * re-read before the write, so another process's choice is kept. */
12
+ export declare function setPreference<K extends keyof Preferences>(key: K, value: NonNullable<Preferences[K]>): boolean;
13
+ /** Tests: back to the state before startup. */
14
+ export declare function resetPreferences(): void;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 0.40.6 — the choices kiso remembers for you, in
3
+ * `<KISO_HOME>/preferences.json`.
4
+ *
5
+ * `config.json` is the human's file: kiso reads it and never writes it. A
6
+ * choice made with a key — ctrl+t's thinking display, today the only one —
7
+ * has to survive a restart, so it lands here instead: kiso-owned, private
8
+ * (0600, tmp + rename), and read back at start-up. A missing or unreadable
9
+ * file is an empty one; an unknown value is ignored rather than guessed.
10
+ *
11
+ * Nothing is read until the CLI's startup names the file
12
+ * (`usePreferences`) — the same rule as learned-windows.ts, so in-process
13
+ * tests never read a home directory.
14
+ */
15
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ import { kisoHome } from "./state.js";
18
+ let prefs = null;
19
+ let prefsPath = null;
20
+ export function preferencesPath(home = kisoHome()) {
21
+ return join(home, "preferences.json");
22
+ }
23
+ function read(path) {
24
+ try {
25
+ const raw = JSON.parse(readFileSync(path, "utf8"));
26
+ return raw.thinking === "shown" || raw.thinking === "hidden" ? { thinking: raw.thinking } : {};
27
+ }
28
+ catch {
29
+ return {};
30
+ }
31
+ }
32
+ /** Startup: read the file, and keep choices made from here on in it. */
33
+ export function usePreferences(path = preferencesPath()) {
34
+ prefsPath = path;
35
+ prefs = read(path);
36
+ }
37
+ /** The remembered choices; empty before startup names the file. */
38
+ export function preferences() {
39
+ return prefs ?? {};
40
+ }
41
+ /** Remember a choice. Returns false when no file was named. The file is
42
+ * re-read before the write, so another process's choice is kept. */
43
+ export function setPreference(key, value) {
44
+ const path = prefsPath;
45
+ if (path === null)
46
+ return false;
47
+ const next = { ...read(path), [key]: value };
48
+ prefs = next;
49
+ try {
50
+ mkdirSync(dirname(path), { recursive: true });
51
+ const tmp = `${path}.${process.pid}.tmp`;
52
+ writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
53
+ renameSync(tmp, path);
54
+ }
55
+ catch {
56
+ // an unwritable home: the choice still holds for this process
57
+ }
58
+ return true;
59
+ }
60
+ /** Tests: back to the state before startup. */
61
+ export function resetPreferences() {
62
+ prefs = null;
63
+ prefsPath = null;
64
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 0.40.6 — `/settings`: what kiso is running with, where each value came
3
+ * from, and how to change it.
4
+ *
5
+ * The owner, 2026-09-23, asked whether kiso has a help for what can be
6
+ * configured. `/help` lists the commands and `?` the keys; the settings
7
+ * lived only in docs/configuration.md and in files the user had to open.
8
+ *
9
+ * READ-ONLY by design: the config file stays the human's. The layers are
10
+ * the documented precedence — flag > env > project config > user config >
11
+ * default — and a value that no layer explains was set in this session
12
+ * (`/mode`, `/model`). Pure: every input is passed in, so each row is
13
+ * testable without a session.
14
+ */
15
+ import type { KisoConfig } from "./config.js";
16
+ export interface SettingsInput {
17
+ readonly user: KisoConfig | null;
18
+ readonly project: KisoConfig | null;
19
+ readonly env: Readonly<Record<string, string | undefined>>;
20
+ /** the --mode / --model values on the command line, when given */
21
+ readonly modeFlag?: string;
22
+ readonly modelFlag?: string;
23
+ /** what is live now */
24
+ readonly mode: string;
25
+ readonly model: {
26
+ readonly label: string;
27
+ readonly profile: string | null;
28
+ readonly switched: boolean;
29
+ };
30
+ readonly ground: string;
31
+ readonly floorOn: boolean;
32
+ readonly window: string;
33
+ readonly thinkingHidden: boolean;
34
+ readonly thinkingRemembered: boolean;
35
+ readonly version: string;
36
+ }
37
+ export declare function settingsRows(i: SettingsInput): string[];
@@ -0,0 +1,79 @@
1
+ function modeRow(i) {
2
+ const change = "/mode or shift+tab; \"mode\" in ~/.kiso/config.json";
3
+ const from = i.modeFlag === i.mode
4
+ ? "--mode"
5
+ : i.env.KISO_MODE === i.mode
6
+ ? "env KISO_MODE"
7
+ : i.project?.mode === i.mode
8
+ ? "project config"
9
+ : i.user?.mode === i.mode
10
+ ? "user config"
11
+ : i.mode === "default"
12
+ ? "default"
13
+ : "set in this session";
14
+ return { name: "mode", value: i.mode, from, change };
15
+ }
16
+ function modelRow(i) {
17
+ const change = "/model; \"model\" in ~/.kiso/config.json";
18
+ const p = i.model.profile;
19
+ const from = i.model.switched
20
+ ? "set in this session"
21
+ : i.modelFlag !== undefined
22
+ ? "--model"
23
+ : p !== null && i.project?.model === p
24
+ ? "project config"
25
+ : p !== null && i.user?.model === p
26
+ ? "user config"
27
+ : i.env.OPENAI_API_KEY !== undefined && p === null
28
+ ? "env OPENAI_API_KEY"
29
+ : "default";
30
+ return { name: "model", value: i.model.label, from, change };
31
+ }
32
+ function themeRow(i) {
33
+ const change = "\"theme\": \"dark\" | \"light\" in ~/.kiso/config.json; KISO_THEME for one run";
34
+ if (i.env.KISO_THEME !== undefined)
35
+ return { name: "theme", value: i.env.KISO_THEME, from: "env KISO_THEME", change };
36
+ if (i.user?.theme !== undefined)
37
+ return { name: "theme", value: i.user.theme, from: "user config", change };
38
+ return { name: "theme", value: i.ground, from: "the terminal (detected)", change };
39
+ }
40
+ function layered(i, key) {
41
+ if (i.project?.[key] !== undefined)
42
+ return { value: i.project[key], from: "project config" };
43
+ if (i.user?.[key] !== undefined)
44
+ return { value: i.user[key], from: "user config" };
45
+ return { value: undefined, from: "default" };
46
+ }
47
+ export function settingsRows(i) {
48
+ const trust = layered(i, "projectTrust");
49
+ const auto = i.env.KISO_AUTO_COMPACT !== undefined ? { value: i.env.KISO_AUTO_COMPACT, from: "env KISO_AUTO_COMPACT" } : layered(i, "autoCompact");
50
+ const rows = [
51
+ modelRow(i),
52
+ modeRow(i),
53
+ themeRow(i),
54
+ {
55
+ name: "floor",
56
+ value: i.floorOn ? "on — irrecoverable deletes are refused in every mode" : "off",
57
+ from: i.user?.floor !== undefined ? "user config" : "default",
58
+ change: "\"floor\": \"off\" in ~/.kiso/config.json (user config only)",
59
+ },
60
+ { name: "window", value: i.window, from: "the window chain (see /status)", change: "\"contextWindow\" on the profile; KISO_CONTEXT_WINDOW for one run" },
61
+ { name: "compaction", value: "in-run: past half the window at a phase end, past 80% at once", from: "built in", change: "/compact on demand" },
62
+ {
63
+ name: "auto-compact",
64
+ value: auto.value === undefined ? "off" : typeof auto.value === "object" ? `at ${auto.value.thresholdRatio} of the window, at a run's start` : String(auto.value),
65
+ from: auto.from,
66
+ change: "\"autoCompact\" in config; KISO_AUTO_COMPACT",
67
+ },
68
+ { name: "project trust", value: trust.value ?? "ask", from: trust.from, change: "\"projectTrust\": \"ask\" | \"never\" in config" },
69
+ {
70
+ name: "thinking",
71
+ value: i.thinkingHidden ? "hidden — one line per block" : "shown",
72
+ from: i.thinkingRemembered ? "ctrl+t (remembered)" : "default",
73
+ change: "ctrl+t",
74
+ },
75
+ { name: "version", value: i.version, from: "running", change: "kiso update" },
76
+ ];
77
+ const width = Math.max(...rows.map((r) => r.name.length)) + 2;
78
+ return rows.map((r) => `${r.name.padEnd(width)}${r.value}\n${" ".repeat(width)}from ${r.from} · change: ${r.change}`);
79
+ }
package/dist/state.d.ts CHANGED
@@ -295,6 +295,16 @@ export declare const queuedSwitchLines: string[];
295
295
  /** E1: the extensions loaded by makeAgent — their names feed the banner. */
296
296
  /** 0.40.0: whether the catastrophe floor is on (floor.ts) — the user
297
297
  * config's `floor`, read where the chain is assembled. */
298
+ /** 0.40.6: what /settings needs to name each value's layer — the two config
299
+ * files as read, the command-line flags as given, and whether /model
300
+ * changed the binding in this session. */
301
+ export declare const settingsLayers: {
302
+ user: import("./config.js").KisoConfig | null;
303
+ project: import("./config.js").KisoConfig | null;
304
+ modeFlag: string | undefined;
305
+ modelFlag: string | undefined;
306
+ modelSwitched: boolean;
307
+ };
298
308
  export declare let floorOn: boolean;
299
309
  export declare function setFloorOn(value: boolean): void;
300
310
  /** 0.40.0: the calls a saved allow never carries (protected-writes.ts) —
package/dist/state.js CHANGED
@@ -397,6 +397,10 @@ export const queuedSwitchLines = [];
397
397
  /** E1: the extensions loaded by makeAgent — their names feed the banner. */
398
398
  /** 0.40.0: whether the catastrophe floor is on (floor.ts) — the user
399
399
  * config's `floor`, read where the chain is assembled. */
400
+ /** 0.40.6: what /settings needs to name each value's layer — the two config
401
+ * files as read, the command-line flags as given, and whether /model
402
+ * changed the binding in this session. */
403
+ export const settingsLayers = { user: null, project: null, modeFlag: undefined, modelFlag: undefined, modelSwitched: false };
400
404
  export let floorOn = true;
401
405
  export function setFloorOn(value) {
402
406
  floorOn = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.40.5",
3
+ "version": "0.40.6",
4
4
  "description": "kiso-code \u2014 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",
@@ -18,20 +18,20 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-ask-ext": "0.40.5",
22
- "@vincemakes/kiso-core": "0.40.5",
23
- "@vincemakes/kiso-evals": "0.40.5",
24
- "@vincemakes/kiso-mcp-ext": "0.40.5",
25
- "@vincemakes/kiso-provider-anthropic": "0.40.5",
26
- "@vincemakes/kiso-provider-openai": "0.40.5",
27
- "@vincemakes/kiso-provider-openai-responses": "0.40.5",
28
- "@vincemakes/kiso-runtime": "0.40.5",
29
- "@vincemakes/kiso-skills-ext": "0.40.5",
30
- "@vincemakes/kiso-subagent-ext": "0.40.5",
31
- "@vincemakes/kiso-task-ext": "0.40.5",
32
- "@vincemakes/kiso-tools-node": "0.40.5",
33
- "@vincemakes/kiso-tui": "0.40.5",
34
- "@vincemakes/kiso-tui-cells": "0.40.5"
21
+ "@vincemakes/kiso-ask-ext": "0.40.6",
22
+ "@vincemakes/kiso-core": "0.40.6",
23
+ "@vincemakes/kiso-evals": "0.40.6",
24
+ "@vincemakes/kiso-mcp-ext": "0.40.6",
25
+ "@vincemakes/kiso-provider-anthropic": "0.40.6",
26
+ "@vincemakes/kiso-provider-openai": "0.40.6",
27
+ "@vincemakes/kiso-provider-openai-responses": "0.40.6",
28
+ "@vincemakes/kiso-runtime": "0.40.6",
29
+ "@vincemakes/kiso-skills-ext": "0.40.6",
30
+ "@vincemakes/kiso-subagent-ext": "0.40.6",
31
+ "@vincemakes/kiso-task-ext": "0.40.6",
32
+ "@vincemakes/kiso-tools-node": "0.40.6",
33
+ "@vincemakes/kiso-tui": "0.40.6",
34
+ "@vincemakes/kiso-tui-cells": "0.40.6"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^26.1.2",