@vincemakes/kiso-code 0.46.1 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/oauth/chatgpt.js +25 -8
- package/dist/auth/oauth/page.d.ts +25 -0
- package/dist/auth/oauth/page.js +44 -0
- package/dist/chat.d.ts +12 -1
- package/dist/chat.js +133 -46
- package/dist/coding-prompt.d.ts +7 -0
- package/dist/coding-prompt.js +12 -4
- package/dist/config.d.ts +7 -3
- package/dist/config.js +39 -4
- package/dist/create-coding-agent.js +16 -6
- package/dist/dispatch.d.ts +2 -0
- package/dist/dispatch.js +337 -83
- package/dist/floor.d.ts +21 -0
- package/dist/floor.js +35 -35
- package/dist/git-branch.d.ts +17 -0
- package/dist/git-branch.js +69 -0
- package/dist/in-run-compaction.d.ts +54 -0
- package/dist/in-run-compaction.js +44 -0
- package/dist/index.js +106 -45
- package/dist/opening.d.ts +48 -0
- package/dist/opening.js +43 -0
- package/dist/protected-writes.d.ts +8 -0
- package/dist/protected-writes.js +12 -0
- package/dist/remote-boundary.d.ts +55 -0
- package/dist/remote-boundary.js +217 -0
- package/dist/replay.d.ts +8 -1
- package/dist/replay.js +22 -10
- package/dist/resume.js +21 -16
- package/dist/session-cards.d.ts +2 -0
- package/dist/session-cards.js +3 -3
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +34 -5
- package/dist/skill-invoke.d.ts +11 -0
- package/dist/skill-invoke.js +59 -0
- package/dist/state.d.ts +14 -3
- package/dist/state.js +29 -3
- package/dist/task-notice.d.ts +48 -7
- package/dist/task-notice.js +123 -13
- package/dist/trust-ui.d.ts +7 -0
- package/dist/trust-ui.js +54 -9
- package/dist/window-title.d.ts +43 -23
- package/dist/window-title.js +78 -50
- package/package.json +14 -14
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graphite §7.10 — what the opening says loaded: the session, the
|
|
3
|
+
* project's rules, the skills, the MCP servers, and the extensions. The
|
|
4
|
+
* model, the mode and the folder are the status bar's (§8.9) and are not
|
|
5
|
+
* repeated here — except the faux model, which the bar names but cannot
|
|
6
|
+
* say how to leave.
|
|
7
|
+
*
|
|
8
|
+
* Pure over what the CLI already holds at startup; the banner lays the
|
|
9
|
+
* facts out and knows nothing of where they came from.
|
|
10
|
+
*/
|
|
11
|
+
import type { BannerFact } from "@vincemakes/kiso-tui";
|
|
12
|
+
export interface OpeningInputs {
|
|
13
|
+
/** The session's id — the SESSION row names it (the last sweep: the
|
|
14
|
+
* `session <id>` line above the opening retired on a dock). Null off a
|
|
15
|
+
* dock, where that line still prints: the row keeps its old form. */
|
|
16
|
+
readonly sessionId: string | null;
|
|
17
|
+
/** The events a resumed session opened with; 0 for a new one. */
|
|
18
|
+
readonly resumedEvents: number;
|
|
19
|
+
/** No model resolved and on a dock: the scripted faux model answers
|
|
20
|
+
* (off a dock the `[faux mode — …]` line still prints). */
|
|
21
|
+
readonly faux: boolean;
|
|
22
|
+
/** The instruction file the prompt reads (`projectInstructions`), or null. */
|
|
23
|
+
readonly rules: string | null;
|
|
24
|
+
/** The skills catalog's size, or null when no skills extension loaded. */
|
|
25
|
+
readonly skills: {
|
|
26
|
+
readonly count: number;
|
|
27
|
+
readonly broken: number;
|
|
28
|
+
} | null;
|
|
29
|
+
/** The MCP extension's tools as they stand, or null when it is not loaded. */
|
|
30
|
+
readonly mcp: {
|
|
31
|
+
readonly tools: readonly string[];
|
|
32
|
+
readonly connecting: boolean;
|
|
33
|
+
} | null;
|
|
34
|
+
/** The extensions — `extensionsFact`'s count and names, from the SAME
|
|
35
|
+
* three lists the pipe's `[N extensions: …]` line is built from (so the
|
|
36
|
+
* two still cannot disagree about what loaded, and the 0.40.0 `ask (off
|
|
37
|
+
* in dontAsk)` note stays beside the tier that turns it off); null when
|
|
38
|
+
* nothing loaded. The last sweep (owner, 2026-10-06): the row carried
|
|
39
|
+
* the pipe's bracketed line verbatim — as it still does off a dock
|
|
40
|
+
* (`{ value: <the line> }`). */
|
|
41
|
+
readonly extensions: {
|
|
42
|
+
readonly value: string;
|
|
43
|
+
readonly note?: string;
|
|
44
|
+
} | null;
|
|
45
|
+
/** DC-49 — the workspace is the home directory. */
|
|
46
|
+
readonly homeWorkspace: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function openingFacts(o: OpeningInputs): BannerFact[];
|
package/dist/opening.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graphite §7.10 — what the opening says loaded: the session, the
|
|
3
|
+
* project's rules, the skills, the MCP servers, and the extensions. The
|
|
4
|
+
* model, the mode and the folder are the status bar's (§8.9) and are not
|
|
5
|
+
* repeated here — except the faux model, which the bar names but cannot
|
|
6
|
+
* say how to leave.
|
|
7
|
+
*
|
|
8
|
+
* Pure over what the CLI already holds at startup; the banner lays the
|
|
9
|
+
* facts out and knows nothing of where they came from.
|
|
10
|
+
*/
|
|
11
|
+
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
12
|
+
export function openingFacts(o) {
|
|
13
|
+
const facts = [];
|
|
14
|
+
if (o.sessionId === null)
|
|
15
|
+
facts.push(o.resumedEvents > 0 ? { label: "SESSION", value: "resumed", note: plural(o.resumedEvents, "event") } : { label: "SESSION", value: "new", note: "resumable after kill -9" });
|
|
16
|
+
else
|
|
17
|
+
facts.push({ label: "SESSION", value: o.sessionId, note: o.resumedEvents > 0 ? `resumed \u00b7 ${plural(o.resumedEvents, "event")}` : "new \u00b7 resumable after kill -9" });
|
|
18
|
+
// the last sweep: no model resolved — the bar says `faux`, and this row
|
|
19
|
+
// says how to leave it (the line that said so was painted over at once)
|
|
20
|
+
// (as short as the RULES note: beside the wordmark a longer note is cut)
|
|
21
|
+
if (o.faux)
|
|
22
|
+
facts.push({ label: "MODEL", value: "faux", note: "set an API key, or add a model to config.json" });
|
|
23
|
+
facts.push(o.rules !== null ? { label: "RULES", value: o.rules } : { label: "RULES", value: "none", note: "an AGENTS.md or CLAUDE.md here is read" });
|
|
24
|
+
if (o.skills !== null) {
|
|
25
|
+
facts.push({ label: "SKILLS", value: String(o.skills.count), note: o.skills.broken > 0 ? `${o.skills.broken} cannot load · /skills` : "/skills lists them" });
|
|
26
|
+
}
|
|
27
|
+
if (o.mcp !== null) {
|
|
28
|
+
// the status tool is the extension's own, not a server's
|
|
29
|
+
const served = o.mcp.tools.filter((t) => t.startsWith("mcp__") && t !== "mcp__status");
|
|
30
|
+
const servers = new Set(served.map((t) => /^mcp__(.+?)__/.exec(t)?.[1] ?? t)).size;
|
|
31
|
+
if (servers === 0)
|
|
32
|
+
facts.push({ label: "MCP", value: o.mcp.connecting ? "connecting…" : "none" });
|
|
33
|
+
else
|
|
34
|
+
facts.push({ label: "MCP", value: plural(servers, "server"), note: `${plural(served.length, "tool")}${o.mcp.connecting ? " · connecting…" : ""}` });
|
|
35
|
+
}
|
|
36
|
+
if (o.extensions !== null)
|
|
37
|
+
facts.push({ label: "EXTENSIONS", value: o.extensions.value, ...(o.extensions.note !== undefined ? { note: o.extensions.note } : {}) });
|
|
38
|
+
// DC-49: STATES the fact and names the remedy; it does not warn — the
|
|
39
|
+
// configuration is allowed (owner, 2026-09-06)
|
|
40
|
+
if (o.homeWorkspace)
|
|
41
|
+
facts.push({ label: "", value: "home directory as workspace — cd into a project to narrow it" });
|
|
42
|
+
return facts;
|
|
43
|
+
}
|
|
@@ -18,6 +18,14 @@
|
|
|
18
18
|
import type { PolicyCall } from "@vincemakes/kiso-core";
|
|
19
19
|
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
20
20
|
export declare function isProtectedWrite(call: PolicyCall, workspaceRoot: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The calls a saved allow never carries — they reach a person every time
|
|
23
|
+
* in the asking tiers: a write into .git/ or .kiso/, a destructive
|
|
24
|
+
* command (floor.ts, which also answers yes for a line it cannot read),
|
|
25
|
+
* and a command across the remote boundary (remote-boundary.ts, 0.46.2).
|
|
26
|
+
* create-coding-agent.ts wires it into every guarded saved allow.
|
|
27
|
+
*/
|
|
28
|
+
export declare function neverInheritedCall(call: PolicyCall, workspaceRoot: string): boolean;
|
|
21
29
|
/**
|
|
22
30
|
* A saved allow ("yes, don't ask again for <tool>") allows by TOOL, so on
|
|
23
31
|
* its own it would carry every later call of that tool — including the
|
package/dist/protected-writes.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { relative, sep } from "node:path";
|
|
19
19
|
import { resolveShellPath } from "./shell-words.js";
|
|
20
|
+
import { isDestructiveCall } from "./floor.js";
|
|
21
|
+
import { isRemoteBoundaryCall } from "./remote-boundary.js";
|
|
20
22
|
const PROTECTED = new Set([".git", ".kiso"]);
|
|
21
23
|
const WRITERS = new Set(["write_file", "edit_file"]);
|
|
22
24
|
export function isProtectedWrite(call, workspaceRoot) {
|
|
@@ -34,6 +36,16 @@ export function isProtectedWrite(call, workspaceRoot) {
|
|
|
34
36
|
.some((seg) => PROTECTED.has(seg.toLowerCase()));
|
|
35
37
|
}
|
|
36
38
|
const ABSTAIN = { action: "abstain" };
|
|
39
|
+
/**
|
|
40
|
+
* The calls a saved allow never carries — they reach a person every time
|
|
41
|
+
* in the asking tiers: a write into .git/ or .kiso/, a destructive
|
|
42
|
+
* command (floor.ts, which also answers yes for a line it cannot read),
|
|
43
|
+
* and a command across the remote boundary (remote-boundary.ts, 0.46.2).
|
|
44
|
+
* create-coding-agent.ts wires it into every guarded saved allow.
|
|
45
|
+
*/
|
|
46
|
+
export function neverInheritedCall(call, workspaceRoot) {
|
|
47
|
+
return isProtectedWrite(call, workspaceRoot) || isDestructiveCall(call) || isRemoteBoundaryCall(call);
|
|
48
|
+
}
|
|
37
49
|
/**
|
|
38
50
|
* A saved allow ("yes, don't ask again for <tool>") allows by TOOL, so on
|
|
39
51
|
* its own it would carry every later call of that tool — including the
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.46.2 — the remote boundary (owner-ratified 2026-10-06, rev 2 with the
|
|
3
|
+
* review's amendments).
|
|
4
|
+
*
|
|
5
|
+
* Two families of shell command change state that other people share:
|
|
6
|
+
*
|
|
7
|
+
* A. a destructive git push — history rewritten or refs removed:
|
|
8
|
+
* --force / -f, --force-with-lease, --force-if-includes, a refspec
|
|
9
|
+
* with a leading `+`, --mirror, --delete / -d, a deleting refspec
|
|
10
|
+
* (`:branch`), --prune. A dry run is exempt, read as git reads it: the
|
|
11
|
+
* last of -n / --dry-run / --no-dry-run wins, and git takes any
|
|
12
|
+
* unambiguous prefix of a long option, so `--mirr` is --mirror and
|
|
13
|
+
* `--no-dry` turns the dry run off. A prefix is read generously — a
|
|
14
|
+
* prefix git would reject as ambiguous costs a question, not a pass.
|
|
15
|
+
* B. a registry write — a package published, unpublished or deprecated,
|
|
16
|
+
* or a dist-tag moved: npm or pnpm with `publish`, `unpublish`,
|
|
17
|
+
* `deprecate`, or `dist-tag` / `dist-tags` with add (a, set, s) or rm
|
|
18
|
+
* (r, del, d, remove) — pnpm hands the last three to npm; yarn with
|
|
19
|
+
* `publish`, `npm publish`, or `tag` / `npm tag` with add, rm or
|
|
20
|
+
* remove. Not as the script `npm run publish` names. A dry run is
|
|
21
|
+
* exempt only where the command honours one, read as its parser reads
|
|
22
|
+
* it (`--dry-run false` is a real publish): npm and pnpm publish and
|
|
23
|
+
* unpublish take --dry-run, `yarn npm publish` takes -n / --dry-run;
|
|
24
|
+
* deprecate, dist-tag and yarn classic's publish have none, so the
|
|
25
|
+
* flag exempts nothing there. npm's abbreviations of a command
|
|
26
|
+
* (`npm pub`) are read too, in the command's place — not after an
|
|
27
|
+
* option that takes a value (`npm --registry x pub`). Listing tags
|
|
28
|
+
* and `npm view` stay out.
|
|
29
|
+
*
|
|
30
|
+
* A saved "don't ask again for shell" never carries either
|
|
31
|
+
* (protected-writes.ts neverInheritedCall): permission remembered by TOOL
|
|
32
|
+
* NAME from some earlier call does not cross into a change other people
|
|
33
|
+
* share. Authority given now still does — every tier decides these as it
|
|
34
|
+
* decides any shell call, so full-access runs them, and an explicit user
|
|
35
|
+
* extension that allows them still allows them. That is the extension of
|
|
36
|
+
* launch-weekend plan §4 ("destructive commands never inherit a saved
|
|
37
|
+
* allow in the asking modes") to the remote; "bypass stays bypass" is
|
|
38
|
+
* untouched.
|
|
39
|
+
*
|
|
40
|
+
* The list is closed: a new family needs the owner's word. It reads
|
|
41
|
+
* direct invocations only, words as written — `npm run release`, a script
|
|
42
|
+
* file, a git alias, a `+` refspec in git config and a variable's value
|
|
43
|
+
* are not seen. A guardrail, not a sandbox. A line it cannot read is not
|
|
44
|
+
* its question: isDestructiveCall already answers yes for one, so a saved
|
|
45
|
+
* allow abstains either way, and the two readings never disagree on what
|
|
46
|
+
* "unreadable" means.
|
|
47
|
+
*/
|
|
48
|
+
import type { PolicyCall } from "@vincemakes/kiso-core";
|
|
49
|
+
/** Whether a command line crosses the remote boundary — read as the floor
|
|
50
|
+
* reads one: past wrappers (sudo, env, timeout), into `sh -c` and eval,
|
|
51
|
+
* along every command of a chain. */
|
|
52
|
+
export declare function isRemoteBoundary(commandLine: string): boolean;
|
|
53
|
+
/** The policy-call form: a shell call whose command crosses. A throw is
|
|
54
|
+
* not this reading's to answer — see the head comment. */
|
|
55
|
+
export declare function isRemoteBoundaryCall(call: PolicyCall): boolean;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.46.2 — the remote boundary (owner-ratified 2026-10-06, rev 2 with the
|
|
3
|
+
* review's amendments).
|
|
4
|
+
*
|
|
5
|
+
* Two families of shell command change state that other people share:
|
|
6
|
+
*
|
|
7
|
+
* A. a destructive git push — history rewritten or refs removed:
|
|
8
|
+
* --force / -f, --force-with-lease, --force-if-includes, a refspec
|
|
9
|
+
* with a leading `+`, --mirror, --delete / -d, a deleting refspec
|
|
10
|
+
* (`:branch`), --prune. A dry run is exempt, read as git reads it: the
|
|
11
|
+
* last of -n / --dry-run / --no-dry-run wins, and git takes any
|
|
12
|
+
* unambiguous prefix of a long option, so `--mirr` is --mirror and
|
|
13
|
+
* `--no-dry` turns the dry run off. A prefix is read generously — a
|
|
14
|
+
* prefix git would reject as ambiguous costs a question, not a pass.
|
|
15
|
+
* B. a registry write — a package published, unpublished or deprecated,
|
|
16
|
+
* or a dist-tag moved: npm or pnpm with `publish`, `unpublish`,
|
|
17
|
+
* `deprecate`, or `dist-tag` / `dist-tags` with add (a, set, s) or rm
|
|
18
|
+
* (r, del, d, remove) — pnpm hands the last three to npm; yarn with
|
|
19
|
+
* `publish`, `npm publish`, or `tag` / `npm tag` with add, rm or
|
|
20
|
+
* remove. Not as the script `npm run publish` names. A dry run is
|
|
21
|
+
* exempt only where the command honours one, read as its parser reads
|
|
22
|
+
* it (`--dry-run false` is a real publish): npm and pnpm publish and
|
|
23
|
+
* unpublish take --dry-run, `yarn npm publish` takes -n / --dry-run;
|
|
24
|
+
* deprecate, dist-tag and yarn classic's publish have none, so the
|
|
25
|
+
* flag exempts nothing there. npm's abbreviations of a command
|
|
26
|
+
* (`npm pub`) are read too, in the command's place — not after an
|
|
27
|
+
* option that takes a value (`npm --registry x pub`). Listing tags
|
|
28
|
+
* and `npm view` stay out.
|
|
29
|
+
*
|
|
30
|
+
* A saved "don't ask again for shell" never carries either
|
|
31
|
+
* (protected-writes.ts neverInheritedCall): permission remembered by TOOL
|
|
32
|
+
* NAME from some earlier call does not cross into a change other people
|
|
33
|
+
* share. Authority given now still does — every tier decides these as it
|
|
34
|
+
* decides any shell call, so full-access runs them, and an explicit user
|
|
35
|
+
* extension that allows them still allows them. That is the extension of
|
|
36
|
+
* launch-weekend plan §4 ("destructive commands never inherit a saved
|
|
37
|
+
* allow in the asking modes") to the remote; "bypass stays bypass" is
|
|
38
|
+
* untouched.
|
|
39
|
+
*
|
|
40
|
+
* The list is closed: a new family needs the owner's word. It reads
|
|
41
|
+
* direct invocations only, words as written — `npm run release`, a script
|
|
42
|
+
* file, a git alias, a `+` refspec in git config and a variable's value
|
|
43
|
+
* are not seen. A guardrail, not a sandbox. A line it cannot read is not
|
|
44
|
+
* its question: isDestructiveCall already answers yes for one, so a saved
|
|
45
|
+
* allow abstains either way, and the two readings never disagree on what
|
|
46
|
+
* "unreadable" means.
|
|
47
|
+
*/
|
|
48
|
+
import { homedir } from "node:os";
|
|
49
|
+
import { basename } from "node:path";
|
|
50
|
+
import { MAX_INNER_DEPTH, MAX_INNER_LINES, gitInvocation, innerLines, unwrap } from "./floor.js";
|
|
51
|
+
import { looseCommands, parseShellLooseChecked } from "./shell-words.js";
|
|
52
|
+
/** git push options that take the next word as their value. */
|
|
53
|
+
const PUSH_VALUED = new Set(["-o", "--push-option", "--repo", "--receive-pack", "--exec"]);
|
|
54
|
+
/** git push options that rewrite or remove remote refs. */
|
|
55
|
+
const PUSH_REWRITES = new Set(["--force", "--force-with-lease", "--force-if-includes", "--mirror", "--delete", "--prune"]);
|
|
56
|
+
function destructivePush(args) {
|
|
57
|
+
let crosses = false;
|
|
58
|
+
let dryRun = false;
|
|
59
|
+
let options = true;
|
|
60
|
+
const positionals = [];
|
|
61
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
62
|
+
const t = args[i].text;
|
|
63
|
+
if (!options || !t.startsWith("-") || t === "-") {
|
|
64
|
+
positionals.push(t);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (t === "--") {
|
|
68
|
+
options = false;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (PUSH_VALUED.has(t)) {
|
|
72
|
+
i += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (t.startsWith("--")) {
|
|
76
|
+
const flag = t.split("=")[0];
|
|
77
|
+
if (flag === "--dry-run")
|
|
78
|
+
dryRun = true;
|
|
79
|
+
else if (flag.length >= "--no-d".length && "--no-dry-run".startsWith(flag))
|
|
80
|
+
dryRun = false;
|
|
81
|
+
else if (flag.length > 2 && [...PUSH_REWRITES].some((r) => r.startsWith(flag)))
|
|
82
|
+
crosses = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// a short cluster, `-uf`; `-o` takes the rest of the word, or the next
|
|
86
|
+
for (let k = 1; k < t.length; k += 1) {
|
|
87
|
+
const c = t[k];
|
|
88
|
+
if (c === "o") {
|
|
89
|
+
if (k === t.length - 1)
|
|
90
|
+
i += 1;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (c === "n")
|
|
94
|
+
dryRun = true;
|
|
95
|
+
if (c === "f" || c === "d")
|
|
96
|
+
crosses = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// the first positional is the repository; the rest are refspecs
|
|
100
|
+
if (positionals.slice(1).some((r) => r.startsWith("+") || r.startsWith(":")))
|
|
101
|
+
crosses = true;
|
|
102
|
+
return crosses && !dryRun;
|
|
103
|
+
}
|
|
104
|
+
/** Whether a dry run is on, read as npm's option parser reads it: the last
|
|
105
|
+
* setting wins, `--dry-run false` and `--dry-run=false` turn it off, and
|
|
106
|
+
* only an explicit true counts. `-n` is a dry run where yarn says so. */
|
|
107
|
+
function dryRun(words, short) {
|
|
108
|
+
let on = false;
|
|
109
|
+
for (let i = 0; i < words.length; i += 1) {
|
|
110
|
+
const w = words[i];
|
|
111
|
+
if (w === "--dry-run" || (short && w === "-n")) {
|
|
112
|
+
const next = words[i + 1];
|
|
113
|
+
on = next !== "false";
|
|
114
|
+
if (next === "true" || next === "false")
|
|
115
|
+
i += 1;
|
|
116
|
+
}
|
|
117
|
+
else if (w.startsWith("--dry-run="))
|
|
118
|
+
on = w === "--dry-run=true";
|
|
119
|
+
else if (w === "--no-dry-run")
|
|
120
|
+
on = false;
|
|
121
|
+
}
|
|
122
|
+
return on;
|
|
123
|
+
}
|
|
124
|
+
/** dist-tag (npm) and tag (yarn) write with these subcommands, and list
|
|
125
|
+
* with the rest — a bare `npm dist-tag <pkg>` lists too. */
|
|
126
|
+
const NPM_TAG_WRITES = new Set(["add", "a", "set", "s", "rm", "r", "del", "d", "remove"]);
|
|
127
|
+
const NPM_TAG_READS = new Set(["ls", "l", "sl", "list"]);
|
|
128
|
+
const YARN_TAG_WRITES = new Set(["add", "rm", "remove"]);
|
|
129
|
+
const YARN_TAG_READS = new Set(["ls", "list"]);
|
|
130
|
+
/** A tag command writes when a write subcommand follows it — unless the
|
|
131
|
+
* first word after it names a listing (`npm dist-tag ls d` lists "d"). */
|
|
132
|
+
function tagWrite(words, at, writes, reads) {
|
|
133
|
+
const after = words.slice(at + 1).filter((w) => !w.startsWith("-"));
|
|
134
|
+
if (after[0] !== undefined && reads.has(after[0]))
|
|
135
|
+
return false;
|
|
136
|
+
return after.some((w) => writes.has(w));
|
|
137
|
+
}
|
|
138
|
+
/** npm runs a unique prefix of a command name: `npm pub` is publish,
|
|
139
|
+
* `npm unp` unpublish, `npm dep` deprecate (npm 10's own resolution).
|
|
140
|
+
* Read generously, as git's options are — a prefix npm would reject as
|
|
141
|
+
* ambiguous (`npm de`) costs a question. `un` is npm's alias for
|
|
142
|
+
* uninstall; dist-tag has no abbreviation, every prefix of it being
|
|
143
|
+
* ambiguous with dist-tags. */
|
|
144
|
+
function npmCommand(first) {
|
|
145
|
+
if (first === undefined || first.length < 2 || first === "un")
|
|
146
|
+
return first;
|
|
147
|
+
return ["publish", "unpublish", "deprecate"].find((w) => w.startsWith(first)) ?? first;
|
|
148
|
+
}
|
|
149
|
+
function registryWrite(pm, args) {
|
|
150
|
+
const words = args.map((a) => a.text);
|
|
151
|
+
const first = words.find((w) => !w.startsWith("-"));
|
|
152
|
+
if (first === "run" || first === "run-script")
|
|
153
|
+
return false;
|
|
154
|
+
if (pm === "yarn") {
|
|
155
|
+
// berry spells the registry commands `yarn npm …`; classic has no
|
|
156
|
+
// unpublish or deprecate, and its publish takes no dry run
|
|
157
|
+
const berry = words.includes("npm");
|
|
158
|
+
if (words.includes("publish") && !(berry && dryRun(words, true)))
|
|
159
|
+
return true;
|
|
160
|
+
const tag = words.indexOf("tag");
|
|
161
|
+
return tag !== -1 && tagWrite(words, tag, YARN_TAG_WRITES, YARN_TAG_READS);
|
|
162
|
+
}
|
|
163
|
+
const command = pm === "npm" ? npmCommand(first) : first;
|
|
164
|
+
const publishes = words.includes("publish") || words.includes("unpublish") || command === "publish" || command === "unpublish";
|
|
165
|
+
if (publishes && !dryRun(words, false))
|
|
166
|
+
return true;
|
|
167
|
+
if (words.includes("deprecate") || command === "deprecate")
|
|
168
|
+
return true;
|
|
169
|
+
const tag = words.findIndex((w) => w === "dist-tag" || w === "dist-tags");
|
|
170
|
+
return tag !== -1 && tagWrite(words, tag, NPM_TAG_WRITES, NPM_TAG_READS);
|
|
171
|
+
}
|
|
172
|
+
function crossing(argv) {
|
|
173
|
+
const name = basename(argv[0]?.text ?? "");
|
|
174
|
+
if (name === "git") {
|
|
175
|
+
const inv = gitInvocation(argv.slice(1));
|
|
176
|
+
return inv.sub === "push" && destructivePush(inv.rest);
|
|
177
|
+
}
|
|
178
|
+
if (name === "npm" || name === "pnpm" || name === "yarn")
|
|
179
|
+
return registryWrite(name, argv.slice(1));
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
/** Whether a command line crosses the remote boundary — read as the floor
|
|
183
|
+
* reads one: past wrappers (sudo, env, timeout), into `sh -c` and eval,
|
|
184
|
+
* along every command of a chain. */
|
|
185
|
+
export function isRemoteBoundary(commandLine) {
|
|
186
|
+
const seen = new Set();
|
|
187
|
+
const visit = (line, depth) => {
|
|
188
|
+
const read = parseShellLooseChecked(line);
|
|
189
|
+
return looseCommands(read.nodes).some(({ argv: raw }) => {
|
|
190
|
+
const argv = unwrap(raw);
|
|
191
|
+
const inner = innerLines(argv, homedir());
|
|
192
|
+
if (inner !== null) {
|
|
193
|
+
return (depth < MAX_INNER_DEPTH &&
|
|
194
|
+
inner.lines.some((l) => {
|
|
195
|
+
if (seen.has(l) || seen.size >= MAX_INNER_LINES)
|
|
196
|
+
return false;
|
|
197
|
+
seen.add(l);
|
|
198
|
+
return visit(l, depth + 1);
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
return crossing(argv);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
return visit(commandLine, 0);
|
|
205
|
+
}
|
|
206
|
+
/** The policy-call form: a shell call whose command crosses. A throw is
|
|
207
|
+
* not this reading's to answer — see the head comment. */
|
|
208
|
+
export function isRemoteBoundaryCall(call) {
|
|
209
|
+
if (call.name !== "shell" || typeof call.input.command !== "string")
|
|
210
|
+
return false;
|
|
211
|
+
try {
|
|
212
|
+
return isRemoteBoundary(call.input.command);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
package/dist/replay.d.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* summary (what the model sees next) and the turns it covers, read in
|
|
20
20
|
* the viewer — then any uncovered earlier turns folded as above.
|
|
21
21
|
*/
|
|
22
|
+
import type { TaskInfo } from "@vincemakes/kiso-runtime/internal";
|
|
23
|
+
import type { NoticeMark } from "@vincemakes/kiso-tui";
|
|
22
24
|
/** The Body surface the replay drives — the live run's own mutations. */
|
|
23
25
|
export interface ReplayBody {
|
|
24
26
|
userLine(text: string): void;
|
|
@@ -34,6 +36,11 @@ export interface ReplayBody {
|
|
|
34
36
|
untimed?: boolean;
|
|
35
37
|
}): void;
|
|
36
38
|
notice(text: string): void;
|
|
39
|
+
metaNotice(text: string, rows: readonly {
|
|
40
|
+
readonly label: string;
|
|
41
|
+
readonly sentence: string;
|
|
42
|
+
readonly mark?: NoticeMark;
|
|
43
|
+
}[]): void;
|
|
37
44
|
endTurn(thoughtSeconds: number): void;
|
|
38
45
|
fold(label: string, replay: () => void, summary?: string | null): void;
|
|
39
46
|
raw(lines: string[]): void;
|
|
@@ -49,5 +56,5 @@ export declare function turnsOf(events: readonly Ev[]): Ev[][];
|
|
|
49
56
|
* Replay a session's durable events into the body. Returns the number of
|
|
50
57
|
* turns it found (0: a fresh session — nothing is drawn).
|
|
51
58
|
*/
|
|
52
|
-
export declare function replayInto(body: ReplayBody, events: readonly Ev[], W?: number): number;
|
|
59
|
+
export declare function replayInto(body: ReplayBody, events: readonly Ev[], W?: number, tasks?: readonly TaskInfo[]): number;
|
|
53
60
|
export {};
|
package/dist/replay.js
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
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 {
|
|
23
|
+
import { bandHeader } from "@vincemakes/kiso-tui-cells/strings";
|
|
24
|
+
import { taskNoticeLines, taskNoticeRows, taskWhat } from "./task-notice.js";
|
|
24
25
|
/** How many turns stay on screen in full. */
|
|
25
26
|
const FULL_TURNS = 2;
|
|
26
27
|
/** A user turn's opening line, or null when this input does not open one
|
|
@@ -48,7 +49,7 @@ export function turnsOf(events) {
|
|
|
48
49
|
}
|
|
49
50
|
/** One turn through the Body — consumeRun's mapping, minus everything that
|
|
50
51
|
* needs the live process (clocks, tailers, usage, the recap). */
|
|
51
|
-
function replayTurn(body, turn) {
|
|
52
|
+
function replayTurn(body, turn, tasks) {
|
|
52
53
|
let thinking = false;
|
|
53
54
|
let said = false;
|
|
54
55
|
const results = new Set();
|
|
@@ -62,8 +63,17 @@ function replayTurn(body, turn) {
|
|
|
62
63
|
const ask = askOf(e);
|
|
63
64
|
if (ask !== null)
|
|
64
65
|
body.userLine(ask);
|
|
65
|
-
else if (e.via?.kind === "tasks")
|
|
66
|
-
|
|
66
|
+
else if (e.via?.kind === "tasks") {
|
|
67
|
+
// the tasks round: the same rows a live session drew, from the
|
|
68
|
+
// journal; the pipe's lines are taskNoticeLines' (Amendment 8: a
|
|
69
|
+
// lost task's line names what it ran)
|
|
70
|
+
const items = e.via.items;
|
|
71
|
+
const what = (taskId) => {
|
|
72
|
+
const t = tasks.find((x) => x.id === taskId);
|
|
73
|
+
return t === undefined ? undefined : taskWhat(t);
|
|
74
|
+
};
|
|
75
|
+
body.metaNotice(taskNoticeLines(items, what).join("\n"), taskNoticeRows(items, tasks));
|
|
76
|
+
}
|
|
67
77
|
else if (e.source === "system") {
|
|
68
78
|
body.notice("verification pass");
|
|
69
79
|
body.notice(` ${typeof e.content === "string" ? e.content : ""}`);
|
|
@@ -124,7 +134,7 @@ const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
|
124
134
|
* Replay a session's durable events into the body. Returns the number of
|
|
125
135
|
* turns it found (0: a fresh session — nothing is drawn).
|
|
126
136
|
*/
|
|
127
|
-
export function replayInto(body, events, W = 80) {
|
|
137
|
+
export function replayInto(body, events, W = 80, tasks = []) {
|
|
128
138
|
// The latest checkpoint: the turns it covers are what the model will
|
|
129
139
|
// read as its summary, not as turns.
|
|
130
140
|
let checkpoint = null;
|
|
@@ -140,22 +150,24 @@ export function replayInto(body, events, W = 80) {
|
|
|
140
150
|
const shown = rest.slice(-FULL_TURNS);
|
|
141
151
|
const earlier = rest.slice(0, rest.length - shown.length);
|
|
142
152
|
const label = total > shown.length ? `resuming · ${plural(total, "turn")}, showing the last ${shown.length}` : `resuming · ${plural(total, "turn")}`;
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
// Graphite §8.1 (R3b): the resumed history opens like every band — the
|
|
154
|
+
// hairline with its name, `resuming` bold gold, the counts dim (the same
|
|
155
|
+
// words the dim rule carried)
|
|
156
|
+
body.raw([bandHeader(label, Math.max(1, W))]);
|
|
145
157
|
if (checkpoint !== null) {
|
|
146
158
|
const summary = checkpoint.summary;
|
|
147
159
|
body.fold(`checkpoint · summarizes ${plural(covered.length, "earlier turn")} · ctrl+r to read`, () => {
|
|
148
160
|
for (const t of covered)
|
|
149
|
-
replayTurn(body, t);
|
|
161
|
+
replayTurn(body, t, tasks);
|
|
150
162
|
}, summary);
|
|
151
163
|
}
|
|
152
164
|
if (earlier.length > 0) {
|
|
153
165
|
body.fold(`${plural(earlier.length, "earlier turn")} · ctrl+r to read`, () => {
|
|
154
166
|
for (const t of earlier)
|
|
155
|
-
replayTurn(body, t);
|
|
167
|
+
replayTurn(body, t, tasks);
|
|
156
168
|
});
|
|
157
169
|
}
|
|
158
170
|
for (const t of shown)
|
|
159
|
-
replayTurn(body, t);
|
|
171
|
+
replayTurn(body, t, tasks);
|
|
160
172
|
return total;
|
|
161
173
|
}
|
package/dist/resume.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* The ergonomics batch B4 (pure move) — resume: the RECOVERY flow (Area 2/7). The body
|
|
3
3
|
* moved verbatim from index.ts.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { agentModel, dock, retryOnRow, floorOn } from "./state.js";
|
|
5
|
+
import { STATUS_GLYPHS, workingRow } from "@vincemakes/kiso-tui";
|
|
6
|
+
import { dock, retryOnRow } from "./state.js";
|
|
8
7
|
import { pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
9
8
|
import { failOnFauxExhaustion } from "./faux-glue.js";
|
|
10
|
-
import {
|
|
9
|
+
import { setTitleState } from "./window-title.js";
|
|
10
|
+
import { barFor, consumeRun, estimateCtxRatio, startStatusSpinner } from "./chat.js";
|
|
11
11
|
/**
|
|
12
12
|
* Resume = the RECOVERY flow (Area 2/7): uncertain executions are decided,
|
|
13
13
|
* the interrupted run is continued via session.resume() — never faked with
|
|
@@ -21,23 +21,26 @@ export async function resume(session, prompt, faux, input) {
|
|
|
21
21
|
let turnNo = 0;
|
|
22
22
|
// v3 §03: the two-state status bar (see chat — same shapes).
|
|
23
23
|
let runUsage = { in: null, out: null, cache: null, known: false };
|
|
24
|
-
let runGlyph =
|
|
24
|
+
let runGlyph = STATUS_GLYPHS[0];
|
|
25
25
|
let runStart = Date.now();
|
|
26
26
|
// KC2 §5: the rows the REPL and this flow used to build separately are
|
|
27
|
-
// ONE formatter now
|
|
28
|
-
|
|
27
|
+
// ONE formatter now. Graphite §8.7 / §8.9: the running turn's facts ride
|
|
28
|
+
// the live row, the session's the status bar — the chat's own two
|
|
29
|
+
// (barFor), so the recovery flow now spells plan's posture the same way.
|
|
30
|
+
const width = () => (process.stdout.columns > 0 ? process.stdout.columns : 80);
|
|
31
|
+
const statusCb = (u, _ctx) => {
|
|
29
32
|
runUsage = u;
|
|
30
|
-
if (dock.active)
|
|
31
|
-
|
|
33
|
+
if (!dock.active)
|
|
34
|
+
return;
|
|
35
|
+
dock.setLive(workingRow(runGlyph, runStart, u.out, null, width(), retryOnRow()));
|
|
36
|
+
dock.setBar(barFor(session, { tokPerSec: null }));
|
|
32
37
|
};
|
|
33
|
-
// the recovery flow prints the BARE mode (chat spells plan's posture) —
|
|
34
|
-
// the extraction keeps that difference, it was not asked to settle it.
|
|
35
38
|
const paintIdle = () => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
setTitleState("ready");
|
|
40
|
+
if (!dock.active)
|
|
41
|
+
return;
|
|
42
|
+
dock.setLive(null);
|
|
43
|
+
dock.setBar(barFor(session, { tokPerSec: null }));
|
|
41
44
|
};
|
|
42
45
|
const withRun = async (run) => {
|
|
43
46
|
currentRun = run;
|
|
@@ -47,6 +50,8 @@ export async function resume(session, prompt, faux, input) {
|
|
|
47
50
|
runGlyph = g;
|
|
48
51
|
statusCb(runUsage, estimateCtxRatio(session));
|
|
49
52
|
});
|
|
53
|
+
statusCb(runUsage, estimateCtxRatio(session)); // the live row from the run's first frame
|
|
54
|
+
setTitleState("working");
|
|
50
55
|
try {
|
|
51
56
|
turnNo += 1;
|
|
52
57
|
const last = await consumeRun(session, run, input, turnNo, faux, statusCb);
|
package/dist/session-cards.d.ts
CHANGED
|
@@ -88,6 +88,8 @@ export declare function projectSessionCard(input: {
|
|
|
88
88
|
readonly asks: number;
|
|
89
89
|
readonly workspace?: string | null;
|
|
90
90
|
readonly profileName?: string | null;
|
|
91
|
+
/** Graphite R3d: the session's `/name`, which wins over the derived title */
|
|
92
|
+
readonly name?: string | null;
|
|
91
93
|
}): SessionCard;
|
|
92
94
|
/** The shapes this module needs from the agent — structural, so the
|
|
93
95
|
* projection is testable without standing up a provider. */
|
package/dist/session-cards.js
CHANGED
|
@@ -76,7 +76,7 @@ export function projectSessionCard(input) {
|
|
|
76
76
|
: "failed";
|
|
77
77
|
return {
|
|
78
78
|
id: input.id,
|
|
79
|
-
title: sessionTitle(input.records),
|
|
79
|
+
title: input.name ?? sessionTitle(input.records),
|
|
80
80
|
badge,
|
|
81
81
|
turns,
|
|
82
82
|
updatedAt: input.updatedAt,
|
|
@@ -128,11 +128,11 @@ export function cardFromListing(l) {
|
|
|
128
128
|
const s = l.summary;
|
|
129
129
|
const base = { id: l.id, workspace: l.workspace, profileName: l.profileName, ...(l.inferred === true ? { inferred: true } : {}) };
|
|
130
130
|
if (s === null)
|
|
131
|
-
return { ...base, title: l.id, badge: "unknown", turns: null, updatedAt: l.mtime, uncertain: 0, asks: 0, outcome: "no summary" };
|
|
131
|
+
return { ...base, title: l.name ?? l.id, badge: "unknown", turns: null, updatedAt: l.mtime, uncertain: 0, asks: 0, outcome: "no summary" };
|
|
132
132
|
const badge = s.uncertain > 0 ? "uncertain" : s.asks > 0 ? "ask" : s.state === "open" ? "interrupted" : s.state === "completed" ? "completed" : s.state === null ? "unknown" : "failed";
|
|
133
133
|
return {
|
|
134
134
|
...base,
|
|
135
|
-
title: s.title ?? l.id,
|
|
135
|
+
title: l.name ?? s.title ?? l.id,
|
|
136
136
|
badge,
|
|
137
137
|
turns: s.turns,
|
|
138
138
|
updatedAt: s.updatedAt,
|
package/dist/settings.d.ts
CHANGED
|
@@ -37,4 +37,23 @@ export interface SettingsInput {
|
|
|
37
37
|
readonly thinkingRemembered: boolean;
|
|
38
38
|
readonly version: string;
|
|
39
39
|
}
|
|
40
|
+
export interface Row {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly value: string;
|
|
43
|
+
readonly from: string;
|
|
44
|
+
readonly change: string;
|
|
45
|
+
/** Graphite P3: the value as a table cell (`on`, `1M`) where `value` is
|
|
46
|
+
* a sentence; absent, the value is already short */
|
|
47
|
+
readonly brief?: string;
|
|
48
|
+
/** Graphite P3: what the sentence said that the cell does not — the
|
|
49
|
+
* panel's opened row carries it */
|
|
50
|
+
readonly means?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Graphite R3e: the settings as data — the /settings panel's rows and
|
|
53
|
+
* the printed form below read the same list. */
|
|
54
|
+
export declare function settingsFacts(i: SettingsInput): Row[];
|
|
55
|
+
/** The printed form: one block per setting — what it is, where it came
|
|
56
|
+
* from and how to change it (a pipe, a dock-less TTY, and a panel row's
|
|
57
|
+
* enter). */
|
|
58
|
+
export declare function settingRow(r: Row, width?: number): string;
|
|
40
59
|
export declare function settingsRows(i: SettingsInput): string[];
|