@illuminis/comprism 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +281 -0
- package/out/agent/command.d.ts +86 -0
- package/out/agent/command.js +259 -0
- package/out/agent/render.d.ts +97 -0
- package/out/agent/render.js +255 -0
- package/out/agent/session.d.ts +175 -0
- package/out/agent/session.js +573 -0
- package/out/commands/ask.d.ts +1 -0
- package/out/commands/ask.js +146 -0
- package/out/commands/codemap.d.ts +2 -0
- package/out/commands/codemap.js +151 -0
- package/out/commands/commands-thin.d.ts +39 -0
- package/out/commands/commands-thin.js +182 -0
- package/out/commands/install.d.ts +163 -0
- package/out/commands/install.js +543 -0
- package/out/commands/keys.d.ts +55 -0
- package/out/commands/keys.js +344 -0
- package/out/commands/login.d.ts +9 -0
- package/out/commands/login.js +384 -0
- package/out/commands/repl.d.ts +1 -0
- package/out/commands/repl.js +752 -0
- package/out/commands/settings.d.ts +21 -0
- package/out/commands/settings.js +244 -0
- package/out/commands/welcome.d.ts +1 -0
- package/out/commands/welcome.js +196 -0
- package/out/executor/documents.d.ts +40 -0
- package/out/executor/documents.js +170 -0
- package/out/executor/files.d.ts +2 -0
- package/out/executor/files.js +360 -0
- package/out/executor/git.d.ts +48 -0
- package/out/executor/git.js +132 -0
- package/out/executor/hooks.d.ts +67 -0
- package/out/executor/hooks.js +247 -0
- package/out/executor/index.d.ts +29 -0
- package/out/executor/index.js +221 -0
- package/out/executor/notebook.d.ts +2 -0
- package/out/executor/notebook.js +147 -0
- package/out/executor/paths.d.ts +15 -0
- package/out/executor/paths.js +126 -0
- package/out/executor/shell.d.ts +41 -0
- package/out/executor/shell.js +336 -0
- package/out/graph/build.d.ts +45 -0
- package/out/graph/build.js +91 -0
- package/out/graph/facts.d.ts +47 -0
- package/out/graph/facts.js +12 -0
- package/out/graph/files.d.ts +45 -0
- package/out/graph/files.js +207 -0
- package/out/graph/read-locales.d.ts +29 -0
- package/out/graph/read-locales.js +246 -0
- package/out/graph/read-python.d.ts +11 -0
- package/out/graph/read-python.js +115 -0
- package/out/graph/read-typescript.d.ts +16 -0
- package/out/graph/read-typescript.js +292 -0
- package/out/graph/sync.d.ts +66 -0
- package/out/graph/sync.js +242 -0
- package/out/lib/attach.d.ts +62 -0
- package/out/lib/attach.js +228 -0
- package/out/lib/config.d.ts +93 -0
- package/out/lib/config.js +198 -0
- package/out/lib/connection.d.ts +73 -0
- package/out/lib/connection.js +188 -0
- package/out/lib/gateway.d.ts +239 -0
- package/out/lib/gateway.js +171 -0
- package/out/lib/prompt.d.ts +34 -0
- package/out/lib/prompt.js +108 -0
- package/out/lib/types.d.ts +417 -0
- package/out/lib/types.js +21 -0
- package/out/lib/ui.d.ts +114 -0
- package/out/lib/ui.js +265 -0
- package/out/lib/version.d.ts +24 -0
- package/out/lib/version.js +27 -0
- package/out/lib/voice.d.ts +50 -0
- package/out/lib/voice.js +218 -0
- package/out/postinstall.d.ts +2 -0
- package/out/postinstall.js +92 -0
- package/out/thin.d.ts +2 -0
- package/out/thin.js +259 -0
- package/package.json +101 -0
- package/scripts/read_python.py +270 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a coding agent looks like in a terminal.
|
|
3
|
+
*
|
|
4
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
|
|
5
|
+
* items A6 (a real coding agent in the terminal), I8 (a terminal panel, real
|
|
6
|
+
* output, watchable) and I3 (a live action timeline).
|
|
7
|
+
*
|
|
8
|
+
* A terminal has no panels, so the whole interface is a stream of lines and the
|
|
9
|
+
* scrollback IS the record. That constrains everything here:
|
|
10
|
+
*
|
|
11
|
+
* **Every line has to make sense on its own, later.** Somebody scrolls back
|
|
12
|
+
* through a twenty minute run looking for the moment it went wrong. A line that
|
|
13
|
+
* only means something next to the line above it has failed them.
|
|
14
|
+
*
|
|
15
|
+
* **Nothing is redrawn or erased.** Spinners that rewrite their own line leave
|
|
16
|
+
* nothing behind, and a terminal being piped to a file fills with escape codes.
|
|
17
|
+
* What is printed stays printed.
|
|
18
|
+
*
|
|
19
|
+
* **Color is never the only signal.** Terminals get piped, logged and read by
|
|
20
|
+
* people who cannot separate red from green, so every state carries a word or a
|
|
21
|
+
* symbol as well as a color.
|
|
22
|
+
*/
|
|
23
|
+
export declare const dim: (t: string) => string;
|
|
24
|
+
export declare const bold: (t: string) => string;
|
|
25
|
+
export declare const red: (t: string) => string;
|
|
26
|
+
export declare const green: (t: string) => string;
|
|
27
|
+
export declare const yellow: (t: string) => string;
|
|
28
|
+
export declare const gray: (t: string) => string;
|
|
29
|
+
/** Money, at the precision a person can act on.
|
|
30
|
+
*
|
|
31
|
+
* Four decimal places below a cent, because a coding step often costs less
|
|
32
|
+
* than one and rounding to $0.00 would show the cheapest work, which is what
|
|
33
|
+
* this product is best at, as free. */
|
|
34
|
+
export declare function money(usd: number | null | undefined): string;
|
|
35
|
+
export declare function duration(ms: number | null | undefined): string;
|
|
36
|
+
/** One action, as it happens. A word as well as a color, so this still reads
|
|
37
|
+
* when piped to a file. */
|
|
38
|
+
export declare function action(name: string, target: string, ok: boolean, ms?: number): string;
|
|
39
|
+
/** The share of a step's context the provider served from its cache, or null
|
|
40
|
+
* when it reported no counts. Anthropic counts cached tokens OUTSIDE the input
|
|
41
|
+
* figure, so the whole context is the three counts added together. */
|
|
42
|
+
export declare function reusePct(tokensIn: number | null | undefined, cacheRead: number | null | undefined, cacheWrite: number | null | undefined): number | null;
|
|
43
|
+
/** One model call: which model, what it cost, and how much of its context the
|
|
44
|
+
* provider had already seen. The last figure is the one study 005 found
|
|
45
|
+
* missing from our receipt: without it nobody could tell a warm step from a
|
|
46
|
+
* cold one, and every step was cold. */
|
|
47
|
+
export declare function step(index: number, model: string, usd: number | null, ms: number | null, reused?: number | null): string;
|
|
48
|
+
/** The task list, reprinted whenever it changes.
|
|
49
|
+
*
|
|
50
|
+
* Reprinted rather than redrawn in place: what the agent intended at each point
|
|
51
|
+
* is worth keeping, and it is often the fastest way to see where a job went off
|
|
52
|
+
* course. */
|
|
53
|
+
export declare function todos(items: Array<{
|
|
54
|
+
content: string;
|
|
55
|
+
status: string;
|
|
56
|
+
}>): string;
|
|
57
|
+
/** A change, before it is made.
|
|
58
|
+
*
|
|
59
|
+
* Full text for a small edit and a summary for a large one. Pasting four
|
|
60
|
+
* hundred lines into a terminal is not review, it is a wall somebody presses
|
|
61
|
+
* through, and an approval people press through protects nobody. */
|
|
62
|
+
export declare function diff(name: string, input: Record<string, unknown>): string;
|
|
63
|
+
/** How the job ended, in the words a person reads. */
|
|
64
|
+
export declare function outcome(o: string, detail: string | null | undefined): string;
|
|
65
|
+
/** One model's share of a job, as our server reported it. */
|
|
66
|
+
export interface ModelShare {
|
|
67
|
+
model: string | null;
|
|
68
|
+
steps: number;
|
|
69
|
+
usd: number | null;
|
|
70
|
+
unpriced?: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The receipt under a finished job.
|
|
74
|
+
*
|
|
75
|
+
* ── The terminal does not compute this, and must not ──────────────────────
|
|
76
|
+
*
|
|
77
|
+
* This used to assemble its own sentence from four loose numbers, so the
|
|
78
|
+
* terminal quoted a different receipt from the browser and the desktop app for
|
|
79
|
+
* the same job: no models, no comparison model and no saving, which is the
|
|
80
|
+
* entire claim the product is sold on.
|
|
81
|
+
*
|
|
82
|
+
* Our server owns the receipt. It holds the price list, the comparison model
|
|
83
|
+
* and the arithmetic, it writes the wording once, and every client shows that
|
|
84
|
+
* wording. All this function does is make it read well in a terminal: indent
|
|
85
|
+
* it, quieten it, and turn the address in it into something clickable. That is
|
|
86
|
+
* formatting, which belongs to the client, and it is the only part that does.
|
|
87
|
+
*/
|
|
88
|
+
export declare function receipt(text: string, models?: ModelShare[], testsPassed?: boolean): string;
|
|
89
|
+
/**
|
|
90
|
+
* Plain text, from a model that writes markdown.
|
|
91
|
+
*
|
|
92
|
+
* A terminal has no bold and no bullet glyphs, so `**like this**` arrives on
|
|
93
|
+
* screen as literal asterisks and a heading looks like a typing mistake. The
|
|
94
|
+
* emphasis is real, so it is kept as terminal bold rather than thrown away;
|
|
95
|
+
* everything that only exists to be rendered by a browser is removed.
|
|
96
|
+
*/
|
|
97
|
+
export declare function plain(text: string): string;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What a coding agent looks like in a terminal.
|
|
4
|
+
*
|
|
5
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
|
|
6
|
+
* items A6 (a real coding agent in the terminal), I8 (a terminal panel, real
|
|
7
|
+
* output, watchable) and I3 (a live action timeline).
|
|
8
|
+
*
|
|
9
|
+
* A terminal has no panels, so the whole interface is a stream of lines and the
|
|
10
|
+
* scrollback IS the record. That constrains everything here:
|
|
11
|
+
*
|
|
12
|
+
* **Every line has to make sense on its own, later.** Somebody scrolls back
|
|
13
|
+
* through a twenty minute run looking for the moment it went wrong. A line that
|
|
14
|
+
* only means something next to the line above it has failed them.
|
|
15
|
+
*
|
|
16
|
+
* **Nothing is redrawn or erased.** Spinners that rewrite their own line leave
|
|
17
|
+
* nothing behind, and a terminal being piped to a file fills with escape codes.
|
|
18
|
+
* What is printed stays printed.
|
|
19
|
+
*
|
|
20
|
+
* **Color is never the only signal.** Terminals get piped, logged and read by
|
|
21
|
+
* people who cannot separate red from green, so every state carries a word or a
|
|
22
|
+
* symbol as well as a color.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.gray = exports.yellow = exports.green = exports.red = exports.bold = exports.dim = void 0;
|
|
26
|
+
exports.money = money;
|
|
27
|
+
exports.duration = duration;
|
|
28
|
+
exports.action = action;
|
|
29
|
+
exports.reusePct = reusePct;
|
|
30
|
+
exports.step = step;
|
|
31
|
+
exports.todos = todos;
|
|
32
|
+
exports.diff = diff;
|
|
33
|
+
exports.outcome = outcome;
|
|
34
|
+
exports.receipt = receipt;
|
|
35
|
+
exports.plain = plain;
|
|
36
|
+
/** Built from a character code rather than written as a literal escape, so this
|
|
37
|
+
* file contains no control characters and stays safe to grep, diff and paste. */
|
|
38
|
+
const CSI = String.fromCharCode(27) + "[";
|
|
39
|
+
const supportsColour = process.stdout.isTTY && process.env.NO_COLOR === undefined;
|
|
40
|
+
function paint(code, text) {
|
|
41
|
+
return supportsColour ? CSI + code + "m" + text + CSI + "0m" : text;
|
|
42
|
+
}
|
|
43
|
+
const dim = (t) => paint("2", t);
|
|
44
|
+
exports.dim = dim;
|
|
45
|
+
const bold = (t) => paint("1", t);
|
|
46
|
+
exports.bold = bold;
|
|
47
|
+
const red = (t) => paint("31", t);
|
|
48
|
+
exports.red = red;
|
|
49
|
+
const green = (t) => paint("32", t);
|
|
50
|
+
exports.green = green;
|
|
51
|
+
const yellow = (t) => paint("33", t);
|
|
52
|
+
exports.yellow = yellow;
|
|
53
|
+
const gray = (t) => paint("90", t);
|
|
54
|
+
exports.gray = gray;
|
|
55
|
+
/** Money, at the precision a person can act on.
|
|
56
|
+
*
|
|
57
|
+
* Four decimal places below a cent, because a coding step often costs less
|
|
58
|
+
* than one and rounding to $0.00 would show the cheapest work, which is what
|
|
59
|
+
* this product is best at, as free. */
|
|
60
|
+
function money(usd) {
|
|
61
|
+
if (usd === null || usd === undefined)
|
|
62
|
+
return "unpriced";
|
|
63
|
+
return usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;
|
|
64
|
+
}
|
|
65
|
+
function duration(ms) {
|
|
66
|
+
if (!ms)
|
|
67
|
+
return "";
|
|
68
|
+
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
69
|
+
}
|
|
70
|
+
/** One action, as it happens. A word as well as a color, so this still reads
|
|
71
|
+
* when piped to a file. */
|
|
72
|
+
function action(name, target, ok, ms) {
|
|
73
|
+
const mark = ok ? (0, exports.green)("ok ") : (0, exports.red)("fail");
|
|
74
|
+
const tail = ms ? (0, exports.dim)(` ${duration(ms)}`) : "";
|
|
75
|
+
return ` ${mark} ${(0, exports.bold)(name)} ${(0, exports.dim)(target)}${tail}`;
|
|
76
|
+
}
|
|
77
|
+
/** The share of a step's context the provider served from its cache, or null
|
|
78
|
+
* when it reported no counts. Anthropic counts cached tokens OUTSIDE the input
|
|
79
|
+
* figure, so the whole context is the three counts added together. */
|
|
80
|
+
function reusePct(tokensIn, cacheRead, cacheWrite) {
|
|
81
|
+
if (cacheRead === null || cacheRead === undefined)
|
|
82
|
+
return null;
|
|
83
|
+
const whole = (tokensIn ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
|
|
84
|
+
if (!whole)
|
|
85
|
+
return null;
|
|
86
|
+
return Math.round((cacheRead / whole) * 100);
|
|
87
|
+
}
|
|
88
|
+
/** One model call: which model, what it cost, and how much of its context the
|
|
89
|
+
* provider had already seen. The last figure is the one study 005 found
|
|
90
|
+
* missing from our receipt: without it nobody could tell a warm step from a
|
|
91
|
+
* cold one, and every step was cold. */
|
|
92
|
+
function step(index, model, usd, ms, reused = null) {
|
|
93
|
+
const warm = reused === null ? "" : ` cached ${reused}%`;
|
|
94
|
+
return (0, exports.dim)(` step ${index} ${model || "?"} ${money(usd)} ${duration(ms)}${warm}`);
|
|
95
|
+
}
|
|
96
|
+
/** The task list, reprinted whenever it changes.
|
|
97
|
+
*
|
|
98
|
+
* Reprinted rather than redrawn in place: what the agent intended at each point
|
|
99
|
+
* is worth keeping, and it is often the fastest way to see where a job went off
|
|
100
|
+
* course. */
|
|
101
|
+
function todos(items) {
|
|
102
|
+
if (!items.length)
|
|
103
|
+
return "";
|
|
104
|
+
const rows = items.map((t) => {
|
|
105
|
+
const mark = t.status === "completed" ? (0, exports.green)("done")
|
|
106
|
+
: t.status === "in_progress" ? (0, exports.yellow)("now ") : (0, exports.dim)("todo");
|
|
107
|
+
const text = t.status === "completed" ? (0, exports.dim)(t.content) : t.content;
|
|
108
|
+
return ` ${mark} ${text}`;
|
|
109
|
+
});
|
|
110
|
+
return `\n ${(0, exports.bold)("Plan")}\n${rows.join("\n")}\n`;
|
|
111
|
+
}
|
|
112
|
+
/** A change, before it is made.
|
|
113
|
+
*
|
|
114
|
+
* Full text for a small edit and a summary for a large one. Pasting four
|
|
115
|
+
* hundred lines into a terminal is not review, it is a wall somebody presses
|
|
116
|
+
* through, and an approval people press through protects nobody. */
|
|
117
|
+
function diff(name, input) {
|
|
118
|
+
const target = String(input.path ?? input.to ?? "");
|
|
119
|
+
const head = `\n ${(0, exports.bold)(name)} ${target}\n`;
|
|
120
|
+
const show = (from, to) => {
|
|
121
|
+
const before = from.split("\n"), after = to.split("\n");
|
|
122
|
+
if (before.length + after.length > 60) {
|
|
123
|
+
return ` ${(0, exports.dim)(`${before.length} lines replaced by ${after.length} lines`)}\n`;
|
|
124
|
+
}
|
|
125
|
+
return [
|
|
126
|
+
...before.map((l) => ` ${(0, exports.red)("-")} ${l}`),
|
|
127
|
+
...after.map((l) => ` ${(0, exports.green)("+")} ${l}`),
|
|
128
|
+
].join("\n") + "\n";
|
|
129
|
+
};
|
|
130
|
+
if (name === "edit_file") {
|
|
131
|
+
return head + show(String(input.old_text ?? ""), String(input.new_text ?? ""));
|
|
132
|
+
}
|
|
133
|
+
if (name === "multi_edit") {
|
|
134
|
+
const edits = input.edits ?? [];
|
|
135
|
+
return head + edits.map((e) => show(e.old_text, e.new_text)).join(` ${(0, exports.dim)("--")}\n`);
|
|
136
|
+
}
|
|
137
|
+
if (name === "write_file") {
|
|
138
|
+
const bodyText = String(input.content ?? "");
|
|
139
|
+
const lines = bodyText.split("\n");
|
|
140
|
+
if (lines.length <= 40) {
|
|
141
|
+
return head + lines.map((l) => ` ${(0, exports.green)("+")} ${l}`).join("\n") + "\n";
|
|
142
|
+
}
|
|
143
|
+
return head + ` ${(0, exports.dim)(`${lines.length} lines, ${bodyText.length.toLocaleString()} characters`)}\n`;
|
|
144
|
+
}
|
|
145
|
+
if (name === "run_command" || name === "run_background") {
|
|
146
|
+
return `\n ${(0, exports.bold)("run")} ${String(input.command ?? "")}\n`;
|
|
147
|
+
}
|
|
148
|
+
if (name === "install_dependency") {
|
|
149
|
+
return `\n ${(0, exports.bold)("install")} ${String(input.package ?? "")}`
|
|
150
|
+
+ ` ${(0, exports.dim)("(this changes the lockfile for everyone who works here)")}\n`;
|
|
151
|
+
}
|
|
152
|
+
if (name === "delete_file") {
|
|
153
|
+
return head + ` ${(0, exports.dim)("A copy is kept, so this can be undone.")}\n`;
|
|
154
|
+
}
|
|
155
|
+
return head;
|
|
156
|
+
}
|
|
157
|
+
/** How the job ended, in the words a person reads. */
|
|
158
|
+
function outcome(o, detail) {
|
|
159
|
+
const word = {
|
|
160
|
+
finished: (0, exports.green)("Done"),
|
|
161
|
+
cancelled: (0, exports.yellow)("Stopped"),
|
|
162
|
+
step_limit: (0, exports.yellow)("Stopped after too many steps"),
|
|
163
|
+
spend_limit: (0, exports.yellow)("Stopped at the spending limit"),
|
|
164
|
+
failed: (0, exports.red)("Could not finish"),
|
|
165
|
+
refused: (0, exports.red)("Refused"),
|
|
166
|
+
};
|
|
167
|
+
return `\n${word[o] ?? o}${detail ? (0, exports.dim)(` ${detail}`) : ""}\n`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A link a person can click, where the terminal supports one.
|
|
171
|
+
*
|
|
172
|
+
* Modern terminals understand an escape that carries an address behind a piece
|
|
173
|
+
* of text; older ones print it as ordinary characters, so the address is shown
|
|
174
|
+
* in words there instead. Never a bare escape with no fallback: a receipt whose
|
|
175
|
+
* proof is unreachable is not proof.
|
|
176
|
+
*/
|
|
177
|
+
function link(text, url) {
|
|
178
|
+
if (!process.stdout.isTTY)
|
|
179
|
+
return `${text} (${url})`;
|
|
180
|
+
return `]8;;${url}${text}]8;;`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The receipt under a finished job.
|
|
184
|
+
*
|
|
185
|
+
* ── The terminal does not compute this, and must not ──────────────────────
|
|
186
|
+
*
|
|
187
|
+
* This used to assemble its own sentence from four loose numbers, so the
|
|
188
|
+
* terminal quoted a different receipt from the browser and the desktop app for
|
|
189
|
+
* the same job: no models, no comparison model and no saving, which is the
|
|
190
|
+
* entire claim the product is sold on.
|
|
191
|
+
*
|
|
192
|
+
* Our server owns the receipt. It holds the price list, the comparison model
|
|
193
|
+
* and the arithmetic, it writes the wording once, and every client shows that
|
|
194
|
+
* wording. All this function does is make it read well in a terminal: indent
|
|
195
|
+
* it, quieten it, and turn the address in it into something clickable. That is
|
|
196
|
+
* formatting, which belongs to the client, and it is the only part that does.
|
|
197
|
+
*/
|
|
198
|
+
function receipt(text, models = [], testsPassed) {
|
|
199
|
+
const lines = [];
|
|
200
|
+
for (const line of String(text || "").split("\n")) {
|
|
201
|
+
if (!line.trim())
|
|
202
|
+
continue;
|
|
203
|
+
// The address our server put in the sentence becomes a real link, and the
|
|
204
|
+
// words around it are left exactly as they were written.
|
|
205
|
+
const shown = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => link(String(label), String(url)));
|
|
206
|
+
lines.push((0, exports.dim)(` ${shown}`));
|
|
207
|
+
}
|
|
208
|
+
// Which models did the work, cheapest first, because the cheap ones doing
|
|
209
|
+
// most of the steps IS the mechanism and a reader should see it at a glance.
|
|
210
|
+
const priced = models.filter((m) => m.model);
|
|
211
|
+
if (priced.length > 1) {
|
|
212
|
+
const order = [...priced].sort((a, b) => (a.usd ?? 0) - (b.usd ?? 0));
|
|
213
|
+
const width = Math.max(...order.map((m) => String(m.model).length));
|
|
214
|
+
for (const m of order) {
|
|
215
|
+
const steps = `${m.steps} step${m.steps === 1 ? "" : "s"}`;
|
|
216
|
+
const cost = m.usd === null || m.usd === undefined
|
|
217
|
+
// Never a zero standing in for a price nobody has. A step we could not
|
|
218
|
+
// price is said so, because a total that hides one is a smaller total.
|
|
219
|
+
? "not priced"
|
|
220
|
+
: money(m.usd);
|
|
221
|
+
lines.push((0, exports.dim)(` ${String(m.model).padEnd(width)} ${steps.padEnd(9)} ${cost}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (testsPassed !== undefined) {
|
|
225
|
+
lines.push(testsPassed ? (0, exports.green)(" tests passed") : (0, exports.red)(" tests failed"));
|
|
226
|
+
}
|
|
227
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Plain text, from a model that writes markdown.
|
|
231
|
+
*
|
|
232
|
+
* A terminal has no bold and no bullet glyphs, so `**like this**` arrives on
|
|
233
|
+
* screen as literal asterisks and a heading looks like a typing mistake. The
|
|
234
|
+
* emphasis is real, so it is kept as terminal bold rather than thrown away;
|
|
235
|
+
* everything that only exists to be rendered by a browser is removed.
|
|
236
|
+
*/
|
|
237
|
+
function plain(text) {
|
|
238
|
+
return text
|
|
239
|
+
// Fenced code keeps its content and loses its fence markers.
|
|
240
|
+
.replace(/^```[a-zA-Z0-9]*\n?/gm, '')
|
|
241
|
+
.replace(/^```$/gm, '')
|
|
242
|
+
// Headings become their own words, emphasized.
|
|
243
|
+
.replace(/^#{1,6}\s+(.+)$/gm, (_m, t) => (0, exports.bold)(String(t)))
|
|
244
|
+
// Bold and italic become the terminal's own emphasis.
|
|
245
|
+
.replace(/\*\*([^*]+)\*\*/g, (_m, t) => (0, exports.bold)(String(t)))
|
|
246
|
+
.replace(/__([^_]+)__/g, (_m, t) => (0, exports.bold)(String(t)))
|
|
247
|
+
.replace(/(^|[^*])\*([^*\n]+)\*/g, (_m, pre, t) => pre + String(t))
|
|
248
|
+
// A bullet is a bullet, not an asterisk.
|
|
249
|
+
.replace(/^\s*[*-]\s+/gm, ' . ')
|
|
250
|
+
// Inline code keeps its text; the backticks were never for a terminal.
|
|
251
|
+
.replace(/`([^`]+)`/g, (_m, t) => String(t))
|
|
252
|
+
// A link reads as its words, with the address after it.
|
|
253
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => `${t} (${u})`)
|
|
254
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
255
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import * as ui from "./render";
|
|
2
|
+
export interface SessionOptions {
|
|
3
|
+
/** Where the workspace lives, and the URL of the server that owns it. */
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
/** The workspace credential the device flow minted. Never a decodable token. */
|
|
6
|
+
credential: string;
|
|
7
|
+
root: string;
|
|
8
|
+
/** read_only, approve_writes, auto_edit or full_auto. Named, not numbered. */
|
|
9
|
+
permissionMode?: string;
|
|
10
|
+
/** Investigate and propose, change nothing. */
|
|
11
|
+
planOnly?: boolean;
|
|
12
|
+
/** The plan was read and approved, so the files in it are not asked about
|
|
13
|
+
* again. The server checks this against what the account permits; sending it
|
|
14
|
+
* is a statement of what the person said, not a grant. */
|
|
15
|
+
planApproved?: boolean;
|
|
16
|
+
/** A model the person named. Absent means the router chooses, per step, which
|
|
17
|
+
* is the point of the product. On its own this TELLS the router what is
|
|
18
|
+
* running, so the receipt can price the saving against it; the router is
|
|
19
|
+
* still free to move up. Set `pinModel` to be given that model and no other. */
|
|
20
|
+
model?: string;
|
|
21
|
+
/** Run `model` and nothing else, whatever the router would prefer. Off by
|
|
22
|
+
* default. For a person who wants to decide for themselves, and for the
|
|
23
|
+
* corpus study, which has to measure each model as itself. */
|
|
24
|
+
pinModel?: boolean;
|
|
25
|
+
maxSpendUsd?: number;
|
|
26
|
+
/** Commands the person has approved in advance, matched exactly.
|
|
27
|
+
*
|
|
28
|
+
* The one thing a run with nobody watching cannot otherwise do is verify its
|
|
29
|
+
* own work: every mode asks before running an arbitrary command, correctly,
|
|
30
|
+
* and there is nobody there to answer. Naming the command up front is how
|
|
31
|
+
* that is settled without widening anything, which is why it is a list of
|
|
32
|
+
* exact commands and not a flag that turns the check off. */
|
|
33
|
+
approvedCommands?: string[];
|
|
34
|
+
/** Files already uploaded and waiting, to be read before this question.
|
|
35
|
+
* Ids only: the server holds the text for half an hour and nothing here has
|
|
36
|
+
* ever seen the contents. */
|
|
37
|
+
attachmentIds?: string[];
|
|
38
|
+
/** Nobody is at the keyboard. Approvals cannot be asked for, so the
|
|
39
|
+
* permission mode has to have settled them in advance. */
|
|
40
|
+
headless?: boolean;
|
|
41
|
+
/** Ask the person a yes or no question, using a keyboard somebody else
|
|
42
|
+
* already owns.
|
|
43
|
+
*
|
|
44
|
+
* A session that is reading its own prompt cannot also let this class open a
|
|
45
|
+
* reader: the two divide the typing between them and the second gets
|
|
46
|
+
* nothing, so every approval hung or came back refused. Whoever started the
|
|
47
|
+
* process owns the keyboard and lends it here. Absent, this class opens its
|
|
48
|
+
* own reader as before, which is the standalone command's case. */
|
|
49
|
+
confirm?: (question: string) => Promise<boolean>;
|
|
50
|
+
/** Continue a job that already exists, rather than starting one. */
|
|
51
|
+
resumeJobId?: string;
|
|
52
|
+
/** The job this question follows on from. The server loads that job's
|
|
53
|
+
* conversation from the record, so a terminal with no store of its own still
|
|
54
|
+
* holds a thread. */
|
|
55
|
+
continuesJob?: string;
|
|
56
|
+
/** Go without the code map for this job. See `command.ts`. */
|
|
57
|
+
withoutMap?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface JobResult {
|
|
60
|
+
outcome: string;
|
|
61
|
+
detail?: string | null;
|
|
62
|
+
jobId?: string;
|
|
63
|
+
steps: number;
|
|
64
|
+
actions: number;
|
|
65
|
+
totalUsd: number;
|
|
66
|
+
/** What routing saved against the comparison model, or null when there is
|
|
67
|
+
* nothing to compare this job against. Never 0 in place of unknown: a saving
|
|
68
|
+
* of zero and a saving nobody can price are different answers. */
|
|
69
|
+
savedUsd: number | null;
|
|
70
|
+
/** The receipt sentence, exactly as our server wrote it. The terminal
|
|
71
|
+
* formats this and never composes its own: the price list, the comparison
|
|
72
|
+
* model and the arithmetic are all on the server, so a client that wrote its
|
|
73
|
+
* own wording would be a second answer to the same question. */
|
|
74
|
+
receipt: string;
|
|
75
|
+
/** Which models did the work, and each one's share, as the server reported
|
|
76
|
+
* it. Taken as sent, never recalculated here. */
|
|
77
|
+
models: ui.ModelShare[];
|
|
78
|
+
text: string;
|
|
79
|
+
testsPassed?: boolean;
|
|
80
|
+
/** Share of everything the job sent that the provider served from cache. */
|
|
81
|
+
reusedPct?: number | null;
|
|
82
|
+
}
|
|
83
|
+
export declare class TerminalSession {
|
|
84
|
+
private socket;
|
|
85
|
+
private readonly executor;
|
|
86
|
+
private readonly opts;
|
|
87
|
+
private readonly out;
|
|
88
|
+
private lastTodos;
|
|
89
|
+
/** The last thing it said on screen, so the ending does not say it twice.
|
|
90
|
+
* Every step prints what the model said, and the closing frame carries the
|
|
91
|
+
* final text again, so the answer to a one step job appeared twice in a row
|
|
92
|
+
* with nothing between the copies. */
|
|
93
|
+
private lastSaid;
|
|
94
|
+
private canceled;
|
|
95
|
+
constructor(opts: SessionOptions, write?: (s: string) => void);
|
|
96
|
+
/** Where the socket lives, derived from the workspace URL.
|
|
97
|
+
*
|
|
98
|
+
* Derived rather than configured: a second setting for the socket address is
|
|
99
|
+
* a second thing that can point at the wrong server, and the failure would be
|
|
100
|
+
* a job running against somebody else's workspace. */
|
|
101
|
+
/**
|
|
102
|
+
* A pass for this job's stream, from the one service.
|
|
103
|
+
*
|
|
104
|
+
* The job is authorized THERE, where everything else in the product is
|
|
105
|
+
* authorized, and this returns only permission to open the stream. Null when
|
|
106
|
+
* the service refused or could not be reached, and the caller then reports
|
|
107
|
+
* the service's own words rather than guessing at them.
|
|
108
|
+
*/
|
|
109
|
+
private pass;
|
|
110
|
+
/** Where the stream lives, derived from the workspace URL.
|
|
111
|
+
*
|
|
112
|
+
* Derived rather than configured: a second setting for the stream address is
|
|
113
|
+
* a second thing that can point at the wrong server, and the failure would be
|
|
114
|
+
* a job running against somebody else's workspace.
|
|
115
|
+
*
|
|
116
|
+
* The credential still travels because the pass is redeemed against the
|
|
117
|
+
* tenant it was issued in, and the pass is what actually authorizes the job.
|
|
118
|
+
* Without a pass this falls back to the old behavior, which is what a
|
|
119
|
+
* workspace running an older build still understands. */
|
|
120
|
+
private socketUrl;
|
|
121
|
+
/** The name the map is held under: the folder's own, which is exactly what
|
|
122
|
+
* the run frame already sends as the workspace. One expression, so the map
|
|
123
|
+
* and the job record can never be about two different projects. */
|
|
124
|
+
private project;
|
|
125
|
+
/** Why the one service would not start this job, in its own words. */
|
|
126
|
+
private refusal;
|
|
127
|
+
/** The workspace does not carry the permission question yet, so fall back. */
|
|
128
|
+
private olderWorkspace;
|
|
129
|
+
/** Set while one is actually running, so two never overlap. */
|
|
130
|
+
private remapping;
|
|
131
|
+
/**
|
|
132
|
+
* The agent just changed something on disk. Bring the map back in step.
|
|
133
|
+
*
|
|
134
|
+
* Only for actions that can change a file. A read or a search leaves the
|
|
135
|
+
* project exactly as it was, and re-reading it would be a round trip bought
|
|
136
|
+
* for nothing on the most common actions there are.
|
|
137
|
+
*
|
|
138
|
+
* The refresh itself is the ordinary one: it compares the files against what
|
|
139
|
+
* the service holds and sends back only what moved, which after one edit is
|
|
140
|
+
* one file. Debounced by a short wait, because an agent that writes four
|
|
141
|
+
* files in one step should pay for one update, not four.
|
|
142
|
+
*/
|
|
143
|
+
private noteChanged;
|
|
144
|
+
/**
|
|
145
|
+
* Put the files this job changed back into the map.
|
|
146
|
+
*
|
|
147
|
+
* Runs after the job, without being waited on: the person has their answer
|
|
148
|
+
* and a re-read of three files must not hold up their prompt. A failure here
|
|
149
|
+
* costs nothing, because the next job checks the files anyway and would read
|
|
150
|
+
* them then.
|
|
151
|
+
*/
|
|
152
|
+
private remap;
|
|
153
|
+
/** Run one request to its end. Resolves with how it went. */
|
|
154
|
+
run(request: string): Promise<JobResult>;
|
|
155
|
+
/** Stop the running job. Takes effect mid action, not at the end of the run. */
|
|
156
|
+
cancel(): void;
|
|
157
|
+
/** Something typed while it works. Joins the conversation at the next step. */
|
|
158
|
+
steer(text: string): void;
|
|
159
|
+
private announce;
|
|
160
|
+
private narrate;
|
|
161
|
+
private targetOf;
|
|
162
|
+
/** Put a change in front of the person and wait.
|
|
163
|
+
*
|
|
164
|
+
* Three cases, in order. A caller that owns the keyboard lent us one, so the
|
|
165
|
+
* question goes there. Nobody lent one and somebody is watching, so we open
|
|
166
|
+
* our own reader. Nobody is watching, so it is refused.
|
|
167
|
+
*
|
|
168
|
+
* In headless mode there is nobody to ask, so it is refused rather than
|
|
169
|
+
* assumed. Treating silence as consent would let a script approve changes
|
|
170
|
+
* nobody ever saw, which is the opposite of what a permission mode is for:
|
|
171
|
+
* a script that needs to change files says so by choosing a mode that allows
|
|
172
|
+
* it, up front. */
|
|
173
|
+
private ask;
|
|
174
|
+
private report;
|
|
175
|
+
}
|