@vincemakes/kiso-code 0.30.0 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/credentials.d.ts +39 -0
- package/dist/auth/credentials.js +170 -0
- package/dist/auth/oauth/chatgpt.d.ts +40 -0
- package/dist/auth/oauth/chatgpt.js +203 -0
- package/dist/auth/oauth/index.d.ts +37 -0
- package/dist/auth/oauth/index.js +1 -0
- package/dist/auth/oauth/pkce.d.ts +5 -0
- package/dist/auth/oauth/pkce.js +12 -0
- package/dist/auth/refresh.d.ts +12 -0
- package/dist/auth/refresh.js +40 -0
- package/dist/auth/token.d.ts +17 -0
- package/dist/auth/token.js +26 -0
- package/dist/chat.d.ts +10 -1
- package/dist/chat.js +50 -8
- package/dist/config.d.ts +42 -6
- package/dist/config.js +87 -12
- package/dist/dispatch.js +117 -11
- package/dist/index.js +238 -30
- package/dist/mode.d.ts +2 -2
- package/dist/mode.js +2 -2
- package/dist/state.d.ts +7 -2
- package/dist/state.js +5 -1
- package/dist/trust-ui.d.ts +13 -0
- package/dist/trust-ui.js +17 -2
- package/dist/update-check.d.ts +9 -3
- package/dist/update-check.js +27 -28
- package/package.json +15 -14
- package/dist/diff.d.ts +0 -32
- package/dist/diff.js +0 -122
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
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";
|
|
@@ -39,9 +40,10 @@ import { agentModel, atFiles, body, bodyLog, kisoHome, builtInExtensions, curren
|
|
|
39
40
|
import { askUi, resolveProjectTrust } from "./trust-ui.js";
|
|
40
41
|
import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
41
42
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
42
|
-
import { chat, contextWindowTokens, displayCtxRatio } from "./chat.js";
|
|
43
|
+
import { chat, contextWindowTokens, displayCtxRatio, statusModelLabel } from "./chat.js";
|
|
43
44
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
44
|
-
import {
|
|
45
|
+
import { oauthTokenThunk } from "./auth/token.js";
|
|
46
|
+
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
45
47
|
import { resume } from "./resume.js";
|
|
46
48
|
import { resumeTail } from "./resume-tail.js";
|
|
47
49
|
import { armByteTrace } from "./byte-trace.js";
|
|
@@ -61,6 +63,29 @@ export { applyProjectMerges } from "./trust-ui.js";
|
|
|
61
63
|
* exits, so the turn runs to its terminal and nothing else is read.
|
|
62
64
|
* Built on an EMPTY readline so every other member keeps the pipe
|
|
63
65
|
* path's exact semantics (auto-denied asks, no chips, no pops). */
|
|
66
|
+
/** OR-4: hand a URL to the platform's opener, detached, and never let a
|
|
67
|
+
* missing or failing opener touch the sign-in — the printed URL is the
|
|
68
|
+
* path that always works. macOS `open`, Linux `xdg-open`; elsewhere the
|
|
69
|
+
* URL stays printed (Windows is unsupported by the CLI as a whole). */
|
|
70
|
+
function openInBrowser(url) {
|
|
71
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
|
|
72
|
+
if (opener === null)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// the URL is on screen; opening it is the person's fallback
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** OR-3: the commands that own their own stdin reader (a hidden key prompt,
|
|
82
|
+
* an OAuth paste prompt) or read nothing at all — never the editor.
|
|
83
|
+
* OR-9 adds `update`: npm inherits the terminal for the install's own
|
|
84
|
+
* progress and prompts, so no editor may hold stdin while it runs. */
|
|
85
|
+
const CREDENTIAL_COMMANDS = new Set(["login", "logout", "auth", "update"]);
|
|
86
|
+
/** OR-9: the one install command the README gives, which `kiso update`
|
|
87
|
+
* runs and the failure message names. */
|
|
88
|
+
const INSTALL_COMMAND = ["npm", "i", "-g", "@vincemakes/kiso-code@latest"];
|
|
64
89
|
function taskFileInput(content) {
|
|
65
90
|
const base = readlineInput(createInterface({ input: Readable.from([]), output: process.stdout }));
|
|
66
91
|
return {
|
|
@@ -383,28 +408,42 @@ function bannerExtensionText() {
|
|
|
383
408
|
* resize; the resume list re-gates with the tier); off-TTY: the
|
|
384
409
|
* historical `[N extensions: ...]` standalone line (zero change). */
|
|
385
410
|
/**
|
|
386
|
-
* The update
|
|
411
|
+
* The update card, when there is one, appended after the opening.
|
|
387
412
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
413
|
+
* Two painters, one card. The boot paints what the CACHE already knows,
|
|
414
|
+
* synchronously, so a known newer version is under the banner on the
|
|
415
|
+
* first frame; the check then runs fired-and-forgotten (at most one
|
|
416
|
+
* request a day) and paints only a version the boot did not. If the check
|
|
417
|
+
* fails, times out, is switched off, or finds nothing newer, it does
|
|
418
|
+
* nothing at all and says nothing about having tried.
|
|
392
419
|
*/
|
|
420
|
+
/** OR-9: the version this process has already carded — the boot's cache
|
|
421
|
+
* paint and the async check's answer never both card one version. */
|
|
422
|
+
let updateShown = null;
|
|
423
|
+
const updateDeps = () => ({ kisoHome: kisoHome(), version: VERSION, isTTY: process.stdout.isTTY === true, faux: currentFaux });
|
|
424
|
+
/** OR-9 (owner, 2026-09-09): the update is a CARD under the banner — a
|
|
425
|
+
* rule, a bold title, the version with the command that installs it, the
|
|
426
|
+
* changelog, a rule — the shape the reference shows at every start. The
|
|
427
|
+
* RAW channel (not `notice`, which strips SGR through escapeTerminal); a
|
|
428
|
+
* raw cell lands at the transcript's end, never spliced mid-frame. A
|
|
429
|
+
* declared exception to R2 law 1.1 (a notice wears no edge): a card is
|
|
430
|
+
* not a notice, and the owner asked for the edges. */
|
|
431
|
+
function paintUpdateCard(latest) {
|
|
432
|
+
if (latest === updateShown)
|
|
433
|
+
return;
|
|
434
|
+
updateShown = latest;
|
|
435
|
+
const p = palette();
|
|
436
|
+
const width = Math.min(process.stdout.columns ?? 80, 80);
|
|
437
|
+
const rule = `${p.dim}${"─".repeat(width)}${p.reset}`;
|
|
438
|
+
const [title, line, changelog] = updateCardLines(latest);
|
|
439
|
+
bodyLog(`${rule}\n${p.bold}${title}${p.reset}\n${line}\n${changelog}\n${rule}`, "words");
|
|
440
|
+
}
|
|
393
441
|
async function announceUpdate() {
|
|
394
442
|
try {
|
|
395
|
-
const
|
|
396
|
-
if (
|
|
443
|
+
const latest = await checkForUpdate(updateDeps());
|
|
444
|
+
if (latest === null)
|
|
397
445
|
return;
|
|
398
|
-
|
|
399
|
-
// which calls `escapeTerminal` — so every SGR in it is stripped and
|
|
400
|
-
// the line could only ever be full-strength prose. The raw channel
|
|
401
|
-
// is the one the banner itself uses: the styling is applied HERE, at
|
|
402
|
-
// composition, and the fold keeps the span intact (#16b).
|
|
403
|
-
//
|
|
404
|
-
// A turn may have started while the request was in flight. A raw
|
|
405
|
-
// cell is a cell like any other, so it lands at the transcript's
|
|
406
|
-
// end — never spliced into a frame the reader is mid-way through.
|
|
407
|
-
bodyLog(`${palette().dim}${line}${palette().reset}`, "words");
|
|
446
|
+
paintUpdateCard(latest);
|
|
408
447
|
}
|
|
409
448
|
catch {
|
|
410
449
|
// belt and braces: checkForUpdate does not throw, and if it ever
|
|
@@ -607,7 +646,7 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
607
646
|
setCurrentFaux(false);
|
|
608
647
|
setCurrentModelName(resolved.name);
|
|
609
648
|
}
|
|
610
|
-
setAgentModel(model); // v2b: the status bar shows it
|
|
649
|
+
setAgentModel(model, resolved?.profile.baseUrl); // v2b: the status bar shows it; OR-1: the endpoint rides along
|
|
611
650
|
// W21: the extensions array is built ONCE per agent and shared with
|
|
612
651
|
// the runtime by reference — the don't-ask-again writer pushes the
|
|
613
652
|
// generated extension into it so a first-time rule joins the chain
|
|
@@ -647,13 +686,24 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
647
686
|
// field survives for the callers that want a bound (subagents, the
|
|
648
687
|
// SDK, `kiso run`); the interactive front door does not set one.
|
|
649
688
|
// Modes: the five tiers join at the CHAIN HEAD, before the user/
|
|
650
|
-
// project extensions (the deny>ask
|
|
689
|
+
// project extensions (the deny>allow>ask composition keeps a user
|
|
651
690
|
// deny winning over any mode tier — bypass included).
|
|
652
691
|
extensions,
|
|
653
692
|
...(resolved !== null
|
|
654
693
|
? {
|
|
655
694
|
provider: resolved.profile.kind,
|
|
656
|
-
|
|
695
|
+
// OR-1: exactly one sign-in shape reaches the adapter. An
|
|
696
|
+
// OAuth profile has no key to pass — it passes the thunk
|
|
697
|
+
// the adapter re-resolves per request instead.
|
|
698
|
+
...(resolved.oauthProviderId !== undefined
|
|
699
|
+
? { oauth: oauthTokenThunk(resolved.oauthProviderId) }
|
|
700
|
+
: { apiKey: resolved.apiKey ?? "none" }),
|
|
701
|
+
// OR-1: the ChatGPT backend's cache lane is the SESSION —
|
|
702
|
+
// one conversation's requests share a key, different
|
|
703
|
+
// conversations never do. The one entry point that hands
|
|
704
|
+
// no session id is `kiso sessions`, a read-only listing
|
|
705
|
+
// that streams nothing, so its absence costs no cache.
|
|
706
|
+
...(sessionId !== undefined ? { promptCacheKey: sessionId } : {}),
|
|
657
707
|
...(resolved.profile.baseUrl !== undefined ? { baseUrl: resolved.profile.baseUrl } : {}),
|
|
658
708
|
...(resolved.profile.promptCaching !== undefined ? { promptCaching: resolved.profile.promptCaching } : {}),
|
|
659
709
|
}
|
|
@@ -730,7 +780,7 @@ async function pickSession(agent, input) {
|
|
|
730
780
|
function paintBootStatus(session) {
|
|
731
781
|
if (!dock.active)
|
|
732
782
|
return;
|
|
733
|
-
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(),
|
|
783
|
+
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(), statusModelLabel(session), displayCtxRatio(session)));
|
|
734
784
|
}
|
|
735
785
|
/** PH-1a (finding PH-F12): a usage error raised from inside the TUI —
|
|
736
786
|
* main's finally still runs (dock teardown, agent close, temp cleanup)
|
|
@@ -766,11 +816,16 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
766
816
|
// throws on profile drift — so one drifted session anywhere in the
|
|
767
817
|
// history stopped kiso from starting at all.
|
|
768
818
|
extensionsBanner();
|
|
769
|
-
//
|
|
770
|
-
// a newer
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
819
|
+
// OR-9 (owner, 2026-09-09): the update card under the banner, at
|
|
820
|
+
// EVERY start while a newer version is known — §7.10's once-only
|
|
821
|
+
// line is superseded. What the cache knows is painted NOW, with
|
|
822
|
+
// no request, so the card is part of the opening rather than a
|
|
823
|
+
// late arrival; the check itself stays FIRED AND FORGOTTEN — the
|
|
824
|
+
// banner is already on screen and it never delays it — and it
|
|
825
|
+
// paints only a version the boot did not know.
|
|
826
|
+
const known = knownUpdate(updateDeps());
|
|
827
|
+
if (known !== null)
|
|
828
|
+
paintUpdateCard(known);
|
|
774
829
|
void announceUpdate();
|
|
775
830
|
}
|
|
776
831
|
else {
|
|
@@ -789,7 +844,7 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
789
844
|
// XP-1 §2.1: the switched-to session's OWN truth repaints the row —
|
|
790
845
|
// the global display state never outlives the session it described
|
|
791
846
|
// (pre-XP the row kept the previous session's /model selection).
|
|
792
|
-
setAgentModel(session.model);
|
|
847
|
+
setAgentModel(session.model, session.baseUrl);
|
|
793
848
|
setCurrentModelName(session.model);
|
|
794
849
|
paintBootStatus(session);
|
|
795
850
|
const nav = {
|
|
@@ -803,6 +858,56 @@ async function chatLoop(agent, firstId, input, autoCompact) {
|
|
|
803
858
|
id = end.id;
|
|
804
859
|
}
|
|
805
860
|
}
|
|
861
|
+
/** A secret from the terminal: hidden input on a TTY, the whole of stdin
|
|
862
|
+
* otherwise (a pipe, a heredoc) — never an argument, which shell history
|
|
863
|
+
* would keep. */
|
|
864
|
+
async function readSecret(prompt) {
|
|
865
|
+
if (process.stdin.isTTY !== true) {
|
|
866
|
+
return await new Promise((resolve) => {
|
|
867
|
+
let buf = "";
|
|
868
|
+
process.stdin.setEncoding("utf8");
|
|
869
|
+
process.stdin.on("data", (d) => {
|
|
870
|
+
buf += d;
|
|
871
|
+
});
|
|
872
|
+
process.stdin.on("end", () => resolve(buf.split("\n")[0] ?? ""));
|
|
873
|
+
process.stdin.resume();
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
return await new Promise((resolve) => {
|
|
877
|
+
let buf = "";
|
|
878
|
+
// OR-3: raw mode BEFORE the prompt — the hidden prompt must be hidden
|
|
879
|
+
// before it is shown. Written the other way round, a key that arrives
|
|
880
|
+
// between the prompt and setRawMode is echoed by the tty line
|
|
881
|
+
// discipline (the auth-tty gate caught it: a driver that types the
|
|
882
|
+
// instant the prompt appears saw the key on screen two runs in three).
|
|
883
|
+
process.stdin.setRawMode(true);
|
|
884
|
+
process.stdout.write(prompt);
|
|
885
|
+
process.stdin.resume();
|
|
886
|
+
process.stdin.setEncoding("utf8");
|
|
887
|
+
const onData = (chunk) => {
|
|
888
|
+
for (const ch of chunk) {
|
|
889
|
+
if (ch === "\r" || ch === "\n") {
|
|
890
|
+
process.stdin.setRawMode(false);
|
|
891
|
+
process.stdin.pause();
|
|
892
|
+
process.stdin.off("data", onData);
|
|
893
|
+
process.stdout.write("\n");
|
|
894
|
+
resolve(buf);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (ch === "\u0003") {
|
|
898
|
+
process.stdin.setRawMode(false);
|
|
899
|
+
process.stdout.write("\n");
|
|
900
|
+
process.exit(130);
|
|
901
|
+
}
|
|
902
|
+
if (ch === "\u007f" || ch === "\b")
|
|
903
|
+
buf = buf.slice(0, -1);
|
|
904
|
+
else
|
|
905
|
+
buf += ch;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
process.stdin.on("data", onData);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
806
911
|
async function main() {
|
|
807
912
|
// E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
|
|
808
913
|
// turns the in-flight stdout/stderr writes into EIO, and node's
|
|
@@ -918,7 +1023,20 @@ async function main() {
|
|
|
918
1023
|
// (entered here, dock-bound, trusted before any extension loads),
|
|
919
1024
|
// readline elsewhere. The trust question, chat, and resume all read
|
|
920
1025
|
// through it; main's finally closes it on every exit path.
|
|
921
|
-
|
|
1026
|
+
// OR-3 (owner, 2026-09-09): the credential commands never read the shared
|
|
1027
|
+
// line input, and on a TTY makeLineInput() enters the raw-mode editor and
|
|
1028
|
+
// sends the ground probe (CSI ?996n + OSC 11) BEFORE the command switch.
|
|
1029
|
+
// `kiso login chatgpt` then had two readers on stdin: the editor swallowed
|
|
1030
|
+
// the keystrokes, the terminal's OSC reply was typed into the login's own
|
|
1031
|
+
// readline, the "open this URL" line vanished under the dock's repaint,
|
|
1032
|
+
// and the sign-in never completed. These commands get an input over an
|
|
1033
|
+
// EMPTY readable instead — no editor, no probe, stdin untouched — so the
|
|
1034
|
+
// hidden key prompt and the OAuth paste prompt are the only readers.
|
|
1035
|
+
const input = taskFile !== undefined
|
|
1036
|
+
? taskFileInput(taskFile)
|
|
1037
|
+
: CREDENTIAL_COMMANDS.has(command ?? "")
|
|
1038
|
+
? readlineInput(createInterface({ input: Readable.from([]), output: process.stdout }))
|
|
1039
|
+
: makeLineInput();
|
|
922
1040
|
// R3a — cross-session input history: ~/.kiso/history, one line per
|
|
923
1041
|
// entry, appended on submit, tail-500 at load (truncated by REWRITE
|
|
924
1042
|
// at startup so the file never grows unbounded). Unreadable file =
|
|
@@ -1095,6 +1213,91 @@ async function main() {
|
|
|
1095
1213
|
}
|
|
1096
1214
|
break;
|
|
1097
1215
|
}
|
|
1216
|
+
case "login":
|
|
1217
|
+
case "logout":
|
|
1218
|
+
case "auth": {
|
|
1219
|
+
// the sign-in plan (2026-09-08), step 1: the credential store.
|
|
1220
|
+
// `login <provider>` reads the key from a hidden prompt on a TTY
|
|
1221
|
+
// or from stdin otherwise (never from an argument — shell history);
|
|
1222
|
+
// `logout <provider>` deletes; `auth` lists, keys masked.
|
|
1223
|
+
const { KNOWN_PROVIDERS, OAUTH_PROVIDERS, authPath, deleteCredential, maskSecret, readAuthFile, setCredential } = await import("./auth/credentials.js");
|
|
1224
|
+
const provider = arg;
|
|
1225
|
+
if (command === "auth") {
|
|
1226
|
+
const file = readAuthFile();
|
|
1227
|
+
const rows = Object.entries(file.credentials);
|
|
1228
|
+
const lines = [`credentials: ${authPath()}${rows.length === 0 ? " (none stored)" : ""}`];
|
|
1229
|
+
for (const [id, cred] of rows) {
|
|
1230
|
+
if (cred.type === "api-key")
|
|
1231
|
+
lines.push(` ${id.padEnd(10)} api-key ${maskSecret(cred.key)} saved ${new Date(cred.savedAt).toISOString().slice(0, 10)}`);
|
|
1232
|
+
else
|
|
1233
|
+
lines.push(` ${id.padEnd(10)} oauth ${cred.accountId ?? ""} expires ${new Date(cred.expires).toISOString()}${cred.expires < Date.now() ? " (expired)" : ""}`);
|
|
1234
|
+
}
|
|
1235
|
+
const envs = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"].filter((k) => process.env[k] !== undefined);
|
|
1236
|
+
lines.push(`env vars set: ${envs.length ? envs.join(", ") : "none"} (a stored credential owns its provider; env applies only when nothing is stored)`);
|
|
1237
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
1238
|
+
break;
|
|
1239
|
+
}
|
|
1240
|
+
if (provider === undefined || !KNOWN_PROVIDERS.includes(provider)) {
|
|
1241
|
+
throw new CliUsageError(`kiso ${command} <provider> — one of: ${KNOWN_PROVIDERS.join(", ")}`);
|
|
1242
|
+
}
|
|
1243
|
+
if (command === "logout") {
|
|
1244
|
+
const had = deleteCredential(provider);
|
|
1245
|
+
process.stdout.write(had ? `signed out of ${provider}\n` : `nothing stored for ${provider}\n`);
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
if (OAUTH_PROVIDERS.includes(provider)) {
|
|
1249
|
+
// the subscription sign-in (plan step 2): the browser flow with a
|
|
1250
|
+
// paste path; the callback host/port can be moved by env for tests
|
|
1251
|
+
const { chatgptFlow } = await import("./auth/oauth/chatgpt.js");
|
|
1252
|
+
const { createInterface } = await import("node:readline");
|
|
1253
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1254
|
+
const cbPort = process.env.KISO_OAUTH_CALLBACK_PORT !== undefined ? Number(process.env.KISO_OAUTH_CALLBACK_PORT) : undefined;
|
|
1255
|
+
try {
|
|
1256
|
+
const cred = await chatgptFlow.login({
|
|
1257
|
+
notify: (line) => process.stdout.write(`${line}\n`),
|
|
1258
|
+
prompt: (q) => new Promise((resolve) => rl.question(q, resolve)),
|
|
1259
|
+
// OR-4: a person at a terminal gets the browser opened for
|
|
1260
|
+
// them; a pipe, a test rig or KISO_NO_BROWSER=1 gets the URL only.
|
|
1261
|
+
...(process.stdout.isTTY === true && process.env.KISO_NO_BROWSER === undefined ? { open: openInBrowser } : {}),
|
|
1262
|
+
...(cbPort !== undefined ? { callback: { host: process.env.KISO_OAUTH_CALLBACK_HOST ?? "127.0.0.1", port: cbPort } } : {}),
|
|
1263
|
+
...(process.env.KISO_OAUTH_TOKEN_URL !== undefined ? { tokenUrl: process.env.KISO_OAUTH_TOKEN_URL } : {}),
|
|
1264
|
+
});
|
|
1265
|
+
setCredential(provider, cred);
|
|
1266
|
+
process.stdout.write(`signed in to ${provider} (${chatgptFlow.label}) — account ${cred.accountId ?? "?"}, expires ${new Date(cred.expires).toISOString()} — stored in ${authPath()}\n`);
|
|
1267
|
+
}
|
|
1268
|
+
finally {
|
|
1269
|
+
rl.close();
|
|
1270
|
+
}
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
const key = (await readSecret(`API key for ${provider}: `)).trim();
|
|
1274
|
+
if (key === "")
|
|
1275
|
+
throw new CliUsageError(`kiso login ${provider}: no key given`);
|
|
1276
|
+
setCredential(provider, { type: "api-key", key, savedAt: Date.now() });
|
|
1277
|
+
process.stdout.write(`signed in to ${provider} with an API key (${maskSecret(key)}) — stored in ${authPath()}\n`);
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
case "update": {
|
|
1281
|
+
// OR-9: the command the update card names. A thin hand-off to
|
|
1282
|
+
// the ONE install command the README gives — npm resolved from
|
|
1283
|
+
// PATH, its stdio inherited so its progress, its prompts and
|
|
1284
|
+
// its own errors are what the person sees, its exit code the
|
|
1285
|
+
// outcome. kiso adds nothing to what npm knows: no version
|
|
1286
|
+
// pinning, no registry, no elevation. A failure (a global
|
|
1287
|
+
// prefix this user cannot write, a network that is down) is
|
|
1288
|
+
// npm's to explain; the message names the manual command so
|
|
1289
|
+
// the person can run it with whatever their setup needs.
|
|
1290
|
+
const [bin, ...installArgs] = INSTALL_COMMAND;
|
|
1291
|
+
const r = spawnSync(bin, installArgs, { stdio: "inherit" });
|
|
1292
|
+
if (r.error !== undefined || r.status !== 0) {
|
|
1293
|
+
const why = r.error !== undefined ? `could not run npm (${r.error.message})` : `npm exited ${r.status ?? "on a signal"}`;
|
|
1294
|
+
process.stderr.write(`kiso update: ${why} — run it yourself: ${INSTALL_COMMAND.join(" ")}\n`);
|
|
1295
|
+
process.exitCode = r.status ?? 1;
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
process.stdout.write("kiso updated — the next `kiso` runs the new version\n");
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1098
1301
|
case "help": {
|
|
1099
1302
|
// PH-1b (finding PH-F21): the CLI must be able to describe its
|
|
1100
1303
|
// own configuration — the old help listed five commands and
|
|
@@ -1108,6 +1311,11 @@ async function main() {
|
|
|
1108
1311
|
" kiso resume pick a session to continue (TTY picker)\n" +
|
|
1109
1312
|
" kiso resume <id> [prompt] continue a session (one-shot)\n" +
|
|
1110
1313
|
" kiso sessions list durable sessions\n" +
|
|
1314
|
+
" kiso login <provider> anthropic|openai|deepseek|zai: store an API key (hidden prompt, or stdin when piped);\n" +
|
|
1315
|
+
" chatgpt: sign in with a ChatGPT subscription (browser; unofficial third-party flow)\n" +
|
|
1316
|
+
" kiso logout <provider> remove the stored credential\n" +
|
|
1317
|
+
" kiso auth list stored credentials (keys masked)\n" +
|
|
1318
|
+
" kiso update install the latest release (npm i -g @vincemakes/kiso-code@latest)\n" +
|
|
1111
1319
|
" kiso help this help\n\n" +
|
|
1112
1320
|
"flags (any position):\n" +
|
|
1113
1321
|
" --model <profile|provider/model> pick the model (also /model in-session)\n" +
|
package/dist/mode.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* switches take effect immediately. The extension NAME rides the runtime's decidedBy
|
|
7
7
|
* field: an automated denial records decidedBy: "mode:<name>" — the
|
|
8
8
|
* audit sell. User-level extensions stay on the chain AFTER the mode
|
|
9
|
-
* tiers; a user deny always wins (the chain's deny>ask
|
|
9
|
+
* tiers; a user deny always wins (the chain's deny>allow>ask
|
|
10
10
|
* monotonicity — bypass cannot override an extension deny).
|
|
11
11
|
*/
|
|
12
12
|
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
@@ -29,7 +29,7 @@ export declare function modeFromEnv(): Mode | undefined;
|
|
|
29
29
|
* extension name). The CURRENT tier is first: an all-allow chain records
|
|
30
30
|
* decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
|
|
31
31
|
* names that mode honestly. Order never affects verdicts — the chain is
|
|
32
|
-
* deny>ask
|
|
32
|
+
* deny>allow>ask over the SPEAKING verdicts (abstain = no opinion), so a
|
|
33
33
|
* user extension's deny wins over any mode tier, bypass included (the
|
|
34
34
|
* monotonicity e2e pins it). */
|
|
35
35
|
export declare function modeExtensions(): readonly KisoExtension[];
|
package/dist/mode.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* switches take effect immediately. The extension NAME rides the runtime's decidedBy
|
|
7
7
|
* field: an automated denial records decidedBy: "mode:<name>" — the
|
|
8
8
|
* audit sell. User-level extensions stay on the chain AFTER the mode
|
|
9
|
-
* tiers; a user deny always wins (the chain's deny>ask
|
|
9
|
+
* tiers; a user deny always wins (the chain's deny>allow>ask
|
|
10
10
|
* monotonicity — bypass cannot override an extension deny).
|
|
11
11
|
*/
|
|
12
12
|
export const MODES = ["manual", "default", "accept-edits", "plan", "bypass"];
|
|
@@ -78,7 +78,7 @@ function tierVerdict(tier, call) {
|
|
|
78
78
|
* extension name). The CURRENT tier is first: an all-allow chain records
|
|
79
79
|
* decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
|
|
80
80
|
* names that mode honestly. Order never affects verdicts — the chain is
|
|
81
|
-
* deny>ask
|
|
81
|
+
* deny>allow>ask over the SPEAKING verdicts (abstain = no opinion), so a
|
|
82
82
|
* user extension's deny wins over any mode tier, bypass included (the
|
|
83
83
|
* monotonicity e2e pins it). */
|
|
84
84
|
export function modeExtensions() {
|
package/dist/state.d.ts
CHANGED
|
@@ -156,7 +156,10 @@ export declare function setSessionStore(value: {
|
|
|
156
156
|
}): void;
|
|
157
157
|
/** The model name for the status bar — set by makeAgent. */
|
|
158
158
|
export declare let agentModel: string;
|
|
159
|
-
|
|
159
|
+
/** OR-1: the live model's ENDPOINT, set in the same call as the model so
|
|
160
|
+
* the two can never drift: the window lookup keys on (model, endpoint). */
|
|
161
|
+
export declare let agentBaseUrl: string | undefined;
|
|
162
|
+
export declare function setAgentModel(value: string, baseUrl?: string): void;
|
|
160
163
|
/** merge round B: whether the agent runs on the faux provider (no real key) —
|
|
161
164
|
* set inside makeAgent, read by main for chat/resume's exhaustion check. */
|
|
162
165
|
export declare let currentFaux: boolean;
|
|
@@ -213,7 +216,9 @@ export declare const CANCELLED: unique symbol;
|
|
|
213
216
|
export interface LastBinding {
|
|
214
217
|
readonly adapter: import("@vincemakes/kiso-core").Adapter;
|
|
215
218
|
readonly model: string;
|
|
216
|
-
readonly provider?: "anthropic" | "openai-compat";
|
|
219
|
+
readonly provider?: "anthropic" | "openai-compat" | "openai-responses";
|
|
220
|
+
/** OR-1: the endpoint inherits with the adapter (the cost path keys on it). */
|
|
221
|
+
readonly baseUrl?: string;
|
|
217
222
|
readonly scope?: import("@vincemakes/kiso-core").ContinuationScope;
|
|
218
223
|
readonly reasoning?: import("@vincemakes/kiso-runtime/internal").ReasoningSetting;
|
|
219
224
|
}
|
package/dist/state.js
CHANGED
|
@@ -213,8 +213,12 @@ export function setSessionStore(value) {
|
|
|
213
213
|
}
|
|
214
214
|
/** The model name for the status bar — set by makeAgent. */
|
|
215
215
|
export let agentModel = "faux";
|
|
216
|
-
|
|
216
|
+
/** OR-1: the live model's ENDPOINT, set in the same call as the model so
|
|
217
|
+
* the two can never drift: the window lookup keys on (model, endpoint). */
|
|
218
|
+
export let agentBaseUrl;
|
|
219
|
+
export function setAgentModel(value, baseUrl) {
|
|
217
220
|
agentModel = value;
|
|
221
|
+
agentBaseUrl = baseUrl;
|
|
218
222
|
}
|
|
219
223
|
/** merge round B: whether the agent runs on the faux provider (no real key) —
|
|
220
224
|
* set inside makeAgent, read by main for chat/resume's exhaustion check. */
|
package/dist/trust-ui.d.ts
CHANGED
|
@@ -37,6 +37,19 @@ export declare let pendingAsk: (() => void) | null;
|
|
|
37
37
|
export declare function askPanel(input: LineInput, view: PanelView, opts?: {
|
|
38
38
|
safer?: () => Promise<SaferAnswer>;
|
|
39
39
|
}): Promise<PanelVerdict>;
|
|
40
|
+
/**
|
|
41
|
+
* The dock-less fallback line → a verdict. `y…` allows. `n <reason>` / `no
|
|
42
|
+
* <reason>` DENIES WITH THE REASON — the words become the tool result and
|
|
43
|
+
* the run continues with the model seeing them, the same verdict the panel's
|
|
44
|
+
* "No + words" produces (chat.ts, the deny case). A bare `n`/`no`, or any
|
|
45
|
+
* other line, is the bare denial that aborts the run — unchanged. (LH-1 ruling
|
|
46
|
+
* 2026-09-08: the default refusal across arms is refuse-with-a-reason-and-
|
|
47
|
+
* continue, and the surface a piped or 0-row leg drives is this one.)
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseFallbackAnswer(answer: string): {
|
|
50
|
+
action: "allow" | "deny";
|
|
51
|
+
reason: string;
|
|
52
|
+
};
|
|
40
53
|
/**
|
|
41
54
|
* KC3.5 — the AskUI bridge: the panel the ask extension asks through.
|
|
42
55
|
*
|
package/dist/trust-ui.js
CHANGED
|
@@ -104,12 +104,27 @@ opts) {
|
|
|
104
104
|
}
|
|
105
105
|
settled = true;
|
|
106
106
|
pendingAsk = null;
|
|
107
|
-
|
|
108
|
-
resolve(yes ? { action: "allow", reason: "" } : { action: "deny", reason: "" });
|
|
107
|
+
resolve(parseFallbackAnswer(answer));
|
|
109
108
|
});
|
|
110
109
|
}
|
|
111
110
|
});
|
|
112
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* The dock-less fallback line → a verdict. `y…` allows. `n <reason>` / `no
|
|
114
|
+
* <reason>` DENIES WITH THE REASON — the words become the tool result and
|
|
115
|
+
* the run continues with the model seeing them, the same verdict the panel's
|
|
116
|
+
* "No + words" produces (chat.ts, the deny case). A bare `n`/`no`, or any
|
|
117
|
+
* other line, is the bare denial that aborts the run — unchanged. (LH-1 ruling
|
|
118
|
+
* 2026-09-08: the default refusal across arms is refuse-with-a-reason-and-
|
|
119
|
+
* continue, and the surface a piped or 0-row leg drives is this one.)
|
|
120
|
+
*/
|
|
121
|
+
export function parseFallbackAnswer(answer) {
|
|
122
|
+
const line = answer.trim();
|
|
123
|
+
if (line.toLowerCase().startsWith("y"))
|
|
124
|
+
return { action: "allow", reason: "" };
|
|
125
|
+
const m = /^(?:n|no)\b[\s:,-]*(.*)$/is.exec(line);
|
|
126
|
+
return { action: "deny", reason: (m?.[1] ?? "").trim() };
|
|
127
|
+
}
|
|
113
128
|
/**
|
|
114
129
|
* KC3.5 — the AskUI bridge: the panel the ask extension asks through.
|
|
115
130
|
*
|
package/dist/update-check.d.ts
CHANGED
|
@@ -32,8 +32,6 @@ export interface UpdateCache {
|
|
|
32
32
|
readonly checkedAt: number;
|
|
33
33
|
/** the latest version the registry reported, if the check succeeded */
|
|
34
34
|
readonly latest?: string;
|
|
35
|
-
/** the version already mentioned to this human — never mentioned twice */
|
|
36
|
-
readonly told?: string;
|
|
37
35
|
}
|
|
38
36
|
/**
|
|
39
37
|
* Semver-ish "is b newer than a", for the shapes this line ever sees:
|
|
@@ -44,7 +42,15 @@ export interface UpdateCache {
|
|
|
44
42
|
export declare function isNewer(current: string, latest: string): boolean;
|
|
45
43
|
/** The one line, when there is one. Words only — a human can read it,
|
|
46
44
|
* paste it, and nothing about it needs colour to be understood (§1.2). */
|
|
47
|
-
export declare const
|
|
45
|
+
export declare const CHANGELOG_URL = "https://github.com/vincemakes/kiso/releases";
|
|
46
|
+
/** OR-9 (owner, 2026-09-09): the card under the banner, as WORDS — a
|
|
47
|
+
* title, the version with the command that installs it, the changelog.
|
|
48
|
+
* The CLI styles them at composition; nothing here needs colour. */
|
|
49
|
+
export declare function updateCardLines(latest: string): readonly string[];
|
|
50
|
+
/** OR-9: what the cache already knows, with NO request — the boot paints
|
|
51
|
+
* it under the banner before any check runs. Same silences as the check
|
|
52
|
+
* (opt-out, faux without a seam, a pipe); a merely-recent cache speaks. */
|
|
53
|
+
export declare function knownUpdate(deps: UpdateCheckDeps): string | null;
|
|
48
54
|
export interface UpdateCheckDeps {
|
|
49
55
|
readonly kisoHome: string;
|
|
50
56
|
readonly version: string;
|
package/dist/update-check.js
CHANGED
|
@@ -51,7 +51,6 @@ function readCache(kisoHome) {
|
|
|
51
51
|
return {
|
|
52
52
|
checkedAt: c.checkedAt,
|
|
53
53
|
...(typeof c.latest === "string" ? { latest: c.latest } : {}),
|
|
54
|
-
...(typeof c.told === "string" ? { told: c.told } : {}),
|
|
55
54
|
};
|
|
56
55
|
}
|
|
57
56
|
catch {
|
|
@@ -97,7 +96,23 @@ export function isNewer(current, latest) {
|
|
|
97
96
|
}
|
|
98
97
|
/** The one line, when there is one. Words only — a human can read it,
|
|
99
98
|
* paste it, and nothing about it needs colour to be understood (§1.2). */
|
|
100
|
-
export const
|
|
99
|
+
export const CHANGELOG_URL = "https://github.com/vincemakes/kiso/releases";
|
|
100
|
+
/** OR-9 (owner, 2026-09-09): the card under the banner, as WORDS — a
|
|
101
|
+
* title, the version with the command that installs it, the changelog.
|
|
102
|
+
* The CLI styles them at composition; nothing here needs colour. */
|
|
103
|
+
export function updateCardLines(latest) {
|
|
104
|
+
return ["Update available", `New version ${latest} is available. Run kiso update`, `Changelog: ${CHANGELOG_URL}`];
|
|
105
|
+
}
|
|
106
|
+
/** OR-9: what the cache already knows, with NO request — the boot paints
|
|
107
|
+
* it under the banner before any check runs. Same silences as the check
|
|
108
|
+
* (opt-out, faux without a seam, a pipe); a merely-recent cache speaks. */
|
|
109
|
+
export function knownUpdate(deps) {
|
|
110
|
+
const why = reasonNotToCheck(deps);
|
|
111
|
+
if (why !== null && why !== "recent")
|
|
112
|
+
return null;
|
|
113
|
+
const cached = readCache(deps.kisoHome);
|
|
114
|
+
return cached?.latest !== undefined && isNewer(deps.version, cached.latest) ? cached.latest : null;
|
|
115
|
+
}
|
|
101
116
|
/**
|
|
102
117
|
* Should this launch even ask? Pure, so the reasons are testable without
|
|
103
118
|
* a network, a clock or a terminal.
|
|
@@ -142,21 +157,14 @@ function reasonNotToCheck({ kisoHome, isTTY, faux, now = Date.now() }) {
|
|
|
142
157
|
export async function checkForUpdate(deps) {
|
|
143
158
|
const { kisoHome, version, now = Date.now() } = deps;
|
|
144
159
|
const why = reasonNotToCheck(deps);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const cached = readCache(kisoHome);
|
|
154
|
-
if (cached?.latest !== undefined && cached.told !== cached.latest && isNewer(version, cached.latest)) {
|
|
155
|
-
writeCache(kisoHome, { ...cached, told: cached.latest });
|
|
156
|
-
return updateLine(cached.latest);
|
|
157
|
-
}
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
+
// ONLY a recent check may still speak from cache. The other three
|
|
161
|
+
// reasons are about the CARD, not the request, so the cache is no way
|
|
162
|
+
// around them. OR-9: no `told` gate — a newer version is known at
|
|
163
|
+
// EVERY start (the card is shown every start; the once-only rule of
|
|
164
|
+
// §7.10 is superseded by the owner's 2026-09-09 ruling), only the
|
|
165
|
+
// REQUEST stays at most once a day.
|
|
166
|
+
if (why !== null)
|
|
167
|
+
return why === "recent" ? knownUpdate(deps) : null;
|
|
160
168
|
let latest;
|
|
161
169
|
try {
|
|
162
170
|
const ctl = new AbortController();
|
|
@@ -177,15 +185,6 @@ export async function checkForUpdate(deps) {
|
|
|
177
185
|
catch {
|
|
178
186
|
// every failure is silence — see the header
|
|
179
187
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// `told` survives a check that learned nothing: forgetting it would
|
|
183
|
-
// re-announce a version this human has already been told about.
|
|
184
|
-
const told = show ? latest : prev?.told;
|
|
185
|
-
writeCache(kisoHome, {
|
|
186
|
-
checkedAt: now,
|
|
187
|
-
...(latest === undefined ? {} : { latest }),
|
|
188
|
-
...(told === undefined ? {} : { told }),
|
|
189
|
-
});
|
|
190
|
-
return show && latest !== undefined ? updateLine(latest) : null;
|
|
188
|
+
writeCache(kisoHome, { checkedAt: now, ...(latest === undefined ? {} : { latest }) });
|
|
189
|
+
return latest !== undefined && isNewer(version, latest) ? latest : null;
|
|
191
190
|
}
|