@vincemakes/kiso-code 0.14.0 → 0.15.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 +51 -6
- package/dist/chat.d.ts +18 -2
- package/dist/chat.js +53 -8
- package/dist/config.d.ts +9 -2
- package/dist/config.js +15 -5
- package/dist/dispatch.d.ts +7 -0
- package/dist/dispatch.js +113 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +233 -32
- package/dist/state.d.ts +5 -0
- package/package.json +18 -14
package/README.md
CHANGED
|
@@ -1,9 +1,54 @@
|
|
|
1
1
|
# @vincemakes/kiso-code
|
|
2
2
|
|
|
3
|
-
The coding
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
switch to real providers.
|
|
3
|
+
The coding agent that survives kill -9: durable sessions (append-only
|
|
4
|
+
JSONL), pre-effect approvals, and honest recovery — an interrupted side
|
|
5
|
+
effect is surfaced and asked about, never silently re-run.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
## Install and first session
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install -g @vincemakes/kiso-code
|
|
11
|
+
kiso # or: npx @vincemakes/kiso-code
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
With no key set you get **faux mode** — a scripted four-round demo of
|
|
15
|
+
the full CLI (tools, approvals, sessions), zero keys. When the script
|
|
16
|
+
runs out (about two user turns) the session exits with a set-a-key
|
|
17
|
+
message; that exit is the design, not a crash.
|
|
18
|
+
|
|
19
|
+
## A real model
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
# OpenAI-compatible (checked first; the key alone talks to OpenAI —
|
|
23
|
+
# OPENAI_BASE_URL optionally retargets DeepSeek or any compat endpoint):
|
|
24
|
+
export OPENAI_API_KEY=... # OPENAI_MODEL (default gpt-4o)
|
|
25
|
+
|
|
26
|
+
# or Anthropic:
|
|
27
|
+
export ANTHROPIC_API_KEY=... # ANTHROPIC_MODEL (default claude-sonnet-5)
|
|
28
|
+
|
|
29
|
+
kiso
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Named model profiles live in `~/.kiso/config.json` (the config stores
|
|
33
|
+
the NAME of the env var holding each key, never the key itself); switch
|
|
34
|
+
in-session with `/model`. Approval tiers: `--mode
|
|
35
|
+
manual|default|accept-edits|plan|bypass` or `/mode` in-session.
|
|
36
|
+
|
|
37
|
+
## The commands
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
kiso [sessionId] interactive session (default command)
|
|
41
|
+
kiso resume pick a session to continue (TTY picker)
|
|
42
|
+
kiso resume <id> ["prompt"] continue a session, one-shot
|
|
43
|
+
kiso sessions list durable sessions
|
|
44
|
+
kiso help usage + configuration reference
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Sessions live under `~/.kiso/sessions`; kill the process — `kill -9`
|
|
48
|
+
included — and `kiso resume` continues exactly where the log ends.
|
|
49
|
+
|
|
50
|
+
**Platforms:** macOS / Linux (Node ≥ 22). Windows is unsupported.
|
|
51
|
+
|
|
52
|
+
Full documentation — extensions (MCP, skills, subagents), the approval
|
|
53
|
+
model, session recovery, configuration reference — in the
|
|
54
|
+
[repository README](https://github.com/vincemakes/kiso#readme).
|
package/dist/chat.d.ts
CHANGED
|
@@ -76,7 +76,7 @@ export interface UsageDelta {
|
|
|
76
76
|
* absent provider resolves like the tracer's "adapter" identity — the
|
|
77
77
|
* total convention (INPUT_CONVENTIONS), never a crash.
|
|
78
78
|
*/
|
|
79
|
-
export declare function usageFromEvent(route: string | undefined, ev: import("@vincemakes/kiso-core").Usage, prevTotal: number | null): UsageDelta;
|
|
79
|
+
export declare function usageFromEvent(route: string | undefined, ev: import("@vincemakes/kiso-core").Usage, prevTotal: number | null, model?: string): UsageDelta;
|
|
80
80
|
/** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
|
|
81
81
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
82
82
|
* first event. */
|
|
@@ -158,4 +158,20 @@ export declare function consumeRun(session: AgentSession, run: Run, input: LineI
|
|
|
158
158
|
* (resume) where a dropped amend is noticed instead. */
|
|
159
159
|
submitTurn?: (line: string) => void): Promise<import("@vincemakes/kiso-core").Event | undefined>;
|
|
160
160
|
/** Interactive REPL: stream events, pause for approvals, Ctrl+C aborts. */
|
|
161
|
-
|
|
161
|
+
/** The chat loop's ENDING: exit closes the process's REPL for good; a
|
|
162
|
+
* switch hands main another session id to re-enter chat with — the
|
|
163
|
+
* editor survives, the durable law is untouched (the /resume+/clear
|
|
164
|
+
* mini-spec). */
|
|
165
|
+
export type ChatEnd = {
|
|
166
|
+
readonly next: "exit";
|
|
167
|
+
} | {
|
|
168
|
+
readonly next: "switch";
|
|
169
|
+
readonly id: string;
|
|
170
|
+
};
|
|
171
|
+
/** The session-navigation seam main provides: the OTHER sessions'
|
|
172
|
+
* ids, and (when a dock is up) the existing picker. */
|
|
173
|
+
export interface ChatNav {
|
|
174
|
+
readonly sessions: () => readonly string[];
|
|
175
|
+
readonly pick?: () => Promise<string | null>;
|
|
176
|
+
}
|
|
177
|
+
export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav): Promise<ChatEnd>;
|
package/dist/chat.js
CHANGED
|
@@ -9,8 +9,10 @@ import { escapeTerminal, cacheHitPct, idleStatus, palette, renderEvent, renderRe
|
|
|
9
9
|
import { deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
10
|
import { canonicalTargetPath, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
11
11
|
import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
|
|
12
|
+
import { canonicalizeUsageForModel } from "@vincemakes/kiso-runtime/internal";
|
|
12
13
|
import { dispatch } from "./dispatch.js";
|
|
13
14
|
import { agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
|
|
15
|
+
import { lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
|
|
14
16
|
import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
15
17
|
import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
16
18
|
import { MODES, getMode, setMode } from "./mode.js";
|
|
@@ -37,7 +39,18 @@ export function contextWindowTokens() {
|
|
|
37
39
|
if (windowOverride !== undefined)
|
|
38
40
|
return windowOverride;
|
|
39
41
|
const window = Number.parseInt(process.env.KISO_CONTEXT_WINDOW ?? "", 10);
|
|
40
|
-
|
|
42
|
+
if (Number.isFinite(window) && window > 0)
|
|
43
|
+
return window;
|
|
44
|
+
// PH-1c (finding PH-F15): the window follows the LIVE model when the
|
|
45
|
+
// metadata registry knows it — /model to a known model moves the
|
|
46
|
+
// window (and the microcompact threshold derived from it) without an
|
|
47
|
+
// env var. agentModel is the same live binding the status row shows;
|
|
48
|
+
// an unknown model keeps the 200k default — the registry never
|
|
49
|
+
// guesses, so neither do we.
|
|
50
|
+
const known = lookupModelMetadata(agentModel)?.capabilities.contextWindow;
|
|
51
|
+
if (known !== undefined && known !== null)
|
|
52
|
+
return known;
|
|
53
|
+
return DEFAULT_CONTEXT_WINDOW;
|
|
41
54
|
}
|
|
42
55
|
/**
|
|
43
56
|
* B area: approximate context ratio — chars/4 of the projected messages vs
|
|
@@ -78,8 +91,13 @@ const CACHE_MISS_FLOOR = 1024;
|
|
|
78
91
|
* absent provider resolves like the tracer's "adapter" identity — the
|
|
79
92
|
* total convention (INPUT_CONVENTIONS), never a crash.
|
|
80
93
|
*/
|
|
81
|
-
export function usageFromEvent(route, ev, prevTotal
|
|
82
|
-
|
|
94
|
+
export function usageFromEvent(route, ev, prevTotal,
|
|
95
|
+
// PH-1c: the LIVE model — when given, the $cost keys on the model's
|
|
96
|
+
// metadata entry (an unpriced model shows null, never a route-table
|
|
97
|
+
// guess); omitted, the legacy route-keyed path stands (old callers,
|
|
98
|
+
// old tests, unchanged bytes).
|
|
99
|
+
model) {
|
|
100
|
+
const c = model === undefined ? canonicalizeUsage(route ?? "adapter", ev) : canonicalizeUsageForModel(model, undefined, route ?? "adapter", ev);
|
|
83
101
|
const total = c.input + c.cacheRead + (c.cacheWrite ?? 0);
|
|
84
102
|
let missed = null;
|
|
85
103
|
// R-C item 4: min(prevTotal, total) is the part that could have been
|
|
@@ -596,7 +614,7 @@ submitTurn) {
|
|
|
596
614
|
body.textEnd();
|
|
597
615
|
break;
|
|
598
616
|
case "usage": {
|
|
599
|
-
const delta = usageFromEvent(session.provider, ev, prevTotal);
|
|
617
|
+
const delta = usageFromEvent(session.provider, ev, prevTotal, agentModel);
|
|
600
618
|
usage = delta.usage;
|
|
601
619
|
prevTotal = delta.total;
|
|
602
620
|
missed = delta.missed;
|
|
@@ -775,8 +793,14 @@ submitTurn) {
|
|
|
775
793
|
}
|
|
776
794
|
return last;
|
|
777
795
|
}
|
|
778
|
-
|
|
779
|
-
|
|
796
|
+
export async function chat(session, faux, input, autoCompact, nav) {
|
|
797
|
+
// the switch directive — set once by dispatch's /clear or /resume,
|
|
798
|
+
// resolved through the end signal so the final awaits still run
|
|
799
|
+
let switchTo = null;
|
|
800
|
+
let resolveEnd = () => { };
|
|
801
|
+
const endSignal = new Promise((r) => {
|
|
802
|
+
resolveEnd = r;
|
|
803
|
+
});
|
|
780
804
|
let currentRun = null;
|
|
781
805
|
let cancelled = false;
|
|
782
806
|
// E group (the graceful-exit gate ③, R-G 0.1.48): the terminal can
|
|
@@ -1096,6 +1120,15 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1096
1120
|
submitTurn,
|
|
1097
1121
|
estimateCtx: () => estimateCtxRatio(session),
|
|
1098
1122
|
contextWindow: () => contextWindowTokens(),
|
|
1123
|
+
// the /resume+/clear mini-spec: the switch directive and the
|
|
1124
|
+
// session-navigation seam (absent nav = the commands degrade to
|
|
1125
|
+
// an honest refusal in dispatch)
|
|
1126
|
+
requestSwitch: (id) => {
|
|
1127
|
+
switchTo = id;
|
|
1128
|
+
resolveEnd();
|
|
1129
|
+
},
|
|
1130
|
+
sessions: () => nav?.sessions() ?? [],
|
|
1131
|
+
...(nav?.pick !== undefined ? { pickSession: nav.pick } : {}),
|
|
1099
1132
|
};
|
|
1100
1133
|
// the ergonomics batch C8: the auto-compact check — the /compact FULL path via the
|
|
1101
1134
|
// shared dispatch (same notices, same chain ordering, same mid-run
|
|
@@ -1125,6 +1158,15 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1125
1158
|
// chain action (the sentinel's control char marks the key, so a typed
|
|
1126
1159
|
// "expand" turn is never intercepted).
|
|
1127
1160
|
input.onExpand(() => dispatch("\x12expand", dispatchCtx));
|
|
1161
|
+
// R3a — Shift+Tab: the approval-tier cycle (the /mode ring, in the
|
|
1162
|
+
// MODES order). The switch is the SAME live-extension flip /mode
|
|
1163
|
+
// performs; the status row repaints at once with a one-line notice.
|
|
1164
|
+
input.onModeCycle?.(() => {
|
|
1165
|
+
const next = MODES[(MODES.indexOf(getMode()) + 1) % MODES.length];
|
|
1166
|
+
setMode(next);
|
|
1167
|
+
paintIdle();
|
|
1168
|
+
body.notice(`mode → ${next} (shift+tab cycles)`);
|
|
1169
|
+
});
|
|
1128
1170
|
// Recovery first: a session with a dangling pause or uncertain
|
|
1129
1171
|
// executions must resolve them BEFORE the REPL accepts new turns —
|
|
1130
1172
|
// otherwise the interrupted run dangles while a new one starts.
|
|
@@ -1156,7 +1198,7 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1156
1198
|
if (cancelled) {
|
|
1157
1199
|
input.close();
|
|
1158
1200
|
await input.closed;
|
|
1159
|
-
return;
|
|
1201
|
+
return { next: "exit" };
|
|
1160
1202
|
}
|
|
1161
1203
|
// The REPL is ready: replay anything that arrived during recovery.
|
|
1162
1204
|
replReady = true;
|
|
@@ -1170,7 +1212,9 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1170
1212
|
}
|
|
1171
1213
|
queuedLines.length = 0;
|
|
1172
1214
|
input.prompt();
|
|
1173
|
-
|
|
1215
|
+
// the REPL ends by CLOSE (exit) or by SWITCH (/clear, /resume) — the
|
|
1216
|
+
// switch leaves the editor alive for the next chat() entry
|
|
1217
|
+
await Promise.race([input.closed, endSignal]);
|
|
1174
1218
|
await chainRef.current; // never exit while a turn is in flight
|
|
1175
1219
|
// the ergonomics batch C8: the auto-compact may have appended ITS segment inside the
|
|
1176
1220
|
// turn (the check runs at the turn's end, after the exit-await above
|
|
@@ -1178,4 +1222,5 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1178
1222
|
// runs before the exit or the chain is already settled. One level is
|
|
1179
1223
|
// enough: the /compact segment appends nothing of its own.
|
|
1180
1224
|
await chainRef.current;
|
|
1225
|
+
return switchTo === null ? { next: "exit" } : { next: "switch", id: switchTo };
|
|
1181
1226
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -32,8 +32,15 @@ export interface ModelProfile {
|
|
|
32
32
|
readonly kind: ProfileKind;
|
|
33
33
|
readonly baseUrl?: string;
|
|
34
34
|
readonly model: string;
|
|
35
|
-
/** The env var NAME holding the key — never the key itself.
|
|
36
|
-
|
|
35
|
+
/** The env var NAME holding the key — never the key itself.
|
|
36
|
+
* PH-1c (finding PH-F19): OPTIONAL — an absent apiKeyEnv means an
|
|
37
|
+
* unauthenticated endpoint (a local Ollama, a LAN proxy); the
|
|
38
|
+
* adapter receives a placeholder key, and the profile no longer
|
|
39
|
+
* demands a dummy env var to exist. */
|
|
40
|
+
readonly apiKeyEnv?: string;
|
|
41
|
+
/** PH-1c.1: opt-in Anthropic prompt caching for this profile —
|
|
42
|
+
* default off (a request-byte cost behavior never flips silently). */
|
|
43
|
+
readonly promptCaching?: boolean;
|
|
37
44
|
}
|
|
38
45
|
export interface AutoCompactConfig {
|
|
39
46
|
readonly thresholdRatio: number;
|
package/dist/config.js
CHANGED
|
@@ -68,15 +68,18 @@ export function parseConfig(text, source) {
|
|
|
68
68
|
fail(`models.${name}.kind`, `expected one of ${KINDS.join(", ")}`);
|
|
69
69
|
if (typeof p.model !== "string" || p.model === "")
|
|
70
70
|
fail(`models.${name}.model`, "expected a model string");
|
|
71
|
-
if (typeof p.apiKeyEnv !== "string" || p.apiKeyEnv === "")
|
|
72
|
-
fail(`models.${name}.apiKeyEnv`, "expected an env var name (the config never stores keys)");
|
|
71
|
+
if (p.apiKeyEnv !== undefined && (typeof p.apiKeyEnv !== "string" || p.apiKeyEnv === ""))
|
|
72
|
+
fail(`models.${name}.apiKeyEnv`, "expected an env var name (the config never stores keys); omit it entirely for an unauthenticated local endpoint");
|
|
73
73
|
if (p.baseUrl !== undefined && typeof p.baseUrl !== "string")
|
|
74
74
|
fail(`models.${name}.baseUrl`, "expected a string");
|
|
75
|
+
if (p.promptCaching !== undefined && typeof p.promptCaching !== "boolean")
|
|
76
|
+
fail(`models.${name}.promptCaching`, "expected a boolean");
|
|
75
77
|
models[name] = {
|
|
76
78
|
kind: p.kind,
|
|
77
79
|
model: p.model,
|
|
78
|
-
apiKeyEnv: p.apiKeyEnv,
|
|
80
|
+
...(typeof p.apiKeyEnv === "string" ? { apiKeyEnv: p.apiKeyEnv } : {}),
|
|
79
81
|
...(typeof p.baseUrl === "string" ? { baseUrl: p.baseUrl } : {}),
|
|
82
|
+
...(typeof p.promptCaching === "boolean" ? { promptCaching: p.promptCaching } : {}),
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
out.models = models;
|
|
@@ -151,7 +154,9 @@ export function mergeConfigs(user, project) {
|
|
|
151
154
|
}
|
|
152
155
|
/** A profile is available when its apiKeyEnv var is set (the key exists). */
|
|
153
156
|
export function profileAvailable(p) {
|
|
154
|
-
|
|
157
|
+
// PH-1c (finding PH-F19): a keyless profile (no apiKeyEnv) is an
|
|
158
|
+
// unauthenticated endpoint — always available.
|
|
159
|
+
return p.apiKeyEnv === undefined || process.env[p.apiKeyEnv] !== undefined;
|
|
155
160
|
}
|
|
156
161
|
/** "provider/model" direct write → a profile. */
|
|
157
162
|
export function directWriteProfile(value) {
|
|
@@ -205,6 +210,9 @@ export function resolveModel(modelFlag, merged) {
|
|
|
205
210
|
kind: "anthropic",
|
|
206
211
|
model: process.env.ANTHROPIC_MODEL ?? "claude-sonnet-5",
|
|
207
212
|
apiKeyEnv: "ANTHROPIC_API_KEY",
|
|
213
|
+
// PH-1c (finding PH-F19): symmetric with OPENAI_BASE_URL —
|
|
214
|
+
// proxies and compat gateways serve the anthropic dialect too.
|
|
215
|
+
...(process.env.ANTHROPIC_BASE_URL !== undefined ? { baseUrl: process.env.ANTHROPIC_BASE_URL } : {}),
|
|
208
216
|
},
|
|
209
217
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
210
218
|
};
|
|
@@ -226,7 +234,9 @@ function resolveProfile(name, p) {
|
|
|
226
234
|
if (!profileAvailable(p)) {
|
|
227
235
|
throw new ConfigError(`model ${name}: unavailable — the env var ${p.apiKeyEnv} is not set (configs never store keys, only the env-var name)`);
|
|
228
236
|
}
|
|
229
|
-
|
|
237
|
+
// PH-1c (finding PH-F19): a keyless profile hands the adapter a
|
|
238
|
+
// placeholder — the SDKs require SOME string; the endpoint ignores it.
|
|
239
|
+
return { name, profile: p, apiKey: p.apiKeyEnv === undefined ? "none" : process.env[p.apiKeyEnv] };
|
|
230
240
|
}
|
|
231
241
|
/** Mode: env (KISO_MODE) beats config.mode; the --mode flag is applied by
|
|
232
242
|
* main before this runs (flags are the top of the chain). */
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface DispatchCtx {
|
|
|
25
25
|
/** TUI2-R1 (E): the model's context window, as the session is
|
|
26
26
|
* configured — the /context ledger's denominator. */
|
|
27
27
|
readonly contextWindow: () => number;
|
|
28
|
+
/** the /resume+/clear mini-spec: end this chat() with a switch to
|
|
29
|
+
* another session — main re-enters chat there; the editor survives. */
|
|
30
|
+
readonly requestSwitch: (id: string) => void;
|
|
31
|
+
/** every durable session id (the /resume validation + listing). */
|
|
32
|
+
readonly sessions: () => readonly string[];
|
|
33
|
+
/** the dock's session picker, when one exists (bare /resume). */
|
|
34
|
+
readonly pickSession?: () => Promise<string | null>;
|
|
28
35
|
}
|
|
29
36
|
/** The ONE dispatcher — slash commands, exit, and turns. The recovery
|
|
30
37
|
* replay routes through it too — a queued "/last" must never become a
|
package/dist/dispatch.js
CHANGED
|
@@ -193,7 +193,11 @@ export function dispatch(line, ctx) {
|
|
|
193
193
|
const marks = [`profile: ${name}`, ...(profileAvailable(profile) ? [] : ["unavailable"]), ...(profile.model === agentModel ? ["current"] : [])];
|
|
194
194
|
return { label: `${profile.kind}/${profile.model}`, note: marks.join(" · ") };
|
|
195
195
|
}),
|
|
196
|
-
|
|
196
|
+
// PH-1a (finding PH-F4): the example must be a syntax
|
|
197
|
+
// directWriteProfile actually ACCEPTS — the old
|
|
198
|
+
// "openai/…" hint failed with "no such model profile"
|
|
199
|
+
// on exactly the fresh-install path that shows it.
|
|
200
|
+
typeHint: names.length === 0 ? "type provider/model directly (e.g. openai-compat/deepseek-reasoner)" : "type provider/model directly",
|
|
197
201
|
// the zero-profile copy is TODAY'S, verbatim: the
|
|
198
202
|
// user who sees it is exactly the user who needs
|
|
199
203
|
// the path spelled out
|
|
@@ -233,10 +237,19 @@ export function dispatch(line, ctx) {
|
|
|
233
237
|
}
|
|
234
238
|
else {
|
|
235
239
|
const adapter = await buildAdapter(profile.kind, {
|
|
236
|
-
|
|
240
|
+
// PH-1c (PH-F19): a keyless profile = an unauthenticated
|
|
241
|
+
// endpoint — the placeholder satisfies the SDK's ctor.
|
|
242
|
+
apiKey: profile.apiKeyEnv === undefined ? "none" : process.env[profile.apiKeyEnv],
|
|
237
243
|
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
244
|
+
...(profile.promptCaching !== undefined ? { promptCaching: profile.promptCaching } : {}),
|
|
238
245
|
});
|
|
239
|
-
|
|
246
|
+
// PH-1a (finding PH-F8, P0): the switch is ATOMIC —
|
|
247
|
+
// adapter, model id, and provider route move together.
|
|
248
|
+
// setAdapter alone left the session's frozen config
|
|
249
|
+
// carrying the OLD model, so the status row claimed the
|
|
250
|
+
// new model while every request still sent the old id
|
|
251
|
+
// (and usage canonicalized under the old route).
|
|
252
|
+
ctx.session.setModelBinding({ adapter, model: profile.model, provider: profile.kind });
|
|
240
253
|
setAgentModel(profile.model);
|
|
241
254
|
setCurrentModelName(arg);
|
|
242
255
|
body.notice(`model → ${arg} (${profile.model}) — takes effect on the next turn`);
|
|
@@ -250,7 +263,7 @@ export function dispatch(line, ctx) {
|
|
|
250
263
|
});
|
|
251
264
|
return;
|
|
252
265
|
}
|
|
253
|
-
if (trimmed === "/compact") {
|
|
266
|
+
if (trimmed === "/compact" || trimmed.startsWith("/compact ")) {
|
|
254
267
|
// /compact (ADR-0044): the older conversation becomes one
|
|
255
268
|
// model summary — an OFF-LOOP call through the session's own
|
|
256
269
|
// adapter, so it must never race a running turn: refused
|
|
@@ -293,8 +306,13 @@ export function dispatch(line, ctx) {
|
|
|
293
306
|
}, 1000);
|
|
294
307
|
};
|
|
295
308
|
try {
|
|
309
|
+
// R3a: /compact <focus> — the words after the command steer
|
|
310
|
+
// the summary ("keep the auth details"); bare /compact is
|
|
311
|
+
// byte-identical to the pre-round call.
|
|
312
|
+
const focus = trimmed.slice(8).trim();
|
|
296
313
|
const result = await ctx.session.summarize({
|
|
297
314
|
signal: abort.signal,
|
|
315
|
+
...(focus !== "" ? { focus } : {}),
|
|
298
316
|
onStart: (info) => {
|
|
299
317
|
compactInfo = info;
|
|
300
318
|
compacting(info);
|
|
@@ -336,9 +354,100 @@ export function dispatch(line, ctx) {
|
|
|
336
354
|
return;
|
|
337
355
|
}
|
|
338
356
|
if (trimmed === "exit" || trimmed === "") {
|
|
357
|
+
// PH-1a (finding PH-F11): closing the input MID-RUN killed the very
|
|
358
|
+
// surface a later approval would ask through ("readline was
|
|
359
|
+
// closed", the v2b-era edge). On the DOCKED interactive surface the
|
|
360
|
+
// close now queues on the chain — the run finishes (approvals and
|
|
361
|
+
// all), then the REPL exits. DOCK-GATED on purpose: the pipe path
|
|
362
|
+
// is byte-pinned (a notice there would corrupt machine-read
|
|
363
|
+
// streams), a pipe's "exit" always arrives mid-run, and the
|
|
364
|
+
// approval panel the fix protects only exists on the dock. An idle
|
|
365
|
+
// exit is immediate, byte-for-byte as before, on every surface.
|
|
366
|
+
if (ctx.isRunning() && dock.active) {
|
|
367
|
+
if (trimmed === "exit")
|
|
368
|
+
body.notice("[exit queued — closing after the current run completes]");
|
|
369
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
370
|
+
ctx.input.close();
|
|
371
|
+
});
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
339
374
|
ctx.input.close();
|
|
340
375
|
return;
|
|
341
376
|
}
|
|
377
|
+
if (trimmed === "/clear") {
|
|
378
|
+
// the mini-spec: /clear = a FRESH conversation. The old session
|
|
379
|
+
// stays on disk, resumable — the append-only law does not move;
|
|
380
|
+
// what clears is the CONTEXT, never the history.
|
|
381
|
+
if (ctx.isRunning()) {
|
|
382
|
+
body.notice("[/clear] a run is in flight — let it finish (esc stops it), then clear");
|
|
383
|
+
ctx.input.prompt();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
ctx.requestSwitch(new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (trimmed === "/resume" || trimmed.startsWith("/resume ")) {
|
|
390
|
+
// the mini-spec: the in-session door to the durable sessions —
|
|
391
|
+
// /resume <id> switches directly; bare /resume opens the SAME
|
|
392
|
+
// picker `kiso resume` owns (dock), or lists ids (no dock).
|
|
393
|
+
if (ctx.isRunning()) {
|
|
394
|
+
body.notice("[/resume] a run is in flight — let it finish (esc stops it), then switch");
|
|
395
|
+
ctx.input.prompt();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const arg = trimmed.slice(7).trim();
|
|
399
|
+
if (arg !== "") {
|
|
400
|
+
// a switch never silently CREATES a session — agent.session()
|
|
401
|
+
// would; the validation is the difference
|
|
402
|
+
if (!ctx.sessions().includes(arg)) {
|
|
403
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
404
|
+
bodyLog(`no such session: ${escapeTerminal(arg)} — /resume lists them`);
|
|
405
|
+
ctx.input.prompt();
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
ctx.requestSwitch(arg);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const others = ctx.sessions().filter((id) => id !== ctx.session.id);
|
|
413
|
+
if (others.length === 0) {
|
|
414
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
415
|
+
bodyLog("no other sessions — /clear starts a fresh one");
|
|
416
|
+
ctx.input.prompt();
|
|
417
|
+
});
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (ctx.pickSession !== undefined && dock.active) {
|
|
421
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
422
|
+
const picked = await ctx.pickSession();
|
|
423
|
+
if (picked === null) {
|
|
424
|
+
ctx.input.prompt(); // esc — nothing switched, nothing said
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
ctx.requestSwitch(picked);
|
|
428
|
+
});
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
432
|
+
for (const id of others)
|
|
433
|
+
bodyLog(` ${escapeTerminal(id)}`);
|
|
434
|
+
bodyLog("switch with /resume <id>");
|
|
435
|
+
ctx.input.prompt();
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// PH-1a (finding PH-F1): an unrecognized slash command is an ERROR,
|
|
440
|
+
// never a turn — the fallthrough used to hand "/clear", "/exit", or a
|
|
441
|
+
// typo to the model, burning a request on text the user meant as a
|
|
442
|
+
// command. A multi-line paste that merely begins with "/" is prose and
|
|
443
|
+
// still submits.
|
|
444
|
+
if (trimmed.startsWith("/") && !trimmed.includes("\n")) {
|
|
445
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
446
|
+
bodyLog(`unknown command: ${escapeTerminal(trimmed.split(" ")[0] ?? trimmed)} — /help lists the commands`);
|
|
447
|
+
ctx.input.prompt();
|
|
448
|
+
});
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
342
451
|
// v2c: a turn submitted while another runs waits on the chain — the
|
|
343
452
|
// live count rides the status bar (+N queued).
|
|
344
453
|
ctx.submitTurn(line);
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* kiso resume <sessionId> continue a session in one-shot mode
|
|
7
7
|
* kiso sessions list durable sessions
|
|
8
8
|
*
|
|
9
|
-
* Provider selection (first match
|
|
9
|
+
* Provider selection (first match — PH-1b corrected this header: the
|
|
10
|
+
* code has always checked OPENAI first, config.ts resolveModel):
|
|
11
|
+
* OPENAI_API_KEY → OpenAI-compatible (OPENAI_MODEL default gpt-4o, OPENAI_BASE_URL)
|
|
10
12
|
* ANTHROPIC_API_KEY → Anthropic (ANTHROPIC_MODEL, default claude-sonnet-5)
|
|
11
|
-
* OPENAI_API_KEY → OpenAI-compatible (OPENAI_MODEL, OPENAI_BASE_URL)
|
|
12
13
|
* neither → faux mode: scripted model, zero keys, full CLI
|
|
13
14
|
*
|
|
14
15
|
* Sessions live under $KISO_HOME/sessions (default ~/.kiso/sessions) as
|
package/dist/index.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* kiso resume <sessionId> continue a session in one-shot mode
|
|
7
7
|
* kiso sessions list durable sessions
|
|
8
8
|
*
|
|
9
|
-
* Provider selection (first match
|
|
9
|
+
* Provider selection (first match — PH-1b corrected this header: the
|
|
10
|
+
* code has always checked OPENAI first, config.ts resolveModel):
|
|
11
|
+
* OPENAI_API_KEY → OpenAI-compatible (OPENAI_MODEL default gpt-4o, OPENAI_BASE_URL)
|
|
10
12
|
* ANTHROPIC_API_KEY → Anthropic (ANTHROPIC_MODEL, default claude-sonnet-5)
|
|
11
|
-
* OPENAI_API_KEY → OpenAI-compatible (OPENAI_MODEL, OPENAI_BASE_URL)
|
|
12
13
|
* neither → faux mode: scripted model, zero keys, full CLI
|
|
13
14
|
*
|
|
14
15
|
* Sessions live under $KISO_HOME/sessions (default ~/.kiso/sessions) as
|
|
@@ -22,24 +23,24 @@
|
|
|
22
23
|
* index.ts keeps the entry: banner, input sources, the A area prompt,
|
|
23
24
|
* makeAgent, and main.
|
|
24
25
|
*/
|
|
25
|
-
import { readFileSync, realpathSync, rmSync } from "node:fs";
|
|
26
|
+
import { appendFileSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
26
27
|
import { createInterface } from "node:readline";
|
|
27
28
|
import { fileURLToPath } from "node:url";
|
|
28
29
|
import { join } from "node:path";
|
|
29
|
-
import { Body, Editor, bannerLines, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListRow } from "@vincemakes/kiso-tui";
|
|
30
|
+
import { BADGE_GLYPH, Body, Editor, bannerLines, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListRow } from "@vincemakes/kiso-tui";
|
|
30
31
|
import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions, SessionStore, } from "@vincemakes/kiso-runtime";
|
|
31
32
|
import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
32
33
|
import { createCodingTools } from "@vincemakes/kiso-tools-node";
|
|
33
34
|
import { MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
|
|
34
35
|
import { builtInLayer } from "./builtin.js";
|
|
35
|
-
import { agentModel, atFiles, body, bodyLog, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, setSessionStore, userExtensions, VERSION } from "./state.js";
|
|
36
|
+
import { agentModel, atFiles, body, bodyLog, kisoHome, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, setSessionStore, userExtensions, VERSION } from "./state.js";
|
|
36
37
|
import { askUi, resolveProjectTrust } from "./trust-ui.js";
|
|
37
38
|
import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
38
39
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
39
40
|
import { autoCompactFromEnv, chat, contextWindowTokens, estimateCtxRatio } from "./chat.js";
|
|
40
41
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
41
42
|
import { resume } from "./resume.js";
|
|
42
|
-
import { collectSessionCards } from "./session-cards.js";
|
|
43
|
+
import { collectSessionCards, projectSessionCard } from "./session-cards.js";
|
|
43
44
|
// The moved exports stay reachable from this entry — the test imports
|
|
44
45
|
// (project-trust, coding-agent) never change (B4: zero assertion changes).
|
|
45
46
|
export { applyProjectMerges } from "./trust-ui.js";
|
|
@@ -116,6 +117,22 @@ function readlineInput(rl) {
|
|
|
116
117
|
closed: new Promise((resolve) => rl.on("close", () => resolve())),
|
|
117
118
|
};
|
|
118
119
|
}
|
|
120
|
+
/* PH-1a (findings PH-F6/PH-F10, the exit-wedge dossier — recorded, NOT
|
|
121
|
+
* fixed here): node keeps TTY fds in blocking mode, macOS pty output
|
|
122
|
+
* buffers are ~1KB, and on an UNREAD terminal a departing process can
|
|
123
|
+
* wedge two ways: (a) process.exit's own flush of pending blocking-TTY
|
|
124
|
+
* writes, and (b) the editor teardown's uv_tty_set_mode → tcsetattr
|
|
125
|
+
* (TCSADRAIN), which waits for that same drain inside an ioctl. Both are
|
|
126
|
+
* pre-existing and both are masked by the DEFAULT SIGTERM/SIGHUP
|
|
127
|
+
* disposition (kernel-level death cuts through a parked loop). Two
|
|
128
|
+
* repairs were built and REVERTED on evidence this round: a JS signal
|
|
129
|
+
* handler (a caught signal needs the loop the wedge just parked — see
|
|
130
|
+
* the tcsetattr ruling at main's handler comment) and a
|
|
131
|
+
* setBlocking(false)-at-exit rule (a non-blocking TTY plus an immediate
|
|
132
|
+
* process.exit DROPS the queued tail — the banner's version line and the
|
|
133
|
+
* dock's CSI r vanished; node made TTYs blocking precisely to prevent
|
|
134
|
+
* that truncation, and that choice is load-bearing). A real fix needs a
|
|
135
|
+
* non-draining native restore path — the PH-F6 mini-spec. */
|
|
119
136
|
/** The v2c TTY path: the editor's events map 1:1 onto the interface; the
|
|
120
137
|
* input row renders on every state change (the CLI's onRender wiring). */
|
|
121
138
|
function editorInput(editor) {
|
|
@@ -171,6 +188,13 @@ function editorInput(editor) {
|
|
|
171
188
|
bindQueue(state, pop) {
|
|
172
189
|
editor.bindQueue(state, pop);
|
|
173
190
|
},
|
|
191
|
+
// R3a: cross-session history — the CLI owns the file I/O.
|
|
192
|
+
bindHistory(seed, persist) {
|
|
193
|
+
editor.bindHistory(seed, persist);
|
|
194
|
+
},
|
|
195
|
+
onModeCycle(cb) {
|
|
196
|
+
editor.onModeCycle(cb);
|
|
197
|
+
},
|
|
174
198
|
emitLine() {
|
|
175
199
|
/* the editor's buffer survives a cancelled question — its text
|
|
176
200
|
* becomes the next turn on Enter (the readline re-emit
|
|
@@ -245,14 +269,32 @@ function extensionsBanner(resume = []) {
|
|
|
245
269
|
* first, the CURRENT session excluded (it is not something to pick back
|
|
246
270
|
* up). Every field already exists behind the store's SessionMeta (the
|
|
247
271
|
* `kiso sessions` line shows the same data) — only the projection and
|
|
248
|
-
* the sort live here.
|
|
249
|
-
|
|
250
|
-
|
|
272
|
+
* the sort live here.
|
|
273
|
+
*
|
|
274
|
+
* TT-1B (W5 unification): each row carries the picker's badge glyph,
|
|
275
|
+
* derived through the SAME projection the picker uses (session-cards —
|
|
276
|
+
* one source of truth about durability; a second derivation would be
|
|
277
|
+
* none). Only the 3 chosen sessions are opened — the read-only listing
|
|
278
|
+
* rule holds (projectSessionCard's own guarantee), and the cost is
|
|
279
|
+
* bounded by the list, never the home. */
|
|
280
|
+
async function recentSessions(id, agent) {
|
|
281
|
+
const picked = agent
|
|
251
282
|
.sessions()
|
|
252
283
|
.filter((m) => m.id !== id)
|
|
253
284
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
254
|
-
.slice(0, 3)
|
|
255
|
-
|
|
285
|
+
.slice(0, 3);
|
|
286
|
+
const store = sessionStoreRef;
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const m of picked) {
|
|
289
|
+
let badge;
|
|
290
|
+
if (store !== null) {
|
|
291
|
+
const session = await agent.session({ id: m.id });
|
|
292
|
+
const card = projectSessionCard({ id: m.id, updatedAt: m.updatedAt, records: store.load(m.id), asks: session.pendingApprovals().length });
|
|
293
|
+
badge = BADGE_GLYPH[card.badge];
|
|
294
|
+
}
|
|
295
|
+
out.push({ title: m.title, events: m.events, runs: m.runs, updatedAt: m.updatedAt, ...(badge === undefined ? {} : { badge }) });
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
256
298
|
}
|
|
257
299
|
/**
|
|
258
300
|
* A area: the coding-agent system prompt — ONE constant, byte-stable for the
|
|
@@ -452,6 +494,7 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
452
494
|
provider: resolved.profile.kind,
|
|
453
495
|
apiKey: resolved.apiKey,
|
|
454
496
|
...(resolved.profile.baseUrl !== undefined ? { baseUrl: resolved.profile.baseUrl } : {}),
|
|
497
|
+
...(resolved.profile.promptCaching !== undefined ? { promptCaching: resolved.profile.promptCaching } : {}),
|
|
455
498
|
}
|
|
456
499
|
: { adapter: createFauxProvider(readFauxScript().slice(fauxSkipTurns)) }),
|
|
457
500
|
};
|
|
@@ -488,9 +531,12 @@ async function pickSession(agent, input) {
|
|
|
488
531
|
}
|
|
489
532
|
// a dock-less TTY (rows < 4) has no band to draw the picker in, so the
|
|
490
533
|
// honest answer is the usage line this command has always printed.
|
|
534
|
+
// PH-1a (finding PH-F12): thrown, never process.exit'd from this depth
|
|
535
|
+
// — the old exit(2) skipped main's finally (dock.exit, agent.close,
|
|
536
|
+
// temp cleanup) and could leave the scroll region and lock residue
|
|
537
|
+
// behind. The entry catch translates the error back to exit code 2.
|
|
491
538
|
if (input.pick === undefined || !dock.active) {
|
|
492
|
-
|
|
493
|
-
process.exit(2);
|
|
539
|
+
throw new CliUsageError('usage: kiso resume <sessionId> ["prompt"]');
|
|
494
540
|
}
|
|
495
541
|
dock.setStatus("", PICKER_HINT);
|
|
496
542
|
const picked = await new Promise((resolve) => {
|
|
@@ -525,6 +571,44 @@ function paintBootStatus(session) {
|
|
|
525
571
|
return;
|
|
526
572
|
dock.setStatus(idleStatus(getMode() === "plan" ? "plan (read-only)" : getMode(), agentModel, estimateCtxRatio(session)));
|
|
527
573
|
}
|
|
574
|
+
/** PH-1a (finding PH-F12): a usage error raised from inside the TUI —
|
|
575
|
+
* main's finally still runs (dock teardown, agent close, temp cleanup)
|
|
576
|
+
* and the entry catch exits with the historical code 2. */
|
|
577
|
+
class CliUsageError extends Error {
|
|
578
|
+
exitCode = 2;
|
|
579
|
+
}
|
|
580
|
+
/** The /resume+/clear mini-spec — the chat LOOP: chat() ends with a
|
|
581
|
+
* directive; a switch re-enters it on another session with the SAME
|
|
582
|
+
* editor. First entry paints the banner; a switch paints one notice
|
|
583
|
+
* line (the previous conversation stays resumable — clear/switch
|
|
584
|
+
* never erase history). Faux sessions re-arm the scripted adapter at
|
|
585
|
+
* the NEW session's durable position, exactly like the picker path. */
|
|
586
|
+
async function chatLoop(agent, firstId, input, autoCompact) {
|
|
587
|
+
let id = firstId;
|
|
588
|
+
let prev = null;
|
|
589
|
+
for (;;) {
|
|
590
|
+
const session = await agent.session({ id });
|
|
591
|
+
if (prev === null) {
|
|
592
|
+
bodyLog(`session ${id}\n`);
|
|
593
|
+
extensionsBanner(await recentSessions(id, agent));
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
bodyLog(`session ${id} (switched — previous: ${prev}, /resume ${prev} returns)\n`);
|
|
597
|
+
if (currentFaux)
|
|
598
|
+
session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
|
|
599
|
+
}
|
|
600
|
+
paintBootStatus(session);
|
|
601
|
+
const nav = {
|
|
602
|
+
sessions: () => agent.sessions().map((m) => m.id),
|
|
603
|
+
...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
|
|
604
|
+
};
|
|
605
|
+
const end = await chat(session, currentFaux, input, autoCompact, nav);
|
|
606
|
+
if (end.next === "exit")
|
|
607
|
+
return;
|
|
608
|
+
prev = id;
|
|
609
|
+
id = end.id;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
528
612
|
async function main() {
|
|
529
613
|
// E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
|
|
530
614
|
// turns the in-flight stdout/stderr writes into EIO, and node's
|
|
@@ -539,6 +623,36 @@ async function main() {
|
|
|
539
623
|
// first makeAgent (the tier extensions read `current` live). The flag
|
|
540
624
|
// is stripped from the positional args, so it works in any position.
|
|
541
625
|
const args = process.argv.slice(2);
|
|
626
|
+
// PH-1a (finding PH-F2): --help/-h/--version/-v are FLAGS, not session
|
|
627
|
+
// ids. They used to fall through the default case and START A SESSION
|
|
628
|
+
// literally named "--help" (writing ~/.kiso/sessions/--help.jsonl) —
|
|
629
|
+
// the single highest-frequency new-user gesture, failing silently and
|
|
630
|
+
// destructively. Checked FIRST, before any other flag parsing, so
|
|
631
|
+
// `kiso --help` never trips the --model usage error either.
|
|
632
|
+
if (args.some((a) => a === "--help" || a === "-h")) {
|
|
633
|
+
args.length = 0;
|
|
634
|
+
args.push("help");
|
|
635
|
+
}
|
|
636
|
+
else if (args.some((a) => a === "--version" || a === "-v")) {
|
|
637
|
+
console.log(VERSION);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
// R3a: -p/--print — the one-shot prompt mode (the F3 adjudication's
|
|
641
|
+
// forward path: a bare quoted argument stays a session id; the
|
|
642
|
+
// PROMPT is explicit). `kiso -p "fix the bug"` runs one turn on a
|
|
643
|
+
// fresh session and exits; an optional trailing session id continues
|
|
644
|
+
// that session one-shot instead. Exit code: 0 only when the turn's
|
|
645
|
+
// terminal is `completed` — scripts can trust it.
|
|
646
|
+
let printPrompt;
|
|
647
|
+
const printIdx = args.findIndex((a) => a === "-p" || a === "--print");
|
|
648
|
+
if (printIdx !== -1) {
|
|
649
|
+
printPrompt = args[printIdx + 1];
|
|
650
|
+
if (printPrompt === undefined) {
|
|
651
|
+
console.error('usage: kiso -p "prompt" [sessionId]');
|
|
652
|
+
process.exit(2);
|
|
653
|
+
}
|
|
654
|
+
args.splice(printIdx, 2);
|
|
655
|
+
}
|
|
542
656
|
// merge round B: --model <profile|provider/model> — the top of the model
|
|
543
657
|
// precedence chain; the value flows into makeAgent's config resolution.
|
|
544
658
|
let modelFlag;
|
|
@@ -579,6 +693,48 @@ async function main() {
|
|
|
579
693
|
// readline elsewhere. The trust question, chat, and resume all read
|
|
580
694
|
// through it; main's finally closes it on every exit path.
|
|
581
695
|
const input = makeLineInput();
|
|
696
|
+
// R3a — cross-session input history: ~/.kiso/history, one line per
|
|
697
|
+
// entry, appended on submit, tail-500 at load (truncated by REWRITE
|
|
698
|
+
// at startup so the file never grows unbounded). Unreadable file =
|
|
699
|
+
// an empty history, silently — recall is a convenience, never a
|
|
700
|
+
// startup risk. Control-character lines never enter the file (the
|
|
701
|
+
// editor's own recall excludes them by construction: a submitted
|
|
702
|
+
// line is printable input).
|
|
703
|
+
if (input.bindHistory !== undefined) {
|
|
704
|
+
const historyPath = join(kisoHome(), "history");
|
|
705
|
+
let seed = [];
|
|
706
|
+
try {
|
|
707
|
+
seed = readFileSync(historyPath, "utf8").split("\n").filter((l) => l !== "").slice(-500);
|
|
708
|
+
writeFileSync(historyPath, seed.length > 0 ? seed.join("\n") + "\n" : "");
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// no file yet, or unreadable — start empty
|
|
712
|
+
}
|
|
713
|
+
input.bindHistory(seed, (line) => {
|
|
714
|
+
try {
|
|
715
|
+
mkdirSync(kisoHome(), { recursive: true });
|
|
716
|
+
appendFileSync(historyPath, line.replaceAll("\n", " ") + "\n");
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// best-effort — a full disk never breaks a submit
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
// PH-1a (finding PH-F6, RESOLVED AS WON'T-FIX-IN-JS — the tcsetattr
|
|
724
|
+
// ruling): SIGTERM/SIGHUP deliberately keep their DEFAULT disposition.
|
|
725
|
+
// A JS handler that restored the terminal was built and then reverted
|
|
726
|
+
// on hard evidence: libuv's uv_tty_set_mode calls tcsetattr with
|
|
727
|
+
// TCSADRAIN, which WAITS for the pty's pending output to drain — on an
|
|
728
|
+
// unread terminal (exactly where signals tend to arrive) the editor's
|
|
729
|
+
// teardown parks the event loop in that ioctl forever, and a CAUGHT
|
|
730
|
+
// signal can only be dispatched by the loop it just parked. Catching
|
|
731
|
+
// the signal therefore converts kernel-guaranteed death into a death
|
|
732
|
+
// that may never happen — strictly worse than a dirty terminal. The
|
|
733
|
+
// default disposition keeps SIGTERM/SIGHUP lethal under every state;
|
|
734
|
+
// a signal death leaves raw mode/mouse on and `reset` is the fix (the
|
|
735
|
+
// same contract kill -9 has always had). A safe restore needs a
|
|
736
|
+
// non-draining native path (bytes-only restore, or tcflush-then-set)
|
|
737
|
+
// — its own mini-spec, not a hotfix.
|
|
582
738
|
// v2d: the body renderer — active only where the dock is (a color
|
|
583
739
|
// TTY with a real size); pipes run it in passthrough, byte-for-byte.
|
|
584
740
|
setBody(new Body({
|
|
@@ -596,6 +752,19 @@ async function main() {
|
|
|
596
752
|
if (modeFlag === -1 && process.env.KISO_MODE === undefined && mergedConfig.mode !== undefined)
|
|
597
753
|
setMode(mergedConfig.mode);
|
|
598
754
|
};
|
|
755
|
+
if (printPrompt !== undefined) {
|
|
756
|
+
// the -p flow: recovery-first one-shot, the resume() machinery
|
|
757
|
+
// verbatim (a fresh id makes the recovery a no-op)
|
|
758
|
+
const id = command ?? new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
759
|
+
agent = await makeAgent(id, input, modelFlag);
|
|
760
|
+
applyConfigMode();
|
|
761
|
+
const session = await agent.session({ id });
|
|
762
|
+
faux = currentFaux;
|
|
763
|
+
await resume(session, printPrompt, faux, input);
|
|
764
|
+
const last = [...session.log.all].reverse().find((e) => e.type === "terminal");
|
|
765
|
+
process.exitCode = last !== undefined && last.outcome.kind === "completed" ? 0 : 1;
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
599
768
|
switch (command) {
|
|
600
769
|
case "chat": {
|
|
601
770
|
const id = arg ?? new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16);
|
|
@@ -606,12 +775,8 @@ async function main() {
|
|
|
606
775
|
// position — never restarts it (fauxSkip).
|
|
607
776
|
agent = await makeAgent(id, input, modelFlag);
|
|
608
777
|
applyConfigMode();
|
|
609
|
-
const session = await agent.session({ id });
|
|
610
|
-
bodyLog(`session ${id}\n`);
|
|
611
|
-
extensionsBanner(recentSessions(id, agent));
|
|
612
778
|
faux = currentFaux;
|
|
613
|
-
|
|
614
|
-
await chat(session, faux, input, resolveAutoCompact(mergedConfig));
|
|
779
|
+
await chatLoop(agent, id, input, resolveAutoCompact(mergedConfig));
|
|
615
780
|
break;
|
|
616
781
|
}
|
|
617
782
|
case "resume": {
|
|
@@ -638,7 +803,14 @@ async function main() {
|
|
|
638
803
|
// an error, never a session started behind their back.
|
|
639
804
|
if (picked === null)
|
|
640
805
|
break;
|
|
641
|
-
|
|
806
|
+
// the mini-spec (a DECLARED SUPERSESSION of the one-shot
|
|
807
|
+
// picker flow): a PICKED session enters the full REPL —
|
|
808
|
+
// "resume and keep working" no longer requires knowing to
|
|
809
|
+
// type `kiso chat <id>`. The explicit-id one-shot form
|
|
810
|
+
// (`kiso resume <id> ["prompt"]`) keeps its exact bytes.
|
|
811
|
+
faux = currentFaux;
|
|
812
|
+
await chatLoop(agent, picked, input, resolveAutoCompact(mergedConfig));
|
|
813
|
+
break;
|
|
642
814
|
}
|
|
643
815
|
const session = await agent.session({ id });
|
|
644
816
|
faux = currentFaux;
|
|
@@ -683,14 +855,29 @@ async function main() {
|
|
|
683
855
|
break;
|
|
684
856
|
}
|
|
685
857
|
case "help": {
|
|
858
|
+
// PH-1b (finding PH-F21): the CLI must be able to describe its
|
|
859
|
+
// own configuration — the old help listed five commands and
|
|
860
|
+
// stopped, so a new user could never learn from the tool
|
|
861
|
+
// itself how to hand it a key.
|
|
686
862
|
const p = palette();
|
|
687
863
|
console.log(`${p.dim}${bannerLines(80, process.stdout.rows ?? 0, VERSION, "").join("\n")}${p.reset}\n\n` +
|
|
688
864
|
"kiso — the coding agent that survives kill -9\n\n" +
|
|
689
865
|
" kiso [sessionId] interactive session (default command)\n" +
|
|
690
866
|
" kiso chat [sessionId] same as above\n" +
|
|
867
|
+
" kiso resume pick a session to continue (TTY picker)\n" +
|
|
691
868
|
" kiso resume <id> [prompt] continue a session (one-shot)\n" +
|
|
692
869
|
" kiso sessions list durable sessions\n" +
|
|
693
|
-
" kiso help this help\n"
|
|
870
|
+
" kiso help this help\n\n" +
|
|
871
|
+
"flags (any position):\n" +
|
|
872
|
+
" --model <profile|provider/model> pick the model (also /model in-session)\n" +
|
|
873
|
+
" --mode <tier> approval tier: manual|default|accept-edits|plan|bypass\n" +
|
|
874
|
+
" --version print the version\n\n" +
|
|
875
|
+
"configuration:\n" +
|
|
876
|
+
" no key keyless faux demo (a scripted four-round session)\n" +
|
|
877
|
+
" OPENAI_API_KEY OpenAI-compatible (OPENAI_MODEL, default gpt-4o;\n" +
|
|
878
|
+
" OPENAI_BASE_URL for DeepSeek/compat endpoints) — checked first\n" +
|
|
879
|
+
" ANTHROPIC_API_KEY Anthropic (ANTHROPIC_MODEL, default claude-sonnet-5)\n" +
|
|
880
|
+
" ~/.kiso/config.json named model profiles (keys stay in env vars; see the README)\n");
|
|
694
881
|
break;
|
|
695
882
|
}
|
|
696
883
|
case undefined:
|
|
@@ -706,17 +893,9 @@ async function main() {
|
|
|
706
893
|
// undefined" at the ask (panelAsk with the dock, question on
|
|
707
894
|
// the dock-less fallback).
|
|
708
895
|
agent = await makeAgent(id, input, modelFlag);
|
|
709
|
-
|
|
710
|
-
bodyLog(`session ${id}\n`);
|
|
711
|
-
extensionsBanner(recentSessions(id, agent));
|
|
712
|
-
// finding E4-1: the same faux resolution the chat/resume cases
|
|
713
|
-
// carry (faux = currentFaux) — pre-patch the initial faux=true
|
|
714
|
-
// was passed here, so ANY provider failure in a bare-command
|
|
715
|
-
// session surfaced as "[faux mode] the scripted model failed:
|
|
716
|
-
// <real error>" (a false accusation of the keyless demo).
|
|
896
|
+
// finding E4-1's faux resolution rides chatLoop (currentFaux).
|
|
717
897
|
faux = currentFaux;
|
|
718
|
-
|
|
719
|
-
await chat(session, faux, input, autoCompactFromEnv());
|
|
898
|
+
await chatLoop(agent, id, input, autoCompactFromEnv());
|
|
720
899
|
break;
|
|
721
900
|
}
|
|
722
901
|
}
|
|
@@ -756,9 +935,31 @@ async function main() {
|
|
|
756
935
|
// test imported src/index.js for its functions (harmless before the
|
|
757
936
|
// first-run scaffold existed; the scaffold WRITES the home). The bin is
|
|
758
937
|
// a symlink, so argv[1] is realpathed before the comparison.
|
|
938
|
+
/** PH-1a (finding PH-F10): the explicit exit stays (v2a: natural drain is
|
|
939
|
+
* racy on a TTY — readline leaves the stdio handles active), but on a
|
|
940
|
+
* PIPE it now waits for both stdio streams to flush first — process.exit
|
|
941
|
+
* does not drain a pipe's pending async writes, so `kiso sessions | head`
|
|
942
|
+
* could lose its tail. TTY-GATED on purpose: a pipe's flush callbacks
|
|
943
|
+
* always settle (the reader drains, or the break surfaces as EPIPE), but
|
|
944
|
+
* an unread TTY's never do — waiting there would trade a truncation bug
|
|
945
|
+
* for a hang (the exit-wedge dossier above editorInput). The TTY path keeps the
|
|
946
|
+
* v2a immediate exit, byte-for-byte. */
|
|
947
|
+
function exitFlushed(code) {
|
|
948
|
+
if (process.stdout.isTTY === true) {
|
|
949
|
+
process.exit(code);
|
|
950
|
+
}
|
|
951
|
+
let pending = 2;
|
|
952
|
+
const done = () => {
|
|
953
|
+
pending -= 1;
|
|
954
|
+
if (pending === 0)
|
|
955
|
+
process.exit(code);
|
|
956
|
+
};
|
|
957
|
+
process.stdout.write("", done);
|
|
958
|
+
process.stderr.write("", done);
|
|
959
|
+
}
|
|
759
960
|
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
760
961
|
main()
|
|
761
|
-
.then(() => process.
|
|
962
|
+
.then(() => exitFlushed(typeof process.exitCode === "number" ? process.exitCode : 0))
|
|
762
963
|
.catch((err) => {
|
|
763
964
|
// round 10: top-level errors are terminal-escaped. v2a: the exit is EXPLICIT
|
|
764
965
|
// — natural drain is racy on a TTY (readline leaves the stdio handles
|
|
@@ -766,6 +967,6 @@ if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === fileURLTo
|
|
|
766
967
|
// ran (agent.close, dispose, temp cleanup) — nothing is skipped, no
|
|
767
968
|
// lock is left behind; the exit code is honest.
|
|
768
969
|
console.error(escapeTerminal(err instanceof Error ? err.message : String(err)));
|
|
769
|
-
|
|
970
|
+
exitFlushed(err instanceof CliUsageError ? err.exitCode : 1);
|
|
770
971
|
});
|
|
771
972
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -100,6 +100,11 @@ export interface LineInput {
|
|
|
100
100
|
* live slots (each pop cancels the turn), esc ends the walk after
|
|
101
101
|
* one more pop. The chips are the compositor's own bindQueue. */
|
|
102
102
|
bindQueue(state: () => readonly string[], pop: () => string | null): void;
|
|
103
|
+
/** R3a: cross-session history — seed the recall buffer, register the
|
|
104
|
+
* append sink. The pipe path has no recall keys; optional. */
|
|
105
|
+
bindHistory?(seed: readonly string[], persist: (line: string) => void): void;
|
|
106
|
+
/** R3a: Shift+Tab cycles the approval tier (TTY editor only). */
|
|
107
|
+
onModeCycle?(cb: () => void): void;
|
|
103
108
|
emitLine(line: string): void;
|
|
104
109
|
line(): string;
|
|
105
110
|
clearLine(): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "kiso CLI — the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,19 +18,19 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-ask-ext": "0.
|
|
22
|
-
"@vincemakes/kiso-core": "0.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
27
|
-
"@vincemakes/kiso-runtime": "0.
|
|
28
|
-
"@vincemakes/kiso-skills-ext": "0.
|
|
29
|
-
"@vincemakes/kiso-subagent-ext": "0.
|
|
30
|
-
"@vincemakes/kiso-task-ext": "0.
|
|
31
|
-
"@vincemakes/kiso-tools-node": "0.
|
|
32
|
-
"@vincemakes/kiso-tui": "0.
|
|
33
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.15.1",
|
|
22
|
+
"@vincemakes/kiso-core": "0.15.1",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.15.1",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.15.1",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.15.1",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.15.1",
|
|
27
|
+
"@vincemakes/kiso-runtime": "0.15.1",
|
|
28
|
+
"@vincemakes/kiso-skills-ext": "0.15.1",
|
|
29
|
+
"@vincemakes/kiso-subagent-ext": "0.15.1",
|
|
30
|
+
"@vincemakes/kiso-task-ext": "0.15.1",
|
|
31
|
+
"@vincemakes/kiso-tools-node": "0.15.1",
|
|
32
|
+
"@vincemakes/kiso-tui": "0.15.1",
|
|
33
|
+
"@vincemakes/kiso-tui-cells": "0.15.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^26.1.2",
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=22"
|
|
42
42
|
},
|
|
43
|
+
"os": [
|
|
44
|
+
"darwin",
|
|
45
|
+
"linux"
|
|
46
|
+
],
|
|
43
47
|
"repository": {
|
|
44
48
|
"type": "git",
|
|
45
49
|
"url": "https://github.com/vincemakes/kiso.git",
|