@vincemakes/kiso-code 0.40.4 → 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
@@ -14,8 +14,9 @@ import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
14
14
  import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
15
15
  import { dispatch, abortBangCommand } from "./dispatch.js";
16
16
  import { paintWindowTitle } from "./window-title.js";
17
- import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock, retryOnRow, retryShown, setRetryShown, floorOn, protectedFiles, upstreamOf } from "./state.js";
17
+ import { agentBaseUrl, agentModel, body, bodyLog, configuredWindow, dock, retryOnRow, retryShown, setRetryShown, floorOn, protectedFiles, upstreamOf, VERSION } from "./state.js";
18
18
  import { attachImages } from "./attachments.js";
19
+ import { installedVersion, staleVersionNotice } from "./stale-version.js";
19
20
  import { learnedWindowFor } from "./learned-windows.js";
20
21
  import { lookupContextWindow, lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
21
22
  import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
@@ -121,15 +122,12 @@ export function windowLabel(tokens) {
121
122
  return `${tokens / 1000}K`;
122
123
  return tokens.toLocaleString("en-US");
123
124
  }
124
- /** CW-1: the source, said. The short form rides a /model row; the long
125
- * form is /status's. Only the model step is marked in the short form —
126
- * it is the one figure no row states for this endpoint. */
127
- 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) {
128
128
  if (w === null)
129
- 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`;
130
130
  const size = windowLabel(w.tokens);
131
- if (form === "short")
132
- return w.source === "model" ? `ctx ${size} inferred` : `ctx ${size}`;
133
131
  switch (w.source) {
134
132
  case "set":
135
133
  return `window ${size}, as you set it`;
@@ -1365,6 +1363,12 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
1365
1363
  stopSpinner();
1366
1364
  paintIdle();
1367
1365
  currentRun = null;
1366
+ // 0.40.5: the kiso on disk may no longer be the one this
1367
+ // session runs (an upgrade since it started) — said once per
1368
+ // installed version (stale-version.ts).
1369
+ const stale = staleVersionNotice(installedVersion(), VERSION, session.id);
1370
+ if (stale !== null)
1371
+ body.notice(stale);
1368
1372
  // round 8: a faux script that ran out of declared turns exits
1369
1373
  // loudly with a non-zero status — never a silent status 0.
1370
1374
  // round 4 (adversarial): the exhaustion is a CONTROLLED rejection of
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
@@ -9,9 +9,14 @@ import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveRea
9
9
  import { MODES, MODE_NOTE, OFFERED_MODES, getMode, setMode } from "./mode.js";
10
10
  import { clipboardWrite, lastAnswer } from "./clipboard.js";
11
11
  import { protectedBangReason, protectedShellVerdict } from "./protected-shell.js";
12
- import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, setLastBinding } from "./state.js";
12
+ import { agentBaseUrl, currentProfileName, setCurrentProfileName, currentModelName, agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, upstreamOf, VERSION, setLastBinding } from "./state.js";
13
13
  import { adapterOptionsFor } from "./auth/adapter-options.js";
14
14
  import { profileProviderLabel, providerLabel } from "./provider-label.js";
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";
15
20
  import { queuedSwitchLines } from "./state.js";
16
21
  import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner, statedContextWindow, windowSourceNote } from "./chat.js";
17
22
  import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
@@ -109,14 +114,6 @@ function signInNote(p) {
109
114
  }
110
115
  return p.apiKeyEnv ?? "no key";
111
116
  }
112
- /** CW-1 (owner, 2026-09-23): a profile's window on its /model row — the
113
- * profile's own stated figure first, then the registry's chain for its
114
- * model, endpoint and upstream; `inferred` marks the one no row states
115
- * for that endpoint. */
116
- function windowNoteOf(p) {
117
- const w = statedContextWindow({ model: p.model, ...(p.baseUrl !== undefined ? { baseUrl: p.baseUrl } : {}), ...(p.upstream !== undefined ? { upstream: p.upstream } : {}) }, { configured: resolveContextWindow(mergedConfig, p) });
118
- return windowSourceNote(w, "short");
119
- }
120
117
  /** The ONE dispatcher — slash commands, exit, and turns. The recovery
121
118
  * replay routes through it too — a queued "/last" must never become a
122
119
  * user turn (v2c: the rl lives in main, so lines arrive earlier and the
@@ -382,18 +379,15 @@ export function dispatch(line, ctx) {
382
379
  return;
383
380
  }
384
381
  if (trimmed === "\x14think") {
385
- // §2.3 — ctrl+t folds the committed thinking blocks, and folds them
386
- // back. DC-50's mechanism, not a second one: one boolean, then the
387
- // session is reprinted, so the blocks already on screen obey the
388
- // switch rather than only the next ones.
389
- //
390
- // The folded row is `foldThinking`'s, which is what the PIPE writes
391
- // — so thinking has two renderings in the product and not three,
392
- // and the pipe's byte-identity gate is this row's gate too.
393
- //
394
- // The live `thinking…` placeholder is NOT touched: it belongs to
395
- // 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.
396
389
  body.toggleThinking();
390
+ setPreference("thinking", body.thinkingHidden() ? "hidden" : "shown");
397
391
  ctx.input.prompt();
398
392
  return;
399
393
  }
@@ -486,6 +480,31 @@ export function dispatch(line, ctx) {
486
480
  });
487
481
  return;
488
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
+ }
489
508
  if (trimmed === "/status") {
490
509
  // B area: session id, durable event count, and the ~ context
491
510
  // estimate — all read straight from the live session, nothing
@@ -498,7 +517,7 @@ export function dispatch(line, ctx) {
498
517
  // CW-1: the percentage names its denominator and who stated it — a
499
518
  // window inferred from the model reads differently from one the
500
519
  // registry states for this endpoint.
501
- bodyLog(`ctx ${ctxPct} · ${windowSourceNote(statedContextWindow(), "long")}`);
520
+ bodyLog(`ctx ${ctxPct} · ${windowSourceNote(statedContextWindow())}`);
502
521
  // The owner, 2026-09-21: /status is where "what am I actually
503
522
  // running on" is answered, so the identity is spelled out HERE —
504
523
  // the model, the host the request goes to, the profile it came
@@ -514,6 +533,9 @@ export function dispatch(line, ctx) {
514
533
  // variable; until then, a label that cannot lie.)
515
534
  const profileOf = currentProfileName === null ? "" : ` · profile ${currentProfileName}`;
516
535
  bodyLog(`model ${agentModel}${providerLabel(agentBaseUrl, upstreamOf(agentBaseUrl))}${profileOf}`);
536
+ // 0.40.5: the version this session RUNS, and the installed one when
537
+ // an upgrade (or a rollback) since start-up made them differ.
538
+ bodyLog(versionStatusLine(installedVersion(), VERSION));
517
539
  ctx.input.prompt();
518
540
  });
519
541
  return;
@@ -618,9 +640,11 @@ export function dispatch(line, ctx) {
618
640
  // profile key is what `/model <name>` takes, not what a chooser
619
641
  // reading a list needs. What remains is what the chooser CANNOT
620
642
  // infer: availability, and which one is live.
621
- // CW-1: the window rides LAST — availability and the live mark
622
- // are what a narrow row must keep.
623
- 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"] : [])];
624
648
  const host = profileProviderLabel(profile.kind, profile.baseUrl, profile.upstream);
625
649
  return { label: `${profile.kind}/${profile.model}${host === "" ? "" : ` ${host}`}`, note: marks.join(" · "), ...effortAxis(profile) };
626
650
  }),
@@ -672,7 +696,7 @@ export function dispatch(line, ctx) {
672
696
  for (const name of names) {
673
697
  const p = configModels[name];
674
698
  const hostOf = profileProviderLabel(p.kind, p.baseUrl, p.upstream);
675
- 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)}`);
676
700
  }
677
701
  }
678
702
  bodyLog("switch with /model <profile-name|provider/model>");
@@ -769,6 +793,7 @@ export function dispatch(line, ctx) {
769
793
  setAgentModel(profile.model, profile.baseUrl);
770
794
  setCurrentModelName(arg);
771
795
  setCurrentProfileName(direct === null ? null : profName);
796
+ settingsLayers.modelSwitched = true;
772
797
  // §2.5: the ONE source a reload reads for the model — a
773
798
  // switch made here must survive the rebuild.
774
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
+ }
@@ -0,0 +1,14 @@
1
+ /** The version of the kiso installed where this one was loaded from, read
2
+ * now — null on any failure, never a guess. KISO_INSTALLED_PACKAGE_JSON
3
+ * names another file: the test rigs' knob (like KISO_FAUX_SCRIPT), so an
4
+ * end-to-end run can "upgrade" underneath a live session without touching
5
+ * the checkout's own package.json. */
6
+ export declare function installedVersion(pkgPath?: string): string | null;
7
+ /** The transcript notice, once per installed version that differs from the
8
+ * running one (a newer install, or a rollback); null otherwise. */
9
+ export declare function staleVersionNotice(installed: string | null, running: string, sessionId: string): string | null;
10
+ /** /status's version line: the running version, and the installed one when
11
+ * they differ. */
12
+ export declare function versionStatusLine(installed: string | null, running: string): string;
13
+ /** Tests: forget which version was noticed. */
14
+ export declare function resetStaleVersionNotice(): void;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * 0.40.5 — a session says when the kiso on disk is not the kiso it runs.
3
+ *
4
+ * The owner, 2026-09-23: "I am on 0.40.3 — why does edit_file still fail?"
5
+ * The session's process had started at 13:03 on 0.40.2; 0.40.3 was
6
+ * installed at ~14:05. The process kept the code it loaded, so its
7
+ * edit_file still refused overlapping hunks, while `kiso --version` in
8
+ * another shell printed the version ON DISK. Nothing in the session said
9
+ * otherwise. A process that loads a module lazily after an upgrade (a
10
+ * provider on its first /model switch) would also mix the new version with
11
+ * the old — the notice is the cue to restart before that.
12
+ *
13
+ * `VERSION` (state.ts) is read once at start-up from the package.json next
14
+ * to the build; a global install replaces that file, so reading it AGAIN
15
+ * sees what a new start would run. One small read per turn and per
16
+ * /status; no timer, no watcher, no network.
17
+ */
18
+ import { readFileSync } from "node:fs";
19
+ import { dirname, join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ /** The version of the kiso installed where this one was loaded from, read
22
+ * now — null on any failure, never a guess. KISO_INSTALLED_PACKAGE_JSON
23
+ * names another file: the test rigs' knob (like KISO_FAUX_SCRIPT), so an
24
+ * end-to-end run can "upgrade" underneath a live session without touching
25
+ * the checkout's own package.json. */
26
+ export function installedVersion(pkgPath = process.env.KISO_INSTALLED_PACKAGE_JSON ?? join(dirname(fileURLToPath(import.meta.url)), "..", "package.json")) {
27
+ try {
28
+ const v = JSON.parse(readFileSync(pkgPath, "utf8")).version;
29
+ return typeof v === "string" && v !== "" ? v : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ let lastNoticed = null;
36
+ /** The transcript notice, once per installed version that differs from the
37
+ * running one (a newer install, or a rollback); null otherwise. */
38
+ export function staleVersionNotice(installed, running, sessionId) {
39
+ if (installed === null || installed === running || running === "?" || installed === lastNoticed)
40
+ return null;
41
+ lastNoticed = installed;
42
+ return `✦ kiso ${installed} is installed — this session runs ${running}; exit and resume it (kiso resume ${sessionId}) to use it`;
43
+ }
44
+ /** /status's version line: the running version, and the installed one when
45
+ * they differ. */
46
+ export function versionStatusLine(installed, running) {
47
+ return installed === null || installed === running ? `version ${running}` : `version ${running} (running) · ${installed} installed — restart to use it`;
48
+ }
49
+ /** Tests: forget which version was noticed. */
50
+ export function resetStaleVersionNotice() {
51
+ lastNoticed = null;
52
+ }
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.4",
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.4",
22
- "@vincemakes/kiso-core": "0.40.4",
23
- "@vincemakes/kiso-evals": "0.40.4",
24
- "@vincemakes/kiso-mcp-ext": "0.40.4",
25
- "@vincemakes/kiso-provider-anthropic": "0.40.4",
26
- "@vincemakes/kiso-provider-openai": "0.40.4",
27
- "@vincemakes/kiso-provider-openai-responses": "0.40.4",
28
- "@vincemakes/kiso-runtime": "0.40.4",
29
- "@vincemakes/kiso-skills-ext": "0.40.4",
30
- "@vincemakes/kiso-subagent-ext": "0.40.4",
31
- "@vincemakes/kiso-task-ext": "0.40.4",
32
- "@vincemakes/kiso-tools-node": "0.40.4",
33
- "@vincemakes/kiso-tui": "0.40.4",
34
- "@vincemakes/kiso-tui-cells": "0.40.4"
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",