@vincemakes/kiso-code 0.1.14 → 0.1.16
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/body.d.ts +121 -0
- package/dist/body.js +478 -0
- package/dist/diff.d.ts +32 -0
- package/dist/diff.js +122 -0
- package/dist/dock.d.ts +13 -15
- package/dist/dock.js +11 -37
- package/dist/index.js +231 -142
- package/dist/mode.d.ts +33 -0
- package/dist/mode.js +93 -0
- package/dist/render.d.ts +1 -0
- package/dist/render.js +2 -2
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { createInterface } from "node:readline";
|
|
20
|
+
import { Body } from "./body.js";
|
|
21
|
+
import { editFileDiff, writeFileDiff } from "./diff.js";
|
|
22
|
+
import { MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
|
|
20
23
|
import { Editor, PROMPT as EDITOR_PROMPT } from "./editor.js";
|
|
21
24
|
import { homedir, tmpdir } from "node:os";
|
|
22
25
|
import { dirname, join } from "node:path";
|
|
@@ -26,17 +29,6 @@ import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
|
26
29
|
import { createCodingTools } from "@vincemakes/kiso-tools-node";
|
|
27
30
|
import { escapeTerminal, foldResult, foldThinking, palette, renderEvent, renderSessionLine, renderStatusLine, renderTerminalGap, renderToolSummary, } from "./render.js";
|
|
28
31
|
import { Dock } from "./dock.js";
|
|
29
|
-
const PERMISSION_POLICY = {
|
|
30
|
-
rules: [
|
|
31
|
-
{ tool: "read_file", action: "allow" },
|
|
32
|
-
{ tool: "list_dir", action: "allow" },
|
|
33
|
-
{ tool: "search_text", action: "allow" },
|
|
34
|
-
{ tool: "write_file", action: "defer" },
|
|
35
|
-
{ tool: "edit_file", action: "defer" },
|
|
36
|
-
{ tool: "shell", action: "defer" },
|
|
37
|
-
],
|
|
38
|
-
default: "deny",
|
|
39
|
-
};
|
|
40
32
|
/** 发现#11: KISO_HOME is the ONE root — every default path derives from
|
|
41
33
|
* it (sessions, trust, extensions, mcp config, skills). The dedicated
|
|
42
34
|
* env vars (KISO_EXTENSIONS_DIR / KISO_MCP_CONFIG / KISO_SKILLS_DIR)
|
|
@@ -209,17 +201,15 @@ function makeLineInput() {
|
|
|
209
201
|
/** v2b: the bottom-anchored UI — docked only on a color TTY; pipes and
|
|
210
202
|
* NO_COLOR stay the v2a line mode byte-for-byte. */
|
|
211
203
|
const dock = new Dock();
|
|
212
|
-
/**
|
|
213
|
-
* the
|
|
214
|
-
*
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
process.stdout.write(text);
|
|
220
|
-
}
|
|
204
|
+
/** v2d: the body renderer — the ONE writer of the stdout scroll region
|
|
205
|
+
* (the frozen area + the active tail). Pipes run it in passthrough (the
|
|
206
|
+
* v2b/v2c line-mode bytes, byte-for-byte). Created in main; closed on
|
|
207
|
+
* every exit path. */
|
|
208
|
+
let body;
|
|
209
|
+
/** v2d: body output routes through the cell renderer — the single writer.
|
|
210
|
+
* bodyLog adds the trailing newline; internal newlines are preserved. */
|
|
221
211
|
function bodyLog(text) {
|
|
222
|
-
|
|
212
|
+
body.raw(text.split("\n"));
|
|
223
213
|
}
|
|
224
214
|
/** v2b: the spinner merged into the STATUS BAR (the v2a standalone glyph
|
|
225
215
|
* is gone) — docked only, 200ms rotation between the request and the
|
|
@@ -261,6 +251,21 @@ function bannerExtensionText() {
|
|
|
261
251
|
parts.push(`project: ${projectExtensions.map((e) => e.name).join(", ")}`);
|
|
262
252
|
return ` · [${total} extension${total === 1 ? "" : "s"}: ${parts.join(" · ")}]`;
|
|
263
253
|
}
|
|
254
|
+
/** Modes: the status-bar indicator — the default tier shows nothing; the
|
|
255
|
+
* others show their blue name, the dangerous ones (plan/bypass) with the
|
|
256
|
+
* ⚠ prefix. */
|
|
257
|
+
function modeStatusText() {
|
|
258
|
+
if (getMode() === "default")
|
|
259
|
+
return "";
|
|
260
|
+
const p = palette();
|
|
261
|
+
const danger = getMode() === "plan" || getMode() === "bypass" ? "⚠ " : "";
|
|
262
|
+
return `${p.blue}${danger}${getMode()}${p.reset}`;
|
|
263
|
+
}
|
|
264
|
+
/** Modes: append the mode indicator to a composed status base. */
|
|
265
|
+
function statusWithMode(base) {
|
|
266
|
+
const mode = modeStatusText();
|
|
267
|
+
return mode === "" ? base : `${base} · ${mode}`;
|
|
268
|
+
}
|
|
264
269
|
/** E1: the startup banner line(s) — TTY: logo + merged extensions; off-TTY:
|
|
265
270
|
* the historical `[N extensions: ...]` standalone line (zero change). */
|
|
266
271
|
function extensionsBanner() {
|
|
@@ -519,15 +524,26 @@ async function makeAgent(fauxSkipTurns = 0, input) {
|
|
|
519
524
|
// Area 5: the coding tools are bound to the workspace — every path
|
|
520
525
|
// they touch is canonicalized inside cwd, escapes are refused.
|
|
521
526
|
tools: [...createCodingTools({ workspaceRoot: process.cwd() })],
|
|
522
|
-
|
|
523
|
-
|
|
527
|
+
// Modes: the five tiers ride the E1 policy chain (mode:<tier>
|
|
528
|
+
// extensions, current tier first) — the old static PERMISSION_POLICY
|
|
529
|
+
// is gone, its semantics live in the "default" tier. The banner
|
|
530
|
+
// still counts loadedExtensions only — the modes are in-process,
|
|
531
|
+
// never a file extension.
|
|
532
|
+
systemPrompt: (() => {
|
|
533
|
+
const sp = composeSystemPrompt(process.cwd());
|
|
534
|
+
const extra = modeSystemPrompt();
|
|
535
|
+
return extra === undefined ? sp : `${sp}\n\n${extra}`;
|
|
536
|
+
})(),
|
|
524
537
|
// C 区: microcompact is ON by default in the product — threshold =
|
|
525
538
|
// half the model window (KISO_CONTEXT_WINDOW override included;
|
|
526
539
|
// 200k window → 100k tokens). Long sessions compact old read/list/
|
|
527
540
|
// search/shell outputs instead of silently growing past the window.
|
|
528
541
|
microcompact: { thresholdTokens: contextWindowTokens() / 2 },
|
|
529
542
|
maxTurns: 20,
|
|
530
|
-
|
|
543
|
+
// Modes: the five tiers join at the CHAIN HEAD, before the user/
|
|
544
|
+
// project extensions (the deny>ask>allow composition keeps a user
|
|
545
|
+
// deny winning over any mode tier — bypass included).
|
|
546
|
+
extensions: [...modeExtensions(), ...loadedExtensions],
|
|
531
547
|
...(provider !== undefined
|
|
532
548
|
? {
|
|
533
549
|
provider,
|
|
@@ -700,43 +716,48 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
|
700
716
|
* line's form; `liveInput` (non-null only in interactive chat) carries the
|
|
701
717
|
* last line THIS process's readline consumed — the double-echo filter.
|
|
702
718
|
*/
|
|
703
|
-
|
|
719
|
+
/** v2e: the approval-moment mini-diff — edit_file/write_file changes as
|
|
720
|
+
* ± lines; other tools get null (no diff, no cost). The file read is
|
|
721
|
+
* best-effort: an unreadable file yields NO diff, never a failure —
|
|
722
|
+
* the diff must never break the approval. */
|
|
723
|
+
function approvalDiff(name, input) {
|
|
724
|
+
if (name !== "edit_file" && name !== "write_file")
|
|
725
|
+
return null;
|
|
726
|
+
const path = typeof input.path === "string" ? input.path : "";
|
|
727
|
+
if (path === "")
|
|
728
|
+
return null;
|
|
729
|
+
let oldContent = null;
|
|
730
|
+
try {
|
|
731
|
+
oldContent = readFileSync(path, "utf8");
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
// a new write_file target (or an unreadable one) — all + degrades
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
if (name === "edit_file") {
|
|
738
|
+
const search = typeof input.search === "string" ? input.search : "";
|
|
739
|
+
const replace = typeof input.replace === "string" ? input.replace : "";
|
|
740
|
+
if (search === "")
|
|
741
|
+
return null;
|
|
742
|
+
return editFileDiff(oldContent ?? "", search, replace);
|
|
743
|
+
}
|
|
744
|
+
const content = typeof input.content === "string" ? input.content : "";
|
|
745
|
+
return writeFileDiff(oldContent, content);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
return null; // never let the diff break the approval
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
async function consumeRun(session, run, input, turnNo, faux, liveInput, statusCb) {
|
|
704
752
|
let last;
|
|
705
|
-
// B 区: tool_call_end → (name, input) for the summary; tool_result →
|
|
706
|
-
// one summary line. Usage events feed the status line.
|
|
707
|
-
const pendingCalls = new Map();
|
|
708
753
|
let usage = { in: null, out: null, cache: null, known: false };
|
|
709
|
-
// v2b: thinking blocks buffer and fold to ONE dim line at the block's
|
|
710
|
-
// end (foldThinking); the FULL text goes to /think.
|
|
711
|
-
let thinkingBuf = "";
|
|
712
|
-
const flushThinking = () => {
|
|
713
|
-
if (thinkingBuf === "")
|
|
714
|
-
return;
|
|
715
|
-
lastThinking.current = thinkingBuf;
|
|
716
|
-
bodyWrite(foldThinking(thinkingBuf));
|
|
717
|
-
thinkingBuf = "";
|
|
718
|
-
};
|
|
719
|
-
let thinkingOpen = false;
|
|
720
|
-
// v2b: liveness merged into the status bar (docked); a running timer
|
|
721
|
-
// shows "running <tool> Ns" during a tool execution.
|
|
722
|
-
const stopSpinner = startStatusSpinner();
|
|
723
|
-
let stopRunning = null;
|
|
724
|
-
let firstEvent = true;
|
|
725
754
|
try {
|
|
726
755
|
for await (const ev of run) {
|
|
727
|
-
if (firstEvent) {
|
|
728
|
-
firstEvent = false;
|
|
729
|
-
stopSpinner();
|
|
730
|
-
}
|
|
731
756
|
last = ev;
|
|
732
|
-
// v2a (双回显): the interactive
|
|
733
|
-
//
|
|
734
|
-
// Replayed history (recovery/resume — nobody typed) keeps the event
|
|
735
|
-
// render. Deterministic: exact content match with the consumed line,
|
|
736
|
-
// on a TTY (the only place an echo exists to hand over).
|
|
757
|
+
// v2a (双回显): the interactive echo was already rendered by the
|
|
758
|
+
// input source — rendering the event again is the double echo.
|
|
737
759
|
// v2b: DOCKED — the echo lives in the input row (H), NOT the body;
|
|
738
|
-
// the body render is the ONLY visible copy of the sent line.
|
|
739
|
-
// user's typed text must not vanish after Enter.
|
|
760
|
+
// the body render is the ONLY visible copy of the sent line.
|
|
740
761
|
if (ev.type === "user_input" &&
|
|
741
762
|
liveInput !== null &&
|
|
742
763
|
liveInput.current === (typeof ev.content === "string" ? ev.content : "") &&
|
|
@@ -744,78 +765,91 @@ async function consumeRun(session, run, input, turnNo, lastToolRef, faux, liveIn
|
|
|
744
765
|
!dock.active) {
|
|
745
766
|
continue;
|
|
746
767
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
768
|
+
// v2d: EVERY event only mutates a cell — the Body is the single
|
|
769
|
+
// writer of the scroll region, so interleaving is impossible by
|
|
770
|
+
// construction (ADR-0040).
|
|
771
|
+
switch (ev.type) {
|
|
772
|
+
case "user_input":
|
|
773
|
+
body.userLine(typeof ev.content === "string" ? ev.content : "");
|
|
774
|
+
break;
|
|
775
|
+
case "thinking":
|
|
776
|
+
body.thinkingAppend(ev.text);
|
|
777
|
+
break;
|
|
778
|
+
case "tool_call_end":
|
|
779
|
+
body.toolStart(ev.name, ev.callId, ev.input ?? {});
|
|
780
|
+
break;
|
|
781
|
+
case "tool_execution_started":
|
|
782
|
+
body.toolRunning(ev.callId);
|
|
783
|
+
break;
|
|
784
|
+
case "tool_execution_succeeded":
|
|
785
|
+
body.toolSucceeded(ev.callId);
|
|
786
|
+
break;
|
|
787
|
+
case "tool_execution_failed":
|
|
788
|
+
body.toolFailed(ev.callId, ev.error);
|
|
789
|
+
break;
|
|
790
|
+
case "tool_result": {
|
|
767
791
|
const text = typeof ev.content === "string" ? ev.content : "";
|
|
768
|
-
|
|
769
|
-
|
|
792
|
+
body.toolResult(ev.callId, { content: text, isError: ev.isError });
|
|
793
|
+
break;
|
|
770
794
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
795
|
+
case "text_delta":
|
|
796
|
+
body.textAppend(ev.text);
|
|
797
|
+
break;
|
|
798
|
+
case "text_end":
|
|
799
|
+
body.textEnd();
|
|
800
|
+
break;
|
|
801
|
+
case "usage":
|
|
802
|
+
usage = { in: ev.inputTokens, out: ev.outputTokens, cache: ev.cacheRead, known: ev.known };
|
|
803
|
+
statusCb?.(usage, estimateCtxRatio(session));
|
|
804
|
+
break;
|
|
805
|
+
case "uncertain_pending":
|
|
806
|
+
// 裁决 #12 (ADR-0038): the ⚠ line is pure INFORMATION now — the
|
|
807
|
+
// approval chain guards retries, and the human question belongs
|
|
808
|
+
// only to the crash window's recovery flow (resolveUncertains).
|
|
809
|
+
body.notice(`⚠ ${escapeTerminal(ev.name)} FAILED — the side effect may have applied. ${escapeTerminal(ev.error)}`);
|
|
810
|
+
break;
|
|
811
|
+
case "permission_requested": {
|
|
812
|
+
// v2d: the ToolCell shows the ⏸ badge; the question takes over
|
|
813
|
+
// the dock status position; the answer lands at the input line.
|
|
814
|
+
// v2e: the mini-diff for edit/write at the approval moment —
|
|
815
|
+
// the human sees the change BEFORE deciding (auto-allowed tools
|
|
816
|
+
// skip the diff: nobody is looking).
|
|
817
|
+
const name = ev.name;
|
|
818
|
+
body.toolApproval(ev.callId, approvalDiff(name, ev.input ?? {}));
|
|
819
|
+
const decisionId = ev.decisionId;
|
|
820
|
+
const answer = await ask(input, `approve ${escapeTerminal(name)}? (y/n) `);
|
|
821
|
+
if (answer === CANCELLED) {
|
|
822
|
+
// 十: a cancellation is a CONSERVATIVE denial, explicitly
|
|
823
|
+
// distinguished from the user typing "n".
|
|
824
|
+
body.notice("[approval cancelled — treated as a denial]");
|
|
825
|
+
await session.approve(decisionId, false);
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
await session.approve(decisionId, answer.trim().toLowerCase().startsWith("y"));
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case "terminal":
|
|
832
|
+
statusCb?.(usage, estimateCtxRatio(session));
|
|
833
|
+
statusCb?.(usage, estimateCtxRatio(session));
|
|
834
|
+
// v2a rhythm: the honest label (\ndone\n — the completed
|
|
835
|
+
// marker), the status line hugging it, then EXACTLY one blank
|
|
836
|
+
// line before the next prompt.
|
|
837
|
+
body.terminal(renderEvent(ev).text, renderStatusLine(turnNo, usage, estimateCtxRatio(session), faux) ?? "");
|
|
838
|
+
break;
|
|
839
|
+
default: {
|
|
840
|
+
// Events without a cell (stop, …) — the generic render, byte-
|
|
841
|
+
// preserved for the pipe path.
|
|
842
|
+
const rendered = renderEvent(ev);
|
|
843
|
+
if (rendered.text !== "") {
|
|
844
|
+
body.raw(rendered.text.replace(/\n$/, "").split("\n"));
|
|
845
|
+
}
|
|
846
|
+
break;
|
|
800
847
|
}
|
|
801
|
-
await session.approve(decisionId, answer.trim().toLowerCase().startsWith("y"));
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
bodyWrite(rendered.text);
|
|
805
|
-
}
|
|
806
|
-
if (ev.type === "terminal") {
|
|
807
|
-
statusCb?.(usage, estimateCtxRatio(session));
|
|
808
|
-
// v2a rhythm: the status line hugs the terminal (有什么显什么 —
|
|
809
|
-
// null = nothing to show), then EXACTLY one blank line before
|
|
810
|
-
// the next prompt.
|
|
811
|
-
bodyWrite(renderTerminalGap(renderStatusLine(turnNo, usage, estimateCtxRatio(session), faux)));
|
|
812
848
|
}
|
|
813
849
|
}
|
|
814
|
-
|
|
850
|
+
body.thinkingEnd(); // a trailing thinking block folds at the run's end
|
|
815
851
|
}
|
|
816
852
|
finally {
|
|
817
|
-
stopSpinner();
|
|
818
|
-
stopRunning?.();
|
|
819
853
|
}
|
|
820
854
|
return last;
|
|
821
855
|
}
|
|
@@ -861,7 +895,7 @@ async function chat(session, faux, input) {
|
|
|
861
895
|
(async () => {
|
|
862
896
|
let last;
|
|
863
897
|
try {
|
|
864
|
-
last = await consumeRun(session, run, input, myTurn,
|
|
898
|
+
last = await consumeRun(session, run, input, myTurn, faux, liveInput, statusCb);
|
|
865
899
|
currentRun = null;
|
|
866
900
|
// 八: a faux script that ran out of declared turns exits
|
|
867
901
|
// loudly with a non-zero status — never a silent status 0.
|
|
@@ -935,24 +969,29 @@ async function chat(session, faux, input) {
|
|
|
935
969
|
let chain = Promise.resolve();
|
|
936
970
|
let replReady = false;
|
|
937
971
|
const queuedLines = [];
|
|
938
|
-
// B 区: user-turn counter for the status line
|
|
972
|
+
// B 区: user-turn counter for the status line. /last and /think read
|
|
973
|
+
// the body (the ToolCell / ThinkingCell final states).
|
|
939
974
|
let turnNo = 0;
|
|
940
|
-
const lastToolRef = { current: null };
|
|
941
975
|
// v2a: the last line THIS process's readline consumed — the double-echo
|
|
942
976
|
// filter (see consumeRun). Only interactive chat sets it.
|
|
943
977
|
const liveInput = { current: null };
|
|
944
|
-
// v2b: the last complete thinking block, for /think.
|
|
945
|
-
const lastThinking = { current: null };
|
|
946
978
|
// v2c: turns submitted while another runs are QUEUED on the chain — the
|
|
947
979
|
// live count rides the status bar (+N queued).
|
|
948
980
|
let queued = 0;
|
|
949
|
-
// v2b: the live status bar (docked only).
|
|
981
|
+
// v2b: the live status bar (docked only). Modes: /mode switches repaint
|
|
982
|
+
// it immediately through paintStatus (the last turn stats are kept).
|
|
983
|
+
let statusSt = null;
|
|
950
984
|
const statusCb = (u, ctx) => {
|
|
951
985
|
if (!dock.active)
|
|
952
986
|
return;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
987
|
+
statusSt = renderStatusLine(turnNo, u, ctx, faux);
|
|
988
|
+
paintStatus();
|
|
989
|
+
};
|
|
990
|
+
const paintStatus = () => {
|
|
991
|
+
if (!dock.active)
|
|
992
|
+
return;
|
|
993
|
+
const base = statusSt === null ? `${session.id} · ${agentModel}` : `${session.id} · ${agentModel} · ${statusSt}`;
|
|
994
|
+
dock.setStatus(`${statusWithMode(base)}${queued > 0 ? ` · +${queued} queued` : ""}`);
|
|
956
995
|
};
|
|
957
996
|
// The ONE dispatcher: slash commands, exit, and turns. The recovery
|
|
958
997
|
// replay routes through it too — a queued "/last" must never become a
|
|
@@ -970,16 +1009,17 @@ async function chat(session, faux, input) {
|
|
|
970
1009
|
bodyLog(cmd("/think", "show the last full thinking block"));
|
|
971
1010
|
bodyLog(cmd("/last", "show the most recent tool call's input and output"));
|
|
972
1011
|
bodyLog(cmd("/status", "show session id, event count, and context estimate"));
|
|
1012
|
+
bodyLog(cmd("/mode", "show the approval tier; /mode <name> switches (manual/default/accept-edits/plan/bypass)"));
|
|
973
1013
|
bodyLog(cmd("exit", "leave the session"));
|
|
974
1014
|
input.prompt();
|
|
975
1015
|
});
|
|
976
1016
|
return;
|
|
977
1017
|
}
|
|
978
1018
|
if (trimmed === "/think") {
|
|
979
|
-
// v2b: print the last COMPLETE thinking block —
|
|
980
|
-
//
|
|
1019
|
+
// v2b/v2d: print the last COMPLETE thinking block — the body holds
|
|
1020
|
+
// it (the ThinkingCell's fold closes at the block's end).
|
|
981
1021
|
chain = chain.then(async () => {
|
|
982
|
-
const t = lastThinking
|
|
1022
|
+
const t = body.lastThinking();
|
|
983
1023
|
if (t === null) {
|
|
984
1024
|
bodyLog("[no thinking yet]");
|
|
985
1025
|
}
|
|
@@ -991,11 +1031,11 @@ async function chat(session, faux, input) {
|
|
|
991
1031
|
return;
|
|
992
1032
|
}
|
|
993
1033
|
if (trimmed === "/last") {
|
|
994
|
-
// B
|
|
995
|
-
//
|
|
996
|
-
//
|
|
1034
|
+
// B 区/v2d: print the FULL input/output of the most recent tool
|
|
1035
|
+
// call — the body holds it (the ToolCell's final state). Runs on
|
|
1036
|
+
// the chain: after any in-flight turn completes.
|
|
997
1037
|
chain = chain.then(async () => {
|
|
998
|
-
const tool =
|
|
1038
|
+
const tool = body.lastTool();
|
|
999
1039
|
if (tool === null) {
|
|
1000
1040
|
bodyLog("[no tool call yet]");
|
|
1001
1041
|
}
|
|
@@ -1023,6 +1063,29 @@ async function chat(session, faux, input) {
|
|
|
1023
1063
|
});
|
|
1024
1064
|
return;
|
|
1025
1065
|
}
|
|
1066
|
+
if (trimmed === "/mode" || trimmed.startsWith("/mode ")) {
|
|
1067
|
+
// Modes: /mode alone prints the current tier + the list;
|
|
1068
|
+
// /mode <name> switches — the notice cell leaves the audit
|
|
1069
|
+
// line in the body, the status bar repaints at once.
|
|
1070
|
+
chain = chain.then(async () => {
|
|
1071
|
+
const m = MODES.find((x) => x === trimmed.slice(5).trim());
|
|
1072
|
+
if (trimmed.slice(5).trim() === "") {
|
|
1073
|
+
bodyLog(`mode ${getMode()}`);
|
|
1074
|
+
bodyLog(`tiers: ${MODES.join(" ")}`);
|
|
1075
|
+
}
|
|
1076
|
+
else if (m === undefined) {
|
|
1077
|
+
bodyLog(`no such mode: ${trimmed.slice(5).trim()}`);
|
|
1078
|
+
bodyLog(`tiers: ${MODES.join(" ")}`);
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
setMode(m);
|
|
1082
|
+
body.notice(`mode → ${m}`);
|
|
1083
|
+
paintStatus();
|
|
1084
|
+
}
|
|
1085
|
+
input.prompt();
|
|
1086
|
+
});
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1026
1089
|
if (trimmed === "exit" || trimmed === "") {
|
|
1027
1090
|
input.close();
|
|
1028
1091
|
return;
|
|
@@ -1049,7 +1112,7 @@ async function chat(session, faux, input) {
|
|
|
1049
1112
|
const recoveryRun = session.resume();
|
|
1050
1113
|
currentRun = recoveryRun;
|
|
1051
1114
|
turnNo += 1;
|
|
1052
|
-
const last = await consumeRun(session, recoveryRun, input, turnNo,
|
|
1115
|
+
const last = await consumeRun(session, recoveryRun, input, turnNo, faux, liveInput, statusCb);
|
|
1053
1116
|
currentRun = null;
|
|
1054
1117
|
failOnFauxExhaustion(last, faux, input);
|
|
1055
1118
|
}
|
|
@@ -1084,20 +1147,19 @@ async function resume(session, prompt, faux, input) {
|
|
|
1084
1147
|
let currentRun = null;
|
|
1085
1148
|
let cancelled = false;
|
|
1086
1149
|
let turnNo = 0;
|
|
1087
|
-
const lastToolRef = { current: null };
|
|
1088
|
-
const lastThinking = { current: null };
|
|
1089
1150
|
// v2b: the live status bar (docked only).
|
|
1090
1151
|
const statusCb = (u, ctx) => {
|
|
1091
1152
|
if (!dock.active)
|
|
1092
1153
|
return;
|
|
1093
1154
|
const st = renderStatusLine(turnNo, u, ctx, faux);
|
|
1094
|
-
|
|
1155
|
+
const base = st === null ? `${session.id} · ${agentModel}` : `${session.id} · ${agentModel} · ${st}`;
|
|
1156
|
+
dock.setStatus(statusWithMode(base));
|
|
1095
1157
|
};
|
|
1096
1158
|
const withRun = async (run) => {
|
|
1097
1159
|
currentRun = run;
|
|
1098
1160
|
try {
|
|
1099
1161
|
turnNo += 1;
|
|
1100
|
-
const last = await consumeRun(session, run, input, turnNo,
|
|
1162
|
+
const last = await consumeRun(session, run, input, turnNo, faux, null, statusCb);
|
|
1101
1163
|
failOnFauxExhaustion(last, faux, input);
|
|
1102
1164
|
}
|
|
1103
1165
|
finally {
|
|
@@ -1150,7 +1212,24 @@ async function resume(session, prompt, faux, input) {
|
|
|
1150
1212
|
}
|
|
1151
1213
|
}
|
|
1152
1214
|
async function main() {
|
|
1153
|
-
|
|
1215
|
+
// Modes: --mode <name> wins over KISO_MODE — both applied before the
|
|
1216
|
+
// first makeAgent (the tier extensions read `current` live). The flag
|
|
1217
|
+
// is stripped from the positional args, so it works in any position.
|
|
1218
|
+
const args = process.argv.slice(2);
|
|
1219
|
+
const modeFlag = args.indexOf("--mode");
|
|
1220
|
+
if (modeFlag !== -1) {
|
|
1221
|
+
const m = MODES.find((x) => x === args[modeFlag + 1]);
|
|
1222
|
+
if (m === undefined) {
|
|
1223
|
+
console.error(`unknown mode: ${args[modeFlag + 1]} (tiers: ${MODES.join(", ")})`);
|
|
1224
|
+
process.exit(2);
|
|
1225
|
+
}
|
|
1226
|
+
setMode(m);
|
|
1227
|
+
args.splice(modeFlag, 2);
|
|
1228
|
+
}
|
|
1229
|
+
else {
|
|
1230
|
+
setMode(modeFromEnv());
|
|
1231
|
+
}
|
|
1232
|
+
const [command, arg] = args;
|
|
1154
1233
|
// 八: faux mode is the keyless demo script — an exhausted script must
|
|
1155
1234
|
// exit non-zero, never masquerade as a successful provider run.
|
|
1156
1235
|
const faux = process.env.ANTHROPIC_API_KEY === undefined && process.env.OPENAI_API_KEY === undefined;
|
|
@@ -1160,6 +1239,15 @@ async function main() {
|
|
|
1160
1239
|
// readline elsewhere. The trust question, chat, and resume all read
|
|
1161
1240
|
// through it; main's finally closes it on every exit path.
|
|
1162
1241
|
const input = makeLineInput();
|
|
1242
|
+
// v2d: the body renderer — active only where the dock is (a color
|
|
1243
|
+
// TTY with a real size); pipes run it in passthrough, byte-for-byte.
|
|
1244
|
+
body = new Body({
|
|
1245
|
+
active: () => process.stdin.isTTY && palette().blue !== "" && (process.stdout.rows ?? 0) >= 4,
|
|
1246
|
+
height: () => process.stdout.rows ?? 24,
|
|
1247
|
+
width: () => process.stdout.columns ?? 80,
|
|
1248
|
+
editCol: () => dock.editCol(),
|
|
1249
|
+
onDock: () => dock.redraw(), // v2d-B: the freeze scrolls the dock up — re-pin it
|
|
1250
|
+
});
|
|
1163
1251
|
try {
|
|
1164
1252
|
switch (command) {
|
|
1165
1253
|
case "chat": {
|
|
@@ -1224,6 +1312,7 @@ async function main() {
|
|
|
1224
1312
|
}
|
|
1225
1313
|
}
|
|
1226
1314
|
finally {
|
|
1315
|
+
body.close(); // flush the pending frame, stop the heartbeat
|
|
1227
1316
|
input.close();
|
|
1228
1317
|
// E 组: every normal and abnormal exit releases the fds and writer
|
|
1229
1318
|
// locks — no lock file is left behind.
|
package/dist/mode.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modes — the five built-in approval tiers, built ON the E1 policy chain
|
|
3
|
+
* (the kernel is untouched). Each tier is an in-process "mode:<name>"
|
|
4
|
+
* extension whose decide() is live — it only speaks when it is the
|
|
5
|
+
* CURRENT tier (otherwise abstain = no opinion, ADR-0042), so /mode
|
|
6
|
+
* switches take effect immediately. The extension NAME rides the runtime's decidedBy
|
|
7
|
+
* field: an automated denial records decidedBy: "mode:<name>" — the
|
|
8
|
+
* audit sell. User-level extensions stay on the chain AFTER the mode
|
|
9
|
+
* tiers; a user deny always wins (the chain's deny>ask>allow
|
|
10
|
+
* monotonicity — bypass cannot override an extension deny).
|
|
11
|
+
*/
|
|
12
|
+
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
13
|
+
export type Mode = "manual" | "default" | "accept-edits" | "plan" | "bypass";
|
|
14
|
+
export declare const MODES: readonly Mode[];
|
|
15
|
+
export declare function getMode(): Mode;
|
|
16
|
+
export declare function setMode(m: Mode): void;
|
|
17
|
+
/** The startup mode: KISO_MODE env (or the --mode flag — the CLI applies
|
|
18
|
+
* it before the first makeAgent). */
|
|
19
|
+
export declare function modeFromEnv(): Mode;
|
|
20
|
+
/** The five built-in mode tiers as chain extensions — named "mode:<tier>"
|
|
21
|
+
* so the runtime's decidedBy records exactly that (the runtime derives
|
|
22
|
+
* approvalPolicies from extensions[].approvals, tagging each with the
|
|
23
|
+
* extension name). The CURRENT tier is first: an all-allow chain records
|
|
24
|
+
* decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
|
|
25
|
+
* names that mode honestly. Order never affects verdicts — the chain is
|
|
26
|
+
* deny>ask>allow over the SPEAKING verdicts (abstain = no opinion), so a
|
|
27
|
+
* user extension's deny wins over any mode tier, bypass included (the
|
|
28
|
+
* monotonicity e2e pins it). */
|
|
29
|
+
export declare function modeExtensions(): readonly KisoExtension[];
|
|
30
|
+
/** The plan tier's system prompt addition — injected at startup when the
|
|
31
|
+
* initial mode is plan (the session prompt is fixed at creation; runtime
|
|
32
|
+
* switches are guided by the deny reason). */
|
|
33
|
+
export declare function modeSystemPrompt(): string | undefined;
|