@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/readonly-shell.js
CHANGED
|
@@ -32,24 +32,26 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { statSync } from "node:fs";
|
|
34
34
|
import { homedir } from "node:os";
|
|
35
|
-
import {
|
|
35
|
+
import { join } from "node:path";
|
|
36
36
|
import { isCredentialName, isCredentialPath } from "@vincemakes/kiso-tools-node";
|
|
37
37
|
import { getMode } from "./mode.js";
|
|
38
|
-
import {
|
|
38
|
+
import { homeSubtrees, hostDialect, parseShell, realCase, resolveShellPath } from "./shell-words.js";
|
|
39
39
|
// ── the predicates ──────────────────────────────────────────────────────
|
|
40
40
|
function scope(ctx, word) {
|
|
41
41
|
if (word === "-")
|
|
42
42
|
return null; // stdin
|
|
43
43
|
for (const cwd of ctx.cwds) {
|
|
44
|
-
const { canonical, inside } = resolveShellPath(ctx.root, cwd, word);
|
|
44
|
+
const { canonical, inside, opaque } = resolveShellPath(ctx.root, cwd, word);
|
|
45
|
+
if (opaque !== undefined)
|
|
46
|
+
return `${word} is ${opaque}`;
|
|
45
47
|
if (!inside)
|
|
46
48
|
return `${word} is outside the workspace`;
|
|
47
49
|
// B4: under a protected directory NOTHING is read or listed unasked —
|
|
48
50
|
// kiso's own home, and ~/.ssh ~/.config ~/.aws ~/.gnupg ~/.kiso when
|
|
49
51
|
// the workspace is the home directory. Over-asking is an ask.
|
|
50
52
|
for (const p of ctx.protectedRoots) {
|
|
51
|
-
const rel = relative(p, canonical);
|
|
52
|
-
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)))
|
|
53
|
+
const rel = hostDialect().path.relative(p, canonical);
|
|
54
|
+
if (rel === "" || (!rel.startsWith("..") && !hostDialect().path.isAbsolute(rel)))
|
|
53
55
|
return `${word} is inside a protected directory (${p})`;
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -59,7 +61,7 @@ function scope(ctx, word) {
|
|
|
59
61
|
* the file tools ask ONE rule — names (`isCredentialName`) and the
|
|
60
62
|
* directory-scoped paths (`isCredentialPath`) — on the canonical path. */
|
|
61
63
|
function shellCredential(canonical) {
|
|
62
|
-
return isCredentialName(basename(canonical).toLowerCase()) || isCredentialPath(canonical);
|
|
64
|
+
return isCredentialName(hostDialect().path.basename(canonical).toLowerCase()) || isCredentialPath(canonical);
|
|
63
65
|
}
|
|
64
66
|
function content(ctx, word) {
|
|
65
67
|
const s = scope(ctx, word);
|
|
@@ -69,11 +71,11 @@ function content(ctx, word) {
|
|
|
69
71
|
return null;
|
|
70
72
|
// B2: the disk is case-insensitive and the names are not — `.ENV` and
|
|
71
73
|
// `ID_RSA` are `.env` and `id_rsa` to the file system, so to the rule
|
|
72
|
-
if (isCredentialName(basename(word).toLowerCase()))
|
|
74
|
+
if (isCredentialName(hostDialect().path.basename(word).toLowerCase()))
|
|
73
75
|
return `${word} is a credential file`;
|
|
74
76
|
for (const cwd of ctx.cwds) {
|
|
75
77
|
const { canonical } = resolveShellPath(ctx.root, cwd, word);
|
|
76
|
-
if (isCredentialName(basename(canonical).toLowerCase()) || shellCredential(canonical))
|
|
78
|
+
if (isCredentialName(hostDialect().path.basename(canonical).toLowerCase()) || shellCredential(canonical))
|
|
77
79
|
return `${word} leads to a credential file`;
|
|
78
80
|
}
|
|
79
81
|
return null;
|
|
@@ -552,11 +554,11 @@ export function classifyReadOnly(commandLine, workspaceRoot, protectedRoots = []
|
|
|
552
554
|
}
|
|
553
555
|
return { allow: true };
|
|
554
556
|
}
|
|
555
|
-
/** The modes in which a provable read runs unasked
|
|
556
|
-
* allow is what it still honours. Manual promises that
|
|
557
|
-
* plan denies shell outright;
|
|
558
|
-
* member has nothing to add to any of them. */
|
|
559
|
-
const ALLOWING_MODES = new Set(["default", "accept-edits"
|
|
557
|
+
/** The modes in which a provable read runs unasked — with the don't-ask
|
|
558
|
+
* switch on too: an allow is what it still honours. Manual promises that
|
|
559
|
+
* every tool asks; plan denies shell outright; full-access allows
|
|
560
|
+
* everything already — this member has nothing to add to any of them. */
|
|
561
|
+
const ALLOWING_MODES = new Set(["default", "accept-edits"]);
|
|
560
562
|
const ABSTAIN = { action: "abstain" };
|
|
561
563
|
/**
|
|
562
564
|
* The chain member. ALLOW or ABSTAIN, never anything else: an allow here
|
|
@@ -577,7 +579,7 @@ export function readOnlyShellExtension(roots) {
|
|
|
577
579
|
if (typeof command !== "string")
|
|
578
580
|
return ABSTAIN;
|
|
579
581
|
const { workspaceRoot, excludeRoots } = roots();
|
|
580
|
-
const protectedRoots = [...excludeRoots, ...
|
|
582
|
+
const protectedRoots = [...excludeRoots, ...homeSubtrees().map((s) => join(homedir(), s))];
|
|
581
583
|
return classifyReadOnly(command, workspaceRoot, protectedRoots).allow ? { action: "allow" } : ABSTAIN;
|
|
582
584
|
},
|
|
583
585
|
},
|
package/dist/replay.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* the viewer — then any uncovered earlier turns folded as above.
|
|
21
21
|
*/
|
|
22
22
|
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
23
|
+
import { taskNoticeRow } from "./task-notice.js";
|
|
23
24
|
/** How many turns stay on screen in full. */
|
|
24
25
|
const FULL_TURNS = 2;
|
|
25
26
|
/** A user turn's opening line, or null when this input does not open one
|
|
@@ -61,6 +62,8 @@ function replayTurn(body, turn) {
|
|
|
61
62
|
const ask = askOf(e);
|
|
62
63
|
if (ask !== null)
|
|
63
64
|
body.userLine(ask);
|
|
65
|
+
else if (e.via?.kind === "tasks")
|
|
66
|
+
body.notice(taskNoticeRow(e.via.items));
|
|
64
67
|
else if (e.source === "system") {
|
|
65
68
|
body.notice("verification pass");
|
|
66
69
|
body.notice(` ${typeof e.content === "string" ? e.content : ""}`);
|
package/dist/resume.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* moved verbatim from index.ts.
|
|
4
4
|
*/
|
|
5
5
|
import { idleStatus, runningStatus } from "@vincemakes/kiso-tui";
|
|
6
|
-
import {
|
|
6
|
+
import { modeDisplay } from "./mode.js";
|
|
7
7
|
import { agentModel, dock, retryOnRow, floorOn } from "./state.js";
|
|
8
8
|
import { pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
9
9
|
import { failOnFauxExhaustion } from "./faux-glue.js";
|
|
@@ -37,7 +37,7 @@ export async function resume(session, prompt, faux, input) {
|
|
|
37
37
|
// same budget. It has no meter, so nothing here can be dropped that
|
|
38
38
|
// was not already at risk of being cut.
|
|
39
39
|
if (dock.active)
|
|
40
|
-
dock.setStatus(idleStatus(
|
|
40
|
+
dock.setStatus(idleStatus(modeDisplay(), agentModel, estimateCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80, !floorOn));
|
|
41
41
|
};
|
|
42
42
|
const withRun = async (run) => {
|
|
43
43
|
currentRun = run;
|
package/dist/settings.d.ts
CHANGED
|
@@ -20,8 +20,11 @@ export interface SettingsInput {
|
|
|
20
20
|
/** the --mode / --model values on the command line, when given */
|
|
21
21
|
readonly modeFlag?: string;
|
|
22
22
|
readonly modelFlag?: string;
|
|
23
|
+
/** whether --dont-ask was on the command line */
|
|
24
|
+
readonly dontAskFlag?: boolean;
|
|
23
25
|
/** what is live now */
|
|
24
26
|
readonly mode: string;
|
|
27
|
+
readonly dontAsk?: boolean;
|
|
25
28
|
readonly model: {
|
|
26
29
|
readonly label: string;
|
|
27
30
|
readonly profile: string | null;
|
package/dist/settings.js
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
|
+
import { envModeLayer, parseMode, resolveModeLayers } from "./mode.js";
|
|
2
|
+
const MODE_LAYER = { flag: "--mode", env: "env KISO_MODE", project: "project config", user: "user config", default: "default" };
|
|
3
|
+
/** What a layer wrote for the tier — an old name is named, so a person
|
|
4
|
+
* learns the new one from the row that shows it. */
|
|
5
|
+
function writtenMode(i, source) {
|
|
6
|
+
return source === "flag" ? i.modeFlag : source === "env" ? i.env.KISO_MODE : source === "project" ? i.project?.mode : source === "user" ? i.user?.mode : undefined;
|
|
7
|
+
}
|
|
8
|
+
function modeFrom(i, source) {
|
|
9
|
+
const w = writtenMode(i, source);
|
|
10
|
+
return w === "bypass" || w === "dontAsk" ? `${MODE_LAYER[source]} (written "${w}")` : MODE_LAYER[source];
|
|
11
|
+
}
|
|
12
|
+
/** The switch's layer: its own key there, or — the resolution's other
|
|
13
|
+
* way in — that layer's tier written as the old name dontAsk. */
|
|
14
|
+
function dontAskFrom(i, source) {
|
|
15
|
+
if (source === "default")
|
|
16
|
+
return "default";
|
|
17
|
+
const own = source === "flag" ? i.dontAskFlag === true : source === "env" ? envModeLayer(i.env).dontAsk !== undefined : source === "project" ? i.project?.dontAsk !== undefined : i.user?.dontAsk !== undefined;
|
|
18
|
+
if (own)
|
|
19
|
+
return source === "flag" ? "--dont-ask" : source === "env" ? "env KISO_DONT_ASK" : MODE_LAYER[source];
|
|
20
|
+
return `${MODE_LAYER[source]} (written "dontAsk")`;
|
|
21
|
+
}
|
|
22
|
+
function modeLayers(i) {
|
|
23
|
+
return {
|
|
24
|
+
flag: { ...(i.modeFlag !== undefined ? { mode: i.modeFlag } : {}), ...(i.dontAskFlag === true ? { dontAsk: true } : {}) },
|
|
25
|
+
env: envModeLayer(i.env),
|
|
26
|
+
project: i.project,
|
|
27
|
+
user: i.user,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
1
30
|
function modeRow(i) {
|
|
2
31
|
const change = "/mode or shift+tab; \"mode\" in ~/.kiso/config.json";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
: i.project?.mode === i.mode
|
|
8
|
-
? "project config"
|
|
9
|
-
: i.user?.mode === i.mode
|
|
10
|
-
? "user config"
|
|
11
|
-
: i.mode === "default"
|
|
12
|
-
? "default"
|
|
13
|
-
: "set in this session";
|
|
32
|
+
// the layers decided at start-up; a live tier they do not explain was
|
|
33
|
+
// set in this session
|
|
34
|
+
const resolved = resolveModeLayers(modeLayers(i));
|
|
35
|
+
const from = parseMode(i.mode)?.mode === resolved.mode ? modeFrom(i, resolved.from.mode) : "set in this session";
|
|
14
36
|
return { name: "mode", value: i.mode, from, change };
|
|
15
37
|
}
|
|
38
|
+
function dontAskRow(i) {
|
|
39
|
+
const change = "/dont-ask; --dont-ask; \"dontAsk\": true in ~/.kiso/config.json";
|
|
40
|
+
const on = i.dontAsk === true;
|
|
41
|
+
const resolved = resolveModeLayers(modeLayers(i));
|
|
42
|
+
const from = (resolved.dontAsk !== "off") === on ? dontAskFrom(i, resolved.from.dontAsk) : "set in this session";
|
|
43
|
+
return { name: "don't ask", value: on ? "on — what would ask is refused" : "off", from, change };
|
|
44
|
+
}
|
|
16
45
|
function modelRow(i) {
|
|
17
46
|
const change = "/model; \"model\" in ~/.kiso/config.json";
|
|
18
47
|
const p = i.model.profile;
|
|
@@ -50,6 +79,7 @@ export function settingsRows(i) {
|
|
|
50
79
|
const rows = [
|
|
51
80
|
modelRow(i),
|
|
52
81
|
modeRow(i),
|
|
82
|
+
dontAskRow(i),
|
|
53
83
|
themeRow(i),
|
|
54
84
|
{
|
|
55
85
|
name: "floor",
|
package/dist/shell-words.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* newline counts as `;`), each pipeline a series of simple commands joined
|
|
16
16
|
* by `|`, each command an argv plus its redirections.
|
|
17
17
|
*/
|
|
18
|
+
import { posix } from "node:path";
|
|
18
19
|
export interface Redirect {
|
|
19
20
|
/** The descriptor written before the operator (`2>`), null when none
|
|
20
21
|
* was written, or "&" for `&>` (stdout and stderr together). */
|
|
@@ -55,16 +56,48 @@ export declare function parseShell(src: string): ParseResult;
|
|
|
55
56
|
export interface ResolvedPath {
|
|
56
57
|
readonly canonical: string;
|
|
57
58
|
readonly inside: boolean;
|
|
59
|
+
/** Windows P2: why the word cannot be read as a path here — never
|
|
60
|
+
* inside, and refused under a destructive verb */
|
|
61
|
+
readonly opaque?: string;
|
|
58
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Windows P2 — how a path word reads. `posix` is every host but Windows,
|
|
65
|
+
* unchanged. `msys` is Git Bash on win32 (P1 runs commands through it):
|
|
66
|
+
* `C:\x`, `C:/x` and `/c/x` are `C:\x`, either separator separates, the
|
|
67
|
+
* disk is case-insensitive, and every form that cannot be read exactly is
|
|
68
|
+
* opaque (kiso-doc plan-windows-p2-design-2026-10-01.md).
|
|
69
|
+
*/
|
|
70
|
+
export interface PathDialect {
|
|
71
|
+
readonly msys: boolean;
|
|
72
|
+
readonly path: typeof posix;
|
|
73
|
+
}
|
|
74
|
+
/** The dialect of the shell that runs commands here — read per call. */
|
|
75
|
+
export declare function hostDialect(): PathDialect;
|
|
76
|
+
/**
|
|
77
|
+
* A Git Bash word as a Windows path, `/`-separated (`C:/x`, or relative),
|
|
78
|
+
* each component as Win32 reads it (trailing dots and spaces dropped) —
|
|
79
|
+
* or why it cannot be read: another machine, a device or namespace path,
|
|
80
|
+
* a drive-relative or drive-rooted path, an MSYS mount inside Git's own
|
|
81
|
+
* tree, a data stream, a device name. (An 8.3 short name needs no rule:
|
|
82
|
+
* Windows resolves an existing one, and the real path expands it.)
|
|
83
|
+
*/
|
|
84
|
+
export declare function msysPath(word: string): {
|
|
85
|
+
readonly native: string;
|
|
86
|
+
} | {
|
|
87
|
+
readonly opaque: string;
|
|
88
|
+
};
|
|
59
89
|
/** The well-known home subtrees that hold credentials or configuration
|
|
60
90
|
* that runs — one list for both chain members: the read-only allow
|
|
61
91
|
* never reads or lists under them, the floor never deletes them. */
|
|
62
92
|
export declare const HOME_SUBTREES: readonly string[];
|
|
93
|
+
/** The home subtrees for this host: on Windows, AppData too — credentials
|
|
94
|
+
* and configuration that runs live there (Windows P2). */
|
|
95
|
+
export declare function homeSubtrees(): readonly string[];
|
|
63
96
|
/** A real path in the case the DISK holds it. The JS realpath keeps the
|
|
64
97
|
* case it was given, so on a case-insensitive disk `.ENV` stayed `.ENV`
|
|
65
98
|
* and walked past every name-based predicate (the lead's review, B2). */
|
|
66
99
|
export declare function realCase(p: string): string;
|
|
67
|
-
export declare function resolveShellPath(workspaceRoot: string, cwd: string, word: string): ResolvedPath;
|
|
100
|
+
export declare function resolveShellPath(workspaceRoot: string, cwd: string, word: string, dialect?: PathDialect): ResolvedPath;
|
|
68
101
|
/**
|
|
69
102
|
* The FLOOR's reading of a command line (0.40.0 item 4) — the opposite
|
|
70
103
|
* contract to `parseShell`. The read-only allow must refuse whatever it
|
package/dist/shell-words.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* by `|`, each command an argv plus its redirections.
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, realpathSync } from "node:fs";
|
|
19
|
-
import {
|
|
19
|
+
import { posix, win32 } from "node:path";
|
|
20
20
|
const GLOB = new Set(["*", "?", "["]);
|
|
21
21
|
/**
|
|
22
22
|
* Parse a command line, or say why not. Pure: no filesystem, no
|
|
@@ -268,10 +268,72 @@ export function parseShell(src) {
|
|
|
268
268
|
return fail(f);
|
|
269
269
|
return { ok: true, list };
|
|
270
270
|
}
|
|
271
|
+
const POSIX_DIALECT = { msys: false, path: posix };
|
|
272
|
+
const MSYS_DIALECT = { msys: true, path: win32 };
|
|
273
|
+
/** The dialect of the shell that runs commands here — read per call. */
|
|
274
|
+
export function hostDialect() {
|
|
275
|
+
return process.platform === "win32" ? MSYS_DIALECT : POSIX_DIALECT;
|
|
276
|
+
}
|
|
277
|
+
const DEVICE_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
|
|
278
|
+
/**
|
|
279
|
+
* A Git Bash word as a Windows path, `/`-separated (`C:/x`, or relative),
|
|
280
|
+
* each component as Win32 reads it (trailing dots and spaces dropped) —
|
|
281
|
+
* or why it cannot be read: another machine, a device or namespace path,
|
|
282
|
+
* a drive-relative or drive-rooted path, an MSYS mount inside Git's own
|
|
283
|
+
* tree, a data stream, a device name. (An 8.3 short name needs no rule:
|
|
284
|
+
* Windows resolves an existing one, and the real path expands it.)
|
|
285
|
+
*/
|
|
286
|
+
export function msysPath(word) {
|
|
287
|
+
const w = word.replace(/\\/g, "/");
|
|
288
|
+
if (/^\/\/[?.]\//.test(w))
|
|
289
|
+
return { opaque: w[2] === "?" ? "a namespaced path" : "a device path" };
|
|
290
|
+
if (w.startsWith("//"))
|
|
291
|
+
return { opaque: "a UNC path (another machine)" };
|
|
292
|
+
let rest;
|
|
293
|
+
let drive = "";
|
|
294
|
+
const lettered = /^([A-Za-z]):(.*)$/s.exec(w);
|
|
295
|
+
const mounted = /^\/([A-Za-z])(\/.*)?$/s.exec(w);
|
|
296
|
+
if (lettered !== null) {
|
|
297
|
+
if (!lettered[2].startsWith("/"))
|
|
298
|
+
return { opaque: "a drive-relative path" };
|
|
299
|
+
drive = `${lettered[1].toUpperCase()}:`;
|
|
300
|
+
rest = lettered[2];
|
|
301
|
+
}
|
|
302
|
+
else if (mounted !== null) {
|
|
303
|
+
drive = `${mounted[1].toUpperCase()}:`;
|
|
304
|
+
rest = mounted[2] ?? "/";
|
|
305
|
+
}
|
|
306
|
+
else if (w.startsWith("/")) {
|
|
307
|
+
return { opaque: word.startsWith("\\") ? "a drive-rooted path" : "an MSYS mount (Git's own tree)" };
|
|
308
|
+
}
|
|
309
|
+
else
|
|
310
|
+
rest = w;
|
|
311
|
+
const parts = [];
|
|
312
|
+
for (const part of rest.split("/")) {
|
|
313
|
+
if (part === "" || part === "." || part === "..") {
|
|
314
|
+
parts.push(part);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (part.includes(":"))
|
|
318
|
+
return { opaque: "an alternate data stream" };
|
|
319
|
+
const name = part.replace(/[. ]+$/, "");
|
|
320
|
+
if (name === "")
|
|
321
|
+
return { opaque: "a name Win32 rewrites" };
|
|
322
|
+
if (DEVICE_NAME.test(name))
|
|
323
|
+
return { opaque: "a device name" };
|
|
324
|
+
parts.push(name);
|
|
325
|
+
}
|
|
326
|
+
return { native: drive + parts.join("/") };
|
|
327
|
+
}
|
|
271
328
|
/** The well-known home subtrees that hold credentials or configuration
|
|
272
329
|
* that runs — one list for both chain members: the read-only allow
|
|
273
330
|
* never reads or lists under them, the floor never deletes them. */
|
|
274
331
|
export const HOME_SUBTREES = [".ssh", ".config", ".kiso", ".gnupg", ".aws"];
|
|
332
|
+
/** The home subtrees for this host: on Windows, AppData too — credentials
|
|
333
|
+
* and configuration that runs live there (Windows P2). */
|
|
334
|
+
export function homeSubtrees() {
|
|
335
|
+
return hostDialect().msys ? [...HOME_SUBTREES, "AppData"] : HOME_SUBTREES;
|
|
336
|
+
}
|
|
275
337
|
/** A real path in the case the DISK holds it. The JS realpath keeps the
|
|
276
338
|
* case it was given, so on a case-insensitive disk `.ENV` stayed `.ENV`
|
|
277
339
|
* and walked past every name-based predicate (the lead's review, B2). */
|
|
@@ -283,16 +345,26 @@ export function realCase(p) {
|
|
|
283
345
|
return p;
|
|
284
346
|
}
|
|
285
347
|
}
|
|
286
|
-
export function resolveShellPath(workspaceRoot, cwd, word) {
|
|
348
|
+
export function resolveShellPath(workspaceRoot, cwd, word, dialect = hostDialect()) {
|
|
349
|
+
const { dirname, isAbsolute, join, relative } = dialect.path;
|
|
350
|
+
if (dialect.msys) {
|
|
351
|
+
const read = msysPath(word);
|
|
352
|
+
if ("opaque" in read)
|
|
353
|
+
return { canonical: word, inside: false, opaque: read.opaque };
|
|
354
|
+
if (!isAbsolute(read.native) && "opaque" in msysPath(cwd))
|
|
355
|
+
return { canonical: word, inside: false, opaque: "relative to a directory that cannot be read" };
|
|
356
|
+
word = read.native;
|
|
357
|
+
}
|
|
287
358
|
// B1 (the lead's review): `..` is taken on the REAL path, one component
|
|
288
359
|
// at a time — never collapsed as text first. `link-out/..` is the parent
|
|
289
360
|
// of where link-out POINTS, which is where the shell goes; collapsed as
|
|
290
361
|
// text it was the workspace, and `cat link-out/../etc/hosts` printed.
|
|
291
|
-
|
|
362
|
+
const top = isAbsolute(word) ? dialect.path.parse(word).root : "";
|
|
363
|
+
let current = top === "" ? realCase(cwd) : dialect.msys ? dialect.path.normalize(top) : top;
|
|
292
364
|
// past the last component that exists, the rest is text: nothing on
|
|
293
365
|
// disk can redirect a path that does not exist
|
|
294
366
|
const missing = [];
|
|
295
|
-
for (const part of word.split("/")) {
|
|
367
|
+
for (const part of word.slice(top.length).split("/")) {
|
|
296
368
|
if (part === "" || part === ".")
|
|
297
369
|
continue;
|
|
298
370
|
if (missing.length > 0) {
|
|
@@ -313,6 +385,14 @@ export function resolveShellPath(workspaceRoot, cwd, word) {
|
|
|
313
385
|
missing.push(part);
|
|
314
386
|
}
|
|
315
387
|
const canonical = missing.length > 0 ? join(current, ...missing) : current;
|
|
388
|
+
// Windows: a `..` after a link — Win32 takes `..` as text, the POSIX
|
|
389
|
+
// walk above takes it on the real path. Where the two land apart, the
|
|
390
|
+
// word is not read at all.
|
|
391
|
+
if (dialect.msys && word.split("/").includes("..")) {
|
|
392
|
+
const lexical = resolveShellPath(workspaceRoot, cwd, dialect.path.resolve(realCase(cwd), word), dialect);
|
|
393
|
+
if (lexical.canonical.toLowerCase() !== canonical.toLowerCase())
|
|
394
|
+
return { canonical: word, inside: false, opaque: "a `..` after a link (Windows and Git Bash may read it apart)" };
|
|
395
|
+
}
|
|
316
396
|
const rel = relative(realCase(workspaceRoot), canonical);
|
|
317
397
|
return { canonical, inside: rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)) };
|
|
318
398
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Dock, type AtItem, type Body, type PanelVerdict, type PanelView, type SaferAnswer, type SessionCardView } from "@vincemakes/kiso-tui";
|
|
9
9
|
import type { KisoExtension, StoreRecord } from "@vincemakes/kiso-runtime";
|
|
10
|
+
import { TaskManager } from "@vincemakes/kiso-runtime/internal";
|
|
11
|
+
import { type ShellTasks } from "@vincemakes/kiso-tools-node";
|
|
10
12
|
/** finding #11: KISO_HOME is the ONE root — every default path derives from
|
|
11
13
|
* it (sessions, trust, extensions, mcp config, skills). The dedicated
|
|
12
14
|
* env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
|
|
@@ -30,7 +32,27 @@ export declare function codingToolOptions(): {
|
|
|
30
32
|
readonly excludeRoots: readonly string[];
|
|
31
33
|
readonly secretEnvNames: readonly string[];
|
|
32
34
|
readonly protectedFiles: readonly string[];
|
|
35
|
+
readonly tasks: (sessionId: string | undefined) => ShellTasks | undefined;
|
|
33
36
|
};
|
|
37
|
+
export declare function tasksFor(sessionId: string | undefined): TaskManager | undefined;
|
|
38
|
+
export declare function setExitTasks(choice: "stop" | "leave"): void;
|
|
39
|
+
/** 3e: the unknown tasks the person has looked at in `/tasks` in this
|
|
40
|
+
* process — the status row stops counting them. */
|
|
41
|
+
export declare const seenUnknownTasks: Set<string>;
|
|
42
|
+
/** 3e: the live tasks of every session this process opened — a runner's
|
|
43
|
+
* (`durable`, it survives this process) and moved commands (`moved`). */
|
|
44
|
+
export declare function liveTasks(): {
|
|
45
|
+
readonly durable: number;
|
|
46
|
+
readonly moved: number;
|
|
47
|
+
};
|
|
48
|
+
/** A clean exit stops the session's tasks — all of them, or (the exit
|
|
49
|
+
* question's "leave") only the moved commands — and waits for their
|
|
50
|
+
* terminals. It returns what it could NOT confirm stopped, and what it
|
|
51
|
+
* left running: a stop requested is never reported as a stop (3e). */
|
|
52
|
+
export declare function stopAllTasks(): Promise<{
|
|
53
|
+
readonly unconfirmed: readonly string[];
|
|
54
|
+
readonly left: readonly string[];
|
|
55
|
+
}>;
|
|
34
56
|
export declare function setUserProtectedPaths(paths: readonly string[] | undefined): void;
|
|
35
57
|
/** kiso never serves its own credential store to a model. The store is
|
|
36
58
|
* `auth.json` under KISO_HOME — the path auth/credentials.ts writes (a
|
|
@@ -134,6 +156,9 @@ export interface LineInput {
|
|
|
134
156
|
onThink?(cb: () => void): void;
|
|
135
157
|
/** §2.4: the external-editor key (ctrl+g). */
|
|
136
158
|
onEditor?(cb: () => void): void;
|
|
159
|
+
/** ADR-0058 (3e): the background key (ctrl+b). Optional: the readline
|
|
160
|
+
* input has no such key — `/tasks` and a steer still reach the tasks. */
|
|
161
|
+
onBackground?(cb: () => void): void;
|
|
137
162
|
/** §2.4: hand the terminal over, run `edit`, take it back. The editor
|
|
138
163
|
* owns the handover; the CLI owns the spawn. */
|
|
139
164
|
externalEdit?(edit: (text: string) => string | null): void;
|
|
@@ -220,6 +245,11 @@ export declare function setSessionStore(value: {
|
|
|
220
245
|
/** The folder createCodingAgent built the store on — written there and by
|
|
221
246
|
* the resume picker's folder switch, read by the listings. */
|
|
222
247
|
export declare let activeStoreDir: string;
|
|
248
|
+
/** ADR-0058 3d (D6): a background child's turn budget (`--max-turns`, with
|
|
249
|
+
* `--task-file` only) — the agent's maxTurns. Unset everywhere else: the
|
|
250
|
+
* interactive door has no turn limit (R3e). */
|
|
251
|
+
export declare let childTurnBudget: number | undefined;
|
|
252
|
+
export declare function setChildTurnBudget(n: number | undefined): void;
|
|
223
253
|
export declare function setActiveStoreDir(value: string): void;
|
|
224
254
|
/** The model name for the status bar — set by createCodingAgent. */
|
|
225
255
|
export declare let agentModel: string;
|
|
@@ -309,6 +339,7 @@ export declare const settingsLayers: {
|
|
|
309
339
|
user: import("./config.js").KisoConfig | null;
|
|
310
340
|
project: import("./config.js").KisoConfig | null;
|
|
311
341
|
modeFlag: string | undefined;
|
|
342
|
+
dontAskFlag: boolean;
|
|
312
343
|
modelFlag: string | undefined;
|
|
313
344
|
modelSwitched: boolean;
|
|
314
345
|
};
|
package/dist/state.js
CHANGED
|
@@ -11,6 +11,8 @@ import { homedir } from "node:os";
|
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import { AT_CAP, AT_SKIP, Dock } from "@vincemakes/kiso-tui";
|
|
14
|
+
import { TaskManager } from "@vincemakes/kiso-runtime/internal";
|
|
15
|
+
import { processTaskBackend } from "@vincemakes/kiso-tools-node";
|
|
14
16
|
import { canonicalPath, LEGACY_SESSIONS_DIR, projectDirFor, projectLayoutActive } from "./projects.js";
|
|
15
17
|
/** finding #11: KISO_HOME is the ONE root — every default path derives from
|
|
16
18
|
* it (sessions, trust, extensions, mcp config, skills). The dedicated
|
|
@@ -39,7 +41,84 @@ export function secretEnvNamesOf(models) {
|
|
|
39
41
|
];
|
|
40
42
|
}
|
|
41
43
|
export function codingToolOptions() {
|
|
42
|
-
return { workspaceRoot: process.cwd(), excludeRoots: [kisoHome()], secretEnvNames: secretEnvNamesOf(configModels), protectedFiles: protectedFiles() };
|
|
44
|
+
return { workspaceRoot: process.cwd(), excludeRoots: [kisoHome()], secretEnvNames: secretEnvNamesOf(configModels), protectedFiles: protectedFiles(), tasks: tasksFor };
|
|
45
|
+
}
|
|
46
|
+
/** ADR-0058 (3b): one TaskManager per session, its directory beside the
|
|
47
|
+
* session's log (`<store dir>/<session>.tasks/`). A call without a session
|
|
48
|
+
* — the `!` gesture — has none, and its shell is today's. */
|
|
49
|
+
const taskManagers = new Map();
|
|
50
|
+
let taskBackend;
|
|
51
|
+
export function tasksFor(sessionId) {
|
|
52
|
+
if (sessionId === undefined || activeStoreDir === "")
|
|
53
|
+
return undefined;
|
|
54
|
+
let manager = taskManagers.get(sessionId);
|
|
55
|
+
if (manager === undefined) {
|
|
56
|
+
taskBackend ??= processTaskBackend();
|
|
57
|
+
manager = new TaskManager({ root: join(activeStoreDir, `${sessionId}.tasks`), backend: taskBackend });
|
|
58
|
+
manager.observe();
|
|
59
|
+
taskManagers.set(sessionId, manager);
|
|
60
|
+
}
|
|
61
|
+
return manager;
|
|
62
|
+
}
|
|
63
|
+
/** 3e: what the exit does with the tasks — the exit question's answer.
|
|
64
|
+
* "leave": a runner's task keeps running (the reopened session hears how
|
|
65
|
+
* it ends); a moved command is owned by this process and is stopped. */
|
|
66
|
+
let exitTasks = "stop";
|
|
67
|
+
export function setExitTasks(choice) {
|
|
68
|
+
exitTasks = choice;
|
|
69
|
+
}
|
|
70
|
+
/** 3e: the unknown tasks the person has looked at in `/tasks` in this
|
|
71
|
+
* process — the status row stops counting them. */
|
|
72
|
+
export const seenUnknownTasks = new Set();
|
|
73
|
+
/** 3e: the live tasks of every session this process opened — a runner's
|
|
74
|
+
* (`durable`, it survives this process) and moved commands (`moved`). */
|
|
75
|
+
export function liveTasks() {
|
|
76
|
+
let durable = 0;
|
|
77
|
+
let moved = 0;
|
|
78
|
+
for (const manager of taskManagers.values()) {
|
|
79
|
+
let list = [];
|
|
80
|
+
try {
|
|
81
|
+
list = manager.list();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue; // a corrupt journal is reported where it is read on purpose
|
|
85
|
+
}
|
|
86
|
+
for (const t of list) {
|
|
87
|
+
if (t.state.kind !== "running" && t.state.kind !== "starting")
|
|
88
|
+
continue;
|
|
89
|
+
if (t.backend === "foreground")
|
|
90
|
+
moved += 1;
|
|
91
|
+
else
|
|
92
|
+
durable += 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { durable, moved };
|
|
96
|
+
}
|
|
97
|
+
/** A clean exit stops the session's tasks — all of them, or (the exit
|
|
98
|
+
* question's "leave") only the moved commands — and waits for their
|
|
99
|
+
* terminals. It returns what it could NOT confirm stopped, and what it
|
|
100
|
+
* left running: a stop requested is never reported as a stop (3e). */
|
|
101
|
+
export async function stopAllTasks() {
|
|
102
|
+
const unconfirmed = [];
|
|
103
|
+
const left = [];
|
|
104
|
+
const many = taskManagers.size > 1;
|
|
105
|
+
await Promise.all([...taskManagers].map(async ([sessionId, manager]) => {
|
|
106
|
+
const name = (id) => (many ? `${sessionId}/${id}` : id);
|
|
107
|
+
if (exitTasks === "leave") {
|
|
108
|
+
try {
|
|
109
|
+
for (const t of manager.list())
|
|
110
|
+
if (t.backend === "process" && (t.state.kind === "running" || t.state.kind === "starting"))
|
|
111
|
+
left.push(name(t.id));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// a corrupt journal: nothing to report as left
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const id of await manager.stopAll("exit", 8_000, exitTasks === "leave" ? "moved" : "all"))
|
|
118
|
+
unconfirmed.push(name(id));
|
|
119
|
+
manager.close();
|
|
120
|
+
}));
|
|
121
|
+
return { unconfirmed, left };
|
|
43
122
|
}
|
|
44
123
|
/** The user config's `protectedPaths`, as read at the agent's build (and
|
|
45
124
|
* again at /reload) — `~/` taken against the home directory. */
|
|
@@ -301,6 +380,13 @@ export function setSessionStore(value) {
|
|
|
301
380
|
/** The folder createCodingAgent built the store on — written there and by
|
|
302
381
|
* the resume picker's folder switch, read by the listings. */
|
|
303
382
|
export let activeStoreDir = "";
|
|
383
|
+
/** ADR-0058 3d (D6): a background child's turn budget (`--max-turns`, with
|
|
384
|
+
* `--task-file` only) — the agent's maxTurns. Unset everywhere else: the
|
|
385
|
+
* interactive door has no turn limit (R3e). */
|
|
386
|
+
export let childTurnBudget;
|
|
387
|
+
export function setChildTurnBudget(n) {
|
|
388
|
+
childTurnBudget = n;
|
|
389
|
+
}
|
|
304
390
|
export function setActiveStoreDir(value) {
|
|
305
391
|
activeStoreDir = value;
|
|
306
392
|
}
|
|
@@ -406,7 +492,7 @@ export const queuedSwitchLines = [];
|
|
|
406
492
|
/** 0.40.6: what /settings needs to name each value's layer — the two config
|
|
407
493
|
* files as read, the command-line flags as given, and whether /model
|
|
408
494
|
* changed the binding in this session. */
|
|
409
|
-
export const settingsLayers = { user: null, project: null, modeFlag: undefined, modelFlag: undefined, modelSwitched: false };
|
|
495
|
+
export const settingsLayers = { user: null, project: null, modeFlag: undefined, dontAskFlag: false, modelFlag: undefined, modelSwitched: false };
|
|
410
496
|
export let floorOn = true;
|
|
411
497
|
export function setFloorOn(value) {
|
|
412
498
|
floorOn = value;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TaskDeliveryItem } from "@vincemakes/kiso-core";
|
|
2
|
+
import type { TaskInfo } from "@vincemakes/kiso-runtime/internal";
|
|
3
|
+
export declare function taskNoticeRow(items: readonly TaskDeliveryItem[]): string;
|
|
4
|
+
/** A task's state in the person's words. A stop that was asked for and is
|
|
5
|
+
* not yet confirmed reads `stopping` — never `stopped` before the journal
|
|
6
|
+
* says so; an outcome nobody can know reads `◌ outcome unknown`. */
|
|
7
|
+
export declare function taskStateLabel(task: TaskInfo): string;
|
|
8
|
+
/** What the task is: the command, or the child's role and task. */
|
|
9
|
+
export declare function taskWhat(task: TaskInfo): string;
|
|
10
|
+
/** One `/tasks` row: `t3 running 1m 20s`, with the command beside it. */
|
|
11
|
+
export declare function taskRow(task: TaskInfo, now?: number): {
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly note: string;
|
|
14
|
+
};
|
|
15
|
+
/** A task's last `lines` lines of output — or, for a child, its answer. */
|
|
16
|
+
export declare function taskOutput(task: TaskInfo, lines?: number): string[];
|
|
17
|
+
/** The status row's counts: live tasks, and the unknown ones the person
|
|
18
|
+
* has not looked at in `/tasks` in this process. */
|
|
19
|
+
export declare function taskCounts(tasks: readonly TaskInfo[], seen: ReadonlySet<string>): {
|
|
20
|
+
readonly running: number;
|
|
21
|
+
readonly unknown: number;
|
|
22
|
+
};
|