@vincemakes/kiso-code 0.45.3 → 0.46.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/README.md +2 -1
- package/dist/builtin.d.ts +5 -4
- package/dist/builtin.js +12 -7
- package/dist/chat.js +121 -19
- package/dist/child-result.d.ts +23 -0
- package/dist/child-result.js +59 -0
- package/dist/config.d.ts +13 -6
- package/dist/config.js +30 -16
- package/dist/create-coding-agent.js +13 -1
- package/dist/dispatch.d.ts +2 -0
- package/dist/dispatch.js +170 -33
- package/dist/empty-sessions.d.ts +4 -2
- package/dist/empty-sessions.js +6 -2
- package/dist/external-editor.js +4 -1
- package/dist/floor.js +71 -26
- package/dist/index.js +89 -29
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +43 -0
- package/dist/mode.d.ts +90 -17
- package/dist/mode.js +135 -32
- package/dist/preferences.d.ts +14 -0
- package/dist/preferences.js +34 -3
- package/dist/protected-shell.js +60 -19
- package/dist/protected-writes.d.ts +2 -2
- package/dist/protected-writes.js +2 -2
- package/dist/readonly-shell.js +16 -14
- package/dist/replay.js +3 -0
- package/dist/resume.js +2 -2
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +41 -11
- package/dist/shell-words.d.ts +34 -1
- package/dist/shell-words.js +84 -4
- package/dist/state.d.ts +31 -0
- package/dist/state.js +88 -2
- package/dist/task-notice.d.ts +22 -0
- package/dist/task-notice.js +86 -0
- package/dist/trust-ui.js +6 -6
- package/package.json +14 -14
package/dist/floor.js
CHANGED
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { existsSync, statSync } from "node:fs";
|
|
44
44
|
import { homedir, tmpdir } from "node:os";
|
|
45
|
-
import { basename,
|
|
46
|
-
import {
|
|
45
|
+
import { basename, resolve, win32 } from "node:path";
|
|
46
|
+
import { homeSubtrees, hostDialect, looseCommands, msysPath, parseShellLooseChecked, resolveShellPath } from "./shell-words.js";
|
|
47
47
|
/** R8 (the lead's ruling): a system root AND everything inside it. */
|
|
48
48
|
const SYSTEM_ROOTS = [
|
|
49
49
|
"/bin", "/boot", "/dev", "/etc", "/lib", "/lib64", "/opt", "/proc", "/root", "/run", "/sbin", "/srv", "/sys", "/usr", "/var",
|
|
@@ -59,28 +59,46 @@ const TEMP_ROOTS = ["/tmp", "/private/tmp", "/var/tmp", "/private/var/tmp", "/va
|
|
|
59
59
|
* only the prefix itself; what is inside runs (`brew` put it there and can
|
|
60
60
|
* again). */
|
|
61
61
|
const SOFTWARE_PREFIXES = ["/opt/homebrew", "/usr/local", "/home/linuxbrew/.linuxbrew"];
|
|
62
|
+
/** Windows P2: the same tables from the environment, never a hard-coded
|
|
63
|
+
* user name. System roots: SystemRoot and the program and data roots;
|
|
64
|
+
* the users root works as a mount root (it and each home in it refused);
|
|
65
|
+
* the temp family is TEMP/TMP. Every drive root is refused in
|
|
66
|
+
* `unrecoverable`. */
|
|
67
|
+
function windowsRoots(home) {
|
|
68
|
+
const env = process.env;
|
|
69
|
+
const set = (values) => values.filter((v) => v !== undefined && v !== "");
|
|
70
|
+
return {
|
|
71
|
+
sys: set([env.SystemRoot, env.ProgramFiles, env["ProgramFiles(x86)"], env.ProgramData]),
|
|
72
|
+
mounts: [win32.dirname(home)],
|
|
73
|
+
temps: set([env.TEMP, env.TMP]),
|
|
74
|
+
prefixes: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
62
77
|
/** Memoized per (workspace, home): the roots do not move within a session,
|
|
63
78
|
* and resolving forty of them cost every shell call ~4 ms. */
|
|
64
79
|
const WHERE = new Map();
|
|
65
80
|
function where(root, home) {
|
|
66
|
-
const
|
|
81
|
+
const dialect = hostDialect();
|
|
82
|
+
const key = `${dialect.msys ? "msys" : "posix"}\0${root}\0${home}`;
|
|
67
83
|
const hit = WHERE.get(key);
|
|
68
84
|
if (hit !== undefined)
|
|
69
85
|
return hit;
|
|
70
|
-
const
|
|
86
|
+
const { join } = dialect.path;
|
|
87
|
+
const real = (p) => resolveShellPath(root, dialect.msys ? root : "/", p, dialect).canonical;
|
|
71
88
|
const named = (names) => names.map((name) => ({ name, real: real(name) }));
|
|
72
89
|
const rootReal = real(root);
|
|
90
|
+
const roots = dialect.msys ? windowsRoots(home) : { sys: SYSTEM_ROOTS, mounts: MOUNT_ROOTS, temps: [...TEMP_ROOTS, tmpdir()], prefixes: SOFTWARE_PREFIXES };
|
|
73
91
|
const w = {
|
|
74
92
|
root,
|
|
75
93
|
home,
|
|
76
94
|
rootReal,
|
|
77
95
|
homeReal: real(home),
|
|
78
96
|
gitReal: join(rootReal, ".git"),
|
|
79
|
-
sys: named(
|
|
80
|
-
mounts: named(
|
|
81
|
-
temps: [...new Set(
|
|
82
|
-
prefixes: named(
|
|
83
|
-
subtrees:
|
|
97
|
+
sys: named(roots.sys),
|
|
98
|
+
mounts: named(roots.mounts),
|
|
99
|
+
temps: [...new Set(roots.temps.map(real))],
|
|
100
|
+
prefixes: named(roots.prefixes),
|
|
101
|
+
subtrees: homeSubtrees().map((name) => ({ name, real: real(join(home, name)) })),
|
|
84
102
|
};
|
|
85
103
|
WHERE.set(key, w);
|
|
86
104
|
return w;
|
|
@@ -97,27 +115,35 @@ function capCwds(w, list) {
|
|
|
97
115
|
}
|
|
98
116
|
const canon = (w, cwd, p) => resolveShellPath(w.root, cwd, p).canonical;
|
|
99
117
|
const within = (parent, p) => {
|
|
118
|
+
const { relative, isAbsolute } = hostDialect().path;
|
|
100
119
|
const rel = relative(parent, p);
|
|
101
120
|
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
102
121
|
};
|
|
122
|
+
/** The same path: case-insensitive on Windows (Windows P2). */
|
|
123
|
+
const same = (a, b) => (hostDialect().msys ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
103
124
|
/** Why `p` (canonical) is unrecoverable, or null. `over` is true for a
|
|
104
125
|
* wildcard over `p` rather than `p` itself — the same set, since
|
|
105
126
|
* emptying a directory is removing everything it held. */
|
|
106
127
|
function unrecoverable(w, p, over) {
|
|
107
128
|
const what = (s) => (over ? `a wildcard over ${s}` : s);
|
|
129
|
+
const msys = hostDialect().msys;
|
|
108
130
|
if (p === "/")
|
|
109
131
|
return what("/");
|
|
110
|
-
if (p ===
|
|
132
|
+
if (msys && win32.parse(p).root === p)
|
|
133
|
+
return what(`a drive root (${p})`);
|
|
134
|
+
if (same(p, w.homeReal))
|
|
111
135
|
return what("the home directory");
|
|
112
|
-
if (p
|
|
136
|
+
if (same(p, w.rootReal))
|
|
113
137
|
return what("the workspace root");
|
|
114
138
|
if (within(p, w.rootReal))
|
|
115
139
|
return what(`a directory above the workspace (${p})`);
|
|
116
140
|
// R7: the workspace's history — without a remote, gone
|
|
117
141
|
if (within(w.gitReal, p))
|
|
118
142
|
return what("the workspace's .git");
|
|
143
|
+
// on Windows the temp root sits inside AppData: what is inside a temp
|
|
144
|
+
// root that lies inside the subtree runs
|
|
119
145
|
for (const sub of w.subtrees)
|
|
120
|
-
if (within(sub.real, p))
|
|
146
|
+
if (within(sub.real, p) && !(msys && w.temps.some((t) => within(sub.real, t) && !same(p, t) && within(t, p))))
|
|
121
147
|
return what(`~/${sub.name}`);
|
|
122
148
|
// Inside the workspace or the home directory is the project's and the
|
|
123
149
|
// person's, never a system root's — or a workspace in /opt, or a root
|
|
@@ -125,21 +151,21 @@ function unrecoverable(w, p, over) {
|
|
|
125
151
|
if ((w.rootReal !== "/" && within(w.rootReal, p)) || within(w.homeReal, p))
|
|
126
152
|
return null;
|
|
127
153
|
for (const t of w.temps) {
|
|
128
|
-
if (p
|
|
154
|
+
if (same(p, t))
|
|
129
155
|
return what(`a temp root (${t})`);
|
|
130
156
|
if (within(t, p))
|
|
131
157
|
return null;
|
|
132
158
|
}
|
|
133
159
|
for (const x of w.prefixes) {
|
|
134
|
-
if (p
|
|
160
|
+
if (same(p, x.real))
|
|
135
161
|
return what(`a software prefix (${x.name})`);
|
|
136
162
|
if (within(x.real, p))
|
|
137
163
|
return null;
|
|
138
164
|
}
|
|
139
165
|
for (const m of w.mounts) {
|
|
140
|
-
if (p
|
|
166
|
+
if (same(p, m.real))
|
|
141
167
|
return what(`a system root (${m.name})`);
|
|
142
|
-
if (dirname(p)
|
|
168
|
+
if (same(hostDialect().path.dirname(p), m.real))
|
|
143
169
|
return what(`a mount or home root (${p})`);
|
|
144
170
|
// deeper runs — by RESOLVED path: macOS's /home is
|
|
145
171
|
// /System/Volumes/Data/home, which the /System rule would refuse
|
|
@@ -147,7 +173,7 @@ function unrecoverable(w, p, over) {
|
|
|
147
173
|
return null;
|
|
148
174
|
}
|
|
149
175
|
for (const s of w.sys) {
|
|
150
|
-
if (p
|
|
176
|
+
if (same(p, s.real))
|
|
151
177
|
return what(`a system root (${s.name})`);
|
|
152
178
|
if (within(s.real, p))
|
|
153
179
|
return what(`a path inside a system root (${s.name})`);
|
|
@@ -169,7 +195,9 @@ function reUnknown(text, from) {
|
|
|
169
195
|
/** One target word, from each directory the command might run in. */
|
|
170
196
|
function targetWhy(w, cwds, word) {
|
|
171
197
|
for (const cwd of cwds) {
|
|
172
|
-
|
|
198
|
+
// Windows P2: either separator separates (the same length, so the
|
|
199
|
+
// unknown positions hold)
|
|
200
|
+
let text = hostDialect().msys ? word.text.replace(/\\/g, "/") : word.text;
|
|
173
201
|
let at = word.unknownAt;
|
|
174
202
|
let glob = word.unknownIsGlob;
|
|
175
203
|
let variableOnly = word.variableOnly;
|
|
@@ -189,7 +217,10 @@ function targetWhy(w, cwds, word) {
|
|
|
189
217
|
if (variableOnly)
|
|
190
218
|
return `a target that is only a variable (${word.text})`;
|
|
191
219
|
if (at < 0) {
|
|
192
|
-
const
|
|
220
|
+
const target = resolveShellPath(w.root, cwd, text);
|
|
221
|
+
if (target.opaque !== undefined)
|
|
222
|
+
return `${word.text}, ${target.opaque}`;
|
|
223
|
+
const why = unrecoverable(w, target.canonical, false);
|
|
193
224
|
if (why !== null)
|
|
194
225
|
return why;
|
|
195
226
|
continue;
|
|
@@ -200,7 +231,10 @@ function targetWhy(w, cwds, word) {
|
|
|
200
231
|
continue;
|
|
201
232
|
const prefix = text.slice(0, at);
|
|
202
233
|
const lastSlash = prefix.lastIndexOf("/");
|
|
203
|
-
const
|
|
234
|
+
const over = resolveShellPath(w.root, cwd, lastSlash < 0 ? "." : prefix.slice(0, lastSlash + 1) || "/");
|
|
235
|
+
if (over.opaque !== undefined)
|
|
236
|
+
return `a wildcard in ${word.text}, ${over.opaque}`;
|
|
237
|
+
const dir = over.canonical;
|
|
204
238
|
const head = prefix.slice(lastSlash + 1); // the literal start of the unknown component
|
|
205
239
|
const nextSlash = text.indexOf("/", at);
|
|
206
240
|
const component = text.slice(lastSlash + 1, nextSlash < 0 ? text.length : nextSlash);
|
|
@@ -210,7 +244,7 @@ function targetWhy(w, cwds, word) {
|
|
|
210
244
|
for (const sub of w.subtrees) {
|
|
211
245
|
if (within(sub.real, dir))
|
|
212
246
|
return `a wildcard inside ~/${sub.name}`;
|
|
213
|
-
if (glob && dir
|
|
247
|
+
if (glob && same(dir, w.homeReal) && head !== "" && sub.name.toLowerCase().startsWith(head.toLowerCase()))
|
|
214
248
|
return `a wildcard that reaches ~/${sub.name}`;
|
|
215
249
|
}
|
|
216
250
|
// B9: over the directory ONLY when the unknown component is nothing
|
|
@@ -318,7 +352,7 @@ const SELECTING = new Set([
|
|
|
318
352
|
]);
|
|
319
353
|
/** R5: a checkout/restore operand is a path only when path-shaped — a
|
|
320
354
|
* branch name is never refused. */
|
|
321
|
-
const pathShaped = (t) => t === "." || t === "./" || t.startsWith("/") || t.startsWith("~") || t.includes("/") || t.startsWith(":");
|
|
355
|
+
const pathShaped = (t) => t === "." || t === "./" || t.startsWith("/") || t.startsWith("~") || t.includes("/") || t.startsWith(":") || (hostDialect().msys && (t.includes("\\") || /^[A-Za-z]:/.test(t)));
|
|
322
356
|
/** B7: pathspec magic from the top of the repository */
|
|
323
357
|
const topMagic = (t) => t.startsWith(":/") || t.startsWith(":(top");
|
|
324
358
|
/** The destructive command's targets, or null when it is not destructive. */
|
|
@@ -447,7 +481,7 @@ function destructiveTargets(argv) {
|
|
|
447
481
|
function isGitDir(p) {
|
|
448
482
|
try {
|
|
449
483
|
const st = statSync(p);
|
|
450
|
-
return st.isFile() || (st.isDirectory() && existsSync(join(p, "HEAD")));
|
|
484
|
+
return st.isFile() || (st.isDirectory() && existsSync(hostDialect().path.join(p, "HEAD")));
|
|
451
485
|
}
|
|
452
486
|
catch {
|
|
453
487
|
return false;
|
|
@@ -458,6 +492,7 @@ function isGitDir(p) {
|
|
|
458
492
|
* inside the workspace, the workspace root; outside it, the directory
|
|
459
493
|
* itself — git would refuse to run there, and the floor does not bet on it. */
|
|
460
494
|
function repoRoot(w, cwd) {
|
|
495
|
+
const { dirname, join } = hostDialect().path;
|
|
461
496
|
const start = canon(w, cwd, ".");
|
|
462
497
|
for (let d = start;; d = dirname(d)) {
|
|
463
498
|
if (isGitDir(join(d, ".git")))
|
|
@@ -467,6 +502,16 @@ function repoRoot(w, cwd) {
|
|
|
467
502
|
}
|
|
468
503
|
return within(w.rootReal, start) ? w.rootReal : start;
|
|
469
504
|
}
|
|
505
|
+
/** Windows P2: where a cd to an unreadable path lands — every relative
|
|
506
|
+
* path from it is unreadable too. */
|
|
507
|
+
const UNREADABLE_DIR = "//?/unreadable";
|
|
508
|
+
/** Where `cd text` from `c` lands, as the shell spells it (lexically). */
|
|
509
|
+
function cdTo(c, text) {
|
|
510
|
+
if (!hostDialect().msys)
|
|
511
|
+
return resolve(c, text);
|
|
512
|
+
const read = msysPath(text);
|
|
513
|
+
return "opaque" in read || c === UNREADABLE_DIR ? UNREADABLE_DIR : win32.resolve(c, read.native);
|
|
514
|
+
}
|
|
470
515
|
/** A child shell starts where this node runs — the node's joiner has
|
|
471
516
|
* already been applied — and nothing it does comes back. */
|
|
472
517
|
const child = (s) => ({ cwds: [...s.cwds], lastCd: null, prev: s.prev });
|
|
@@ -544,7 +589,7 @@ export function floorCheck(commandLine, workspaceRoot, home = homedir()) {
|
|
|
544
589
|
else if (to === undefined || to.unknownAt >= 0 || to.variableOnly)
|
|
545
590
|
dest = capCwds(w, [w.home, ...state.cwds]);
|
|
546
591
|
else
|
|
547
|
-
dest = capCwds(w, state.cwds.map((c) =>
|
|
592
|
+
dest = capCwds(w, state.cwds.map((c) => cdTo(c, tilde(to))));
|
|
548
593
|
state.prev = state.cwds;
|
|
549
594
|
state.lastCd = { to: dest, from };
|
|
550
595
|
continue;
|
|
@@ -554,12 +599,12 @@ export function floorCheck(commandLine, workspaceRoot, home = homedir()) {
|
|
|
554
599
|
continue;
|
|
555
600
|
let here = state.cwds;
|
|
556
601
|
for (const cd of d.cds ?? [])
|
|
557
|
-
here = cd.unknownAt >= 0 ? capCwds(w, [...here, w.home]) : here.map((c) =>
|
|
602
|
+
here = cd.unknownAt >= 0 ? capCwds(w, [...here, w.home]) : here.map((c) => cdTo(c, tilde(cd)));
|
|
558
603
|
for (const t of d.targets) {
|
|
559
604
|
let why = null;
|
|
560
605
|
if (t === "repo") {
|
|
561
606
|
for (const c of here)
|
|
562
|
-
if ((why = unrecoverable(w, repoRoot(w, c), false)) !== null)
|
|
607
|
+
if ((why = c === UNREADABLE_DIR ? "a repository in a directory that cannot be read" : unrecoverable(w, repoRoot(w, c), false)) !== null)
|
|
563
608
|
break;
|
|
564
609
|
}
|
|
565
610
|
else
|
package/dist/index.js
CHANGED
|
@@ -37,14 +37,16 @@ import { skillMenuItems } from "./skill-invoke.js";
|
|
|
37
37
|
import { canonicalPath, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
|
|
38
38
|
import { reverseMigration } from "./session-migration.js";
|
|
39
39
|
import { defaultTrashRoot, findEmptySessions, moveToTrash } from "./empty-sessions.js";
|
|
40
|
+
import { browserCommand, launchCommand } from "./launch.js";
|
|
40
41
|
import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
41
42
|
import { isProtectedPath, protectedIdentity, PROTECTED_REFUSAL } from "@vincemakes/kiso-tools-node";
|
|
42
|
-
import {
|
|
43
|
+
import { OFFERED_MODES, applyModeState, envModeLayer, getDontAsk, modeDisplay, parseMode, resolveModeLayers } from "./mode.js";
|
|
43
44
|
import { activeStoreDir, setActiveStoreDir, agentModel, atFiles, body, bodyLog, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, loadedSkillsCatalog, queuedSwitchLines } from "./state.js";
|
|
44
45
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
45
46
|
import { chat, contextWindowTokens, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
|
|
46
47
|
import { preferences, usePreferences } from "./preferences.js";
|
|
47
|
-
import { settingsLayers } from "./state.js";
|
|
48
|
+
import { settingsLayers, setChildTurnBudget, stopAllTasks } from "./state.js";
|
|
49
|
+
import { finishChild } from "./child-result.js";
|
|
48
50
|
import { loadUserConfig, resolveAutoCompact } from "./config.js";
|
|
49
51
|
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
50
52
|
import { tmuxMouseHint } from "./tmux-hint.js";
|
|
@@ -74,14 +76,14 @@ export { applyProjectMerges } from "./trust-ui.js";
|
|
|
74
76
|
* path's exact semantics (auto-denied asks, no chips, no pops). */
|
|
75
77
|
/** OR-4: hand a URL to the platform's opener, detached, and never let a
|
|
76
78
|
* missing or failing opener touch the sign-in — the printed URL is the
|
|
77
|
-
* path that always works. macOS `open`, Linux `xdg-open
|
|
78
|
-
* URL
|
|
79
|
+
* path that always works. macOS `open`, Linux `xdg-open`, Windows
|
|
80
|
+
* rundll32's URL handler (launch.ts); elsewhere the URL stays printed. */
|
|
79
81
|
function openInBrowser(url) {
|
|
80
|
-
const opener =
|
|
82
|
+
const opener = browserCommand(url);
|
|
81
83
|
if (opener === null)
|
|
82
84
|
return;
|
|
83
85
|
try {
|
|
84
|
-
spawn(opener, [
|
|
86
|
+
spawn(opener[0], [...opener[1]], { detached: true, stdio: "ignore" }).unref();
|
|
85
87
|
}
|
|
86
88
|
catch {
|
|
87
89
|
// the URL is on screen; opening it is the person's fallback
|
|
@@ -180,6 +182,9 @@ function readlineInput(rl) {
|
|
|
180
182
|
/* §2.3: same as ctrl+o above — readline has no binding, and a
|
|
181
183
|
* dock-less session has no committed rows to reprint. */
|
|
182
184
|
},
|
|
185
|
+
onBackground() {
|
|
186
|
+
/* 3e: readline has no ctrl+b — /tasks and a steer still work. */
|
|
187
|
+
},
|
|
183
188
|
onEditor() {
|
|
184
189
|
/* §2.4: readline has no ctrl+g, and a piped session has no
|
|
185
190
|
* composer to hand over. */
|
|
@@ -274,6 +279,9 @@ function editorInput(editor) {
|
|
|
274
279
|
onThink(cb) {
|
|
275
280
|
editor.onThink(cb);
|
|
276
281
|
},
|
|
282
|
+
onBackground(cb) {
|
|
283
|
+
editor.onBackground(cb);
|
|
284
|
+
},
|
|
277
285
|
onEditor(cb) {
|
|
278
286
|
editor.onEditor(cb);
|
|
279
287
|
},
|
|
@@ -488,10 +496,10 @@ function bannerExtensionText() {
|
|
|
488
496
|
// terminal layer (extensionsBannerText — a pure function of the three
|
|
489
497
|
// name lists, including the "(connecting…)" in-flight label). Which
|
|
490
498
|
// lists exist is the CLI's fact and stays here.
|
|
491
|
-
// 0.40.0:
|
|
492
|
-
// (builtin.ts offInDontAsk) — the banner says so
|
|
493
|
-
//
|
|
494
|
-
const builtIn = builtInExtensions.map((e) => (e.name === "ask" &&
|
|
499
|
+
// 0.40.0: with the don't-ask switch on the ask extension is loaded but
|
|
500
|
+
// offers no tool (builtin.ts offInDontAsk) — the banner says so, rather
|
|
501
|
+
// than listing it as if it could ask.
|
|
502
|
+
const builtIn = builtInExtensions.map((e) => (e.name === "ask" && getDontAsk() ? { name: e.name, note: "off in dontAsk" } : e));
|
|
495
503
|
return extensionsBannerText(builtIn, userExtensions, projectExtensions);
|
|
496
504
|
}
|
|
497
505
|
/** E1: the startup banner line(s) — TTY: logo + merged extensions + the
|
|
@@ -569,7 +577,7 @@ function extensionsBanner(resume = []) {
|
|
|
569
577
|
};
|
|
570
578
|
body.banner(VERSION, text.replace(/^ · /, ""), resume, {
|
|
571
579
|
model: agentModel,
|
|
572
|
-
mode:
|
|
580
|
+
mode: modeDisplay(),
|
|
573
581
|
cwd: cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd,
|
|
574
582
|
// DC-49 — REALPATH on both sides. A symlinked HOME (or a symlinked
|
|
575
583
|
// cwd) compares unequal as raw strings while being the same
|
|
@@ -808,7 +816,7 @@ function paintBootStatus(session) {
|
|
|
808
816
|
return;
|
|
809
817
|
// DF-0330-F1: the BOOT row gets the budget too — it is the same row, and
|
|
810
818
|
// the opening screen is where a long model id is first seen.
|
|
811
|
-
dock.setStatus(idleStatus(
|
|
819
|
+
dock.setStatus(idleStatus(modeDisplay(), statusModelLabel(session), displayCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80));
|
|
812
820
|
}
|
|
813
821
|
/** PH-1a (finding PH-F12): a usage error raised from inside the TUI —
|
|
814
822
|
* main's finally still runs (dock teardown, agent close, temp cleanup)
|
|
@@ -816,6 +824,9 @@ function paintBootStatus(session) {
|
|
|
816
824
|
class CliUsageError extends Error {
|
|
817
825
|
exitCode = 2;
|
|
818
826
|
}
|
|
827
|
+
/** ADR-0058 3d: set when this process is a background child (its budget
|
|
828
|
+
* and where its answer goes) — finished once its turn is done. */
|
|
829
|
+
let childEnd = null;
|
|
819
830
|
/** The /resume+/clear mini-spec — the chat LOOP: chat() ends with a
|
|
820
831
|
* directive; a switch re-enters it on another session with the SAME
|
|
821
832
|
* editor. First entry paints the banner; a switch paints one notice
|
|
@@ -1103,8 +1114,11 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1103
1114
|
...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
|
|
1104
1115
|
};
|
|
1105
1116
|
const end = await chat(session, currentFaux, input, autoCompact, nav, seed);
|
|
1106
|
-
if (end.next === "exit")
|
|
1117
|
+
if (end.next === "exit") {
|
|
1118
|
+
if (childEnd !== null)
|
|
1119
|
+
await finishChild(session, childEnd);
|
|
1107
1120
|
return;
|
|
1121
|
+
}
|
|
1108
1122
|
// DC-57: the lines that arrived with the switch command ride INTO the
|
|
1109
1123
|
// next entry — they were aimed at the session being asked for.
|
|
1110
1124
|
seed = end.lines ?? [];
|
|
@@ -1250,23 +1264,30 @@ async function main() {
|
|
|
1250
1264
|
}
|
|
1251
1265
|
args.splice(modelArgIdx, 2);
|
|
1252
1266
|
}
|
|
1253
|
-
// Modes:
|
|
1254
|
-
//
|
|
1255
|
-
//
|
|
1267
|
+
// Modes: flag > env > project config > user config > default, for the
|
|
1268
|
+
// tier and the don't-ask switch each (mode.ts resolveModeLayers). The
|
|
1269
|
+
// project config exists only once the trust gate has passed, so it joins
|
|
1270
|
+
// later (applyConfigMode); until then the other three decide.
|
|
1271
|
+
const flagLayer = {};
|
|
1256
1272
|
const modeFlag = args.indexOf("--mode");
|
|
1257
1273
|
if (modeFlag !== -1) {
|
|
1258
|
-
const
|
|
1259
|
-
if (
|
|
1260
|
-
console.error(`unknown mode: ${
|
|
1274
|
+
const raw = args[modeFlag + 1];
|
|
1275
|
+
if (raw === undefined || parseMode(raw) === undefined) {
|
|
1276
|
+
console.error(`unknown mode: ${raw} (tiers: ${OFFERED_MODES.join(", ")})`);
|
|
1261
1277
|
process.exit(2);
|
|
1262
1278
|
}
|
|
1263
|
-
|
|
1264
|
-
settingsLayers.modeFlag =
|
|
1279
|
+
flagLayer.mode = raw;
|
|
1280
|
+
settingsLayers.modeFlag = raw;
|
|
1265
1281
|
args.splice(modeFlag, 2);
|
|
1266
1282
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1283
|
+
const dontAskFlag = args.indexOf("--dont-ask");
|
|
1284
|
+
if (dontAskFlag !== -1) {
|
|
1285
|
+
flagLayer.dontAsk = true;
|
|
1286
|
+
settingsLayers.dontAskFlag = true;
|
|
1287
|
+
args.splice(dontAskFlag, 1);
|
|
1269
1288
|
}
|
|
1289
|
+
const modeLayers = (project) => ({ flag: flagLayer, env: envModeLayer(), project, user: loadUserConfig() });
|
|
1290
|
+
applyModeState(resolveModeLayers(modeLayers(null)));
|
|
1270
1291
|
// CX-1 F5: --task-file <path> — the file is the ONE user turn (the
|
|
1271
1292
|
// structured child entry). Read BEFORE any session exists: an
|
|
1272
1293
|
// unreadable file is a usage error with nothing executed.
|
|
@@ -1291,6 +1312,35 @@ async function main() {
|
|
|
1291
1312
|
args.splice(i, 2);
|
|
1292
1313
|
}
|
|
1293
1314
|
}
|
|
1315
|
+
// ADR-0058 3d: a background child's --max-turns N (its budget, D6) and
|
|
1316
|
+
// --result-file <path> (where its answer goes). Child-only: without
|
|
1317
|
+
// --task-file they are refused — the interactive door has no turn
|
|
1318
|
+
// limit (R3e).
|
|
1319
|
+
{
|
|
1320
|
+
const end = {};
|
|
1321
|
+
for (const flag of ["--max-turns", "--result-file"]) {
|
|
1322
|
+
const i = args.indexOf(flag);
|
|
1323
|
+
if (i === -1)
|
|
1324
|
+
continue;
|
|
1325
|
+
const value = args[i + 1];
|
|
1326
|
+
if (taskFile === undefined)
|
|
1327
|
+
throw new CliUsageError(`${flag} needs --task-file — it is for a delegated child only`);
|
|
1328
|
+
if (value === undefined || value.startsWith("--"))
|
|
1329
|
+
throw new CliUsageError(`${flag} needs a value`);
|
|
1330
|
+
if (flag === "--max-turns") {
|
|
1331
|
+
const n = Number(value);
|
|
1332
|
+
if (!Number.isInteger(n) || n < 1)
|
|
1333
|
+
throw new CliUsageError(`--max-turns: expected a whole number of at least 1, got ${value}`);
|
|
1334
|
+
end.maxTurns = n;
|
|
1335
|
+
}
|
|
1336
|
+
else
|
|
1337
|
+
end.resultFile = value;
|
|
1338
|
+
args.splice(i, 2);
|
|
1339
|
+
}
|
|
1340
|
+
if (end.maxTurns !== undefined || end.resultFile !== undefined)
|
|
1341
|
+
childEnd = end;
|
|
1342
|
+
setChildTurnBudget(end.maxTurns);
|
|
1343
|
+
}
|
|
1294
1344
|
// 0.40.0: `kiso sessions --all | --current` — the one command that takes
|
|
1295
1345
|
// them. Parsed only there, so neither ever becomes a session id. Absent
|
|
1296
1346
|
// = each form's default (TTY: current; pipe: all, today's bytes).
|
|
@@ -1433,10 +1483,9 @@ async function main() {
|
|
|
1433
1483
|
try {
|
|
1434
1484
|
// merge round B: the project config's mode applies AFTER the trust gate
|
|
1435
1485
|
// (its verdict decides whether the project config exists at all) —
|
|
1436
|
-
//
|
|
1486
|
+
// under whatever a higher layer (the flags, the env) already decided.
|
|
1437
1487
|
const applyConfigMode = () => {
|
|
1438
|
-
|
|
1439
|
-
setMode(mergedConfig.mode);
|
|
1488
|
+
applyModeState(resolveModeLayers(modeLayers(settingsLayers.project)));
|
|
1440
1489
|
};
|
|
1441
1490
|
if (printPrompt !== undefined) {
|
|
1442
1491
|
// the -p flow: recovery-first one-shot, the resume() machinery
|
|
@@ -1553,7 +1602,7 @@ async function main() {
|
|
|
1553
1602
|
console.log("kiso sessions --prune-empty --yes moves them to the Trash");
|
|
1554
1603
|
}
|
|
1555
1604
|
else
|
|
1556
|
-
console.log(`moved ${n(empty.length, "empty session")} to ${moveToTrash(empty, defaultTrashRoot())}`);
|
|
1605
|
+
console.log(`moved ${n(empty.length, "empty session")} to ${moveToTrash(empty, defaultTrashRoot(kisoHome()))}`);
|
|
1557
1606
|
if (inUse > 0)
|
|
1558
1607
|
console.log(`skipped ${n(inUse, "empty session")} in use — a live process holds the lock`);
|
|
1559
1608
|
break;
|
|
@@ -1719,7 +1768,9 @@ async function main() {
|
|
|
1719
1768
|
// npm's to explain; the message names the manual command so
|
|
1720
1769
|
// the person can run it with whatever their setup needs.
|
|
1721
1770
|
const [bin, ...installArgs] = INSTALL_COMMAND;
|
|
1722
|
-
|
|
1771
|
+
// Windows: npm is npm.cmd — through cmd.exe (launch.ts)
|
|
1772
|
+
const l = launchCommand(bin, installArgs);
|
|
1773
|
+
const r = spawnSync(l.file, [...l.args], { stdio: "inherit", ...(l.windowsVerbatimArguments === true ? { windowsVerbatimArguments: true } : {}) });
|
|
1723
1774
|
if (r.error !== undefined || r.status !== 0) {
|
|
1724
1775
|
const why = r.error !== undefined ? `could not run npm (${r.error.message})` : `npm exited ${r.status ?? "on a signal"}`;
|
|
1725
1776
|
process.stderr.write(`kiso update: ${why} — run it yourself: ${INSTALL_COMMAND.join(" ")}\n`);
|
|
@@ -1751,7 +1802,8 @@ async function main() {
|
|
|
1751
1802
|
" kiso help this help\n\n" +
|
|
1752
1803
|
"flags (any position):\n" +
|
|
1753
1804
|
" --model <profile|provider/model> pick the model (also /model in-session)\n" +
|
|
1754
|
-
" --mode <tier> approval tier: default|accept-edits|plan|
|
|
1805
|
+
" --mode <tier> approval tier: default|accept-edits|plan|full-access\n" +
|
|
1806
|
+
" --dont-ask never stop for a person: what would ask is refused\n" +
|
|
1755
1807
|
" --version print the version\n\n" +
|
|
1756
1808
|
"configuration:\n" +
|
|
1757
1809
|
" no key keyless faux demo (a scripted four-round session)\n" +
|
|
@@ -1795,11 +1847,19 @@ async function main() {
|
|
|
1795
1847
|
// variable now). A signal death skips this and leaves the dead-pid
|
|
1796
1848
|
// residue — the dead-holder takeover recovers it by design
|
|
1797
1849
|
// (ADR-0050); the lock never outlives a live writer either way.
|
|
1850
|
+
// ADR-0058: a clean exit stops the session's tasks first — or, when
|
|
1851
|
+
// the person chose to leave them (3e), only the moved commands. What
|
|
1852
|
+
// could not be confirmed is said, never called stopped.
|
|
1853
|
+
const tasksAtExit = await stopAllTasks();
|
|
1798
1854
|
agent?.close();
|
|
1799
1855
|
// v2b: the dock tears down on EVERY exit path — CSI r resets the
|
|
1800
1856
|
// scroll region, the cursor lands at the input line, no broken
|
|
1801
1857
|
// terminal (kill -9 excepted; `reset` saves it).
|
|
1802
1858
|
dock.exit();
|
|
1859
|
+
for (const id of tasksAtExit.unconfirmed)
|
|
1860
|
+
console.error(`${id}: stop unconfirmed — outcome unknown`);
|
|
1861
|
+
if (tasksAtExit.left.length > 0)
|
|
1862
|
+
console.error(`left running: ${tasksAtExit.left.join(", ")} — reopen this session to hear how ${tasksAtExit.left.length === 1 ? "it ends" : "they end"}`);
|
|
1803
1863
|
// finding #8 (P1): extension dispose runs on the same exit path — a
|
|
1804
1864
|
// dispose failure prints one line and NEVER changes the exit code.
|
|
1805
1865
|
await disposeExtensions(loadedExtensions);
|
package/dist/launch.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows P3 — how the CLI's small spawns start: `kiso update` (npm), the
|
|
3
|
+
* external editor, and the sign-in page.
|
|
4
|
+
*
|
|
5
|
+
* npm is npm.cmd and an editor is often code.cmd: a .cmd/.bat shim needs
|
|
6
|
+
* cmd.exe to start, and Node will not start one without a shell. Rather
|
|
7
|
+
* than `shell: true`, the call goes to cmd.exe itself — `/d /s /c "<line>"`
|
|
8
|
+
* — with every word quoted by `viaCmd`, the one function here (the
|
|
9
|
+
* cross-spawn rules: CommandLineToArgvW quoting, then a caret before each
|
|
10
|
+
* cmd.exe metacharacter). The shell tool never comes this way: it only
|
|
11
|
+
* ever starts bash (P1).
|
|
12
|
+
*
|
|
13
|
+
* Pure — no process is started here, so importing it moves no test into
|
|
14
|
+
* the pty pool.
|
|
15
|
+
*/
|
|
16
|
+
export interface Launch {
|
|
17
|
+
readonly file: string;
|
|
18
|
+
readonly args: readonly string[];
|
|
19
|
+
/** Windows: the line is cmd.exe's to parse, not Node's to requote */
|
|
20
|
+
readonly windowsVerbatimArguments?: true;
|
|
21
|
+
}
|
|
22
|
+
/** `bin args` as cmd.exe starts it. */
|
|
23
|
+
export declare function viaCmd(bin: string, args: readonly string[], env?: NodeJS.ProcessEnv): Launch;
|
|
24
|
+
/** `bin args` as this platform starts it: directly, or through cmd.exe on Windows. */
|
|
25
|
+
export declare function launchCommand(bin: string, args: readonly string[]): Launch;
|
|
26
|
+
/** The platform's URL opener — `rundll32`'s URL handler on Windows (cmd's
|
|
27
|
+
* `start` would cut a sign-in URL at its first `&`) — or null, and the
|
|
28
|
+
* URL stays printed. */
|
|
29
|
+
export declare function browserCommand(url: string): readonly [string, readonly string[]] | null;
|
package/dist/launch.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows P3 — how the CLI's small spawns start: `kiso update` (npm), the
|
|
3
|
+
* external editor, and the sign-in page.
|
|
4
|
+
*
|
|
5
|
+
* npm is npm.cmd and an editor is often code.cmd: a .cmd/.bat shim needs
|
|
6
|
+
* cmd.exe to start, and Node will not start one without a shell. Rather
|
|
7
|
+
* than `shell: true`, the call goes to cmd.exe itself — `/d /s /c "<line>"`
|
|
8
|
+
* — with every word quoted by `viaCmd`, the one function here (the
|
|
9
|
+
* cross-spawn rules: CommandLineToArgvW quoting, then a caret before each
|
|
10
|
+
* cmd.exe metacharacter). The shell tool never comes this way: it only
|
|
11
|
+
* ever starts bash (P1).
|
|
12
|
+
*
|
|
13
|
+
* Pure — no process is started here, so importing it moves no test into
|
|
14
|
+
* the pty pool.
|
|
15
|
+
*/
|
|
16
|
+
/** cmd.exe's metacharacters: each is escaped with a caret. */
|
|
17
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
18
|
+
/** One argument, quoted as CommandLineToArgvW reads it, then escaped for cmd.exe. */
|
|
19
|
+
function cmdArgument(arg) {
|
|
20
|
+
const quoted = `"${arg.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
|
|
21
|
+
return quoted.replace(CMD_META, "^$1");
|
|
22
|
+
}
|
|
23
|
+
/** `bin args` as cmd.exe starts it. */
|
|
24
|
+
export function viaCmd(bin, args, env = process.env) {
|
|
25
|
+
const line = [bin.replace(CMD_META, "^$1"), ...args.map(cmdArgument)].join(" ");
|
|
26
|
+
return { file: env.ComSpec ?? "cmd.exe", args: ["/d", "/s", "/c", `"${line}"`], windowsVerbatimArguments: true };
|
|
27
|
+
}
|
|
28
|
+
/** `bin args` as this platform starts it: directly, or through cmd.exe on Windows. */
|
|
29
|
+
export function launchCommand(bin, args) {
|
|
30
|
+
return process.platform === "win32" ? viaCmd(bin, args) : { file: bin, args };
|
|
31
|
+
}
|
|
32
|
+
/** The platform's URL opener — `rundll32`'s URL handler on Windows (cmd's
|
|
33
|
+
* `start` would cut a sign-in URL at its first `&`) — or null, and the
|
|
34
|
+
* URL stays printed. */
|
|
35
|
+
export function browserCommand(url) {
|
|
36
|
+
if (process.platform === "darwin")
|
|
37
|
+
return ["open", [url]];
|
|
38
|
+
if (process.platform === "linux")
|
|
39
|
+
return ["xdg-open", [url]];
|
|
40
|
+
if (process.platform === "win32")
|
|
41
|
+
return ["rundll32", ["url.dll,FileProtocolHandler", url]];
|
|
42
|
+
return null;
|
|
43
|
+
}
|