@vincemakes/kiso-code 0.31.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/oauth/chatgpt.js +6 -0
- package/dist/auth/oauth/index.d.ts +5 -0
- package/dist/breaker.d.ts +57 -0
- package/dist/breaker.js +119 -0
- package/dist/chat.d.ts +30 -0
- package/dist/chat.js +112 -5
- package/dist/config.d.ts +3 -0
- package/dist/config.js +20 -0
- package/dist/dispatch.d.ts +24 -4
- package/dist/dispatch.js +257 -6
- package/dist/external-editor.d.ts +30 -0
- package/dist/external-editor.js +55 -0
- package/dist/index.js +303 -34
- package/dist/state.d.ts +35 -0
- package/dist/state.js +27 -0
- package/dist/trust-ui.js +111 -18
- package/dist/update-check.d.ts +9 -3
- package/dist/update-check.js +27 -28
- package/package.json +16 -16
- package/dist/diff.d.ts +0 -32
- package/dist/diff.js +0 -122
package/dist/index.js
CHANGED
|
@@ -24,25 +24,27 @@
|
|
|
24
24
|
* makeAgent, and main.
|
|
25
25
|
*/
|
|
26
26
|
import { appendFileSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
27
28
|
import { newSessionId } from "./session-id.js";
|
|
28
29
|
import { createInterface } from "node:readline";
|
|
29
30
|
import { Readable } from "node:stream";
|
|
30
31
|
import { fileURLToPath } from "node:url";
|
|
31
32
|
import { join } from "node:path";
|
|
32
|
-
import { Body, Editor, bannerLines, currentGround, resolveGround, setGround, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListRow } from "@vincemakes/kiso-tui";
|
|
33
|
+
import { Body, Editor, PROMPT, bannerLines, currentGround, resolveGround, setGround, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListRow } from "@vincemakes/kiso-tui";
|
|
33
34
|
import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions, SessionStore, } from "@vincemakes/kiso-runtime";
|
|
34
35
|
import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
35
36
|
import { createCodingTools } from "@vincemakes/kiso-tools-node";
|
|
36
37
|
import { MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
|
|
38
|
+
import { breakerExtension } from "./breaker.js";
|
|
37
39
|
import { builtInLayer } from "./builtin.js";
|
|
38
|
-
import { agentModel, atFiles, body, bodyLog, kisoHome, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, setSessionStore, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift } from "./state.js";
|
|
40
|
+
import { agentModel, atFiles, body, bodyLog, codingToolOptions, kisoHome, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, setModelChoice, setSessionStore, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift } from "./state.js";
|
|
39
41
|
import { askUi, resolveProjectTrust } from "./trust-ui.js";
|
|
40
42
|
import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
41
43
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
42
|
-
import { chat, contextWindowTokens, displayCtxRatio } from "./chat.js";
|
|
44
|
+
import { chat, contextWindowTokens, displayCtxRatio, statusModelLabel } from "./chat.js";
|
|
43
45
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
44
46
|
import { oauthTokenThunk } from "./auth/token.js";
|
|
45
|
-
import { checkForUpdate } from "./update-check.js";
|
|
47
|
+
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
46
48
|
import { resume } from "./resume.js";
|
|
47
49
|
import { resumeTail } from "./resume-tail.js";
|
|
48
50
|
import { armByteTrace } from "./byte-trace.js";
|
|
@@ -62,6 +64,40 @@ export { applyProjectMerges } from "./trust-ui.js";
|
|
|
62
64
|
* exits, so the turn runs to its terminal and nothing else is read.
|
|
63
65
|
* Built on an EMPTY readline so every other member keeps the pipe
|
|
64
66
|
* path's exact semantics (auto-denied asks, no chips, no pops). */
|
|
67
|
+
/** OR-4: hand a URL to the platform's opener, detached, and never let a
|
|
68
|
+
* missing or failing opener touch the sign-in — the printed URL is the
|
|
69
|
+
* path that always works. macOS `open`, Linux `xdg-open`; elsewhere the
|
|
70
|
+
* URL stays printed (Windows is unsupported by the CLI as a whole). */
|
|
71
|
+
function openInBrowser(url) {
|
|
72
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
|
|
73
|
+
if (opener === null)
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// the URL is on screen; opening it is the person's fallback
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** LT-1: the stream watchdog's bound from the environment — a non-negative
|
|
83
|
+
* number of milliseconds (0 disables it); anything else is ignored. The
|
|
84
|
+
* PTY rigs use it to trip the watchdog in seconds against a stub that
|
|
85
|
+
* never finishes a stream. */
|
|
86
|
+
function streamIdleFromEnv() {
|
|
87
|
+
const raw = process.env.KISO_STREAM_IDLE_MS;
|
|
88
|
+
if (raw === undefined || raw === "")
|
|
89
|
+
return undefined;
|
|
90
|
+
const n = Number(raw);
|
|
91
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
92
|
+
}
|
|
93
|
+
/** OR-3: the commands that own their own stdin reader (a hidden key prompt,
|
|
94
|
+
* an OAuth paste prompt) or read nothing at all — never the editor.
|
|
95
|
+
* OR-9 adds `update`: npm inherits the terminal for the install's own
|
|
96
|
+
* progress and prompts, so no editor may hold stdin while it runs. */
|
|
97
|
+
const CREDENTIAL_COMMANDS = new Set(["login", "logout", "auth", "update"]);
|
|
98
|
+
/** OR-9: the one install command the README gives, which `kiso update`
|
|
99
|
+
* runs and the failure message names. */
|
|
100
|
+
const INSTALL_COMMAND = ["npm", "i", "-g", "@vincemakes/kiso-code@latest"];
|
|
65
101
|
function taskFileInput(content) {
|
|
66
102
|
const base = readlineInput(createInterface({ input: Readable.from([]), output: process.stdout }));
|
|
67
103
|
return {
|
|
@@ -102,6 +138,17 @@ function readlineInput(rl) {
|
|
|
102
138
|
/* readline has no ctrl+o binding — ignored (W15 rides the
|
|
103
139
|
* editor path only). */
|
|
104
140
|
},
|
|
141
|
+
onThink() {
|
|
142
|
+
/* §2.3: same as ctrl+o above — readline has no binding, and a
|
|
143
|
+
* dock-less session has no committed rows to reprint. */
|
|
144
|
+
},
|
|
145
|
+
onEditor() {
|
|
146
|
+
/* §2.4: readline has no ctrl+g, and a piped session has no
|
|
147
|
+
* composer to hand over. */
|
|
148
|
+
},
|
|
149
|
+
externalEdit() {
|
|
150
|
+
/* §2.4: nothing to suspend — this path never took the terminal. */
|
|
151
|
+
},
|
|
105
152
|
onCopy() {
|
|
106
153
|
/* readline has no ctrl+x binding — ignored. `/copy` still
|
|
107
154
|
* works here: it is a typed command, not a key. */
|
|
@@ -186,6 +233,15 @@ function editorInput(editor) {
|
|
|
186
233
|
onExpand(cb) {
|
|
187
234
|
editor.onExpand(cb);
|
|
188
235
|
},
|
|
236
|
+
onThink(cb) {
|
|
237
|
+
editor.onThink(cb);
|
|
238
|
+
},
|
|
239
|
+
onEditor(cb) {
|
|
240
|
+
editor.onEditor(cb);
|
|
241
|
+
},
|
|
242
|
+
externalEdit(edit) {
|
|
243
|
+
editor.externalEdit(edit);
|
|
244
|
+
},
|
|
189
245
|
onCopy(cb) {
|
|
190
246
|
editor.onCopy(cb);
|
|
191
247
|
},
|
|
@@ -343,7 +399,15 @@ function makeLineInput() {
|
|
|
343
399
|
// here", which is the argument W6 made for the box and the only part
|
|
344
400
|
// of it that survives. A prompt character is a third thing saying
|
|
345
401
|
// the same thing, and it cost the row a column.
|
|
346
|
-
|
|
402
|
+
// OR-11 (a): ONE literal. The compositor draws this lead and the
|
|
403
|
+
// editor measures its rows against it; two copies is how they came
|
|
404
|
+
// to disagree by two columns in the first place.
|
|
405
|
+
const COMPOSER_LEAD = "";
|
|
406
|
+
dock.bindInput(() => editor.dockState(), COMPOSER_LEAD);
|
|
407
|
+
// …and BOTH renderers are live: the dock draws this row while it is
|
|
408
|
+
// active, the editor's own selfRender draws it (with the brick) when
|
|
409
|
+
// it is not. The budget follows whichever is drawing.
|
|
410
|
+
editor.setInputLead(() => (dock.active ? COMPOSER_LEAD : PROMPT));
|
|
347
411
|
// TUI2-R3v2 ②: the click hit-test's wiring — the compositor places
|
|
348
412
|
// the panel's option rows, so the compositor is what the editor asks
|
|
349
413
|
// where they are. Neither side computes the other's geometry.
|
|
@@ -384,28 +448,42 @@ function bannerExtensionText() {
|
|
|
384
448
|
* resize; the resume list re-gates with the tier); off-TTY: the
|
|
385
449
|
* historical `[N extensions: ...]` standalone line (zero change). */
|
|
386
450
|
/**
|
|
387
|
-
* The update
|
|
451
|
+
* The update card, when there is one, appended after the opening.
|
|
388
452
|
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
453
|
+
* Two painters, one card. The boot paints what the CACHE already knows,
|
|
454
|
+
* synchronously, so a known newer version is under the banner on the
|
|
455
|
+
* first frame; the check then runs fired-and-forgotten (at most one
|
|
456
|
+
* request a day) and paints only a version the boot did not. If the check
|
|
457
|
+
* fails, times out, is switched off, or finds nothing newer, it does
|
|
458
|
+
* nothing at all and says nothing about having tried.
|
|
393
459
|
*/
|
|
460
|
+
/** OR-9: the version this process has already carded — the boot's cache
|
|
461
|
+
* paint and the async check's answer never both card one version. */
|
|
462
|
+
let updateShown = null;
|
|
463
|
+
const updateDeps = () => ({ kisoHome: kisoHome(), version: VERSION, isTTY: process.stdout.isTTY === true, faux: currentFaux });
|
|
464
|
+
/** OR-9 (owner, 2026-09-09): the update is a CARD under the banner — a
|
|
465
|
+
* rule, a bold title, the version with the command that installs it, the
|
|
466
|
+
* changelog, a rule — the shape the reference shows at every start. The
|
|
467
|
+
* RAW channel (not `notice`, which strips SGR through escapeTerminal); a
|
|
468
|
+
* raw cell lands at the transcript's end, never spliced mid-frame. A
|
|
469
|
+
* declared exception to R2 law 1.1 (a notice wears no edge): a card is
|
|
470
|
+
* not a notice, and the owner asked for the edges. */
|
|
471
|
+
function paintUpdateCard(latest) {
|
|
472
|
+
if (latest === updateShown)
|
|
473
|
+
return;
|
|
474
|
+
updateShown = latest;
|
|
475
|
+
const p = palette();
|
|
476
|
+
const width = Math.min(process.stdout.columns ?? 80, 80);
|
|
477
|
+
const rule = `${p.dim}${"─".repeat(width)}${p.reset}`;
|
|
478
|
+
const [title, line, changelog] = updateCardLines(latest);
|
|
479
|
+
bodyLog(`${rule}\n${p.bold}${title}${p.reset}\n${line}\n${changelog}\n${rule}`, "words");
|
|
480
|
+
}
|
|
394
481
|
async function announceUpdate() {
|
|
395
482
|
try {
|
|
396
|
-
const
|
|
397
|
-
if (
|
|
483
|
+
const latest = await checkForUpdate(updateDeps());
|
|
484
|
+
if (latest === null)
|
|
398
485
|
return;
|
|
399
|
-
|
|
400
|
-
// which calls `escapeTerminal` — so every SGR in it is stripped and
|
|
401
|
-
// the line could only ever be full-strength prose. The raw channel
|
|
402
|
-
// is the one the banner itself uses: the styling is applied HERE, at
|
|
403
|
-
// composition, and the fold keeps the span intact (#16b).
|
|
404
|
-
//
|
|
405
|
-
// A turn may have started while the request was in flight. A raw
|
|
406
|
-
// cell is a cell like any other, so it lands at the transcript's
|
|
407
|
-
// end — never spliced into a frame the reader is mid-way through.
|
|
408
|
-
bodyLog(`${palette().dim}${line}${palette().reset}`, "words");
|
|
486
|
+
paintUpdateCard(latest);
|
|
409
487
|
}
|
|
410
488
|
catch {
|
|
411
489
|
// belt and braces: checkForUpdate does not throw, and if it ever
|
|
@@ -553,6 +631,11 @@ function positiveIntEnv(name) {
|
|
|
553
631
|
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
554
632
|
}
|
|
555
633
|
async function makeAgent(sessionId, input, modelFlag) {
|
|
634
|
+
// §2.5: the ONE source a reload reads for the model, seeded here from
|
|
635
|
+
// the startup flag. Without this a session started with `--model X`
|
|
636
|
+
// would silently revert to the env/config default on its first reload.
|
|
637
|
+
if (modelFlag !== undefined)
|
|
638
|
+
setModelChoice(modelFlag);
|
|
556
639
|
// E3: the project-level trust gate runs BEFORE any extension load (the
|
|
557
640
|
// mcp/skills merges must be in the env when the user-level extensions
|
|
558
641
|
// load). Untrusted project capability is never loaded — never silently.
|
|
@@ -614,16 +697,19 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
614
697
|
// generated extension into it so a first-time rule joins the chain
|
|
615
698
|
// at the NEXT run (the run's policies are fixed at its start; run.ts
|
|
616
699
|
// re-reads the config's extensions array per run).
|
|
617
|
-
|
|
700
|
+
// LT-2: the loop breaker at the chain HEAD — it speaks first when it speaks,
|
|
701
|
+
// so `decidedBy` names it; a deny there beats every tier, bypass included.
|
|
702
|
+
const extensions = [breakerExtension(), ...modeExtensions(), ...loadedExtensions];
|
|
618
703
|
setCurrentAgentExtensions(extensions);
|
|
619
704
|
// E6: the run-start context policy (captured once — exactOptionalPropertyTypes).
|
|
620
705
|
const contextPolicy = contextPolicyFromEnv();
|
|
706
|
+
const idleFromEnv = streamIdleFromEnv(); // read once: a narrowed const, not a call per spread
|
|
621
707
|
const definition = {
|
|
622
708
|
model,
|
|
623
709
|
store,
|
|
624
710
|
// Area 5: the coding tools are bound to the workspace — every path
|
|
625
711
|
// they touch is canonicalized inside cwd, escapes are refused.
|
|
626
|
-
tools: [...createCodingTools(
|
|
712
|
+
tools: [...createCodingTools(codingToolOptions())], // DC-49 — the options live in state.ts, shared with the `!` command's runner
|
|
627
713
|
// Modes: the five tiers ride the E1 policy chain (mode:<tier>
|
|
628
714
|
// extensions, current tier first) — the old static PERMISSION_POLICY
|
|
629
715
|
// is gone, its semantics live in the "default" tier. The banner
|
|
@@ -668,8 +754,13 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
668
754
|
...(sessionId !== undefined ? { promptCacheKey: sessionId } : {}),
|
|
669
755
|
...(resolved.profile.baseUrl !== undefined ? { baseUrl: resolved.profile.baseUrl } : {}),
|
|
670
756
|
...(resolved.profile.promptCaching !== undefined ? { promptCaching: resolved.profile.promptCaching } : {}),
|
|
757
|
+
// LT-1: the profile's stream watchdog bound, if it states one
|
|
758
|
+
...(resolved.profile.streamIdleMs !== undefined ? { streamIdleMs: resolved.profile.streamIdleMs } : {}),
|
|
671
759
|
}
|
|
672
760
|
: { adapter: createFauxProvider(readFauxScript().slice(fauxSkipTurns)) }),
|
|
761
|
+
// LT-1: KISO_STREAM_IDLE_MS (the test rigs' knob) beats the profile —
|
|
762
|
+
// the last spread wins, which is why it sits after the profile's.
|
|
763
|
+
...(idleFromEnv !== undefined ? { streamIdleMs: idleFromEnv } : {}),
|
|
673
764
|
};
|
|
674
765
|
return createAgent(definition);
|
|
675
766
|
}
|
|
@@ -742,7 +833,7 @@ async function pickSession(agent, input) {
|
|
|
742
833
|
function paintBootStatus(session) {
|
|
743
834
|
if (!dock.active)
|
|
744
835
|
return;
|
|
745
|
-
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(),
|
|
836
|
+
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(), statusModelLabel(session), displayCtxRatio(session)));
|
|
746
837
|
}
|
|
747
838
|
/** PH-1a (finding PH-F12): a usage error raised from inside the TUI —
|
|
748
839
|
* main's finally still runs (dock teardown, agent close, temp cleanup)
|
|
@@ -756,12 +847,137 @@ class CliUsageError extends Error {
|
|
|
756
847
|
* line (the previous conversation stays resumable — clear/switch
|
|
757
848
|
* never erase history). Faux sessions re-arm the scripted adapter at
|
|
758
849
|
* the NEW session's durable position, exactly like the picker path. */
|
|
759
|
-
|
|
850
|
+
/** §2.5 — how many skills are IN FORCE, taken from the extension that
|
|
851
|
+
* indexed them rather than by walking the directory again. The first
|
|
852
|
+
* version re-scanned `KISO_SKILLS_DIR` for one number in one log line,
|
|
853
|
+
* which is a second answer to a question already answered and free to
|
|
854
|
+
* disagree with the first (lead's review of 5bee644, observation 2).
|
|
855
|
+
* An extension that reports no count is reported as none, never guessed. */
|
|
856
|
+
function skillCount() {
|
|
857
|
+
// `skills` is declared on the extension's own published type
|
|
858
|
+
// (extensions/skills/index.d.ts), not invented here.
|
|
859
|
+
const skills = loadedExtensions.find((e) => e.name === "skills");
|
|
860
|
+
return skills?.skills ?? 0;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* §2.5 — the reload: the agent is CONSTRUCTED AGAIN on the same session.
|
|
864
|
+
*
|
|
865
|
+
* Nothing short of this reloads the whole surface. `run.ts` composes the
|
|
866
|
+
* system prompt and the approval chain per run, so mutating the shared
|
|
867
|
+
* extensions array reaches those — but the tool registry is built in the
|
|
868
|
+
* agent's constructor and has no unregister, and hooks are frozen into
|
|
869
|
+
* the session's config. Half a reload is worse than none: a prompt that
|
|
870
|
+
* announces a skill whose tool is not in the table is the DC-19 family.
|
|
871
|
+
*
|
|
872
|
+
* LOAD FIRST, SWAP ONLY ON SUCCESS. `loadExtensions` is deliberately
|
|
873
|
+
* loud, so tearing the old agent down first would leave a typo in an
|
|
874
|
+
* extension holding a live session with no agent and no way back. The
|
|
875
|
+
* cost is that two sets of MCP servers are briefly alive; a stdio server
|
|
876
|
+
* holding an exclusive resource — a port, a lock — fails to start twice
|
|
877
|
+
* and so fails the reload cleanly, which is the honest outcome.
|
|
878
|
+
*
|
|
879
|
+
* The conversation survives because the id does not change and its truth
|
|
880
|
+
* is the durable log on disk. The record never moves: a reload is
|
|
881
|
+
* invisible in it, because nothing the model said or did changes.
|
|
882
|
+
*/
|
|
883
|
+
async function reloadAgent(old, id, input) {
|
|
884
|
+
// Snapshot BEFORE makeAgent: it overwrites every one of these, so the
|
|
885
|
+
// old set would be unreachable by the time we needed to dispose it.
|
|
886
|
+
const oldLoaded = loadedExtensions;
|
|
887
|
+
const oldBuiltIn = builtInExtensions;
|
|
888
|
+
const oldUser = userExtensions;
|
|
889
|
+
const oldProject = projectExtensions;
|
|
890
|
+
const oldTemps = mergedTempPaths.splice(0);
|
|
891
|
+
// makeAgent publishes the config side BEFORE it can fail on the model,
|
|
892
|
+
// so a failure past the extension load would leave a NEW window, model
|
|
893
|
+
// name and profile table beside the OLD agent — and the failure line
|
|
894
|
+
// says nothing changed. Rather than soften the line, the snapshot makes
|
|
895
|
+
// it true: a failed reload is atomic, which is what load-then-swap
|
|
896
|
+
// promises. (Lead's review of 5bee644, observation 1.)
|
|
897
|
+
const oldCfg = {
|
|
898
|
+
// RL-F6: makeAgent publishes this BEFORE createAgent, and createAgent
|
|
899
|
+
// throws on a tool-name collision — a user extension exposing
|
|
900
|
+
// `read_file` is enough — so a failure CAN land after it. Left
|
|
901
|
+
// unrestored, the new array stands beside the old agent, whose config
|
|
902
|
+
// holds the old one by reference, and the don't-ask-again writer
|
|
903
|
+
// mutates whatever this is: a rule granted after a failed reload goes
|
|
904
|
+
// into an array nothing reads. RL-F3's family one level up, and the
|
|
905
|
+
// same symptom — the human grants a rule and is asked again on the
|
|
906
|
+
// very next identical call.
|
|
907
|
+
agentExtensions: currentAgentExtensions,
|
|
908
|
+
merged: mergedConfig,
|
|
909
|
+
models: configModels,
|
|
910
|
+
window: configuredWindow,
|
|
911
|
+
faux: currentFaux,
|
|
912
|
+
modelName: currentModelName,
|
|
913
|
+
choice: modelChoice,
|
|
914
|
+
agent: agentModel,
|
|
915
|
+
endpoint: agentBaseUrl,
|
|
916
|
+
delegation: process.env.KISO_DELEGATION_CONFIG_JSON,
|
|
917
|
+
};
|
|
918
|
+
let next;
|
|
919
|
+
try {
|
|
920
|
+
next = await makeAgent(id, input, modelChoice);
|
|
921
|
+
}
|
|
922
|
+
catch (err) {
|
|
923
|
+
// Nothing is swapped. Clean up whatever the failed attempt managed
|
|
924
|
+
// to create, put the old set's own temp paths back so exit still
|
|
925
|
+
// removes them, and restore the lists in case makeAgent got far
|
|
926
|
+
// enough to publish new ones.
|
|
927
|
+
for (const p of mergedTempPaths.splice(0)) {
|
|
928
|
+
try {
|
|
929
|
+
rmSync(p, { recursive: true, force: true });
|
|
930
|
+
}
|
|
931
|
+
catch {
|
|
932
|
+
// best-effort — the temp dir would be reaped by the OS
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
mergedTempPaths.push(...oldTemps);
|
|
936
|
+
setExtensionLists(oldBuiltIn, oldUser, oldProject, oldLoaded);
|
|
937
|
+
setCurrentAgentExtensions(oldCfg.agentExtensions);
|
|
938
|
+
setMergedConfig(oldCfg.merged);
|
|
939
|
+
setConfigModels(oldCfg.models);
|
|
940
|
+
setConfiguredWindow(oldCfg.window);
|
|
941
|
+
setCurrentFaux(oldCfg.faux);
|
|
942
|
+
setCurrentModelName(oldCfg.modelName);
|
|
943
|
+
setModelChoice(oldCfg.choice);
|
|
944
|
+
setAgentModel(oldCfg.agent, oldCfg.endpoint);
|
|
945
|
+
if (oldCfg.delegation === undefined)
|
|
946
|
+
delete process.env.KISO_DELEGATION_CONFIG_JSON;
|
|
947
|
+
else
|
|
948
|
+
process.env.KISO_DELEGATION_CONFIG_JSON = oldCfg.delegation;
|
|
949
|
+
bodyLog(`[reload] ${err instanceof Error ? err.message : String(err)} — nothing changed, the previous set is still in force`);
|
|
950
|
+
return old;
|
|
951
|
+
}
|
|
952
|
+
// The new set is built and sound; only now does the old one go.
|
|
953
|
+
old.close();
|
|
954
|
+
await disposeExtensions(oldLoaded);
|
|
955
|
+
for (const p of oldTemps) {
|
|
956
|
+
try {
|
|
957
|
+
rmSync(p, { recursive: true, force: true });
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
// best-effort — the temp dir would be reaped by the OS
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
bodyLog(`[reload] ${loadedExtensions.length} extensions, ${skillCount()} skills — the conversation is unchanged`);
|
|
964
|
+
return next;
|
|
965
|
+
}
|
|
966
|
+
async function chatLoop(first, firstId, input, autoCompact) {
|
|
760
967
|
let id = firstId;
|
|
761
968
|
let prev = null;
|
|
969
|
+
let agent = first;
|
|
970
|
+
// §2.5: a rebuilt agent re-enters on the SAME conversation, so it prints
|
|
971
|
+
// neither the opening banner nor the "switched — previous" line. The
|
|
972
|
+
// reload said what it did; repeating the session's opening would read
|
|
973
|
+
// as a new session, which is the one thing it is not.
|
|
974
|
+
let rebuilt = false;
|
|
762
975
|
for (;;) {
|
|
763
976
|
const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
764
|
-
if (
|
|
977
|
+
if (rebuilt) {
|
|
978
|
+
rebuilt = false;
|
|
979
|
+
}
|
|
980
|
+
else if (prev === null) {
|
|
765
981
|
bodyLog(`session ${id}\n`);
|
|
766
982
|
// REL-0152-D5: a session with history says what that history WAS.
|
|
767
983
|
// Resuming used to print this one line and drop you at an empty
|
|
@@ -778,11 +994,16 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
778
994
|
// throws on profile drift — so one drifted session anywhere in the
|
|
779
995
|
// history stopped kiso from starting at all.
|
|
780
996
|
extensionsBanner();
|
|
781
|
-
//
|
|
782
|
-
// a newer
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
//
|
|
997
|
+
// OR-9 (owner, 2026-09-09): the update card under the banner, at
|
|
998
|
+
// EVERY start while a newer version is known — §7.10's once-only
|
|
999
|
+
// line is superseded. What the cache knows is painted NOW, with
|
|
1000
|
+
// no request, so the card is part of the opening rather than a
|
|
1001
|
+
// late arrival; the check itself stays FIRED AND FORGOTTEN — the
|
|
1002
|
+
// banner is already on screen and it never delays it — and it
|
|
1003
|
+
// paints only a version the boot did not know.
|
|
1004
|
+
const known = knownUpdate(updateDeps());
|
|
1005
|
+
if (known !== null)
|
|
1006
|
+
paintUpdateCard(known);
|
|
786
1007
|
void announceUpdate();
|
|
787
1008
|
}
|
|
788
1009
|
else {
|
|
@@ -811,6 +1032,11 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
811
1032
|
const end = await chat(session, currentFaux, input, autoCompact, nav);
|
|
812
1033
|
if (end.next === "exit")
|
|
813
1034
|
return;
|
|
1035
|
+
if (end.next === "reload") {
|
|
1036
|
+
agent = await reloadAgent(agent, id, input);
|
|
1037
|
+
rebuilt = true;
|
|
1038
|
+
continue; // the same id, the same conversation, a new agent
|
|
1039
|
+
}
|
|
814
1040
|
prev = id;
|
|
815
1041
|
id = end.id;
|
|
816
1042
|
}
|
|
@@ -830,10 +1056,15 @@ async function readSecret(prompt) {
|
|
|
830
1056
|
process.stdin.resume();
|
|
831
1057
|
});
|
|
832
1058
|
}
|
|
833
|
-
process.stdout.write(prompt);
|
|
834
1059
|
return await new Promise((resolve) => {
|
|
835
1060
|
let buf = "";
|
|
1061
|
+
// OR-3: raw mode BEFORE the prompt — the hidden prompt must be hidden
|
|
1062
|
+
// before it is shown. Written the other way round, a key that arrives
|
|
1063
|
+
// between the prompt and setRawMode is echoed by the tty line
|
|
1064
|
+
// discipline (the auth-tty gate caught it: a driver that types the
|
|
1065
|
+
// instant the prompt appears saw the key on screen two runs in three).
|
|
836
1066
|
process.stdin.setRawMode(true);
|
|
1067
|
+
process.stdout.write(prompt);
|
|
837
1068
|
process.stdin.resume();
|
|
838
1069
|
process.stdin.setEncoding("utf8");
|
|
839
1070
|
const onData = (chunk) => {
|
|
@@ -975,7 +1206,20 @@ async function main() {
|
|
|
975
1206
|
// (entered here, dock-bound, trusted before any extension loads),
|
|
976
1207
|
// readline elsewhere. The trust question, chat, and resume all read
|
|
977
1208
|
// through it; main's finally closes it on every exit path.
|
|
978
|
-
|
|
1209
|
+
// OR-3 (owner, 2026-09-09): the credential commands never read the shared
|
|
1210
|
+
// line input, and on a TTY makeLineInput() enters the raw-mode editor and
|
|
1211
|
+
// sends the ground probe (CSI ?996n + OSC 11) BEFORE the command switch.
|
|
1212
|
+
// `kiso login chatgpt` then had two readers on stdin: the editor swallowed
|
|
1213
|
+
// the keystrokes, the terminal's OSC reply was typed into the login's own
|
|
1214
|
+
// readline, the "open this URL" line vanished under the dock's repaint,
|
|
1215
|
+
// and the sign-in never completed. These commands get an input over an
|
|
1216
|
+
// EMPTY readable instead — no editor, no probe, stdin untouched — so the
|
|
1217
|
+
// hidden key prompt and the OAuth paste prompt are the only readers.
|
|
1218
|
+
const input = taskFile !== undefined
|
|
1219
|
+
? taskFileInput(taskFile)
|
|
1220
|
+
: CREDENTIAL_COMMANDS.has(command ?? "")
|
|
1221
|
+
? readlineInput(createInterface({ input: Readable.from([]), output: process.stdout }))
|
|
1222
|
+
: makeLineInput();
|
|
979
1223
|
// R3a — cross-session input history: ~/.kiso/history, one line per
|
|
980
1224
|
// entry, appended on submit, tail-500 at load (truncated by REWRITE
|
|
981
1225
|
// at startup so the file never grows unbounded). Unreadable file =
|
|
@@ -1195,6 +1439,9 @@ async function main() {
|
|
|
1195
1439
|
const cred = await chatgptFlow.login({
|
|
1196
1440
|
notify: (line) => process.stdout.write(`${line}\n`),
|
|
1197
1441
|
prompt: (q) => new Promise((resolve) => rl.question(q, resolve)),
|
|
1442
|
+
// OR-4: a person at a terminal gets the browser opened for
|
|
1443
|
+
// them; a pipe, a test rig or KISO_NO_BROWSER=1 gets the URL only.
|
|
1444
|
+
...(process.stdout.isTTY === true && process.env.KISO_NO_BROWSER === undefined ? { open: openInBrowser } : {}),
|
|
1198
1445
|
...(cbPort !== undefined ? { callback: { host: process.env.KISO_OAUTH_CALLBACK_HOST ?? "127.0.0.1", port: cbPort } } : {}),
|
|
1199
1446
|
...(process.env.KISO_OAUTH_TOKEN_URL !== undefined ? { tokenUrl: process.env.KISO_OAUTH_TOKEN_URL } : {}),
|
|
1200
1447
|
});
|
|
@@ -1213,6 +1460,27 @@ async function main() {
|
|
|
1213
1460
|
process.stdout.write(`signed in to ${provider} with an API key (${maskSecret(key)}) — stored in ${authPath()}\n`);
|
|
1214
1461
|
break;
|
|
1215
1462
|
}
|
|
1463
|
+
case "update": {
|
|
1464
|
+
// OR-9: the command the update card names. A thin hand-off to
|
|
1465
|
+
// the ONE install command the README gives — npm resolved from
|
|
1466
|
+
// PATH, its stdio inherited so its progress, its prompts and
|
|
1467
|
+
// its own errors are what the person sees, its exit code the
|
|
1468
|
+
// outcome. kiso adds nothing to what npm knows: no version
|
|
1469
|
+
// pinning, no registry, no elevation. A failure (a global
|
|
1470
|
+
// prefix this user cannot write, a network that is down) is
|
|
1471
|
+
// npm's to explain; the message names the manual command so
|
|
1472
|
+
// the person can run it with whatever their setup needs.
|
|
1473
|
+
const [bin, ...installArgs] = INSTALL_COMMAND;
|
|
1474
|
+
const r = spawnSync(bin, installArgs, { stdio: "inherit" });
|
|
1475
|
+
if (r.error !== undefined || r.status !== 0) {
|
|
1476
|
+
const why = r.error !== undefined ? `could not run npm (${r.error.message})` : `npm exited ${r.status ?? "on a signal"}`;
|
|
1477
|
+
process.stderr.write(`kiso update: ${why} — run it yourself: ${INSTALL_COMMAND.join(" ")}\n`);
|
|
1478
|
+
process.exitCode = r.status ?? 1;
|
|
1479
|
+
break;
|
|
1480
|
+
}
|
|
1481
|
+
process.stdout.write("kiso updated — the next `kiso` runs the new version\n");
|
|
1482
|
+
break;
|
|
1483
|
+
}
|
|
1216
1484
|
case "help": {
|
|
1217
1485
|
// PH-1b (finding PH-F21): the CLI must be able to describe its
|
|
1218
1486
|
// own configuration — the old help listed five commands and
|
|
@@ -1230,6 +1498,7 @@ async function main() {
|
|
|
1230
1498
|
" chatgpt: sign in with a ChatGPT subscription (browser; unofficial third-party flow)\n" +
|
|
1231
1499
|
" kiso logout <provider> remove the stored credential\n" +
|
|
1232
1500
|
" kiso auth list stored credentials (keys masked)\n" +
|
|
1501
|
+
" kiso update install the latest release (npm i -g @vincemakes/kiso-code@latest)\n" +
|
|
1233
1502
|
" kiso help this help\n\n" +
|
|
1234
1503
|
"flags (any position):\n" +
|
|
1235
1504
|
" --model <profile|provider/model> pick the model (also /model in-session)\n" +
|
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;
|