@vincemakes/kiso-code 0.1.13 → 0.1.14
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/dock.d.ts +23 -9
- package/dist/dock.js +43 -16
- package/dist/editor.d.ts +64 -0
- package/dist/editor.js +378 -0
- package/dist/index.js +259 -85
- package/package.json +7 -7
package/dist/dock.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* v2b — the bottom-anchored UI (TTY + color only). Borrows pi-tui's
|
|
3
|
-
* a DECSTBM scroll region with a reserved bottom — without its
|
|
2
|
+
* v2b/v2c — the bottom-anchored UI (TTY + color only). Borrows pi-tui's
|
|
3
|
+
* IDEA — a DECSTBM scroll region with a reserved bottom — without its
|
|
4
4
|
* implementation: zero dependencies, line-level ANSI, no differential
|
|
5
5
|
* renderer.
|
|
6
6
|
*
|
|
7
7
|
* Layout (H = terminal height): rows 1..H-3 = the scroll region (the body
|
|
8
8
|
* streams and scrolls here, never touching the bottom), row H-2 = the dim
|
|
9
|
-
* separator, row H-1 = the live status bar (or a takeover
|
|
10
|
-
* H = the input line (blue you> +
|
|
11
|
-
*
|
|
9
|
+
* dotted separator (╌), row H-1 = the live status bar (or a takeover
|
|
10
|
+
* question), row H = the input line (the blue brick ▌you> + the v2c
|
|
11
|
+
* editor's row — readline is gone from the TTY path). Bottom redraws are
|
|
12
|
+
* wrapped in CSI 2026 (synchronized output) to avoid flicker — the pi
|
|
13
|
+
* trick. The visual identity is the kiso brick motif — ▌ half-block,
|
|
14
|
+
* dotted separator — deliberately NOT the CC rounded frame nor the pi
|
|
15
|
+
* editor (ADR-0039 Amendment 2).
|
|
12
16
|
*
|
|
13
17
|
* Pipes / NO_COLOR: the dock never activates; the v2a line mode stays
|
|
14
18
|
* byte-for-byte (the existing e2e assertions guard it).
|
|
@@ -36,9 +40,16 @@ export declare class Dock {
|
|
|
36
40
|
onResize(): void;
|
|
37
41
|
/** Body output: position the cursor inside the scroll region at the
|
|
38
42
|
* body's tracked position, write, and hand the cursor back to the
|
|
39
|
-
* input line. The row/col tracking is approximate for
|
|
40
|
-
* and wide-char lines (documented) — the region clamp
|
|
41
|
-
* bottom rows safe regardless.
|
|
43
|
+
* input line's EDIT position. The row/col tracking is approximate for
|
|
44
|
+
* width-wrapped and wide-char lines (documented) — the region clamp
|
|
45
|
+
* keeps the bottom rows safe regardless.
|
|
46
|
+
*
|
|
47
|
+
* The edit-position return is a correctness requirement, not
|
|
48
|
+
* cosmetics: readline tracks its cursor internally and NEVER
|
|
49
|
+
* re-syncs after an external move — a body write that left the
|
|
50
|
+
* cursor at column 1 made the next keystroke overwrite the prompt
|
|
51
|
+
* (probe-confirmed; the dock's redraw self-repaired ~200ms later,
|
|
52
|
+
* which read the user as cursor drift). */
|
|
42
53
|
writeBody(text: string): void;
|
|
43
54
|
/** The status bar's base text (usage, ctx, session, …). */
|
|
44
55
|
setStatus(text: string): void;
|
|
@@ -50,6 +61,9 @@ export declare class Dock {
|
|
|
50
61
|
clearQuestion(): void;
|
|
51
62
|
/** The bottom three rows, wrapped in CSI 2026 (synchronized output —
|
|
52
63
|
* the pi trick against flicker). The cursor ends at the input line's
|
|
53
|
-
* edit position.
|
|
64
|
+
* edit position. v2c: the separator is the dim dotted ╌ (a weaker
|
|
65
|
+
* presence than the solid ─), the status line is dim (blue accents
|
|
66
|
+
* inside come from the CLI's composition), the input row is the blue
|
|
67
|
+
* brick ▌you> + the editor's visible slice. */
|
|
54
68
|
redraw(): void;
|
|
55
69
|
}
|
package/dist/dock.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* v2b — the bottom-anchored UI (TTY + color only). Borrows pi-tui's
|
|
3
|
-
* a DECSTBM scroll region with a reserved bottom — without its
|
|
2
|
+
* v2b/v2c — the bottom-anchored UI (TTY + color only). Borrows pi-tui's
|
|
3
|
+
* IDEA — a DECSTBM scroll region with a reserved bottom — without its
|
|
4
4
|
* implementation: zero dependencies, line-level ANSI, no differential
|
|
5
5
|
* renderer.
|
|
6
6
|
*
|
|
7
7
|
* Layout (H = terminal height): rows 1..H-3 = the scroll region (the body
|
|
8
8
|
* streams and scrolls here, never touching the bottom), row H-2 = the dim
|
|
9
|
-
* separator, row H-1 = the live status bar (or a takeover
|
|
10
|
-
* H = the input line (blue you> +
|
|
11
|
-
*
|
|
9
|
+
* dotted separator (╌), row H-1 = the live status bar (or a takeover
|
|
10
|
+
* question), row H = the input line (the blue brick ▌you> + the v2c
|
|
11
|
+
* editor's row — readline is gone from the TTY path). Bottom redraws are
|
|
12
|
+
* wrapped in CSI 2026 (synchronized output) to avoid flicker — the pi
|
|
13
|
+
* trick. The visual identity is the kiso brick motif — ▌ half-block,
|
|
14
|
+
* dotted separator — deliberately NOT the CC rounded frame nor the pi
|
|
15
|
+
* editor (ADR-0039 Amendment 2).
|
|
12
16
|
*
|
|
13
17
|
* Pipes / NO_COLOR: the dock never activates; the v2a line mode stays
|
|
14
18
|
* byte-for-byte (the existing e2e assertions guard it).
|
|
15
19
|
*/
|
|
20
|
+
import { displayWidth } from "./editor.js";
|
|
16
21
|
import { palette } from "./render.js";
|
|
17
22
|
export class Dock {
|
|
18
23
|
#active = false;
|
|
@@ -83,9 +88,16 @@ export class Dock {
|
|
|
83
88
|
}
|
|
84
89
|
/** Body output: position the cursor inside the scroll region at the
|
|
85
90
|
* body's tracked position, write, and hand the cursor back to the
|
|
86
|
-
* input line. The row/col tracking is approximate for
|
|
87
|
-
* and wide-char lines (documented) — the region clamp
|
|
88
|
-
* bottom rows safe regardless.
|
|
91
|
+
* input line's EDIT position. The row/col tracking is approximate for
|
|
92
|
+
* width-wrapped and wide-char lines (documented) — the region clamp
|
|
93
|
+
* keeps the bottom rows safe regardless.
|
|
94
|
+
*
|
|
95
|
+
* The edit-position return is a correctness requirement, not
|
|
96
|
+
* cosmetics: readline tracks its cursor internally and NEVER
|
|
97
|
+
* re-syncs after an external move — a body write that left the
|
|
98
|
+
* cursor at column 1 made the next keystroke overwrite the prompt
|
|
99
|
+
* (probe-confirmed; the dock's redraw self-repaired ~200ms later,
|
|
100
|
+
* which read the user as cursor drift). */
|
|
89
101
|
writeBody(text) {
|
|
90
102
|
if (!this.#active) {
|
|
91
103
|
process.stdout.write(text);
|
|
@@ -104,7 +116,17 @@ export class Dock {
|
|
|
104
116
|
this.#bodyCol += 1;
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
|
-
process.stdout.write(`\x1b[${this.#height}
|
|
119
|
+
process.stdout.write(`\x1b[${this.#height};${this.#inputCol()}H`); // back to the edit position
|
|
120
|
+
}
|
|
121
|
+
/** The input line's edit column — prompt width + cursor + 1. The
|
|
122
|
+
* dock's redraw and the body's cursor return both end here, so the
|
|
123
|
+
* ACTUAL cursor always equals what the editor tracks. The width is
|
|
124
|
+
* DISPLAY width (the editor's cursor column is already width-based —
|
|
125
|
+
* the CJK drift root cause, editor.ts). */
|
|
126
|
+
#inputCol() {
|
|
127
|
+
const inp = this.#inputState();
|
|
128
|
+
const promptWidth = displayWidth(this.#inputPrompt.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
129
|
+
return promptWidth + inp.cursor + 1;
|
|
108
130
|
}
|
|
109
131
|
/** The status bar's base text (usage, ctx, session, …). */
|
|
110
132
|
setStatus(text) {
|
|
@@ -128,25 +150,30 @@ export class Dock {
|
|
|
128
150
|
}
|
|
129
151
|
/** The bottom three rows, wrapped in CSI 2026 (synchronized output —
|
|
130
152
|
* the pi trick against flicker). The cursor ends at the input line's
|
|
131
|
-
* edit position.
|
|
153
|
+
* edit position. v2c: the separator is the dim dotted ╌ (a weaker
|
|
154
|
+
* presence than the solid ─), the status line is dim (blue accents
|
|
155
|
+
* inside come from the CLI's composition), the input row is the blue
|
|
156
|
+
* brick ▌you> + the editor's visible slice. */
|
|
132
157
|
redraw() {
|
|
133
158
|
if (!this.#active)
|
|
134
159
|
return;
|
|
135
160
|
const p = palette();
|
|
136
161
|
const H = this.#height;
|
|
137
162
|
const W = this.#width;
|
|
138
|
-
const sep = `${p.dim}${"
|
|
163
|
+
const sep = `${p.dim}${"╌".repeat(W)}${p.reset}`;
|
|
139
164
|
const status = `${this.#status}${this.#tail === "" ? "" : ` · ${this.#tail}`}`;
|
|
140
|
-
const statusLine = this.#question ?? status
|
|
165
|
+
const statusLine = this.#question ?? `${p.dim}${status}${p.reset}`;
|
|
141
166
|
const inp = this.#inputState();
|
|
142
|
-
const promptWidth = this.#inputPrompt.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
143
167
|
const out = [];
|
|
144
|
-
|
|
168
|
+
// P3 (审查): the DEC private-mode SET/RESET needs the "?" prefix —
|
|
169
|
+
// \x1b[?2026h/l, the pi source's exact form. Without it terminals
|
|
170
|
+
// silently ignore the mode and the anti-flicker never engages.
|
|
171
|
+
out.push("\x1b[?2026h"); // synchronized output ON (DEC 2026)
|
|
145
172
|
out.push(`\x1b[${H - 2};1H\x1b[0K${sep}`);
|
|
146
173
|
out.push(`\x1b[${H - 1};1H\x1b[0K${statusLine}`);
|
|
147
174
|
out.push(`\x1b[${H};1H\x1b[0K${this.#inputPrompt}${inp.line}`);
|
|
148
|
-
out.push(`\x1b[${H};${
|
|
149
|
-
out.push("\x1b[2026l"); // synchronized output OFF
|
|
175
|
+
out.push(`\x1b[${H};${this.#inputCol()}H`); // back to the edit position
|
|
176
|
+
out.push("\x1b[?2026l"); // synchronized output OFF
|
|
150
177
|
process.stdout.write(out.join(""));
|
|
151
178
|
}
|
|
152
179
|
}
|
package/dist/editor.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2c — the raw-mode single-line editor that REPLACES readline on the TTY
|
|
3
|
+
* path. Root cause of the v2b drift (plan 2026-08-05-tui-v2b §11): readline
|
|
4
|
+
* re-renders its line by CHARACTER count and assumes it owns the row
|
|
5
|
+
* exclusively — a CJK wide character (2 cells) shifts every following
|
|
6
|
+
* column, and the dock's redraws make the mismatch permanent. Patching
|
|
7
|
+
* readline is a dead end; the TTY path draws its own input row instead.
|
|
8
|
+
* Non-TTY paths keep readline untouched (pipe bytes unchanged).
|
|
9
|
+
*
|
|
10
|
+
* Zero dependencies. The eastAsianWidth table is a ~40-line subset (CJK
|
|
11
|
+
* ideographs/kana/hangul/fullwidth/common wide symbols = 2, everything
|
|
12
|
+
* else = 1). Known limitation, documented in the README: emoji ZWJ
|
|
13
|
+
* clusters (family emoji etc.) are not guaranteed perfect — each code
|
|
14
|
+
* point counts as its width.
|
|
15
|
+
*
|
|
16
|
+
* The editor is a SINGLE line: bracketed paste (?2004h) unwraps and
|
|
17
|
+
* inserts, internal newlines become spaces.
|
|
18
|
+
*/
|
|
19
|
+
/** A code point's display width: 2 for the wide ranges, 1 otherwise. */
|
|
20
|
+
export declare function charWidth(cp: number): number;
|
|
21
|
+
/** Display width of a code-point array (cursor math, scrolling). */
|
|
22
|
+
export declare function widthOf(chars: readonly number[]): number;
|
|
23
|
+
/** Display width of a string. */
|
|
24
|
+
export declare function displayWidth(text: string): number;
|
|
25
|
+
export declare const PROMPT = "\u258Cyou> ";
|
|
26
|
+
export declare const PROMPT_WIDTH: number;
|
|
27
|
+
/**
|
|
28
|
+
* The editor. Raw mode + bracketed paste (?2004h) on enter, restored on
|
|
29
|
+
* exit. The input row is rendered by `onRender` (the CLI wires it to the
|
|
30
|
+
* dock's redraw when docked, the editor's own self-render otherwise) —
|
|
31
|
+
* the editor itself never writes while docked. The event handlers are
|
|
32
|
+
* settable — the chat/resume contexts wire them after construction.
|
|
33
|
+
*/
|
|
34
|
+
export declare class Editor {
|
|
35
|
+
#private;
|
|
36
|
+
readonly closed: Promise<void>;
|
|
37
|
+
constructor(onRender: () => void);
|
|
38
|
+
onLine(cb: (line: string) => void): void;
|
|
39
|
+
onSigint(cb: () => void): void;
|
|
40
|
+
onEot(cb: () => void): void;
|
|
41
|
+
onEscape(cb: () => void): void;
|
|
42
|
+
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
43
|
+
line(): string;
|
|
44
|
+
clearLine(): void;
|
|
45
|
+
/** The visible slice (dim "…" prefix when scrolled) + the cursor's
|
|
46
|
+
* display column within it — the dock's input-row state. */
|
|
47
|
+
dockState(): {
|
|
48
|
+
line: string;
|
|
49
|
+
cursor: number;
|
|
50
|
+
};
|
|
51
|
+
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
52
|
+
question(_query: string, cb: (answer: string) => void): void;
|
|
53
|
+
/** Cancel a pending question — the buffer stays (its text becomes the
|
|
54
|
+
* next turn on Enter, the readline re-emit equivalent). */
|
|
55
|
+
cancelQuestion(): void;
|
|
56
|
+
enter(): void;
|
|
57
|
+
exit(): void;
|
|
58
|
+
/** The row's own render when the dock is inactive (a TTY without a
|
|
59
|
+
* real size): \r + clear + blue brick prompt + visible + cursor
|
|
60
|
+
* column. */
|
|
61
|
+
selfRender(): void;
|
|
62
|
+
/** Feed raw stdin bytes — the parser. Public for unit tests. */
|
|
63
|
+
feed(raw: Uint8Array): void;
|
|
64
|
+
}
|
package/dist/editor.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2c — the raw-mode single-line editor that REPLACES readline on the TTY
|
|
3
|
+
* path. Root cause of the v2b drift (plan 2026-08-05-tui-v2b §11): readline
|
|
4
|
+
* re-renders its line by CHARACTER count and assumes it owns the row
|
|
5
|
+
* exclusively — a CJK wide character (2 cells) shifts every following
|
|
6
|
+
* column, and the dock's redraws make the mismatch permanent. Patching
|
|
7
|
+
* readline is a dead end; the TTY path draws its own input row instead.
|
|
8
|
+
* Non-TTY paths keep readline untouched (pipe bytes unchanged).
|
|
9
|
+
*
|
|
10
|
+
* Zero dependencies. The eastAsianWidth table is a ~40-line subset (CJK
|
|
11
|
+
* ideographs/kana/hangul/fullwidth/common wide symbols = 2, everything
|
|
12
|
+
* else = 1). Known limitation, documented in the README: emoji ZWJ
|
|
13
|
+
* clusters (family emoji etc.) are not guaranteed perfect — each code
|
|
14
|
+
* point counts as its width.
|
|
15
|
+
*
|
|
16
|
+
* The editor is a SINGLE line: bracketed paste (?2004h) unwraps and
|
|
17
|
+
* inserts, internal newlines become spaces.
|
|
18
|
+
*/
|
|
19
|
+
/** A code point's display width: 2 for the wide ranges, 1 otherwise. */
|
|
20
|
+
export function charWidth(cp) {
|
|
21
|
+
if (cp >= 0x1100 && cp <= 0x115f)
|
|
22
|
+
return 2; // hangul jamo
|
|
23
|
+
if (cp >= 0x2e80 && cp <= 0x303e)
|
|
24
|
+
return 2; // radicals .. CJK punctuation
|
|
25
|
+
if (cp >= 0x3041 && cp <= 0x33ff)
|
|
26
|
+
return 2; // kana, CJK compat
|
|
27
|
+
if (cp >= 0x3400 && cp <= 0x4dbf)
|
|
28
|
+
return 2; // CJK ext A
|
|
29
|
+
if (cp >= 0x4e00 && cp <= 0x9fff)
|
|
30
|
+
return 2; // CJK unified
|
|
31
|
+
if (cp >= 0xa000 && cp <= 0xa4cf)
|
|
32
|
+
return 2; // yi
|
|
33
|
+
if (cp >= 0xa960 && cp <= 0xa97f)
|
|
34
|
+
return 2; // hangul jamo ext
|
|
35
|
+
if (cp >= 0xac00 && cp <= 0xd7a3)
|
|
36
|
+
return 2; // hangul syllables
|
|
37
|
+
if (cp >= 0xf900 && cp <= 0xfaff)
|
|
38
|
+
return 2; // CJK compat ideographs
|
|
39
|
+
if (cp >= 0xfe10 && cp <= 0xfe19)
|
|
40
|
+
return 2; // vertical forms
|
|
41
|
+
if (cp >= 0xfe30 && cp <= 0xfe6f)
|
|
42
|
+
return 2; // CJK compat forms
|
|
43
|
+
if (cp >= 0xff00 && cp <= 0xff60)
|
|
44
|
+
return 2; // fullwidth forms
|
|
45
|
+
if (cp >= 0xffe0 && cp <= 0xffe6)
|
|
46
|
+
return 2; // fullwidth signs
|
|
47
|
+
if (cp >= 0x1f300 && cp <= 0x1f64f)
|
|
48
|
+
return 2; // emoji (misc + emoticons)
|
|
49
|
+
if (cp >= 0x1f900 && cp <= 0x1f9ff)
|
|
50
|
+
return 2; // supplemental emoji
|
|
51
|
+
if (cp >= 0x20000 && cp <= 0x3fffd)
|
|
52
|
+
return 2; // CJK ext B..G
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
/** Display width of a code-point array (cursor math, scrolling). */
|
|
56
|
+
export function widthOf(chars) {
|
|
57
|
+
let w = 0;
|
|
58
|
+
for (const cp of chars)
|
|
59
|
+
w += charWidth(cp);
|
|
60
|
+
return w;
|
|
61
|
+
}
|
|
62
|
+
/** Display width of a string. */
|
|
63
|
+
export function displayWidth(text) {
|
|
64
|
+
let w = 0;
|
|
65
|
+
for (const ch of text)
|
|
66
|
+
w += charWidth(ch.codePointAt(0));
|
|
67
|
+
return w;
|
|
68
|
+
}
|
|
69
|
+
import { palette } from "./render.js";
|
|
70
|
+
export const PROMPT = "▌you> "; // the kiso brick motif: one blue half-block, then you>
|
|
71
|
+
export const PROMPT_WIDTH = displayWidth(PROMPT);
|
|
72
|
+
/**
|
|
73
|
+
* The editor. Raw mode + bracketed paste (?2004h) on enter, restored on
|
|
74
|
+
* exit. The input row is rendered by `onRender` (the CLI wires it to the
|
|
75
|
+
* dock's redraw when docked, the editor's own self-render otherwise) —
|
|
76
|
+
* the editor itself never writes while docked. The event handlers are
|
|
77
|
+
* settable — the chat/resume contexts wire them after construction.
|
|
78
|
+
*/
|
|
79
|
+
export class Editor {
|
|
80
|
+
#chars = [];
|
|
81
|
+
#cursor = 0;
|
|
82
|
+
#scroll = 0; // chars scrolled off the left (width-based reflow)
|
|
83
|
+
#questionCb = null;
|
|
84
|
+
#pasting = false;
|
|
85
|
+
#lineCb = null;
|
|
86
|
+
#pendingLines = []; // submits before onLine is wired (startup) — never dropped
|
|
87
|
+
#sigintCb = null;
|
|
88
|
+
#eotCb = null;
|
|
89
|
+
#escapeCb = null;
|
|
90
|
+
#onRender;
|
|
91
|
+
#pending = ""; // an incomplete ESC/CSI prefix across chunks
|
|
92
|
+
#decoder = new TextDecoder();
|
|
93
|
+
#entered = false;
|
|
94
|
+
#onData;
|
|
95
|
+
#closedResolve;
|
|
96
|
+
closed;
|
|
97
|
+
constructor(onRender) {
|
|
98
|
+
this.#onRender = onRender;
|
|
99
|
+
this.#onData = (raw) => this.feed(raw);
|
|
100
|
+
this.closed = new Promise((resolve) => {
|
|
101
|
+
this.#closedResolve = resolve;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
onLine(cb) {
|
|
105
|
+
this.#lineCb = cb;
|
|
106
|
+
// Flush submits that arrived before the handler was wired (typed
|
|
107
|
+
// during startup) — readline buffered these; the editor must too.
|
|
108
|
+
for (const line of this.#pendingLines)
|
|
109
|
+
cb(line);
|
|
110
|
+
this.#pendingLines.length = 0;
|
|
111
|
+
}
|
|
112
|
+
onSigint(cb) {
|
|
113
|
+
this.#sigintCb = cb;
|
|
114
|
+
}
|
|
115
|
+
onEot(cb) {
|
|
116
|
+
this.#eotCb = cb;
|
|
117
|
+
}
|
|
118
|
+
onEscape(cb) {
|
|
119
|
+
this.#escapeCb = cb;
|
|
120
|
+
}
|
|
121
|
+
/** The whole buffer as text (the CLI's line()/clearLine()). */
|
|
122
|
+
line() {
|
|
123
|
+
return String.fromCodePoint(...this.#chars);
|
|
124
|
+
}
|
|
125
|
+
clearLine() {
|
|
126
|
+
this.#chars = [];
|
|
127
|
+
this.#cursor = 0;
|
|
128
|
+
this.#scroll = 0;
|
|
129
|
+
this.#onRender();
|
|
130
|
+
}
|
|
131
|
+
/** The visible slice (dim "…" prefix when scrolled) + the cursor's
|
|
132
|
+
* display column within it — the dock's input-row state. */
|
|
133
|
+
dockState() {
|
|
134
|
+
const visible = String.fromCodePoint(...this.#chars.slice(this.#scroll));
|
|
135
|
+
const prefix = this.#scroll > 0 ? "\x1b[2m…\x1b[0m" : "";
|
|
136
|
+
const col = (this.#scroll > 0 ? 1 : 0) + widthOf(this.#chars.slice(this.#scroll, this.#cursor));
|
|
137
|
+
return { line: `${prefix}${visible}`, cursor: col };
|
|
138
|
+
}
|
|
139
|
+
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
140
|
+
question(_query, cb) {
|
|
141
|
+
this.#questionCb = cb;
|
|
142
|
+
}
|
|
143
|
+
/** Cancel a pending question — the buffer stays (its text becomes the
|
|
144
|
+
* next turn on Enter, the readline re-emit equivalent). */
|
|
145
|
+
cancelQuestion() {
|
|
146
|
+
this.#questionCb = null;
|
|
147
|
+
}
|
|
148
|
+
enter() {
|
|
149
|
+
if (this.#entered)
|
|
150
|
+
return;
|
|
151
|
+
this.#entered = true;
|
|
152
|
+
process.stdin.setRawMode(true);
|
|
153
|
+
process.stdout.write("\x1b[?2004h"); // bracketed paste ON
|
|
154
|
+
process.stdin.on("data", this.#onData);
|
|
155
|
+
this.#onRender();
|
|
156
|
+
}
|
|
157
|
+
exit() {
|
|
158
|
+
if (!this.#entered)
|
|
159
|
+
return;
|
|
160
|
+
this.#entered = false;
|
|
161
|
+
process.stdin.off("data", this.#onData);
|
|
162
|
+
process.stdout.write("\x1b[?2004l"); // bracketed paste OFF
|
|
163
|
+
process.stdin.setRawMode(false);
|
|
164
|
+
this.#closedResolve();
|
|
165
|
+
}
|
|
166
|
+
/** The row's own render when the dock is inactive (a TTY without a
|
|
167
|
+
* real size): \r + clear + blue brick prompt + visible + cursor
|
|
168
|
+
* column. */
|
|
169
|
+
selfRender() {
|
|
170
|
+
const p = palette();
|
|
171
|
+
const st = this.dockState();
|
|
172
|
+
const W = (process.stdout.columns ?? 0) || 80; // a degenerate 0 size (no TIOCSWINSZ) falls back
|
|
173
|
+
const cursorCol = Math.min(1 + PROMPT_WIDTH + st.cursor, W);
|
|
174
|
+
process.stdout.write(`\r\x1b[0K${p.blue}${PROMPT}${p.reset}${st.line}\x1b[${cursorCol}G`);
|
|
175
|
+
}
|
|
176
|
+
// ---- input ----
|
|
177
|
+
/** Feed raw stdin bytes — the parser. Public for unit tests. */
|
|
178
|
+
feed(raw) {
|
|
179
|
+
const text = this.#pending + this.#decoder.decode(raw, { stream: true });
|
|
180
|
+
this.#pending = "";
|
|
181
|
+
let i = 0;
|
|
182
|
+
while (i < text.length) {
|
|
183
|
+
const c = text[i];
|
|
184
|
+
if (c === "\x1b") {
|
|
185
|
+
const rest = text.slice(i + 1);
|
|
186
|
+
if (rest.startsWith("[")) {
|
|
187
|
+
const m = rest.match(/^\[([0-9;?]*)([A-Za-z~])/);
|
|
188
|
+
if (m === null) {
|
|
189
|
+
this.#pending = text.slice(i); // incomplete CSI — wait for more
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
this.#csi(m[1], m[2]);
|
|
193
|
+
i += m[0].length + 1;
|
|
194
|
+
}
|
|
195
|
+
else if (rest.startsWith("O")) {
|
|
196
|
+
i += 3; // SS3 (function keys) — ignored
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
this.#escapeCb?.();
|
|
200
|
+
i += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (c === "\x0d" || c === "\x0a") {
|
|
204
|
+
if (this.#pasting) {
|
|
205
|
+
this.#insert(0x20); // single-line editor: newlines become spaces
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.#submit();
|
|
209
|
+
}
|
|
210
|
+
i += 1;
|
|
211
|
+
}
|
|
212
|
+
else if (c === "\x7f" || c === "\x08") {
|
|
213
|
+
this.#backspace();
|
|
214
|
+
i += 1;
|
|
215
|
+
}
|
|
216
|
+
else if (c === "\x03") {
|
|
217
|
+
this.#sigintCb?.();
|
|
218
|
+
i += 1;
|
|
219
|
+
}
|
|
220
|
+
else if (c === "\x04") {
|
|
221
|
+
this.#eotCb?.();
|
|
222
|
+
i += 1;
|
|
223
|
+
}
|
|
224
|
+
else if (c === "\x15") {
|
|
225
|
+
this.#killToStart();
|
|
226
|
+
i += 1;
|
|
227
|
+
}
|
|
228
|
+
else if (c === "\x0b") {
|
|
229
|
+
this.#killToEnd();
|
|
230
|
+
i += 1;
|
|
231
|
+
}
|
|
232
|
+
else if (c === "\x17") {
|
|
233
|
+
this.#killWord();
|
|
234
|
+
i += 1;
|
|
235
|
+
}
|
|
236
|
+
else if (c === "\x01") {
|
|
237
|
+
this.#cursor = 0;
|
|
238
|
+
this.#reflow();
|
|
239
|
+
this.#onRender();
|
|
240
|
+
i += 1;
|
|
241
|
+
}
|
|
242
|
+
else if (c === "\x05") {
|
|
243
|
+
this.#cursor = this.#chars.length;
|
|
244
|
+
this.#reflow();
|
|
245
|
+
this.#onRender();
|
|
246
|
+
i += 1;
|
|
247
|
+
}
|
|
248
|
+
else if (c !== undefined && c < " ") {
|
|
249
|
+
i += 1; // other control — ignored
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
this.#insert(text.codePointAt(i));
|
|
253
|
+
i += c.length;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
#csi(params, final) {
|
|
258
|
+
if (final === "~") {
|
|
259
|
+
const n = Number(params);
|
|
260
|
+
if (n === 3)
|
|
261
|
+
this.#delete();
|
|
262
|
+
else if (n === 200)
|
|
263
|
+
this.#pasting = true;
|
|
264
|
+
else if (n === 201) {
|
|
265
|
+
this.#pasting = false;
|
|
266
|
+
this.#onRender();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (final === "D") {
|
|
270
|
+
this.#move(-1);
|
|
271
|
+
}
|
|
272
|
+
else if (final === "C") {
|
|
273
|
+
this.#move(1);
|
|
274
|
+
}
|
|
275
|
+
else if (final === "H") {
|
|
276
|
+
this.#cursor = 0;
|
|
277
|
+
this.#reflow();
|
|
278
|
+
}
|
|
279
|
+
else if (final === "F") {
|
|
280
|
+
this.#cursor = this.#chars.length;
|
|
281
|
+
this.#reflow();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// ---- editing ----
|
|
285
|
+
#insert(cp) {
|
|
286
|
+
this.#chars.splice(this.#cursor, 0, cp);
|
|
287
|
+
this.#cursor += 1;
|
|
288
|
+
this.#reflow();
|
|
289
|
+
if (!this.#pasting)
|
|
290
|
+
this.#onRender();
|
|
291
|
+
}
|
|
292
|
+
#backspace() {
|
|
293
|
+
if (this.#cursor === 0)
|
|
294
|
+
return;
|
|
295
|
+
this.#chars.splice(this.#cursor - 1, 1);
|
|
296
|
+
this.#cursor -= 1;
|
|
297
|
+
this.#reflow();
|
|
298
|
+
if (!this.#pasting)
|
|
299
|
+
this.#onRender();
|
|
300
|
+
}
|
|
301
|
+
#delete() {
|
|
302
|
+
if (this.#cursor >= this.#chars.length)
|
|
303
|
+
return;
|
|
304
|
+
this.#chars.splice(this.#cursor, 1);
|
|
305
|
+
this.#reflow();
|
|
306
|
+
if (!this.#pasting)
|
|
307
|
+
this.#onRender();
|
|
308
|
+
}
|
|
309
|
+
#move(delta) {
|
|
310
|
+
this.#cursor = Math.max(0, Math.min(this.#chars.length, this.#cursor + delta));
|
|
311
|
+
this.#reflow();
|
|
312
|
+
if (!this.#pasting)
|
|
313
|
+
this.#onRender();
|
|
314
|
+
}
|
|
315
|
+
#killToStart() {
|
|
316
|
+
this.#chars.splice(0, this.#cursor);
|
|
317
|
+
this.#cursor = 0;
|
|
318
|
+
this.#reflow();
|
|
319
|
+
if (!this.#pasting)
|
|
320
|
+
this.#onRender();
|
|
321
|
+
}
|
|
322
|
+
#killToEnd() {
|
|
323
|
+
this.#chars.length = this.#cursor;
|
|
324
|
+
this.#reflow();
|
|
325
|
+
if (!this.#pasting)
|
|
326
|
+
this.#onRender();
|
|
327
|
+
}
|
|
328
|
+
#killWord() {
|
|
329
|
+
let i = this.#cursor;
|
|
330
|
+
while (i > 0 && this.#chars[i - 1] === 0x20)
|
|
331
|
+
i -= 1; // trailing spaces
|
|
332
|
+
while (i > 0 && this.#chars[i - 1] !== 0x20)
|
|
333
|
+
i -= 1; // the word
|
|
334
|
+
this.#chars.splice(i, this.#cursor - i);
|
|
335
|
+
this.#cursor = i;
|
|
336
|
+
this.#reflow();
|
|
337
|
+
}
|
|
338
|
+
#submit() {
|
|
339
|
+
const line = String.fromCodePoint(...this.#chars);
|
|
340
|
+
this.#chars = [];
|
|
341
|
+
this.#cursor = 0;
|
|
342
|
+
this.#scroll = 0;
|
|
343
|
+
const cb = this.#questionCb;
|
|
344
|
+
this.#questionCb = null;
|
|
345
|
+
if (cb !== null) {
|
|
346
|
+
cb(line);
|
|
347
|
+
}
|
|
348
|
+
else if (this.#lineCb !== null) {
|
|
349
|
+
this.#lineCb(line);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
this.#pendingLines.push(line); // nobody wired yet — hold it
|
|
353
|
+
}
|
|
354
|
+
this.#onRender();
|
|
355
|
+
}
|
|
356
|
+
// ---- width-based horizontal scroll ----
|
|
357
|
+
#reflow() {
|
|
358
|
+
const W = (process.stdout.columns ?? 0) || 80; // degenerate 0 falls back to 80
|
|
359
|
+
const maxW = Math.max(1, W - PROMPT_WIDTH - 1); // 1 col for the "…"
|
|
360
|
+
const curCol = widthOf(this.#chars.slice(0, this.#cursor));
|
|
361
|
+
const scrolledW = widthOf(this.#chars.slice(0, this.#scroll));
|
|
362
|
+
if (curCol < scrolledW) {
|
|
363
|
+
this.#scroll = this.#indexAtWidth(curCol);
|
|
364
|
+
}
|
|
365
|
+
else if (curCol >= scrolledW + maxW) {
|
|
366
|
+
this.#scroll = this.#indexAtWidth(Math.max(0, curCol - maxW + 1));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
#indexAtWidth(target) {
|
|
370
|
+
let w = 0;
|
|
371
|
+
for (let i = 0; i < this.#chars.length; i += 1) {
|
|
372
|
+
if (w >= target)
|
|
373
|
+
return i;
|
|
374
|
+
w += charWidth(this.#chars[i]);
|
|
375
|
+
}
|
|
376
|
+
return this.#chars.length;
|
|
377
|
+
}
|
|
378
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { createInterface } from "node:readline";
|
|
20
|
+
import { Editor, PROMPT as EDITOR_PROMPT } from "./editor.js";
|
|
20
21
|
import { homedir, tmpdir } from "node:os";
|
|
21
22
|
import { dirname, join } from "node:path";
|
|
22
23
|
import { fileURLToPath } from "node:url";
|
|
@@ -87,11 +88,124 @@ function startupBanner() {
|
|
|
87
88
|
return `${p.dim}${LOGO_TOP}${p.blue}${TAGLINE}${p.reset}${p.dim}${LOGO_BOTTOM} v${VERSION}${names}${p.reset}\n`;
|
|
88
89
|
}
|
|
89
90
|
/** v2a: the interactive prompt — blue, the identity accent. readline owns
|
|
90
|
-
* the echo of what the user types; we own the prompt's color.
|
|
91
|
+
* the echo of what the user types; we own the prompt's color. (v2c: the
|
|
92
|
+
* readline prompt keeps "you> " — the brick ▌ is the dock's row only;
|
|
93
|
+
* pipe bytes must not change.) */
|
|
91
94
|
function interactivePrompt() {
|
|
92
95
|
const p = palette();
|
|
93
96
|
return `${p.blue}you> ${p.reset}`;
|
|
94
97
|
}
|
|
98
|
+
/** The v2b behavior, unchanged: readline owns the line, SIGINT, and the
|
|
99
|
+
* prompt. Only ever constructed when stdin is NOT a TTY. The rl starts
|
|
100
|
+
* consuming stdin at construction (main), so 'line' events are buffered
|
|
101
|
+
* until chat() wires the handler — pipe input must never be dropped. */
|
|
102
|
+
function readlineInput(rl) {
|
|
103
|
+
let lineCb = null;
|
|
104
|
+
const pending = [];
|
|
105
|
+
rl.on("line", (line) => {
|
|
106
|
+
if (lineCb === null)
|
|
107
|
+
pending.push(line);
|
|
108
|
+
else
|
|
109
|
+
lineCb(line);
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
onLine(cb) {
|
|
113
|
+
lineCb = cb;
|
|
114
|
+
for (const line of pending)
|
|
115
|
+
cb(line);
|
|
116
|
+
pending.length = 0;
|
|
117
|
+
},
|
|
118
|
+
onSigint(cb) {
|
|
119
|
+
rl.on("SIGINT", cb);
|
|
120
|
+
},
|
|
121
|
+
onEot() {
|
|
122
|
+
/* readline's Ctrl+D on an empty line is EOF → 'close' — the
|
|
123
|
+
* exit path is the close, nothing to wire here. */
|
|
124
|
+
},
|
|
125
|
+
onEscape() {
|
|
126
|
+
/* readline has no bare-Esc semantics — ignored. */
|
|
127
|
+
},
|
|
128
|
+
question(query, cb) {
|
|
129
|
+
rl.question(query, cb);
|
|
130
|
+
},
|
|
131
|
+
cancelQuestion() {
|
|
132
|
+
/* the rl.question stays pending; the settled branch re-emits
|
|
133
|
+
* the answer as a new line. */
|
|
134
|
+
},
|
|
135
|
+
emitLine(line) {
|
|
136
|
+
rl.emit("line", line);
|
|
137
|
+
},
|
|
138
|
+
line() {
|
|
139
|
+
return rl.line;
|
|
140
|
+
},
|
|
141
|
+
clearLine() {
|
|
142
|
+
/* readline: Ctrl+C is exit/abort only — nothing to clear. */
|
|
143
|
+
},
|
|
144
|
+
prompt() {
|
|
145
|
+
rl.setPrompt(interactivePrompt());
|
|
146
|
+
rl.prompt();
|
|
147
|
+
},
|
|
148
|
+
close() {
|
|
149
|
+
rl.close();
|
|
150
|
+
},
|
|
151
|
+
closed: new Promise((resolve) => rl.on("close", () => resolve())),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** The v2c TTY path: the editor's events map 1:1 onto the interface; the
|
|
155
|
+
* input row renders on every state change (the CLI's onRender wiring). */
|
|
156
|
+
function editorInput(editor) {
|
|
157
|
+
return {
|
|
158
|
+
onLine(cb) {
|
|
159
|
+
editor.onLine(cb);
|
|
160
|
+
},
|
|
161
|
+
onSigint(cb) {
|
|
162
|
+
editor.onSigint(cb);
|
|
163
|
+
},
|
|
164
|
+
onEot(cb) {
|
|
165
|
+
editor.onEot(cb);
|
|
166
|
+
},
|
|
167
|
+
onEscape(cb) {
|
|
168
|
+
editor.onEscape(cb);
|
|
169
|
+
},
|
|
170
|
+
question(query, cb) {
|
|
171
|
+
editor.question(query, cb);
|
|
172
|
+
},
|
|
173
|
+
cancelQuestion() {
|
|
174
|
+
editor.cancelQuestion();
|
|
175
|
+
},
|
|
176
|
+
emitLine() {
|
|
177
|
+
/* the editor's buffer survives a cancelled question — its text
|
|
178
|
+
* becomes the next turn on Enter (the readline re-emit
|
|
179
|
+
* equivalent). */
|
|
180
|
+
},
|
|
181
|
+
line() {
|
|
182
|
+
return editor.line();
|
|
183
|
+
},
|
|
184
|
+
clearLine() {
|
|
185
|
+
editor.clearLine();
|
|
186
|
+
},
|
|
187
|
+
prompt() {
|
|
188
|
+
/* the editor renders on every state change — nothing to arm. */
|
|
189
|
+
},
|
|
190
|
+
close() {
|
|
191
|
+
editor.exit();
|
|
192
|
+
},
|
|
193
|
+
closed: editor.closed,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** One input source per process: the raw-mode Editor on a TTY (entered
|
|
197
|
+
* here, bound to the dock once — the trust question, chat, and resume
|
|
198
|
+
* all read through it), readline elsewhere. */
|
|
199
|
+
function makeLineInput() {
|
|
200
|
+
if (process.stdin.isTTY) {
|
|
201
|
+
const editor = new Editor(() => (dock.active ? dock.redraw() : editor.selfRender()));
|
|
202
|
+
editor.enter();
|
|
203
|
+
const p = palette();
|
|
204
|
+
dock.bindInput(() => editor.dockState(), `${p.blue}${EDITOR_PROMPT}${p.reset}`);
|
|
205
|
+
return editorInput(editor);
|
|
206
|
+
}
|
|
207
|
+
return readlineInput(createInterface({ input: process.stdin, output: process.stdout }));
|
|
208
|
+
}
|
|
95
209
|
/** v2b: the bottom-anchored UI — docked only on a color TTY; pipes and
|
|
96
210
|
* NO_COLOR stay the v2a line mode byte-for-byte. */
|
|
97
211
|
const dock = new Dock();
|
|
@@ -171,7 +285,7 @@ function extensionsBanner() {
|
|
|
171
285
|
* record → only a HUMAN may decide, TTY only — non-TTY refuses with one
|
|
172
286
|
* stderr line. Returns the artifacts on grant, null on anything else.
|
|
173
287
|
*/
|
|
174
|
-
async function resolveProjectTrust() {
|
|
288
|
+
async function resolveProjectTrust(input) {
|
|
175
289
|
const artifacts = await projectArtifacts(process.cwd());
|
|
176
290
|
if (artifacts === null)
|
|
177
291
|
return null; // no .kiso artifacts — nothing to gate
|
|
@@ -188,25 +302,19 @@ async function resolveProjectTrust() {
|
|
|
188
302
|
console.error(`[project .kiso] found ${artifacts.files.length} artifact(s) in ${artifacts.root} — not trusted, not loaded (run kiso interactively once to decide)`);
|
|
189
303
|
return null;
|
|
190
304
|
}
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
bodyLog(`
|
|
196
|
-
for (const f of artifacts.files) {
|
|
197
|
-
bodyLog(` ${f.path} (${f.digest.slice(0, 6)})`);
|
|
198
|
-
}
|
|
199
|
-
const answer = await ask(rl, `trust this project's .kiso? (y/n) `);
|
|
200
|
-
const granted = answer !== CANCELLED && answer.trim().toLowerCase().startsWith("y");
|
|
201
|
-
recordTrust({ root: artifacts.root, digest: artifacts.digest, decision: granted ? "granted" : "refused" });
|
|
202
|
-
if (!granted)
|
|
203
|
-
return null;
|
|
204
|
-
applyProjectMerges(artifacts);
|
|
205
|
-
return artifacts;
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
rl.close();
|
|
305
|
+
// v2c: the shared input (the editor on a TTY) reads the answer; the
|
|
306
|
+
// dock shows the question at the status position.
|
|
307
|
+
bodyLog(`[project .kiso] ${artifacts.root}`);
|
|
308
|
+
for (const f of artifacts.files) {
|
|
309
|
+
bodyLog(` ${f.path} (${f.digest.slice(0, 6)})`);
|
|
209
310
|
}
|
|
311
|
+
const answer = await ask(input, `trust this project's .kiso? (y/n) `);
|
|
312
|
+
const granted = answer !== CANCELLED && answer.trim().toLowerCase().startsWith("y");
|
|
313
|
+
recordTrust({ root: artifacts.root, digest: artifacts.digest, decision: granted ? "granted" : "refused" });
|
|
314
|
+
if (!granted)
|
|
315
|
+
return null;
|
|
316
|
+
applyProjectMerges(artifacts);
|
|
317
|
+
return artifacts;
|
|
210
318
|
}
|
|
211
319
|
/**
|
|
212
320
|
* E3 — merge the project's mcp.json and skills into the env BEFORE the
|
|
@@ -368,12 +476,12 @@ function fauxSkip(id) {
|
|
|
368
476
|
events.filter((e) => e.type === "stop" && e.reason === "end_turn").length +
|
|
369
477
|
events.filter((e) => e.type === "tool_call_end" && !results.has(e.callId)).length);
|
|
370
478
|
}
|
|
371
|
-
async function makeAgent(fauxSkipTurns = 0) {
|
|
479
|
+
async function makeAgent(fauxSkipTurns = 0, input) {
|
|
372
480
|
const store = new SessionStore(sessionsDir());
|
|
373
481
|
// E3: the project-level trust gate runs BEFORE any extension load (the
|
|
374
482
|
// mcp/skills merges must be in the env when the user-level extensions
|
|
375
483
|
// load). Untrusted project capability is never loaded — never silently.
|
|
376
|
-
const project = await resolveProjectTrust();
|
|
484
|
+
const project = input !== undefined ? await resolveProjectTrust(input) : await resolveProjectTrust(undefined);
|
|
377
485
|
// E1: the startup extension scan — a broken extension fails the process
|
|
378
486
|
// LOUDLY here (loadExtensions throws), never silently.
|
|
379
487
|
userExtensions = await loadExtensions(extensionsDir());
|
|
@@ -508,15 +616,20 @@ const CANCELLED = Symbol("kiso-question-cancelled");
|
|
|
508
616
|
* question.
|
|
509
617
|
*/
|
|
510
618
|
let pendingAsk = null;
|
|
511
|
-
function ask(
|
|
619
|
+
function ask(input, question) {
|
|
512
620
|
if (!process.stdin.isTTY) {
|
|
513
621
|
console.log(`[non-interactive — no human to ask: ${question}]`);
|
|
514
622
|
return Promise.resolve("");
|
|
515
623
|
}
|
|
516
624
|
// v2b: docked — the question takes over the status position, the
|
|
517
|
-
// answer lands at the input line.
|
|
518
|
-
|
|
625
|
+
// answer lands at the input line. v2c: a TTY without a dock (rows < 4)
|
|
626
|
+
// prints the question into the body — the editor cannot show it.
|
|
627
|
+
if (dock.active) {
|
|
519
628
|
dock.showQuestion(question);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
bodyLog(question);
|
|
632
|
+
}
|
|
520
633
|
return new Promise((resolve) => {
|
|
521
634
|
let settled = false;
|
|
522
635
|
pendingAsk = () => {
|
|
@@ -524,12 +637,18 @@ function ask(rl, question) {
|
|
|
524
637
|
return;
|
|
525
638
|
settled = true;
|
|
526
639
|
pendingAsk = null;
|
|
640
|
+
input.cancelQuestion();
|
|
527
641
|
resolve(CANCELLED); // the run is aborting — the question is dead
|
|
528
642
|
};
|
|
529
|
-
|
|
643
|
+
// v2b: docked — the question reads at the input line, whose prompt
|
|
644
|
+
// is the same blue you> (the editor's brick row; the readline path
|
|
645
|
+
// passes the plain question). An empty prompt would start readline
|
|
646
|
+
// at column 1 while the dock renders "you> " — the typed answer
|
|
647
|
+
// would land on the prompt and drift (probe-confirmed).
|
|
648
|
+
input.question(dock.active ? interactivePrompt() : question, (answer) => {
|
|
530
649
|
if (settled) {
|
|
531
650
|
// The question was cancelled; this line is a NEW user turn.
|
|
532
|
-
|
|
651
|
+
input.emitLine(answer);
|
|
533
652
|
return;
|
|
534
653
|
}
|
|
535
654
|
settled = true;
|
|
@@ -560,9 +679,9 @@ function estimateCtxRatio(session) {
|
|
|
560
679
|
return chars / 4 / contextWindowTokens();
|
|
561
680
|
}
|
|
562
681
|
/** Decide every uncertain execution with the human (r)erun/(a)bandon. */
|
|
563
|
-
async function resolveUncertains(session,
|
|
682
|
+
async function resolveUncertains(session, input, isCancelled) {
|
|
564
683
|
for (const uncertain of session.uncertainExecutions()) {
|
|
565
|
-
const answer = await ask(
|
|
684
|
+
const answer = await ask(input, `⚠ interrupted execution: ${escapeTerminal(uncertain.name)} (${uncertain.executionId}) — did it apply? (r)erun / (a)bandon: `);
|
|
566
685
|
if (isCancelled() || answer === CANCELLED) {
|
|
567
686
|
// 十: a cancellation NEVER records a verdict — the execution
|
|
568
687
|
// stays uncertain and durable; no rerun/abandoned is fabricated.
|
|
@@ -581,7 +700,7 @@ const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
|
581
700
|
* line's form; `liveInput` (non-null only in interactive chat) carries the
|
|
582
701
|
* last line THIS process's readline consumed — the double-echo filter.
|
|
583
702
|
*/
|
|
584
|
-
async function consumeRun(session, run,
|
|
703
|
+
async function consumeRun(session, run, input, turnNo, lastToolRef, faux, liveInput, lastThinking, statusCb) {
|
|
585
704
|
let last;
|
|
586
705
|
// B 区: tool_call_end → (name, input) for the summary; tool_result →
|
|
587
706
|
// one summary line. Usage events feed the status line.
|
|
@@ -615,7 +734,14 @@ async function consumeRun(session, run, rl, turnNo, lastToolRef, faux, liveInput
|
|
|
615
734
|
// Replayed history (recovery/resume — nobody typed) keeps the event
|
|
616
735
|
// render. Deterministic: exact content match with the consumed line,
|
|
617
736
|
// on a TTY (the only place an echo exists to hand over).
|
|
618
|
-
|
|
737
|
+
// 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. The
|
|
739
|
+
// user's typed text must not vanish after Enter.
|
|
740
|
+
if (ev.type === "user_input" &&
|
|
741
|
+
liveInput !== null &&
|
|
742
|
+
liveInput.current === (typeof ev.content === "string" ? ev.content : "") &&
|
|
743
|
+
process.stdin.isTTY &&
|
|
744
|
+
!dock.active) {
|
|
619
745
|
continue;
|
|
620
746
|
}
|
|
621
747
|
const prevThinking = thinkingOpen;
|
|
@@ -664,7 +790,7 @@ async function consumeRun(session, run, rl, turnNo, lastToolRef, faux, liveInput
|
|
|
664
790
|
const decisionId = ev.decisionId;
|
|
665
791
|
const name = ev.name;
|
|
666
792
|
// 八: the tool name is model text — escaped on every output path.
|
|
667
|
-
const answer = await ask(
|
|
793
|
+
const answer = await ask(input, `approve ${escapeTerminal(name)}? (y/n) `);
|
|
668
794
|
if (answer === CANCELLED) {
|
|
669
795
|
// 十: a cancellation is a CONSERVATIVE denial, explicitly
|
|
670
796
|
// distinguished from the user typing "n".
|
|
@@ -707,35 +833,35 @@ class FauxExhaustionError extends Error {
|
|
|
707
833
|
this.name = "FauxExhaustionError";
|
|
708
834
|
}
|
|
709
835
|
}
|
|
710
|
-
function failOnFauxExhaustion(last, faux,
|
|
836
|
+
function failOnFauxExhaustion(last, faux, input) {
|
|
711
837
|
if (!faux)
|
|
712
838
|
return;
|
|
713
839
|
if (last?.type !== "terminal" || last.outcome.kind !== "error")
|
|
714
840
|
return;
|
|
715
841
|
const message = last.outcome.error.message;
|
|
716
|
-
|
|
842
|
+
input?.close(); // the REPL must not stay open waiting for a line
|
|
717
843
|
throw new FauxExhaustionError(message.startsWith("provider stream ended without a stop event")
|
|
718
844
|
? "[faux mode] the scripted demo turns are exhausted — set ANTHROPIC_API_KEY or OPENAI_API_KEY for a real model"
|
|
719
845
|
: `[faux mode] the scripted model failed: ${escapeTerminal(message.slice(0, 200))}`);
|
|
720
846
|
}
|
|
721
847
|
/** Interactive REPL: stream events, pause for approvals, Ctrl+C aborts. */
|
|
722
|
-
async function chat(session, faux) {
|
|
723
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
848
|
+
async function chat(session, faux, input) {
|
|
724
849
|
let currentRun = null;
|
|
725
850
|
let cancelled = false;
|
|
726
|
-
const turn = (
|
|
851
|
+
const turn = (text) => new Promise((resolve, reject) => {
|
|
852
|
+
queued = Math.max(0, queued - 1); // a queued turn starts
|
|
727
853
|
// v2a: the echo filter compares the user_input event against THIS
|
|
728
854
|
// turn's own input — lines that arrive ahead of their turn (piped
|
|
729
855
|
// bursts, queued replays) must not overwrite the reference.
|
|
730
|
-
liveInput.current =
|
|
731
|
-
const run = session.run(
|
|
856
|
+
liveInput.current = text;
|
|
857
|
+
const run = session.run(text);
|
|
732
858
|
currentRun = run;
|
|
733
859
|
turnNo += 1;
|
|
734
860
|
const myTurn = turnNo;
|
|
735
861
|
(async () => {
|
|
736
862
|
let last;
|
|
737
863
|
try {
|
|
738
|
-
last = await consumeRun(session, run,
|
|
864
|
+
last = await consumeRun(session, run, input, myTurn, lastToolRef, faux, liveInput, lastThinking, statusCb);
|
|
739
865
|
currentRun = null;
|
|
740
866
|
// 八: a faux script that ran out of declared turns exits
|
|
741
867
|
// loudly with a non-zero status — never a silent status 0.
|
|
@@ -743,11 +869,10 @@ async function chat(session, faux) {
|
|
|
743
869
|
// this turn's promise — it propagates through the chain to
|
|
744
870
|
// chat to main's finally/catch, never an orphaned
|
|
745
871
|
// unhandled rejection from the IIFE.
|
|
746
|
-
failOnFauxExhaustion(last, faux,
|
|
872
|
+
failOnFauxExhaustion(last, faux, input);
|
|
747
873
|
// 八: after EVERY turn the prompt is re-armed — the human
|
|
748
874
|
// never types blind after the first turn.
|
|
749
|
-
|
|
750
|
-
rl.prompt();
|
|
875
|
+
input.prompt();
|
|
751
876
|
resolve();
|
|
752
877
|
}
|
|
753
878
|
catch (err) {
|
|
@@ -760,13 +885,12 @@ async function chat(session, faux) {
|
|
|
760
885
|
}
|
|
761
886
|
console.error(`\n[run failed] ${err instanceof Error ? err.message : String(err)}\n`);
|
|
762
887
|
currentRun = null;
|
|
763
|
-
|
|
764
|
-
rl.prompt();
|
|
888
|
+
input.prompt();
|
|
765
889
|
resolve();
|
|
766
890
|
}
|
|
767
891
|
})();
|
|
768
892
|
});
|
|
769
|
-
|
|
893
|
+
input.onSigint(() => {
|
|
770
894
|
if (currentRun) {
|
|
771
895
|
// 八: Ctrl+C cancels BOTH the pending question (if one is
|
|
772
896
|
// awaiting a line) and the run — the run then writes its unique
|
|
@@ -775,11 +899,30 @@ async function chat(session, faux) {
|
|
|
775
899
|
pendingAsk?.();
|
|
776
900
|
currentRun.abort();
|
|
777
901
|
}
|
|
778
|
-
else if (
|
|
902
|
+
else if (pendingAsk !== null) {
|
|
903
|
+
pendingAsk?.(); // a startup/trust question — cancel it
|
|
904
|
+
}
|
|
905
|
+
else if (input.line() === "") {
|
|
779
906
|
cancelled = true;
|
|
780
907
|
console.log("\n[exit requested]");
|
|
781
|
-
|
|
782
|
-
|
|
908
|
+
input.close();
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
input.clearLine(); // v2c: Ctrl+C on a non-empty line clears it
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
input.onEot(() => {
|
|
915
|
+
if (!currentRun && pendingAsk === null && input.line() === "") {
|
|
916
|
+
cancelled = true;
|
|
917
|
+
console.log("\n[exit requested]");
|
|
918
|
+
input.close();
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
input.onEscape(() => {
|
|
922
|
+
if (currentRun) {
|
|
923
|
+
console.log("\n[aborting run]");
|
|
924
|
+
pendingAsk?.();
|
|
925
|
+
currentRun.abort();
|
|
783
926
|
}
|
|
784
927
|
});
|
|
785
928
|
// 第五轮(P1-11): the PERSISTENT line listener is installed BEFORE the
|
|
@@ -800,15 +943,22 @@ async function chat(session, faux) {
|
|
|
800
943
|
const liveInput = { current: null };
|
|
801
944
|
// v2b: the last complete thinking block, for /think.
|
|
802
945
|
const lastThinking = { current: null };
|
|
946
|
+
// v2c: turns submitted while another runs are QUEUED on the chain — the
|
|
947
|
+
// live count rides the status bar (+N queued).
|
|
948
|
+
let queued = 0;
|
|
803
949
|
// v2b: the live status bar (docked only).
|
|
804
950
|
const statusCb = (u, ctx) => {
|
|
805
951
|
if (!dock.active)
|
|
806
952
|
return;
|
|
807
953
|
const st = renderStatusLine(turnNo, u, ctx, faux);
|
|
808
|
-
|
|
954
|
+
const base = st === null ? `${session.id} · ${agentModel}` : `${session.id} · ${agentModel} · ${st}`;
|
|
955
|
+
dock.setStatus(queued > 0 ? `${base} · +${queued} queued` : base);
|
|
809
956
|
};
|
|
810
|
-
|
|
811
|
-
|
|
957
|
+
// The ONE dispatcher: slash commands, exit, and turns. The recovery
|
|
958
|
+
// replay routes through it too — a queued "/last" must never become a
|
|
959
|
+
// user turn (v2c: the rl lives in main, so lines arrive earlier and
|
|
960
|
+
// the queue is the common path).
|
|
961
|
+
const dispatch = (line) => {
|
|
812
962
|
const trimmed = line.trim();
|
|
813
963
|
if (trimmed === "/help") {
|
|
814
964
|
// Prints the available commands with one-line descriptions.
|
|
@@ -821,8 +971,7 @@ async function chat(session, faux) {
|
|
|
821
971
|
bodyLog(cmd("/last", "show the most recent tool call's input and output"));
|
|
822
972
|
bodyLog(cmd("/status", "show session id, event count, and context estimate"));
|
|
823
973
|
bodyLog(cmd("exit", "leave the session"));
|
|
824
|
-
|
|
825
|
-
rl.prompt();
|
|
974
|
+
input.prompt();
|
|
826
975
|
});
|
|
827
976
|
return;
|
|
828
977
|
}
|
|
@@ -837,8 +986,7 @@ async function chat(session, faux) {
|
|
|
837
986
|
else {
|
|
838
987
|
bodyLog(escapeTerminal(t));
|
|
839
988
|
}
|
|
840
|
-
|
|
841
|
-
rl.prompt();
|
|
989
|
+
input.prompt();
|
|
842
990
|
});
|
|
843
991
|
return;
|
|
844
992
|
}
|
|
@@ -857,8 +1005,7 @@ async function chat(session, faux) {
|
|
|
857
1005
|
bodyLog(`--- ${tool.name} output${tool.result.isError ? " (error)" : ""} ---`);
|
|
858
1006
|
bodyLog(escapeTerminal(tool.result.content));
|
|
859
1007
|
}
|
|
860
|
-
|
|
861
|
-
rl.prompt();
|
|
1008
|
+
input.prompt();
|
|
862
1009
|
});
|
|
863
1010
|
return;
|
|
864
1011
|
}
|
|
@@ -872,49 +1019,58 @@ async function chat(session, faux) {
|
|
|
872
1019
|
bodyLog(`session ${session.id}`);
|
|
873
1020
|
bodyLog(`${session.log.all.length} events`);
|
|
874
1021
|
bodyLog(`ctx ${ctx}`);
|
|
875
|
-
|
|
876
|
-
rl.prompt();
|
|
1022
|
+
input.prompt();
|
|
877
1023
|
});
|
|
878
1024
|
return;
|
|
879
1025
|
}
|
|
880
1026
|
if (trimmed === "exit" || trimmed === "") {
|
|
881
|
-
|
|
1027
|
+
input.close();
|
|
882
1028
|
return;
|
|
883
1029
|
}
|
|
1030
|
+
// v2c: a turn submitted while another runs waits on the chain — the
|
|
1031
|
+
// live count rides the status bar (+N queued).
|
|
1032
|
+
queued += 1;
|
|
1033
|
+
chain = chain.then(() => turn(line));
|
|
1034
|
+
};
|
|
1035
|
+
input.onLine((line) => {
|
|
884
1036
|
if (!replReady) {
|
|
885
1037
|
queuedLines.push(line);
|
|
886
1038
|
return;
|
|
887
1039
|
}
|
|
888
|
-
|
|
1040
|
+
dispatch(line);
|
|
889
1041
|
});
|
|
890
1042
|
// Recovery first: a session with a dangling pause or uncertain
|
|
891
1043
|
// executions must resolve them BEFORE the REPL accepts new turns —
|
|
892
1044
|
// otherwise the interrupted run dangles while a new one starts.
|
|
893
1045
|
// 八: the startup resume is bound to currentRun — Ctrl+C during it
|
|
894
1046
|
// aborts the recovery, exactly like the interactive turns.
|
|
895
|
-
await resolveUncertains(session,
|
|
1047
|
+
await resolveUncertains(session, input, () => cancelled);
|
|
896
1048
|
if (!cancelled) {
|
|
897
1049
|
const recoveryRun = session.resume();
|
|
898
1050
|
currentRun = recoveryRun;
|
|
899
1051
|
turnNo += 1;
|
|
900
|
-
const last = await consumeRun(session, recoveryRun,
|
|
1052
|
+
const last = await consumeRun(session, recoveryRun, input, turnNo, lastToolRef, faux, liveInput, lastThinking, statusCb);
|
|
901
1053
|
currentRun = null;
|
|
902
|
-
failOnFauxExhaustion(last, faux,
|
|
1054
|
+
failOnFauxExhaustion(last, faux, input);
|
|
903
1055
|
}
|
|
904
1056
|
if (cancelled) {
|
|
905
|
-
|
|
906
|
-
await
|
|
1057
|
+
input.close();
|
|
1058
|
+
await input.closed;
|
|
907
1059
|
return;
|
|
908
1060
|
}
|
|
909
1061
|
// The REPL is ready: replay anything that arrived during recovery.
|
|
910
1062
|
replReady = true;
|
|
1063
|
+
// v2c: dispatch SYNCHRONOUSLY — each call appends its segment to the
|
|
1064
|
+
// chain variable; the final `await chain` then covers every replayed
|
|
1065
|
+
// turn. A chain.then(() => dispatch()) indirection would capture the
|
|
1066
|
+
// chain BEFORE the appends and the replayed turns would never be
|
|
1067
|
+
// awaited (the F-group regression).
|
|
911
1068
|
for (const line of queuedLines) {
|
|
912
|
-
|
|
1069
|
+
dispatch(line);
|
|
913
1070
|
}
|
|
914
1071
|
queuedLines.length = 0;
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
await new Promise((resolve) => rl.on("close", () => resolve()));
|
|
1072
|
+
input.prompt();
|
|
1073
|
+
await input.closed;
|
|
918
1074
|
await chain; // never exit while a turn is in flight
|
|
919
1075
|
}
|
|
920
1076
|
/**
|
|
@@ -924,8 +1080,7 @@ async function chat(session, faux) {
|
|
|
924
1080
|
* E 组: SIGINT aborts the run being resumed; every exit path closes the
|
|
925
1081
|
* session store so no lock is left behind.
|
|
926
1082
|
*/
|
|
927
|
-
async function resume(session, prompt, faux) {
|
|
928
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1083
|
+
async function resume(session, prompt, faux, input) {
|
|
929
1084
|
let currentRun = null;
|
|
930
1085
|
let cancelled = false;
|
|
931
1086
|
let turnNo = 0;
|
|
@@ -938,19 +1093,18 @@ async function resume(session, prompt, faux) {
|
|
|
938
1093
|
const st = renderStatusLine(turnNo, u, ctx, faux);
|
|
939
1094
|
dock.setStatus(st === null ? `${session.id} · ${agentModel}` : `${session.id} · ${agentModel} · ${st}`);
|
|
940
1095
|
};
|
|
941
|
-
dock.bindInput(() => ({ line: rl.line, cursor: rl.cursor }), interactivePrompt());
|
|
942
1096
|
const withRun = async (run) => {
|
|
943
1097
|
currentRun = run;
|
|
944
1098
|
try {
|
|
945
1099
|
turnNo += 1;
|
|
946
|
-
const last = await consumeRun(session, run,
|
|
947
|
-
failOnFauxExhaustion(last, faux,
|
|
1100
|
+
const last = await consumeRun(session, run, input, turnNo, lastToolRef, faux, null, lastThinking, statusCb);
|
|
1101
|
+
failOnFauxExhaustion(last, faux, input);
|
|
948
1102
|
}
|
|
949
1103
|
finally {
|
|
950
1104
|
currentRun = null;
|
|
951
1105
|
}
|
|
952
1106
|
};
|
|
953
|
-
|
|
1107
|
+
input.onSigint(() => {
|
|
954
1108
|
if (currentRun) {
|
|
955
1109
|
// 八: Ctrl+C cancels the pending question AND the run.
|
|
956
1110
|
console.log("\n[aborting run]");
|
|
@@ -965,11 +1119,25 @@ async function resume(session, prompt, faux) {
|
|
|
965
1119
|
cancelled = true;
|
|
966
1120
|
console.log("\n[exit requested]");
|
|
967
1121
|
pendingAsk?.();
|
|
968
|
-
|
|
1122
|
+
input.close();
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
input.onEot(() => {
|
|
1126
|
+
if (!currentRun && !cancelled && input.line() === "") {
|
|
1127
|
+
cancelled = true;
|
|
1128
|
+
console.log("\n[exit requested]");
|
|
1129
|
+
input.close();
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
input.onEscape(() => {
|
|
1133
|
+
if (currentRun) {
|
|
1134
|
+
console.log("\n[aborting run]");
|
|
1135
|
+
pendingAsk?.();
|
|
1136
|
+
currentRun.abort();
|
|
969
1137
|
}
|
|
970
1138
|
});
|
|
971
1139
|
try {
|
|
972
|
-
await resolveUncertains(session,
|
|
1140
|
+
await resolveUncertains(session, input, () => cancelled);
|
|
973
1141
|
if (!cancelled) {
|
|
974
1142
|
await withRun(session.resume());
|
|
975
1143
|
if (prompt !== undefined && prompt !== "") {
|
|
@@ -978,7 +1146,7 @@ async function resume(session, prompt, faux) {
|
|
|
978
1146
|
}
|
|
979
1147
|
}
|
|
980
1148
|
finally {
|
|
981
|
-
|
|
1149
|
+
input.close();
|
|
982
1150
|
}
|
|
983
1151
|
}
|
|
984
1152
|
async function main() {
|
|
@@ -987,6 +1155,11 @@ async function main() {
|
|
|
987
1155
|
// exit non-zero, never masquerade as a successful provider run.
|
|
988
1156
|
const faux = process.env.ANTHROPIC_API_KEY === undefined && process.env.OPENAI_API_KEY === undefined;
|
|
989
1157
|
let agent;
|
|
1158
|
+
// v2c: ONE input source per process — the raw-mode editor on a TTY
|
|
1159
|
+
// (entered here, dock-bound, trusted before any extension loads),
|
|
1160
|
+
// readline elsewhere. The trust question, chat, and resume all read
|
|
1161
|
+
// through it; main's finally closes it on every exit path.
|
|
1162
|
+
const input = makeLineInput();
|
|
990
1163
|
try {
|
|
991
1164
|
switch (command) {
|
|
992
1165
|
case "chat": {
|
|
@@ -996,11 +1169,11 @@ async function main() {
|
|
|
996
1169
|
dock.enter();
|
|
997
1170
|
// E 区: a resumed session continues the script at its durable
|
|
998
1171
|
// position — never restarts it (fauxSkip).
|
|
999
|
-
const agent = await makeAgent(fauxSkip(id));
|
|
1172
|
+
const agent = await makeAgent(fauxSkip(id), input);
|
|
1000
1173
|
const session = await agent.session({ id });
|
|
1001
1174
|
bodyLog(`session ${id}\n`);
|
|
1002
1175
|
extensionsBanner();
|
|
1003
|
-
await chat(session, faux);
|
|
1176
|
+
await chat(session, faux, input);
|
|
1004
1177
|
break;
|
|
1005
1178
|
}
|
|
1006
1179
|
case "resume": {
|
|
@@ -1012,9 +1185,9 @@ async function main() {
|
|
|
1012
1185
|
// (argv = [node, script, resume, id, prompt?]).
|
|
1013
1186
|
const prompt = process.argv[4];
|
|
1014
1187
|
dock.enter();
|
|
1015
|
-
const agent = await makeAgent(fauxSkip(arg));
|
|
1188
|
+
const agent = await makeAgent(fauxSkip(arg), input);
|
|
1016
1189
|
const session = await agent.session({ id: arg });
|
|
1017
|
-
await resume(session, prompt, faux);
|
|
1190
|
+
await resume(session, prompt, faux, input);
|
|
1018
1191
|
break;
|
|
1019
1192
|
}
|
|
1020
1193
|
case "sessions": {
|
|
@@ -1045,12 +1218,13 @@ async function main() {
|
|
|
1045
1218
|
const session = await agent.session({ id });
|
|
1046
1219
|
bodyLog(`session ${id}\n`);
|
|
1047
1220
|
extensionsBanner();
|
|
1048
|
-
await chat(session, faux);
|
|
1221
|
+
await chat(session, faux, input);
|
|
1049
1222
|
break;
|
|
1050
1223
|
}
|
|
1051
1224
|
}
|
|
1052
1225
|
}
|
|
1053
1226
|
finally {
|
|
1227
|
+
input.close();
|
|
1054
1228
|
// E 组: every normal and abnormal exit releases the fds and writer
|
|
1055
1229
|
// locks — no lock file is left behind.
|
|
1056
1230
|
agent?.close();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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,12 +18,12 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-core": "0.1.
|
|
22
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
23
|
-
"@vincemakes/kiso-provider-anthropic": "0.1.
|
|
24
|
-
"@vincemakes/kiso-provider-openai": "0.1.
|
|
25
|
-
"@vincemakes/kiso-runtime": "0.1.
|
|
26
|
-
"@vincemakes/kiso-tools-node": "0.1.
|
|
21
|
+
"@vincemakes/kiso-core": "0.1.14",
|
|
22
|
+
"@vincemakes/kiso-evals": "0.1.14",
|
|
23
|
+
"@vincemakes/kiso-provider-anthropic": "0.1.14",
|
|
24
|
+
"@vincemakes/kiso-provider-openai": "0.1.14",
|
|
25
|
+
"@vincemakes/kiso-runtime": "0.1.14",
|
|
26
|
+
"@vincemakes/kiso-tools-node": "0.1.14"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^26.1.2",
|