@vincemakes/kiso-code 0.1.19 → 0.1.21

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.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 手感批 B4 (pure move) — resume: the RECOVERY flow (Area 2/7). The body
3
+ * moved verbatim from index.ts.
4
+ */
5
+ import type { AgentSession } from "@vincemakes/kiso-runtime";
6
+ import { type LineInput } from "./state.js";
7
+ /**
8
+ * Resume = the RECOVERY flow (Area 2/7): uncertain executions are decided,
9
+ * the interrupted run is continued via session.resume() — never faked with
10
+ * a new prompt. An optional prompt afterwards starts a genuinely new turn.
11
+ * E 组: SIGINT aborts the run being resumed; every exit path closes the
12
+ * session store so no lock is left behind.
13
+ */
14
+ export declare function resume(session: AgentSession, prompt: string | undefined, faux: boolean, input: LineInput): Promise<void>;
package/dist/resume.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * 手感批 B4 (pure move) — resume: the RECOVERY flow (Area 2/7). The body
3
+ * moved verbatim from index.ts.
4
+ */
5
+ import { kUnit } from "@vincemakes/kiso-tui";
6
+ import { getMode } from "./mode.js";
7
+ import { agentModel, dock } from "./state.js";
8
+ import { pendingAsk, resolveUncertains } from "./trust-ui.js";
9
+ import { failOnFauxExhaustion } from "./faux-glue.js";
10
+ import { consumeRun, estimateCtxRatio, startStatusSpinner } from "./chat.js";
11
+ /**
12
+ * Resume = the RECOVERY flow (Area 2/7): uncertain executions are decided,
13
+ * the interrupted run is continued via session.resume() — never faked with
14
+ * a new prompt. An optional prompt afterwards starts a genuinely new turn.
15
+ * E 组: SIGINT aborts the run being resumed; every exit path closes the
16
+ * session store so no lock is left behind.
17
+ */
18
+ export async function resume(session, prompt, faux, input) {
19
+ let currentRun = null;
20
+ let cancelled = false;
21
+ let turnNo = 0;
22
+ // v3 §03: the two-state status bar (see chat — same shapes).
23
+ let runUsage = { in: null, out: null, cache: null, known: false };
24
+ let runGlyph = "▖";
25
+ let runStart = Date.now();
26
+ const statusCb = (u, ctx) => {
27
+ runUsage = u;
28
+ if (!dock.active)
29
+ return;
30
+ const pct = Number.isFinite(ctx) ? Math.round((1 - ctx) * 100) : null;
31
+ const out = runUsage.out !== null ? ` ↓ ${kUnit(runUsage.out)} tokens` : "";
32
+ dock.setStatus(`${runGlyph} working ${Math.max(1, Math.round((Date.now() - runStart) / 1000))}s${out} · esc to interrupt · ctx left ~${pct}%`);
33
+ };
34
+ const paintIdle = () => {
35
+ if (!dock.active)
36
+ return;
37
+ const ratio = estimateCtxRatio(session);
38
+ const pct = Number.isFinite(ratio) ? Math.round((1 - ratio) * 100) : null;
39
+ dock.setStatus(`▸ ${getMode()} · /mode to switch · ${agentModel} · ctx left ~${pct}%`);
40
+ };
41
+ const withRun = async (run) => {
42
+ currentRun = run;
43
+ runStart = Date.now();
44
+ runUsage = { in: null, out: null, cache: null, known: false };
45
+ const stopSpinner = startStatusSpinner((g) => {
46
+ runGlyph = g;
47
+ statusCb(runUsage, estimateCtxRatio(session));
48
+ });
49
+ try {
50
+ turnNo += 1;
51
+ const last = await consumeRun(session, run, input, turnNo, faux, null, statusCb);
52
+ failOnFauxExhaustion(last, faux, input);
53
+ }
54
+ finally {
55
+ stopSpinner();
56
+ paintIdle();
57
+ currentRun = null;
58
+ }
59
+ };
60
+ input.onSigint(() => {
61
+ if (currentRun) {
62
+ // 八: Ctrl+C cancels the pending question AND the run.
63
+ console.log("\n[aborting run]");
64
+ pendingAsk?.();
65
+ currentRun.abort();
66
+ }
67
+ else if (!cancelled) {
68
+ // 第四轮(对抗): also unblock a pending startup question — the
69
+ // readline close alone would leave ask() hanging forever.
70
+ // 第五轮(P2-2): the cancellation is recorded so the recovery is
71
+ // NOT started afterwards — Ctrl+C exits cleanly.
72
+ cancelled = true;
73
+ console.log("\n[exit requested]");
74
+ pendingAsk?.();
75
+ input.close();
76
+ }
77
+ });
78
+ input.onEot(() => {
79
+ if (!currentRun && !cancelled && input.line() === "") {
80
+ cancelled = true;
81
+ console.log("\n[exit requested]");
82
+ input.close();
83
+ }
84
+ });
85
+ input.onEscape(() => {
86
+ if (currentRun) {
87
+ console.log("\n[aborting run]");
88
+ pendingAsk?.();
89
+ currentRun.abort();
90
+ }
91
+ });
92
+ try {
93
+ await resolveUncertains(session, input, () => cancelled);
94
+ if (!cancelled) {
95
+ await withRun(session.resume());
96
+ if (prompt !== undefined && prompt !== "") {
97
+ await withRun(session.run(prompt));
98
+ }
99
+ }
100
+ }
101
+ finally {
102
+ input.close();
103
+ }
104
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * 手感批 B4 — the CLI's shared process state. The module split (dispatch/
3
+ * chat/resume/trust-ui/faux-glue) is a PURE MOVE: every piece that more
4
+ * than one module touches lives here as a live ESM binding. index.ts
5
+ * creates the mutable ones (setBody / setAgentModel / setExtensionLists);
6
+ * the moved modules read and mutate at call time.
7
+ */
8
+ import { Dock, type Body } from "@vincemakes/kiso-tui";
9
+ import type { KisoExtension } from "@vincemakes/kiso-runtime";
10
+ /** 发现#11: KISO_HOME is the ONE root — every default path derives from
11
+ * it (sessions, trust, extensions, mcp config, skills). The dedicated
12
+ * env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
13
+ * still override their own path; nothing hard-codes ~/.kiso anymore. */
14
+ export declare function kisoHome(): string;
15
+ export declare function sessionsDir(): string;
16
+ /** E1: the extension scan directory — KISO_EXTENSIONS_DIR overrides. */
17
+ export declare function extensionsDir(): string;
18
+ /**
19
+ * v2c — the interactive input source. TTYs use the raw-mode Editor (the
20
+ * self-drawn input row — width-aware, the CJK-drift root cause retired,
21
+ * editor.ts); everything else keeps readline exactly as v2b (pipe bytes
22
+ * unchanged). ask()/chat()/resume() talk to this, never to a concrete
23
+ * source.
24
+ */
25
+ export interface LineInput {
26
+ onLine(cb: (line: string) => void): void;
27
+ onSigint(cb: () => void): void;
28
+ onEot(cb: () => void): void;
29
+ onEscape(cb: () => void): void;
30
+ question(query: string, cb: (answer: string) => void): void;
31
+ cancelQuestion(): void;
32
+ emitLine(line: string): void;
33
+ line(): string;
34
+ clearLine(): void;
35
+ prompt(): void;
36
+ close(): void;
37
+ readonly closed: Promise<void>;
38
+ }
39
+ /** v2b: the bottom-anchored UI — docked only on a color TTY; pipes and
40
+ * NO_COLOR stay the v2a line mode byte-for-byte. Created at load, like
41
+ * the pre-split module-scope const. */
42
+ export declare const dock: Dock;
43
+ /** v2d: the body renderer — the ONE writer of the stdout scroll region
44
+ * (the frozen area + the active tail). Pipes run it in passthrough (the
45
+ * v2b/v2c line-mode bytes, byte-for-byte). Created in main; closed on
46
+ * every exit path. */
47
+ export declare let body: Body;
48
+ export declare function setBody(value: Body): void;
49
+ /** v2d: body output routes through the cell renderer — the single writer.
50
+ * bodyLog adds the trailing newline; internal newlines are preserved. */
51
+ export declare function bodyLog(text: string): void;
52
+ /** The model name for the status bar — set by makeAgent. */
53
+ export declare let agentModel: string;
54
+ export declare function setAgentModel(value: string): void;
55
+ /** E1: the extensions loaded by makeAgent — their names feed the banner. */
56
+ export declare let loadedExtensions: readonly KisoExtension[];
57
+ /** E1: the USER-level extensions alone — the banner's unmarked part (E3:
58
+ * loadedExtensions later includes the project-level ones too). */
59
+ export declare let userExtensions: readonly KisoExtension[];
60
+ /** E3: the PROJECT-level extensions (loaded after the trust gate) — the
61
+ * banner distinguishes them from the user-level ones. */
62
+ export declare let projectExtensions: readonly KisoExtension[];
63
+ export declare function setExtensionLists(user: readonly KisoExtension[], project: readonly KisoExtension[], loaded: readonly KisoExtension[]): void;
64
+ /** E3: temp artifacts of the mcp/skills merge — removed on exit. */
65
+ export declare const mergedTempPaths: string[];
66
+ /** The CLI's own version — read from the package.json next to the build. */
67
+ export declare const VERSION: string;
68
+ /** 十: a question cancelled by Ctrl+C — NEVER the empty string, which is a
69
+ * real user answer (the empty line). The empty answer and the cancellation
70
+ * are distinct facts. */
71
+ export declare const CANCELLED: unique symbol;
package/dist/state.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * 手感批 B4 — the CLI's shared process state. The module split (dispatch/
3
+ * chat/resume/trust-ui/faux-glue) is a PURE MOVE: every piece that more
4
+ * than one module touches lives here as a live ESM binding. index.ts
5
+ * creates the mutable ones (setBody / setAgentModel / setExtensionLists);
6
+ * the moved modules read and mutate at call time.
7
+ */
8
+ import { readFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { Dock } from "@vincemakes/kiso-tui";
13
+ /** 发现#11: KISO_HOME is the ONE root — every default path derives from
14
+ * it (sessions, trust, extensions, mcp config, skills). The dedicated
15
+ * env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
16
+ * still override their own path; nothing hard-codes ~/.kiso anymore. */
17
+ export function kisoHome() {
18
+ return process.env.KISO_HOME ?? join(homedir(), ".kiso");
19
+ }
20
+ export function sessionsDir() {
21
+ return join(kisoHome(), "sessions");
22
+ }
23
+ /** E1: the extension scan directory — KISO_EXTENSIONS_DIR overrides. */
24
+ export function extensionsDir() {
25
+ return process.env.KISO_EXTENSIONS_DIR ?? join(kisoHome(), "extensions");
26
+ }
27
+ /** v2b: the bottom-anchored UI — docked only on a color TTY; pipes and
28
+ * NO_COLOR stay the v2a line mode byte-for-byte. Created at load, like
29
+ * the pre-split module-scope const. */
30
+ export const dock = new Dock();
31
+ /** v2d: the body renderer — the ONE writer of the stdout scroll region
32
+ * (the frozen area + the active tail). Pipes run it in passthrough (the
33
+ * v2b/v2c line-mode bytes, byte-for-byte). Created in main; closed on
34
+ * every exit path. */
35
+ export let body;
36
+ export function setBody(value) {
37
+ body = value;
38
+ }
39
+ /** v2d: body output routes through the cell renderer — the single writer.
40
+ * bodyLog adds the trailing newline; internal newlines are preserved. */
41
+ export function bodyLog(text) {
42
+ body.raw(text.split("\n"));
43
+ }
44
+ /** The model name for the status bar — set by makeAgent. */
45
+ export let agentModel = "faux";
46
+ export function setAgentModel(value) {
47
+ agentModel = value;
48
+ }
49
+ /** E1: the extensions loaded by makeAgent — their names feed the banner. */
50
+ export let loadedExtensions = [];
51
+ /** E1: the USER-level extensions alone — the banner's unmarked part (E3:
52
+ * loadedExtensions later includes the project-level ones too). */
53
+ export let userExtensions = [];
54
+ /** E3: the PROJECT-level extensions (loaded after the trust gate) — the
55
+ * banner distinguishes them from the user-level ones. */
56
+ export let projectExtensions = [];
57
+ export function setExtensionLists(user, project, loaded) {
58
+ userExtensions = user;
59
+ projectExtensions = project;
60
+ loadedExtensions = loaded;
61
+ }
62
+ /** E3: temp artifacts of the mcp/skills merge — removed on exit. */
63
+ export const mergedTempPaths = [];
64
+ /** The CLI's own version — read from the package.json next to the build. */
65
+ export const VERSION = (() => {
66
+ try {
67
+ const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
68
+ return pkg.version ?? "?";
69
+ }
70
+ catch {
71
+ // a packed CLI without a readable package.json still works
72
+ return "?";
73
+ }
74
+ })();
75
+ /** 十: a question cancelled by Ctrl+C — NEVER the empty string, which is a
76
+ * real user answer (the empty line). The empty answer and the cancellation
77
+ * are distinct facts. */
78
+ export const CANCELLED = Symbol("kiso-question-cancelled");
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 手感批 B4 (pure move) — the human-facing question UI: the E3 project
3
+ * trust gate (ADR-0037), the mcp/skills env merges, the generic ask()
4
+ * (approvals, trust, uncertain resolutions), and the uncertain-execution
5
+ * decisions. All bodies moved verbatim from index.ts.
6
+ */
7
+ import { type ProjectArtifacts } from "@vincemakes/kiso-runtime";
8
+ import type { AgentSession } from "@vincemakes/kiso-runtime";
9
+ import { CANCELLED, type LineInput } from "./state.js";
10
+ /** v2a: the interactive prompt — blue, the identity accent. readline owns
11
+ * the echo of what the user types; we own the prompt's color. (v2c: the
12
+ * readline prompt keeps "you> " — the brick ▌ is the dock's row only;
13
+ * pipe bytes must not change.) */
14
+ export declare function interactivePrompt(): string;
15
+ /**
16
+ * Ask the human a question. Non-interactive stdin (piped, CI) cannot wait
17
+ * forever: approvals auto-deny and uncertain executions auto-abandon, both
18
+ * printed loudly — never silently ignored, never hung (Area 7).
19
+ *
20
+ * 八/十: the question is ABORTABLE — a pending rl.question is registered in
21
+ * `pendingAsk` and the SIGINT handler resolves it with the CANCELLED
22
+ * sentinel. The rl.question callback is NOT left dangling: an input that
23
+ * arrives after the cancellation is re-emitted as a fresh "line" — it
24
+ * becomes the next user turn instead of being swallowed by the dead
25
+ * question.
26
+ */
27
+ export declare let pendingAsk: (() => void) | null;
28
+ export declare function ask(input: LineInput, question: string): Promise<string | typeof CANCELLED>;
29
+ /**
30
+ * E3 — the project-level trust gate (ADR-0037): capability is trusted by
31
+ * content digest, not by directory. Runs BEFORE any extension loads — the
32
+ * mcp/skills merges must be in the env before the user-level extensions are
33
+ * loaded (the mcp factory reads KISO_MCP_CONFIG at load time, the skills
34
+ * extension scans KISO_SKILLS_DIR at load time).
35
+ *
36
+ * Verdicts: granted → load; refused → never load, never re-ask (refused is
37
+ * sticky — re-evaluate by deleting the trust line or changing a file); no
38
+ * record → only a HUMAN may decide, TTY only — non-TTY refuses with one
39
+ * stderr line. Returns the artifacts on grant, null on anything else.
40
+ */
41
+ export declare function resolveProjectTrust(input: LineInput): Promise<ProjectArtifacts | null>;
42
+ /**
43
+ * E3 — merge the project's mcp.json and skills into the env BEFORE the
44
+ * extension load. A server name in BOTH configs is a LOUD error (a silent
45
+ * override would be a supply-chain surprise); a skill name in both merges
46
+ * with project-wins and a stderr note. Exported for tests.
47
+ */
48
+ export declare function applyProjectMerges(artifacts: ProjectArtifacts): void;
49
+ /** Decide every uncertain execution with the human (r)erun/(a)bandon. */
50
+ export declare function resolveUncertains(session: AgentSession, input: LineInput, isCancelled: () => boolean): Promise<void>;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * 手感批 B4 (pure move) — the human-facing question UI: the E3 project
3
+ * trust gate (ADR-0037), the mcp/skills env merges, the generic ask()
4
+ * (approvals, trust, uncertain resolutions), and the uncertain-execution
5
+ * decisions. All bodies moved verbatim from index.ts.
6
+ */
7
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, symlinkSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { escapeTerminal, palette } from "@vincemakes/kiso-tui";
11
+ import { projectArtifacts, recordTrust, trustFor } from "@vincemakes/kiso-runtime";
12
+ import { CANCELLED, bodyLog, dock, kisoHome, mergedTempPaths } from "./state.js";
13
+ /** v2a: the interactive prompt — blue, the identity accent. readline owns
14
+ * the echo of what the user types; we own the prompt's color. (v2c: the
15
+ * readline prompt keeps "you> " — the brick ▌ is the dock's row only;
16
+ * pipe bytes must not change.) */
17
+ export function interactivePrompt() {
18
+ const p = palette();
19
+ return `${p.blue}you> ${p.reset}`;
20
+ }
21
+ /**
22
+ * Ask the human a question. Non-interactive stdin (piped, CI) cannot wait
23
+ * forever: approvals auto-deny and uncertain executions auto-abandon, both
24
+ * printed loudly — never silently ignored, never hung (Area 7).
25
+ *
26
+ * 八/十: the question is ABORTABLE — a pending rl.question is registered in
27
+ * `pendingAsk` and the SIGINT handler resolves it with the CANCELLED
28
+ * sentinel. The rl.question callback is NOT left dangling: an input that
29
+ * arrives after the cancellation is re-emitted as a fresh "line" — it
30
+ * becomes the next user turn instead of being swallowed by the dead
31
+ * question.
32
+ */
33
+ export let pendingAsk = null;
34
+ export function ask(input, question) {
35
+ if (!process.stdin.isTTY) {
36
+ console.log(`[non-interactive — no human to ask: ${question}]`);
37
+ return Promise.resolve("");
38
+ }
39
+ // v2b: docked — the question takes over the status position, the
40
+ // answer lands at the input line. v2c: a TTY without a dock (rows < 4)
41
+ // prints the question into the body — the editor cannot show it.
42
+ if (dock.active) {
43
+ dock.showQuestion(question);
44
+ }
45
+ else {
46
+ bodyLog(question);
47
+ }
48
+ return new Promise((resolve) => {
49
+ let settled = false;
50
+ pendingAsk = () => {
51
+ if (settled)
52
+ return;
53
+ settled = true;
54
+ pendingAsk = null;
55
+ input.cancelQuestion();
56
+ resolve(CANCELLED); // the run is aborting — the question is dead
57
+ };
58
+ // v2b: docked — the question reads at the input line, whose prompt
59
+ // is the same blue you> (the editor's brick row; the readline path
60
+ // passes the plain question). An empty prompt would start readline
61
+ // at column 1 while the dock renders "you> " — the typed answer
62
+ // would land on the prompt and drift (probe-confirmed).
63
+ input.question(dock.active ? interactivePrompt() : question, (answer) => {
64
+ if (settled) {
65
+ // The question was cancelled; this line is a NEW user turn.
66
+ input.emitLine(answer);
67
+ return;
68
+ }
69
+ settled = true;
70
+ pendingAsk = null;
71
+ if (dock.active)
72
+ dock.clearQuestion();
73
+ resolve(answer);
74
+ });
75
+ });
76
+ }
77
+ /**
78
+ * E3 — the project-level trust gate (ADR-0037): capability is trusted by
79
+ * content digest, not by directory. Runs BEFORE any extension loads — the
80
+ * mcp/skills merges must be in the env before the user-level extensions are
81
+ * loaded (the mcp factory reads KISO_MCP_CONFIG at load time, the skills
82
+ * extension scans KISO_SKILLS_DIR at load time).
83
+ *
84
+ * Verdicts: granted → load; refused → never load, never re-ask (refused is
85
+ * sticky — re-evaluate by deleting the trust line or changing a file); no
86
+ * record → only a HUMAN may decide, TTY only — non-TTY refuses with one
87
+ * stderr line. Returns the artifacts on grant, null on anything else.
88
+ */
89
+ export async function resolveProjectTrust(input) {
90
+ const artifacts = await projectArtifacts(process.cwd());
91
+ if (artifacts === null)
92
+ return null; // no .kiso artifacts — nothing to gate
93
+ const record = trustFor(artifacts.root, artifacts.digest);
94
+ if (record?.decision === "granted") {
95
+ applyProjectMerges(artifacts);
96
+ return artifacts;
97
+ }
98
+ if (record?.decision === "refused")
99
+ return null; // refused is sticky — no re-ask
100
+ // First discovery — list every artifact (file name + digest short
101
+ // prefix) and ask the human ONCE.
102
+ if (!process.stdin.isTTY) {
103
+ console.error(`[project .kiso] found ${artifacts.files.length} artifact(s) in ${artifacts.root} — not trusted, not loaded (run kiso interactively once to decide)`);
104
+ return null;
105
+ }
106
+ // v2c: the shared input (the editor on a TTY) reads the answer; the
107
+ // dock shows the question at the status position.
108
+ bodyLog(`[project .kiso] ${artifacts.root}`);
109
+ for (const f of artifacts.files) {
110
+ bodyLog(` ${f.path} (${f.digest.slice(0, 6)})`);
111
+ }
112
+ const answer = await ask(input, `trust this project's .kiso? (y/n) `);
113
+ const granted = answer !== CANCELLED && answer.trim().toLowerCase().startsWith("y");
114
+ recordTrust({ root: artifacts.root, digest: artifacts.digest, decision: granted ? "granted" : "refused" });
115
+ if (!granted)
116
+ return null;
117
+ applyProjectMerges(artifacts);
118
+ return artifacts;
119
+ }
120
+ /**
121
+ * E3 — merge the project's mcp.json and skills into the env BEFORE the
122
+ * extension load. A server name in BOTH configs is a LOUD error (a silent
123
+ * override would be a supply-chain surprise); a skill name in both merges
124
+ * with project-wins and a stderr note. Exported for tests.
125
+ */
126
+ export function applyProjectMerges(artifacts) {
127
+ if (artifacts.files.some((f) => f.kind === "mcp"))
128
+ applyMcpMerge(artifacts.root);
129
+ if (artifacts.files.some((f) => f.kind === "skill"))
130
+ applySkillsMerge(artifacts.root);
131
+ }
132
+ /** Read an mcp.json with the mcp extension's tolerance: absent/unreadable →
133
+ * {}, present-but-broken → throw (the loader convention). */
134
+ function readMcpConfig(path) {
135
+ let text;
136
+ try {
137
+ text = readFileSync(path, "utf8");
138
+ }
139
+ catch {
140
+ return {};
141
+ }
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(text);
145
+ }
146
+ catch (err) {
147
+ throw new Error(`[project .kiso] cannot parse ${path}: ${err.message}`);
148
+ }
149
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
150
+ throw new Error(`[project .kiso] ${path} must be an object with an mcpServers map`);
151
+ }
152
+ return parsed;
153
+ }
154
+ /** Merge user-level + project-level mcp.json into one temp file and point
155
+ * KISO_MCP_CONFIG at it — the mcp extension reads it at load time. */
156
+ function applyMcpMerge(root) {
157
+ const userPath = process.env.KISO_MCP_CONFIG ?? join(kisoHome(), "mcp.json");
158
+ const user = readMcpConfig(userPath);
159
+ const project = readMcpConfig(join(root, "mcp.json"));
160
+ const userServers = user.mcpServers ?? {};
161
+ const projectServers = project.mcpServers ?? {};
162
+ if (Object.keys(projectServers).length === 0)
163
+ return; // nothing to merge
164
+ for (const name of Object.keys(projectServers)) {
165
+ if (name in userServers) {
166
+ throw new Error(`[project .kiso] mcp server "${name}" exists in both the user-level and the project-level mcp.json`);
167
+ }
168
+ }
169
+ const merged = { mcpServers: { ...userServers, ...projectServers } };
170
+ const temp = join(tmpdir(), `kiso-mcp-merged-${process.pid}.json`);
171
+ writeFileSync(temp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
172
+ process.env.KISO_MCP_CONFIG = temp;
173
+ mergedTempPaths.push(temp);
174
+ }
175
+ /** Merge user-level + project-level skills into one temp scan dir (project
176
+ * skill dirs symlinked first; a name in both → project wins + a stderr
177
+ * note) and point KISO_SKILLS_DIR at it — the skills extension's existing
178
+ * scan reads it at load time and per read_skill call. */
179
+ function applySkillsMerge(root) {
180
+ const userDir = process.env.KISO_SKILLS_DIR ?? join(kisoHome(), "skills");
181
+ const projectDir = join(root, "skills");
182
+ const merged = mkdtempSync(join(tmpdir(), "kiso-skills-"));
183
+ mergedTempPaths.push(merged);
184
+ for (const dir of readdirSyncSafe(projectDir)) {
185
+ symlinkSync(join(projectDir, dir), join(merged, dir)); // project wins on collision
186
+ }
187
+ for (const dir of readdirSyncSafe(userDir)) {
188
+ const target = join(merged, dir);
189
+ if (existsSync(target)) {
190
+ console.error(`[project .kiso] skill "${dir}" exists in both user and project skills — project wins`);
191
+ continue;
192
+ }
193
+ symlinkSync(join(userDir, dir), target);
194
+ }
195
+ process.env.KISO_SKILLS_DIR = merged;
196
+ }
197
+ function readdirSyncSafe(dir) {
198
+ try {
199
+ return readdirSync(dir);
200
+ }
201
+ catch {
202
+ return []; // no skills dir on either level = nothing to merge
203
+ }
204
+ }
205
+ /** Decide every uncertain execution with the human (r)erun/(a)bandon. */
206
+ export async function resolveUncertains(session, input, isCancelled) {
207
+ for (const uncertain of session.uncertainExecutions()) {
208
+ const answer = await ask(input, `⚠ interrupted execution: ${escapeTerminal(uncertain.name)} (${uncertain.executionId}) — did it apply? (r)erun / (a)bandon: `);
209
+ if (isCancelled() || answer === CANCELLED) {
210
+ // 十: a cancellation NEVER records a verdict — the execution
211
+ // stays uncertain and durable; no rerun/abandoned is fabricated.
212
+ return;
213
+ }
214
+ const resolution = answer.trim().toLowerCase().startsWith("r") ? "rerun" : "abandoned";
215
+ await session.resolveUncertain(uncertain.executionId, resolution);
216
+ console.log(` ${resolution}\n`);
217
+ }
218
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-code",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "kiso CLI — the coding-agent reference product: kiso chat / kiso resume / kiso sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,13 +18,13 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "dependencies": {
21
- "@vincemakes/kiso-core": "0.1.19",
22
- "@vincemakes/kiso-evals": "0.1.19",
23
- "@vincemakes/kiso-provider-anthropic": "0.1.19",
24
- "@vincemakes/kiso-provider-openai": "0.1.19",
25
- "@vincemakes/kiso-runtime": "0.1.19",
26
- "@vincemakes/kiso-tools-node": "0.1.19",
27
- "@vincemakes/kiso-tui": "0.1.19"
21
+ "@vincemakes/kiso-core": "0.1.21",
22
+ "@vincemakes/kiso-evals": "0.1.21",
23
+ "@vincemakes/kiso-provider-anthropic": "0.1.21",
24
+ "@vincemakes/kiso-provider-openai": "0.1.21",
25
+ "@vincemakes/kiso-runtime": "0.1.21",
26
+ "@vincemakes/kiso-tools-node": "0.1.21",
27
+ "@vincemakes/kiso-tui": "0.1.21"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^26.1.2",