@illuminis/comprism 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -36
- package/out/agent/command.d.ts +75 -4
- package/out/agent/command.js +220 -25
- package/out/agent/render.d.ts +17 -2
- package/out/agent/render.js +157 -14
- package/out/agent/session.d.ts +141 -2
- package/out/agent/session.js +735 -149
- package/out/commands/agents.d.ts +2 -0
- package/out/commands/agents.js +79 -0
- package/out/commands/ask.d.ts +1 -1
- package/out/commands/ask.js +78 -11
- package/out/commands/commands-thin.js +79 -17
- package/out/commands/config.d.ts +1 -0
- package/out/commands/config.js +138 -0
- package/out/commands/cost.d.ts +1 -0
- package/out/commands/cost.js +167 -0
- package/out/commands/hooks.d.ts +1 -0
- package/out/commands/hooks.js +83 -0
- package/out/commands/install.d.ts +44 -1
- package/out/commands/install.js +198 -4
- package/out/commands/instructions.d.ts +1 -0
- package/out/commands/instructions.js +113 -0
- package/out/commands/integrations.d.ts +3 -0
- package/out/commands/integrations.js +215 -0
- package/out/commands/jobs.d.ts +5 -0
- package/out/commands/jobs.js +157 -0
- package/out/commands/login.js +188 -36
- package/out/commands/memory.d.ts +3 -0
- package/out/commands/memory.js +113 -0
- package/out/commands/permissions.d.ts +1 -0
- package/out/commands/permissions.js +94 -0
- package/out/commands/plugins.d.ts +4 -0
- package/out/commands/plugins.js +192 -0
- package/out/commands/privacy.d.ts +1 -0
- package/out/commands/privacy.js +57 -0
- package/out/commands/providerKey.d.ts +32 -0
- package/out/commands/providerKey.js +108 -0
- package/out/commands/repl.d.ts +8 -1
- package/out/commands/repl.js +1207 -118
- package/out/commands/report.d.ts +39 -0
- package/out/commands/report.js +115 -0
- package/out/commands/review.d.ts +5 -0
- package/out/commands/review.js +223 -0
- package/out/commands/sessions.d.ts +23 -0
- package/out/commands/sessions.js +115 -0
- package/out/commands/settings.d.ts +3 -1
- package/out/commands/settings.js +18 -16
- package/out/commands/skills.d.ts +21 -0
- package/out/commands/skills.js +207 -0
- package/out/commands/unattended.d.ts +7 -0
- package/out/commands/unattended.js +351 -0
- package/out/commands/update.d.ts +1 -0
- package/out/commands/update.js +123 -0
- package/out/commands/worktrees.d.ts +5 -0
- package/out/commands/worktrees.js +186 -0
- package/out/executor/browser.d.ts +14 -0
- package/out/executor/browser.js +270 -0
- package/out/executor/diagnostics.d.ts +2 -0
- package/out/executor/diagnostics.js +181 -0
- package/out/executor/files.js +270 -40
- package/out/executor/git.js +42 -29
- package/out/executor/hooks.d.ts +42 -58
- package/out/executor/hooks.js +89 -182
- package/out/executor/index.d.ts +21 -5
- package/out/executor/index.js +160 -14
- package/out/executor/paths.d.ts +6 -1
- package/out/executor/paths.js +34 -6
- package/out/executor/sandbox.d.ts +40 -0
- package/out/executor/sandbox.js +299 -0
- package/out/executor/shell.d.ts +49 -4
- package/out/executor/shell.js +302 -56
- package/out/executor/toolservers.d.ts +20 -0
- package/out/executor/toolservers.js +189 -0
- package/out/executor/worktree.d.ts +9 -0
- package/out/executor/worktree.js +119 -0
- package/out/graph/read-python.js +2 -1
- package/out/lib/attach.d.ts +56 -12
- package/out/lib/attach.js +230 -63
- package/out/lib/clipboard.d.ts +23 -0
- package/out/lib/clipboard.js +182 -0
- package/out/lib/commandlist.d.ts +20 -0
- package/out/lib/commandlist.js +58 -0
- package/out/lib/decision.d.ts +22 -0
- package/out/lib/decision.js +50 -0
- package/out/lib/fingerprint.d.ts +25 -0
- package/out/lib/fingerprint.js +58 -0
- package/out/lib/gateway.d.ts +186 -2
- package/out/lib/gateway.js +59 -4
- package/out/lib/history.d.ts +24 -0
- package/out/lib/history.js +137 -0
- package/out/lib/ide.d.ts +19 -0
- package/out/lib/ide.js +131 -0
- package/out/lib/keyboard.d.ts +95 -0
- package/out/lib/keyboard.js +383 -0
- package/out/lib/machine.d.ts +21 -0
- package/out/lib/machine.js +91 -0
- package/out/lib/notify.d.ts +4 -0
- package/out/lib/notify.js +52 -0
- package/out/lib/output.d.ts +48 -0
- package/out/lib/output.js +108 -0
- package/out/lib/project-ops.d.ts +19 -0
- package/out/lib/project-ops.js +146 -0
- package/out/lib/project.d.ts +28 -0
- package/out/lib/project.js +114 -0
- package/out/lib/prompt.js +15 -2
- package/out/lib/queue.d.ts +13 -0
- package/out/lib/queue.js +116 -0
- package/out/lib/readiness.d.ts +19 -0
- package/out/lib/readiness.js +170 -1
- package/out/lib/self.d.ts +23 -0
- package/out/lib/self.js +124 -0
- package/out/lib/sessions.d.ts +19 -0
- package/out/lib/sessions.js +221 -0
- package/out/lib/stdin.d.ts +32 -0
- package/out/lib/stdin.js +117 -0
- package/out/lib/store.d.ts +40 -0
- package/out/lib/store.js +138 -0
- package/out/lib/sync.d.ts +18 -0
- package/out/lib/sync.js +81 -0
- package/out/lib/ui.d.ts +2 -4
- package/out/lib/ui.js +31 -25
- package/out/lib/voice.js +24 -0
- package/out/postinstall.js +42 -14
- package/out/providers/anthropic.d.ts +22 -0
- package/out/providers/anthropic.js +80 -0
- package/out/providers/index.d.ts +8 -0
- package/out/providers/index.js +83 -0
- package/out/providers/openai.d.ts +11 -0
- package/out/providers/openai.js +57 -0
- package/out/thin.js +609 -21
- package/package.json +9 -49
package/out/commands/repl.js
CHANGED
|
@@ -67,12 +67,20 @@ exports.runRepl = runRepl;
|
|
|
67
67
|
* session sends the person to `keys` first and will not start until one is
|
|
68
68
|
* stored. Being stopped early and told why beats being stopped late.
|
|
69
69
|
*/
|
|
70
|
+
const node_crypto_1 = require("node:crypto");
|
|
71
|
+
const path = __importStar(require("node:path"));
|
|
70
72
|
const node_readline_1 = __importDefault(require("node:readline"));
|
|
71
73
|
const connection_1 = require("../lib/connection");
|
|
72
74
|
const keys_1 = require("./keys");
|
|
73
75
|
const welcome_1 = require("./welcome");
|
|
74
76
|
const attach = __importStar(require("../lib/attach"));
|
|
77
|
+
const commandlist_1 = require("../lib/commandlist");
|
|
78
|
+
const history_1 = require("../lib/history");
|
|
79
|
+
const keyboard_1 = require("../lib/keyboard");
|
|
80
|
+
const machine_1 = require("../lib/machine");
|
|
75
81
|
const ui = __importStar(require("../lib/ui"));
|
|
82
|
+
const render_1 = require("../agent/render");
|
|
83
|
+
const shell_1 = require("../executor/shell");
|
|
76
84
|
const voice = __importStar(require("../lib/voice"));
|
|
77
85
|
const out = (s = '') => process.stdout.write(s + '\n');
|
|
78
86
|
/**
|
|
@@ -85,29 +93,168 @@ const out = (s = '') => process.stdout.write(s + '\n');
|
|
|
85
93
|
*/
|
|
86
94
|
let reader = null;
|
|
87
95
|
let onLine = null;
|
|
96
|
+
/** Earlier requests, for Up, Down and Ctrl+R (manual 6.5). */
|
|
97
|
+
const earlier = new history_1.History();
|
|
98
|
+
/** Pastes, Alt+Enter and Ctrl+R, between the keyboard and the reader (6.2,
|
|
99
|
+
* 6.3, 6.5). Only in a terminal: piped input is read as lines, as before. */
|
|
100
|
+
const keyboard = new keyboard_1.Keyboard(() => earlier.entries);
|
|
101
|
+
/** Where the session's keystrokes arrive, for a key the session handles
|
|
102
|
+
* itself such as Shift+Tab. Nothing arrives while a command has the keyboard. */
|
|
103
|
+
const keys = () => (process.stdin.isTTY ? keyboard.input : process.stdin);
|
|
88
104
|
function releaseTerminal() {
|
|
89
105
|
reader?.close();
|
|
90
106
|
reader = null;
|
|
107
|
+
// The keyboard goes back with the reader, so the command that asks its own
|
|
108
|
+
// question is the only thing reading it (manual 6.1).
|
|
109
|
+
keyboard.bind(null);
|
|
110
|
+
keyboard.detach();
|
|
91
111
|
}
|
|
92
112
|
function takeTerminal() {
|
|
93
113
|
if (reader || !onLine)
|
|
94
114
|
return;
|
|
115
|
+
const tty = process.stdin.isTTY === true;
|
|
116
|
+
if (tty)
|
|
117
|
+
keyboard.attach();
|
|
95
118
|
reader = node_readline_1.default.createInterface({
|
|
96
|
-
input: process.stdin,
|
|
119
|
+
input: tty ? keyboard.input : process.stdin,
|
|
97
120
|
output: process.stdout,
|
|
98
|
-
terminal:
|
|
121
|
+
terminal: tty,
|
|
122
|
+
// The session's own list, walked by Up and Down. Size 0 so the reader
|
|
123
|
+
// never adds to it itself: it would add each half of a request continued
|
|
124
|
+
// with a backslash, and the session adds the whole request once sent.
|
|
125
|
+
history: earlier.entries,
|
|
126
|
+
historySize: 0,
|
|
127
|
+
completer: complete,
|
|
99
128
|
});
|
|
129
|
+
keyboard.bind(reader);
|
|
100
130
|
reader.on('line', onLine);
|
|
101
|
-
reader.on('SIGINT', () =>
|
|
131
|
+
reader.on('SIGINT', () => interrupt());
|
|
102
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Tab (manual 3.1 and 6.4). After `/` it offers the service's list of
|
|
135
|
+
* commands and skills, one line each; after `@` the project's file names.
|
|
136
|
+
* Anywhere else it offers nothing, so it never rewrites a sentence.
|
|
137
|
+
*/
|
|
138
|
+
function complete(line, done) {
|
|
139
|
+
const slash = /^\/(\S*)$/.exec(line);
|
|
140
|
+
if (!slash) {
|
|
141
|
+
done(null, attach.completeAt(line, () => {
|
|
142
|
+
if (!completion.files || Date.now() - completion.at > 30_000) {
|
|
143
|
+
completion.files = attach.projectFiles(completion.root);
|
|
144
|
+
completion.at = Date.now();
|
|
145
|
+
}
|
|
146
|
+
return completion.files;
|
|
147
|
+
}));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const typed = slash[1].toLowerCase();
|
|
151
|
+
const hits = listedItems().filter((i) => i.name.startsWith(typed));
|
|
152
|
+
if (hits.length === 1) {
|
|
153
|
+
done(null, [[`/${hits[0].name} `], line]);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
done(null, [[], line]);
|
|
157
|
+
if (hits.length === 0 || !reader)
|
|
158
|
+
return;
|
|
159
|
+
out();
|
|
160
|
+
drawList(hits);
|
|
161
|
+
// Filled in as far as every match agrees, then the prompt drawn again under
|
|
162
|
+
// the list with the cursor at the end.
|
|
163
|
+
const common = hits.map((h) => h.name).reduce((a, b) => {
|
|
164
|
+
let n = 0;
|
|
165
|
+
while (n < a.length && a[n] === b[n])
|
|
166
|
+
n += 1;
|
|
167
|
+
return a.slice(0, n);
|
|
168
|
+
});
|
|
169
|
+
const edit = reader;
|
|
170
|
+
if (common.length > typed.length)
|
|
171
|
+
edit.line = `/${common}`;
|
|
172
|
+
edit.cursor = edit.line.length;
|
|
173
|
+
reader.prompt(true);
|
|
174
|
+
}
|
|
175
|
+
/** The service's list for this session, once fetched (manual 6.4). */
|
|
176
|
+
let listed = null;
|
|
177
|
+
/** The session words this tool runs. Sent to the service, which lists only
|
|
178
|
+
* these and counts the rest as needing a newer version. */
|
|
179
|
+
function sessionWords() {
|
|
180
|
+
return [...COMMANDS, 'project', 'folders', 'instructions', 'remember', 'detach', 'output', 'processes', 'stop', 'agents'];
|
|
181
|
+
}
|
|
182
|
+
async function refreshList() {
|
|
183
|
+
const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
|
|
184
|
+
const root = resolveProject(await sessionFolder(), place.added).root;
|
|
185
|
+
const got = await (0, commandlist_1.fetchCommandList)(root, sessionWords());
|
|
186
|
+
// A failed refresh keeps the list already held rather than emptying it.
|
|
187
|
+
if (got.ok || !listed)
|
|
188
|
+
listed = got;
|
|
189
|
+
}
|
|
190
|
+
/** What Tab and `help` offer: the service's list, or, when it could not be
|
|
191
|
+
* asked, this tool's own words with no descriptions. */
|
|
192
|
+
function listedItems() {
|
|
193
|
+
if (listed?.ok)
|
|
194
|
+
return listed.items;
|
|
195
|
+
return sessionWords().map((name) => ({ name, description: '', source: 'session' }));
|
|
196
|
+
}
|
|
197
|
+
function drawList(items) {
|
|
198
|
+
const width = Math.min(18, Math.max(...items.map((i) => i.name.length)) + 1);
|
|
199
|
+
for (const i of items) {
|
|
200
|
+
const from = i.source === 'session' ? '' : ` ${ui.c.dim(i.source)}`;
|
|
201
|
+
out(` ${ui.c.bold(`/${i.name}`.padEnd(width + 1))} ${ui.c.dim(i.description)}${from}`);
|
|
202
|
+
}
|
|
203
|
+
out();
|
|
204
|
+
}
|
|
205
|
+
/** The project whose files Tab offers after `@`, and the list, briefly kept. */
|
|
206
|
+
const completion = {
|
|
207
|
+
root: process.cwd(), files: null, at: 0,
|
|
208
|
+
};
|
|
103
209
|
/** Set by the session so the handover can end it too. */
|
|
104
210
|
let leaveSession = () => { };
|
|
211
|
+
/** Ctrl+C (manual 6.7): stop the job, clear the line, or leave. Set by the
|
|
212
|
+
* session; before one starts it leaves. */
|
|
213
|
+
let interrupt = () => leaveSession();
|
|
105
214
|
/** Words that are commands. Anything else typed is work. */
|
|
106
215
|
const COMMANDS = new Set([
|
|
107
|
-
'new', 'sessions', '
|
|
216
|
+
'new', 'sessions', 'branch', 'remote', 'model', 'providers', 'keys', 'status',
|
|
108
217
|
'voice', 'help', 'exit', 'quit', 'clear', 'login', 'logout', 'signout',
|
|
109
|
-
'disconnect',
|
|
218
|
+
'disconnect', 'cost', 'spend', 'attached', 'paste', 'plan', 'todo', 'mode',
|
|
219
|
+
'queue', 'transcript', 'edit', 'context', 'compact', 'why', 'unpin', 'pin', 'effort',
|
|
110
220
|
]);
|
|
221
|
+
/** `processes` and `stop pN` (manual 5.13): what this session started, and
|
|
222
|
+
* stopping one by its number. Works while a job runs, too. True if handled. */
|
|
223
|
+
function processWord(said) {
|
|
224
|
+
if (/^\/?processes$/i.test(said)) {
|
|
225
|
+
const rows = (0, shell_1.listProcesses)();
|
|
226
|
+
if (!rows.length)
|
|
227
|
+
out(ui.c.dim(' No background processes were started in this session.'));
|
|
228
|
+
for (const p of rows) {
|
|
229
|
+
const age = p.seconds < 60 ? `${p.seconds} s` : `${Math.round(p.seconds / 60)} min`;
|
|
230
|
+
out(` ${ui.c.bold(p.id)} ${p.command} ${ui.c.dim(p.running ? `running ${age}` : 'finished')}`
|
|
231
|
+
+ (p.port ? ui.c.dim(` port ${p.port}`) : ''));
|
|
232
|
+
}
|
|
233
|
+
out();
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
const stop = /^\/?stop\s+(p\d+)$/i.exec(said);
|
|
237
|
+
if (stop) {
|
|
238
|
+
const r = (0, shell_1.stopProcess)(stop[1].toLowerCase());
|
|
239
|
+
out(r.isError ? ui.fail(r.content) : ui.c.dim(` ${r.content}`));
|
|
240
|
+
out();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
/** `agents` between jobs: the helpers defined here (manual 5.24). */
|
|
246
|
+
async function showHelpers() {
|
|
247
|
+
const { helperLines } = await Promise.resolve().then(() => __importStar(require('./agents')));
|
|
248
|
+
for (const line of await helperLines(await sessionFolder()))
|
|
249
|
+
out(` ${line}`);
|
|
250
|
+
out();
|
|
251
|
+
}
|
|
252
|
+
/** The permission modes, in the order Shift+Tab moves through them (4.5). */
|
|
253
|
+
const MODES = ['read_only', 'approve_writes', 'auto_edit', 'full_auto'];
|
|
254
|
+
/** How the next job in this session runs: its mode and whether it plans first
|
|
255
|
+
* (manual 4.1, 4.5). Changed by `mode`, Shift+Tab, `plan` and `plan off`, and
|
|
256
|
+
* applied from the next job; the service still decides what each mode allows. */
|
|
257
|
+
const steering_ = { mode: 'approve_writes', plan: false, pin: '', effort: '' };
|
|
111
258
|
/**
|
|
112
259
|
* The line as a command, with a leading slash taken off.
|
|
113
260
|
*
|
|
@@ -152,51 +299,45 @@ async function withTerminal(run) {
|
|
|
152
299
|
}
|
|
153
300
|
}
|
|
154
301
|
/**
|
|
155
|
-
*
|
|
302
|
+
* This account's earlier sessions, from the service's record (manual 8.5).
|
|
156
303
|
*
|
|
157
304
|
* The in-memory list only ever knew about the current run, so closing the
|
|
158
|
-
* window lost every thread
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* Returns nothing when the workspace cannot be reached. A list is not worth
|
|
164
|
-
* stopping a session over, and the person is told in the line above it.
|
|
305
|
+
* window lost every thread. The record has held all of it the whole time.
|
|
306
|
+
* Sessions of this run are left out, since they are listed above it, and their
|
|
307
|
+
* names are taken from the record, which names a session from its first
|
|
308
|
+
* request. Returns nothing when the workspace cannot be reached: a list is not
|
|
309
|
+
* worth stopping a session over.
|
|
165
310
|
*/
|
|
166
|
-
async function
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// which reads as "no earlier work" rather than as a wrong credential.
|
|
171
|
-
if (!conn?.url || !conn.token)
|
|
172
|
-
return [];
|
|
173
|
-
try {
|
|
174
|
-
const headers = {
|
|
175
|
-
authorization: `Bearer ${conn.token}`,
|
|
176
|
-
};
|
|
177
|
-
if (conn.tenant)
|
|
178
|
-
headers['X-Tenant-ID'] = conn.tenant;
|
|
179
|
-
const res = await fetch(`${conn.url.replace(/\/+$/, '')}/api/v1/agent/jobs?limit=${limit}`, { headers });
|
|
180
|
-
if (!res.ok)
|
|
181
|
-
return [];
|
|
182
|
-
const body = await res.json();
|
|
183
|
-
return (body.jobs ?? []).map((j) => ({
|
|
184
|
-
id: String(j.id ?? ''),
|
|
185
|
-
request: String(j.request ?? ''),
|
|
186
|
-
name: j.name ?? null,
|
|
187
|
-
when: String(j.creation_date ?? ''),
|
|
188
|
-
usd: Number(j.total_usd ?? 0),
|
|
189
|
-
workspace: j.workspace ?? null,
|
|
190
|
-
})).filter((j) => j.id);
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
311
|
+
async function earlierSessions(run) {
|
|
312
|
+
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
313
|
+
const reply = await sessions('list', { limit: 10 });
|
|
314
|
+
if (!reply.ok || !reply.sessions?.sessions)
|
|
193
315
|
return [];
|
|
194
|
-
|
|
316
|
+
const rows = reply.sessions.sessions;
|
|
317
|
+
const lines = reply.sessions.lines;
|
|
318
|
+
const found = [];
|
|
319
|
+
rows.forEach((r, i) => {
|
|
320
|
+
const here = run.find((s) => s.serverId === r.session_id);
|
|
321
|
+
if (here) {
|
|
322
|
+
if (r.title)
|
|
323
|
+
here.title = r.title;
|
|
324
|
+
here.id = r.number;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
found.push({ number: r.number, line: lines[i] ?? r.number });
|
|
328
|
+
});
|
|
329
|
+
return found;
|
|
195
330
|
}
|
|
196
331
|
function newSession() {
|
|
332
|
+
// A UUID, because that is what the job record's session column holds. A
|
|
333
|
+
// dated `cli_...` string was refused by the database, so no job started
|
|
334
|
+
// in a session was ever recorded (found 23 September 2026, prompt 03).
|
|
335
|
+
const serverId = (0, node_crypto_1.randomUUID)();
|
|
197
336
|
return {
|
|
198
|
-
//
|
|
199
|
-
|
|
337
|
+
// The session's number, the same one the portal shows (manual 8.1). The
|
|
338
|
+
// service writes it; `numbered` asks for it as soon as the session opens.
|
|
339
|
+
id: serverId,
|
|
340
|
+
serverId,
|
|
200
341
|
startedAt: new Date().toISOString(),
|
|
201
342
|
jobs: [],
|
|
202
343
|
spentUsd: 0,
|
|
@@ -205,56 +346,239 @@ function newSession() {
|
|
|
205
346
|
};
|
|
206
347
|
}
|
|
207
348
|
const money = (n) => `$${n.toFixed(4)}`;
|
|
349
|
+
/**
|
|
350
|
+
* Ask the service for a new session's number and its opening line (8.2).
|
|
351
|
+
* Returns the line to print, or null when the service could not be asked, in
|
|
352
|
+
* which case the session keeps its full id until the next answer names it.
|
|
353
|
+
*/
|
|
354
|
+
async function numbered(s) {
|
|
355
|
+
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
356
|
+
const reply = await sessions('opened', { session_id: s.serverId });
|
|
357
|
+
if (!reply.ok || !reply.sessions?.number)
|
|
358
|
+
return null;
|
|
359
|
+
s.id = reply.sessions.number;
|
|
360
|
+
return reply.sessions.lines?.[0] ?? null;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* A session the service already holds, opened again (manual 8.4, 8.5).
|
|
364
|
+
*
|
|
365
|
+
* Same id, so every new job files under the same record; its jobs and its cost
|
|
366
|
+
* so far, so the running total carries on without counting anything twice; and
|
|
367
|
+
* the last job to continue from only when the service says the conversation
|
|
368
|
+
* was kept. Session approvals (`a` answers) belong to the run that gave them
|
|
369
|
+
* and are not carried over.
|
|
370
|
+
*/
|
|
371
|
+
function reopened(r) {
|
|
372
|
+
return {
|
|
373
|
+
id: r.number ?? r.session_id ?? '',
|
|
374
|
+
serverId: r.session_id ?? '',
|
|
375
|
+
title: r.title ?? null,
|
|
376
|
+
startedAt: new Date().toISOString(),
|
|
377
|
+
jobs: Array.from({ length: Number(r.jobs ?? 0) }, () => ({ request: '' })),
|
|
378
|
+
spentUsd: Number(r.cost_usd ?? 0),
|
|
379
|
+
savedUsd: 0,
|
|
380
|
+
compared: 0,
|
|
381
|
+
...(r.continues_job ? { lastJobId: r.continues_job } : {}),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/** Ask the service for the session to open, choosing from the list when no
|
|
385
|
+
* number was given. Prints the resume lines, or why there are none. */
|
|
386
|
+
async function opening(how) {
|
|
387
|
+
const { sessions, pick, failure, exitFor } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
388
|
+
let key = how.resume;
|
|
389
|
+
if (how.pick) {
|
|
390
|
+
const listed = await sessions('list', { limit: 20 });
|
|
391
|
+
if (!listed.ok || !listed.sessions) {
|
|
392
|
+
out(ui.fail(failure(listed)));
|
|
393
|
+
process.exitCode = exitFor(listed);
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
const rows = listed.sessions.sessions ?? [];
|
|
397
|
+
if (rows.length === 0) {
|
|
398
|
+
for (const line of listed.sessions.lines)
|
|
399
|
+
out(line);
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const chosen = await pick(listed.sessions.lines);
|
|
403
|
+
if (chosen === null)
|
|
404
|
+
return null;
|
|
405
|
+
key = rows[chosen]?.number;
|
|
406
|
+
}
|
|
407
|
+
const reply = how.latest
|
|
408
|
+
? await sessions('latest')
|
|
409
|
+
: await sessions('resume', { key: key ?? '' });
|
|
410
|
+
if (!reply.ok || !reply.sessions?.session_id) {
|
|
411
|
+
out(ui.fail(failure(reply)));
|
|
412
|
+
process.exitCode = exitFor(reply);
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
out();
|
|
416
|
+
for (const line of reply.sessions.lines)
|
|
417
|
+
out(line);
|
|
418
|
+
out();
|
|
419
|
+
return reopened(reply.sessions);
|
|
420
|
+
}
|
|
421
|
+
/** The command and skill list, as the service sent it (manual 6.4). */
|
|
208
422
|
function printHelp() {
|
|
209
423
|
out();
|
|
210
424
|
out(ui.c.dim(' Type a question and press enter. Or one of these:'));
|
|
211
425
|
out();
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
['providers', 'every vendor and model you can reach'],
|
|
217
|
-
['voice', 'say it instead of typing it'],
|
|
218
|
-
['keys', 'add or replace a provider key'],
|
|
219
|
-
['workspace', 'the folder to work in'],
|
|
220
|
-
['status', 'where you are signed in'],
|
|
221
|
-
['logout', 'sign out, and revoke this machine\'s key'],
|
|
222
|
-
['clear', 'clear the screen'],
|
|
223
|
-
['exit', 'leave, or press Ctrl-C'],
|
|
224
|
-
];
|
|
225
|
-
for (const [cmd, what] of rows) {
|
|
226
|
-
out(` ${ui.c.bold(cmd.padEnd(11))}${ui.c.dim(what)}`);
|
|
426
|
+
drawList(listedItems());
|
|
427
|
+
if (!listed?.ok) {
|
|
428
|
+
out(ui.c.amber(' Your workspace could not be asked for its skills, so only this tool\'s own words are shown.'));
|
|
429
|
+
out();
|
|
227
430
|
}
|
|
228
|
-
|
|
229
|
-
|
|
431
|
+
else if (listed.needsUpdate.length > 0) {
|
|
432
|
+
out(ui.c.dim(` ${listed.needsUpdate.length} more need a newer comprism: npm install -g @illuminis/comprism`));
|
|
433
|
+
out();
|
|
434
|
+
}
|
|
435
|
+
out(ui.c.dim(' A slash is fine as well, so `/model` and `model` do the same thing. Type / and press Tab to pick one.'));
|
|
230
436
|
out(ui.c.dim(' Drag a file onto this window, or type @ and a path, to have it read.'));
|
|
437
|
+
out(ui.c.dim(' End a line with \\ or press Alt+Enter for a new line. Up and Down recall earlier requests; Ctrl+R searches them.'));
|
|
231
438
|
out();
|
|
232
439
|
}
|
|
233
440
|
/**
|
|
234
441
|
* Hand a request to the coding agent, in the folder this session is working in.
|
|
235
442
|
*
|
|
236
|
-
* The agent reads the project,
|
|
443
|
+
* The agent reads the project, shows every change as it makes it, and runs
|
|
237
444
|
* the tests. It is the same agent the standalone command runs; there is one
|
|
238
445
|
* implementation, so the session and the command cannot behave differently.
|
|
239
446
|
*/
|
|
240
|
-
|
|
241
|
-
|
|
447
|
+
/**
|
|
448
|
+
* The folders this session works in (manual 2.1 and 2.3). `project <path>`
|
|
449
|
+
* changes the folder for the rest of the session; `folders add <path>` allows
|
|
450
|
+
* another. Neither changes the saved default, which is `comprism project`.
|
|
451
|
+
*/
|
|
452
|
+
const place = { folder: null, added: [] };
|
|
453
|
+
async function sessionFolder() {
|
|
454
|
+
if (place.folder)
|
|
455
|
+
return place.folder;
|
|
242
456
|
const { currentWorkspace } = await Promise.resolve().then(() => __importStar(require('./settings')));
|
|
243
|
-
|
|
457
|
+
return currentWorkspace().path;
|
|
458
|
+
}
|
|
459
|
+
/** The line this session begins with, as the service wrote it (manual 2.2). */
|
|
460
|
+
async function showProjectLine() {
|
|
461
|
+
const { resolveProject, asWire } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
|
|
462
|
+
const { ask } = await Promise.resolve().then(() => __importStar(require('../lib/gateway')));
|
|
463
|
+
const facts = resolveProject(await sessionFolder(), place.added);
|
|
464
|
+
const reply = await ask({ intent: 'project', prompt: '', project: asWire(facts, 'approve_writes') });
|
|
465
|
+
// Drawn as sent. When the service cannot answer, the folder alone, so the
|
|
466
|
+
// person still knows where they are.
|
|
467
|
+
out(ui.c.bold(reply.ok && reply.project_line ? reply.project_line : ` ${facts.root}`));
|
|
468
|
+
out();
|
|
469
|
+
}
|
|
470
|
+
/** Ctrl+O's choice, kept for the next job too, and notes left over from the
|
|
471
|
+
* last one (manual 6.8, 6.9). */
|
|
472
|
+
const view = { full: false, late: [] };
|
|
473
|
+
/** `context` and `compact [what to keep]`, answered and worded by the
|
|
474
|
+
* service for the session's latest job (manual 6.13, 6.15). */
|
|
475
|
+
async function contextOf(intent, jobId, keep = '', sessionKey) {
|
|
476
|
+
const { ask } = await Promise.resolve().then(() => __importStar(require('../lib/gateway')));
|
|
477
|
+
out();
|
|
478
|
+
if (intent === 'compact')
|
|
479
|
+
out(ui.c.dim(' summarizing the conversation so far to keep it affordable'));
|
|
480
|
+
// The session is named too, so a session with no job yet (questions asked in
|
|
481
|
+
// Work) gets the same figure the web meter shows (web manual 3.11).
|
|
482
|
+
const reply = await ask({
|
|
483
|
+
intent, prompt: keep, ...(jobId ? { jobId } : {}), ...(sessionKey ? { spendSession: sessionKey } : {}),
|
|
484
|
+
});
|
|
485
|
+
if (!reply.ok || !reply.context) {
|
|
486
|
+
out(ui.fail(reply.message || 'Your workspace could not answer that.'));
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
for (const line of reply.context.lines)
|
|
490
|
+
out(line);
|
|
491
|
+
}
|
|
492
|
+
out();
|
|
493
|
+
}
|
|
494
|
+
/** One job's full record, as the service writes it (manual 6.9). */
|
|
495
|
+
async function showTranscript(jobId) {
|
|
496
|
+
out();
|
|
497
|
+
if (!jobId) {
|
|
498
|
+
out(ui.c.dim(' Nothing has run in this session yet, so there is no transcript.'));
|
|
499
|
+
out();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const { ask } = await Promise.resolve().then(() => __importStar(require('../lib/gateway')));
|
|
503
|
+
const reply = await ask({ intent: 'transcript', prompt: '', jobId });
|
|
504
|
+
if (!reply.ok || !reply.transcript) {
|
|
505
|
+
out(ui.fail(reply.message || 'The transcript could not be read from your workspace.'));
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
for (const line of reply.transcript.lines)
|
|
509
|
+
out(line);
|
|
510
|
+
}
|
|
244
511
|
out();
|
|
245
|
-
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Open the request in the person's editor and put what they saved back at the
|
|
515
|
+
* prompt, unsent (manual 6.6). The file exists only while the editor is open,
|
|
516
|
+
* in a folder only this account can read, and is removed the moment it closes.
|
|
517
|
+
*/
|
|
518
|
+
async function editRequest(current) {
|
|
519
|
+
const chosen = process.env.VISUAL || process.env.EDITOR || (0, machine_1.machineSettings)().editor;
|
|
520
|
+
const put = (text) => {
|
|
521
|
+
if (!reader)
|
|
522
|
+
return;
|
|
523
|
+
reader.setPrompt(`${ui.c.bold(ui.c.blue('comprism'))} ${ui.c.dim('>')} `);
|
|
524
|
+
reader.prompt();
|
|
525
|
+
reader.write((0, keyboard_1.asOneLine)(text));
|
|
526
|
+
};
|
|
527
|
+
if (!chosen) {
|
|
528
|
+
out();
|
|
529
|
+
out(ui.fail('No editor is set. Set VISUAL or EDITOR, or run: comprism config set editor <command>'));
|
|
530
|
+
put(current);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const fs = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
534
|
+
const os = await Promise.resolve().then(() => __importStar(require('node:os')));
|
|
535
|
+
const { spawnSync } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
|
|
536
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'comprism-edit-'));
|
|
537
|
+
const file = path.join(dir, 'request.md');
|
|
538
|
+
fs.writeFileSync(file, current, { mode: 0o600 });
|
|
539
|
+
releaseTerminal();
|
|
540
|
+
let back = current;
|
|
541
|
+
try {
|
|
542
|
+
const [program, ...extra] = chosen.split(/\s+/).filter(Boolean);
|
|
543
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
544
|
+
spawnSync(program, [...extra, file], { stdio: 'inherit' });
|
|
545
|
+
const saved = fs.readFileSync(file, 'utf8');
|
|
546
|
+
// Closed without saving leaves the prompt exactly as it was.
|
|
547
|
+
if (saved !== current)
|
|
548
|
+
back = saved.replace(/\r?\n$/, '');
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
out(ui.fail(`${chosen} could not be opened, so the request is as it was.`));
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
555
|
+
takeTerminal();
|
|
556
|
+
}
|
|
557
|
+
put(back);
|
|
558
|
+
}
|
|
559
|
+
async function build(session, request, confirm, onStarted, attachmentIds = [], leftOut = [], ask) {
|
|
560
|
+
const { runAgent } = await Promise.resolve().then(() => __importStar(require('../agent/command')));
|
|
246
561
|
out();
|
|
247
562
|
// Counted the moment it starts, not when it returns. A person who leaves
|
|
248
563
|
// part-way through a job was told "nothing asked in that session" over work
|
|
249
564
|
// that had already been paid for, which is the one number this product cannot
|
|
250
565
|
// get wrong.
|
|
251
566
|
session.jobs.push({ request });
|
|
567
|
+
const began = Date.now();
|
|
568
|
+
let busy = false;
|
|
252
569
|
const code = await runAgent({
|
|
253
570
|
request: [request],
|
|
254
|
-
cwd:
|
|
571
|
+
cwd: await sessionFolder(),
|
|
572
|
+
addFolders: place.added,
|
|
255
573
|
// Carry on from the last question rather than starting again. `new` clears
|
|
256
574
|
// it, which is what `new` is for.
|
|
257
575
|
continuesJob: session.lastJobId,
|
|
576
|
+
// The session the person is actually in. Without it the server files every
|
|
577
|
+
// job of the day under one row, which is why `sessions` here listed three
|
|
578
|
+
// and the Sessions screen showed one.
|
|
579
|
+
conversationId: session.serverId,
|
|
580
|
+
// A server started by one question is still there for the next (5.13).
|
|
581
|
+
keepProcesses: true,
|
|
258
582
|
// The session's own keyboard, handed to the agent.
|
|
259
583
|
//
|
|
260
584
|
// The agent has to put a change in front of the person and wait. It used to
|
|
@@ -264,24 +588,47 @@ async function build(session, request, confirm, onStarted, attachmentIds = []) {
|
|
|
264
588
|
// than by the person. One reader, owned by whoever started the process,
|
|
265
589
|
// passed down.
|
|
266
590
|
confirm,
|
|
591
|
+
prompt: ask,
|
|
267
592
|
onStarted,
|
|
593
|
+
fullView: view.full,
|
|
268
594
|
// Files the person attached, already read and waiting on the server.
|
|
269
595
|
attachments: attachmentIds,
|
|
596
|
+
// Files named that could not be attached, so the agent is told (3.10).
|
|
597
|
+
leftOut,
|
|
270
598
|
// Ask before a change and before a command, because now the person can
|
|
271
599
|
// actually answer. This was auto_edit only because approving a write was
|
|
272
600
|
// impossible from inside a session; it is the mode the standalone command
|
|
273
601
|
// uses, so the two behave the same way.
|
|
274
|
-
mode:
|
|
602
|
+
mode: steering_.mode,
|
|
603
|
+
plan: steering_.plan,
|
|
604
|
+
// A pin and effort set with `pin` and `effort` are the session's, held by
|
|
605
|
+
// the service (manual 4.17, 4.18; web manual 4.5, 4.6), which applies them
|
|
606
|
+
// to this job. So a pin set in the browser applies here, and the reverse.
|
|
275
607
|
// The running total is the whole point of a session, so it is added up
|
|
276
608
|
// from what each job actually cost. It used to be declared and never
|
|
277
609
|
// touched, so a session ended by reporting zero spent and zero saved
|
|
278
610
|
// directly underneath a job that had just charged three cents.
|
|
279
611
|
onFinished: (r) => {
|
|
612
|
+
// Busy in another place (manual 8.8): nothing ran, nothing was charged.
|
|
613
|
+
if (r.outcome === 'busy') {
|
|
614
|
+
busy = true;
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
280
617
|
// Recorded even when the job did not finish. A run that was stopped or
|
|
281
618
|
// ran out of steps still said things, and the next question is usually
|
|
282
619
|
// about exactly that.
|
|
283
620
|
if (r.jobId)
|
|
284
621
|
session.lastJobId = r.jobId;
|
|
622
|
+
// Which job answered which request, for `rewind` (manual 7.4 to 7.6).
|
|
623
|
+
const asked = session.jobs[session.jobs.length - 1];
|
|
624
|
+
if (asked && r.jobId && !asked.jobId)
|
|
625
|
+
asked.jobId = r.jobId;
|
|
626
|
+
session.todos = r.todos;
|
|
627
|
+
if (r.why && r.why.length)
|
|
628
|
+
session.why = r.why;
|
|
629
|
+
// Notes that arrived after the last step become requests of their own
|
|
630
|
+
// (manual 6.8); the service has already said so on screen.
|
|
631
|
+
view.late = r.lateNotes;
|
|
285
632
|
session.spentUsd += r.totalUsd;
|
|
286
633
|
if (r.savedUsd !== null) {
|
|
287
634
|
session.savedUsd += r.savedUsd;
|
|
@@ -289,12 +636,108 @@ async function build(session, request, confirm, onStarted, attachmentIds = []) {
|
|
|
289
636
|
}
|
|
290
637
|
},
|
|
291
638
|
});
|
|
639
|
+
if (busy) {
|
|
640
|
+
session.jobs.pop();
|
|
641
|
+
return whileBusy(session, request, confirm, onStarted, attachmentIds, leftOut, ask);
|
|
642
|
+
}
|
|
292
643
|
out();
|
|
293
644
|
if (code !== 0)
|
|
294
645
|
out(ui.c.amber(' the job did not finish'));
|
|
646
|
+
// A job long enough to switch windows during is worth being told about
|
|
647
|
+
// (manual 6.11).
|
|
648
|
+
if (Date.now() - began > 30_000) {
|
|
649
|
+
const { notify } = await Promise.resolve().then(() => __importStar(require('../lib/notify')));
|
|
650
|
+
notify(code === 0 ? 'Your job finished.' : 'Your job stopped before finishing.', (l) => out(ui.c.dim(l)));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The session is busy in another place (manual 8.8). Offer to wait, or to read
|
|
655
|
+
* it here. Waiting asks the service until the session is free, then runs the
|
|
656
|
+
* request; reading prints the session's turns and runs nothing.
|
|
657
|
+
*/
|
|
658
|
+
async function whileBusy(session, request, confirm, onStarted, attachmentIds, leftOut, ask) {
|
|
659
|
+
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
660
|
+
const said = ((await ask?.('w to wait for it, r to read it here: ')) ?? 'r').trim().toLowerCase();
|
|
661
|
+
if (said.startsWith('w')) {
|
|
662
|
+
out(ui.c.dim(' waiting for the session to be free...'));
|
|
663
|
+
for (let i = 0; i < 120; i++) {
|
|
664
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
665
|
+
const now = await sessions('lease', { key: session.serverId });
|
|
666
|
+
if (now.ok && now.sessions && !now.sessions.busy_in) {
|
|
667
|
+
return build(session, request, confirm, onStarted, attachmentIds, leftOut, ask);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
out(ui.c.amber(' still busy after six minutes, so nothing was run.'));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const read = await sessions('read', { key: session.serverId });
|
|
674
|
+
out();
|
|
675
|
+
for (const line of read.sessions?.lines ?? [read.message ?? ''])
|
|
676
|
+
out(line);
|
|
677
|
+
out(ui.c.dim(' Read only: nothing was run.'));
|
|
678
|
+
out();
|
|
679
|
+
}
|
|
680
|
+
/** What `remote` holds while it is on (manual 8.11). */
|
|
681
|
+
const remote = { timer: null, session: null, take: () => undefined, idle: () => false };
|
|
682
|
+
async function remoteOn(session) {
|
|
683
|
+
const { sessions, failure } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
684
|
+
const reply = await sessions('remote_on', { key: session.serverId });
|
|
685
|
+
if (!reply.ok || !reply.sessions) {
|
|
686
|
+
out(ui.fail(failure(reply)));
|
|
687
|
+
out();
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
for (const line of reply.sessions.lines)
|
|
691
|
+
out(line);
|
|
692
|
+
out();
|
|
693
|
+
remote.session = session.serverId;
|
|
694
|
+
if (remote.timer)
|
|
695
|
+
clearInterval(remote.timer);
|
|
696
|
+
let asking = false;
|
|
697
|
+
remote.timer = setInterval(() => {
|
|
698
|
+
if (asking || !remote.idle() || !remote.session)
|
|
699
|
+
return;
|
|
700
|
+
asking = true;
|
|
701
|
+
void sessions('remote_poll', { key: remote.session }).then((got) => {
|
|
702
|
+
const body = got.sessions;
|
|
703
|
+
if (body?.off) {
|
|
704
|
+
stopRemoteTimer();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
for (const text of body?.requests ?? [])
|
|
708
|
+
remote.take(text);
|
|
709
|
+
}).finally(() => { asking = false; });
|
|
710
|
+
}, 2000);
|
|
711
|
+
remote.timer.unref();
|
|
712
|
+
}
|
|
713
|
+
function stopRemoteTimer() {
|
|
714
|
+
if (remote.timer)
|
|
715
|
+
clearInterval(remote.timer);
|
|
716
|
+
remote.timer = null;
|
|
717
|
+
remote.session = null;
|
|
718
|
+
}
|
|
719
|
+
async function remoteOff(session, quiet = false) {
|
|
720
|
+
const key = remote.session ?? session.serverId;
|
|
721
|
+
const was = remote.session;
|
|
722
|
+
stopRemoteTimer();
|
|
723
|
+
if (quiet && !was)
|
|
724
|
+
return;
|
|
725
|
+
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
726
|
+
const reply = await sessions('remote_off', { key });
|
|
727
|
+
if (!quiet) {
|
|
728
|
+
for (const line of reply.sessions?.lines ?? [' Remote was not on.'])
|
|
729
|
+
out(line);
|
|
730
|
+
out();
|
|
731
|
+
}
|
|
295
732
|
}
|
|
296
733
|
/** The running total, shown when a session ends or another is opened. */
|
|
297
734
|
function summarize(s) {
|
|
735
|
+
// The session is over: where the company keeps no content, its checkpoints
|
|
736
|
+
// end with it (manual 7.3). The service decides; nothing is kept here.
|
|
737
|
+
if (s.jobs.length > 0) {
|
|
738
|
+
void Promise.resolve().then(() => __importStar(require('./review'))).then((m) => m.review('session_ended', { session_id: s.serverId }))
|
|
739
|
+
.catch(() => undefined);
|
|
740
|
+
}
|
|
298
741
|
if (s.jobs.length === 0) {
|
|
299
742
|
out(ui.c.dim(' nothing asked in that session'));
|
|
300
743
|
return;
|
|
@@ -308,7 +751,7 @@ function summarize(s) {
|
|
|
308
751
|
out(` ${ui.c.dim('session')} ${ui.c.bold(s.id)} ${s.jobs.length} job(s)`
|
|
309
752
|
+ ` ${money(s.spentUsd)} ${ui.c.dim('spent')}${saving}`);
|
|
310
753
|
}
|
|
311
|
-
async function runRepl() {
|
|
754
|
+
async function runRepl(how = {}) {
|
|
312
755
|
// Resolved when the person leaves. The reader is closed and reopened around
|
|
313
756
|
// every job, so waiting on it closing would end the session at the first one.
|
|
314
757
|
let done = () => { };
|
|
@@ -352,6 +795,34 @@ async function runRepl() {
|
|
|
352
795
|
}
|
|
353
796
|
const history = [];
|
|
354
797
|
let session = newSession();
|
|
798
|
+
/** Read, or change, this session's pin and effort on the service (manual
|
|
799
|
+
* 4.17, 4.18). Says the service's refusal and returns null on one. */
|
|
800
|
+
const steer = async (change) => {
|
|
801
|
+
const { sessions, failure } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
802
|
+
const changing = Object.keys(change).length > 0;
|
|
803
|
+
const reply = await sessions(changing ? 'steer' : 'steering', { session_id: session.serverId, ...change });
|
|
804
|
+
const said = reply.sessions;
|
|
805
|
+
if (!reply.ok || !said || reply.granted === false) {
|
|
806
|
+
out(ui.fail(failure(reply)));
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
steering_.pin = said.pin ?? '';
|
|
810
|
+
steering_.effort = said.effort === 'normal' ? '' : said.effort;
|
|
811
|
+
return said;
|
|
812
|
+
};
|
|
813
|
+
if (how.latest || how.resume || how.pick) {
|
|
814
|
+
const found = await opening(how);
|
|
815
|
+
if (!found) {
|
|
816
|
+
out();
|
|
817
|
+
if (!process.exitCode)
|
|
818
|
+
process.exitCode = 1;
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
session = found;
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
await numbered(session);
|
|
825
|
+
}
|
|
355
826
|
history.push(session);
|
|
356
827
|
// Ctrl-C leaves, and says what the session cost on the way out. A product
|
|
357
828
|
// that vanishes without telling you what it spent is the thing this one
|
|
@@ -360,7 +831,21 @@ async function runRepl() {
|
|
|
360
831
|
// answer, not the next request, so it is handed straight to whoever is
|
|
361
832
|
// waiting instead of joining the queue.
|
|
362
833
|
let answering = null;
|
|
834
|
+
/** Rings or notifies when an approval has waited 30 seconds (6.11). */
|
|
835
|
+
let waitingTimer = null;
|
|
836
|
+
const startWaiting = () => {
|
|
837
|
+
if (waitingTimer)
|
|
838
|
+
clearTimeout(waitingTimer);
|
|
839
|
+
waitingTimer = setTimeout(() => {
|
|
840
|
+
void Promise.resolve().then(() => __importStar(require('../lib/notify'))).then(({ notify }) => notify('comprism is waiting for your answer.', (l) => out(ui.c.dim(l))));
|
|
841
|
+
}, 30_000);
|
|
842
|
+
waitingTimer.unref();
|
|
843
|
+
};
|
|
363
844
|
const takeAnswer = (line) => {
|
|
845
|
+
if (waitingTimer) {
|
|
846
|
+
clearTimeout(waitingTimer);
|
|
847
|
+
waitingTimer = null;
|
|
848
|
+
}
|
|
364
849
|
if (!answering)
|
|
365
850
|
return false;
|
|
366
851
|
const waiting = answering;
|
|
@@ -381,7 +866,20 @@ async function runRepl() {
|
|
|
381
866
|
resolve(false);
|
|
382
867
|
return;
|
|
383
868
|
}
|
|
384
|
-
answering = (line) => resolve(/^y(es)?$/i.test(line.trim()));
|
|
869
|
+
answering = (line) => resolve(/^y(es)?$/i.test((line ?? '').trim()));
|
|
870
|
+
startWaiting();
|
|
871
|
+
reader.setPrompt(` ${question} `);
|
|
872
|
+
reader.prompt();
|
|
873
|
+
});
|
|
874
|
+
/** The same lent keyboard, for a line of text: the plan question and the
|
|
875
|
+
* agent's questions (manual 4.2, 4.3). Null when the person left. */
|
|
876
|
+
const askLine = (question) => new Promise((resolve) => {
|
|
877
|
+
if (leaving || !reader) {
|
|
878
|
+
resolve(null);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
answering = resolve;
|
|
882
|
+
startWaiting();
|
|
385
883
|
reader.setPrompt(` ${question} `);
|
|
386
884
|
reader.prompt();
|
|
387
885
|
});
|
|
@@ -393,7 +891,7 @@ async function runRepl() {
|
|
|
393
891
|
resolve('');
|
|
394
892
|
return;
|
|
395
893
|
}
|
|
396
|
-
answering = resolve;
|
|
894
|
+
answering = (line) => resolve(line ?? '');
|
|
397
895
|
reader.setPrompt('');
|
|
398
896
|
reader.prompt();
|
|
399
897
|
});
|
|
@@ -433,7 +931,11 @@ async function runRepl() {
|
|
|
433
931
|
const { file, bytes } = await live.stop();
|
|
434
932
|
try {
|
|
435
933
|
if (bytes < 2000) {
|
|
436
|
-
|
|
934
|
+
// Nothing heard is usually the microphone permission, never asked for
|
|
935
|
+
// or refused, so it is named (manual 6.16).
|
|
936
|
+
out(ui.c.dim(process.platform === 'darwin'
|
|
937
|
+
? ' nothing was recorded. Your terminal may not have microphone permission: System Settings, Privacy and Security, Microphone.'
|
|
938
|
+
: ' nothing was recorded. Check that a microphone is connected and not muted.'));
|
|
437
939
|
out();
|
|
438
940
|
return prompt();
|
|
439
941
|
}
|
|
@@ -462,31 +964,94 @@ async function runRepl() {
|
|
|
462
964
|
voice.discard(file);
|
|
463
965
|
}
|
|
464
966
|
};
|
|
967
|
+
/** Files attached and not yet sent: they go with the next request
|
|
968
|
+
* (manual 3.6). `attached` lists them and `detach N` takes one off. */
|
|
969
|
+
let pending = [];
|
|
970
|
+
/** Pastes this session, so each is `clipboard-N.png` with its own N. */
|
|
971
|
+
let pastes = 0;
|
|
972
|
+
/**
|
|
973
|
+
* The clipboard's picture, attached to the next request (manual 3.4). Saved
|
|
974
|
+
* to a temporary file, uploaded like any file, and the file removed
|
|
975
|
+
* whatever happened.
|
|
976
|
+
*/
|
|
977
|
+
const pasteImage = async () => {
|
|
978
|
+
const { readClipboardImage, discard } = await Promise.resolve().then(() => __importStar(require('../lib/clipboard')));
|
|
979
|
+
const clip = await readClipboardImage(pastes + 1);
|
|
980
|
+
out();
|
|
981
|
+
if (!clip.ok) {
|
|
982
|
+
out(ui.c.amber(` ${clip.why}`));
|
|
983
|
+
out();
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
pastes += 1;
|
|
987
|
+
try {
|
|
988
|
+
const { attached, refused } = await attach.upload(conn.url, conn.workspaceKey, [clip.file]);
|
|
989
|
+
for (const line of attach.describe(attached))
|
|
990
|
+
out(ui.c.dim(` ${line}`));
|
|
991
|
+
for (const r of refused)
|
|
992
|
+
out(ui.c.amber(` ${r.why}`));
|
|
993
|
+
pending = [...pending, ...attached];
|
|
994
|
+
}
|
|
995
|
+
finally {
|
|
996
|
+
discard(clip.folder);
|
|
997
|
+
}
|
|
998
|
+
out();
|
|
999
|
+
};
|
|
465
1000
|
/**
|
|
466
1001
|
* Files the person pointed at, read and ready, and the question without the
|
|
467
1002
|
* long paths in it.
|
|
468
1003
|
*
|
|
469
1004
|
* Uploading happens before the job starts and is reported before anything is
|
|
470
1005
|
* charged, because the commonest complaint about any assistant is that it
|
|
471
|
-
* answered without looking at the thing you gave it.
|
|
1006
|
+
* answered without looking at the thing you gave it. A file with a problem
|
|
1007
|
+
* gets one line saying so and is never sent (manual 3.10).
|
|
1008
|
+
*
|
|
1009
|
+
* `send` is false when the line named files and said nothing else: that is
|
|
1010
|
+
* a drag and drop, and the files wait for the question that follows.
|
|
472
1011
|
*/
|
|
473
1012
|
const gather = async (typed) => {
|
|
474
|
-
const {
|
|
475
|
-
const
|
|
1013
|
+
const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
|
|
1014
|
+
const here = await sessionFolder();
|
|
1015
|
+
const root = resolveProject(here, place.added).root;
|
|
1016
|
+
completion.root = root;
|
|
1017
|
+
const found = attach.mentions(typed, root, here);
|
|
1018
|
+
const waiting = () => pending.map((a) => a.id);
|
|
476
1019
|
if (found.length === 0)
|
|
477
|
-
return { text: typed, ids: [] };
|
|
1020
|
+
return { text: typed, ids: waiting(), leftOut: [], send: true };
|
|
478
1021
|
out();
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
1022
|
+
const problems = found.filter((m) => m.problem);
|
|
1023
|
+
const readable = found.filter((m) => !m.problem);
|
|
1024
|
+
const { attached, refused } = readable.length > 0
|
|
1025
|
+
? await attach.upload(conn.url, conn.workspaceKey, readable.map((m) => m.file))
|
|
1026
|
+
: { attached: [], refused: [] };
|
|
1027
|
+
for (const line of attach.describe(attached))
|
|
1028
|
+
out(ui.c.dim(` ${line}`));
|
|
1029
|
+
for (const m of problems)
|
|
1030
|
+
out(ui.c.amber(` ${m.problem?.message}`));
|
|
483
1031
|
for (const r of refused)
|
|
484
|
-
out(ui.c.amber(` ${r.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
1032
|
+
out(ui.c.amber(` ${r.why}`));
|
|
1033
|
+
const failed = [...problems.map((m) => m.file), ...refused.map((r) => r.file)];
|
|
1034
|
+
const leftOut = failed.map((f) => path.basename(f));
|
|
1035
|
+
const landed = found.filter((m) => !failed.includes(m.file));
|
|
1036
|
+
const words = attach.withoutMentions(typed, found);
|
|
1037
|
+
pending = [...pending, ...attached];
|
|
1038
|
+
if (!words) {
|
|
1039
|
+
if (pending.length > 0)
|
|
1040
|
+
out(ui.c.dim(' Waiting for your question. `attached` lists what will go with it.'));
|
|
1041
|
+
out();
|
|
1042
|
+
return { text: '', ids: [], leftOut, send: false };
|
|
1043
|
+
}
|
|
1044
|
+
// Every file failed and the request was about them: nothing is sent until
|
|
1045
|
+
// the person says so (manual 3.10).
|
|
1046
|
+
if (attached.length === 0 && pending.length === 0) {
|
|
1047
|
+
const go = await confirm('None of those files could be attached. Send the request anyway? (y/n)');
|
|
1048
|
+
if (!go) {
|
|
1049
|
+
out(ui.c.dim(' Nothing was sent.'));
|
|
1050
|
+
out();
|
|
1051
|
+
return { text: '', ids: [], leftOut, send: false };
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return { text: attach.withoutPaths(typed, landed), ids: waiting(), leftOut, send: true };
|
|
490
1055
|
};
|
|
491
1056
|
// A way to stop the job that is running, while it is running. Null between
|
|
492
1057
|
// jobs, which is also the answer to "is anything running".
|
|
@@ -497,6 +1062,60 @@ async function runRepl() {
|
|
|
497
1062
|
/** Set while a job is running, so a line typed mid job reaches it instead of
|
|
498
1063
|
* queueing up behind it as the next question. */
|
|
499
1064
|
let steerJob = null;
|
|
1065
|
+
/** Everything else a running job can be asked: pause, carry on, the queue,
|
|
1066
|
+
* the full view (manual 6.7 to 6.9). Null between jobs. */
|
|
1067
|
+
let job = null;
|
|
1068
|
+
// Esc pauses a running job; at the prompt it does nothing (6.7).
|
|
1069
|
+
keyboard.onEscape = () => {
|
|
1070
|
+
if (!job || job.isPaused())
|
|
1071
|
+
return false;
|
|
1072
|
+
job.pause();
|
|
1073
|
+
return true;
|
|
1074
|
+
};
|
|
1075
|
+
// Ctrl+O, the short or the full view, from now on (6.9).
|
|
1076
|
+
keyboard.onFullView = () => {
|
|
1077
|
+
view.full = !view.full;
|
|
1078
|
+
job?.setFull(view.full);
|
|
1079
|
+
out();
|
|
1080
|
+
out(ui.c.dim(view.full ? ' full view: each action\'s input and output in full' : ' short view'));
|
|
1081
|
+
if (!job)
|
|
1082
|
+
prompt();
|
|
1083
|
+
};
|
|
1084
|
+
// Ctrl+G: the request so far, in your own editor (6.6). At the prompt only.
|
|
1085
|
+
keyboard.onEditor = () => {
|
|
1086
|
+
if (job || working)
|
|
1087
|
+
return;
|
|
1088
|
+
const edit = reader;
|
|
1089
|
+
const now = [...draft, edit?.line ?? ''].join('\n');
|
|
1090
|
+
draft = [];
|
|
1091
|
+
void editRequest(now);
|
|
1092
|
+
};
|
|
1093
|
+
// Ctrl+V: the clipboard's picture, as `paste` (3.4). At the prompt only;
|
|
1094
|
+
// what is typed so far stays on the line.
|
|
1095
|
+
keyboard.onPasteImage = () => {
|
|
1096
|
+
if (job || working)
|
|
1097
|
+
return;
|
|
1098
|
+
void pasteImage().then(() => prompt());
|
|
1099
|
+
};
|
|
1100
|
+
keyboard.vi = (0, machine_1.machineSettings)().keys === 'vi';
|
|
1101
|
+
// Ctrl+C: the job, then the line, then the session (6.7).
|
|
1102
|
+
interrupt = () => {
|
|
1103
|
+
if (job) {
|
|
1104
|
+
// An approval on screen is answered no, a question as stopped.
|
|
1105
|
+
takeAnswer(null);
|
|
1106
|
+
job.cancel();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
const edit = reader;
|
|
1110
|
+
if (draft.length > 0 || (edit && edit.line)) {
|
|
1111
|
+
draft = [];
|
|
1112
|
+
if (edit)
|
|
1113
|
+
edit.line = '';
|
|
1114
|
+
out();
|
|
1115
|
+
return prompt();
|
|
1116
|
+
}
|
|
1117
|
+
leave();
|
|
1118
|
+
};
|
|
500
1119
|
let leaving = false;
|
|
501
1120
|
/**
|
|
502
1121
|
* Ctrl-C, or `exit`.
|
|
@@ -515,13 +1134,21 @@ async function runRepl() {
|
|
|
515
1134
|
if (leaving)
|
|
516
1135
|
return;
|
|
517
1136
|
leaving = true;
|
|
518
|
-
|
|
1137
|
+
// The portal link ends with the session (manual 8.11).
|
|
1138
|
+
if (remote.session)
|
|
1139
|
+
void remoteOff(session, true);
|
|
1140
|
+
// A question on screen is answered as stopped, not as a word (4.3).
|
|
1141
|
+
takeAnswer(null);
|
|
519
1142
|
cancelJob?.();
|
|
520
1143
|
const close = () => {
|
|
521
1144
|
out();
|
|
1145
|
+
const said = (0, render_1.stoppedLines)((0, shell_1.stopEverything)());
|
|
1146
|
+
if (said)
|
|
1147
|
+
process.stdout.write(said);
|
|
522
1148
|
summarize(session);
|
|
523
1149
|
out();
|
|
524
1150
|
releaseTerminal();
|
|
1151
|
+
earlier.finish();
|
|
525
1152
|
done();
|
|
526
1153
|
};
|
|
527
1154
|
if (!working)
|
|
@@ -541,7 +1168,17 @@ async function runRepl() {
|
|
|
541
1168
|
reader.setPrompt(`${ui.c.bold(ui.c.blue('comprism'))} ${ui.c.dim('>')} `);
|
|
542
1169
|
reader.prompt();
|
|
543
1170
|
};
|
|
544
|
-
|
|
1171
|
+
await showProjectLine();
|
|
1172
|
+
// The command and skill list, and whether this company lets typed requests
|
|
1173
|
+
// be kept on this machine (manual 6.4, 6.5). Unknown keeps them for this
|
|
1174
|
+
// session only and leaves any file alone.
|
|
1175
|
+
await refreshList();
|
|
1176
|
+
// `config set history off` keeps nothing at all, not even for this session.
|
|
1177
|
+
const historyOff = (0, machine_1.machineSettings)().history === 'off';
|
|
1178
|
+
earlier.start(historyOff ? false : listed?.ok ? listed.keepHistory : null);
|
|
1179
|
+
if (historyOff)
|
|
1180
|
+
earlier.disable();
|
|
1181
|
+
out(ui.c.dim(' Type what you want done, or help. Ctrl+C to leave.'));
|
|
545
1182
|
out();
|
|
546
1183
|
// One line at a time, in the order they arrived.
|
|
547
1184
|
//
|
|
@@ -563,11 +1200,41 @@ async function runRepl() {
|
|
|
563
1200
|
}
|
|
564
1201
|
working = false;
|
|
565
1202
|
};
|
|
566
|
-
|
|
1203
|
+
// Requests sent from the portal while `remote` is on (manual 8.11). This
|
|
1204
|
+
// terminal asks the service for them every two seconds between jobs, which
|
|
1205
|
+
// is also how the service knows the session is still running here; they are
|
|
1206
|
+
// run here, on this machine, exactly as if typed, approvals included.
|
|
1207
|
+
remote.take = (text) => {
|
|
1208
|
+
out(`\n ${ui.c.bold('from the portal:')} ${text}`);
|
|
1209
|
+
queue.push(text);
|
|
1210
|
+
void drain();
|
|
1211
|
+
};
|
|
1212
|
+
remote.idle = () => !job && !working && !leaving;
|
|
1213
|
+
/** The lines of a request continued with `\\` or Alt+Enter, so far. */
|
|
1214
|
+
let draft = [];
|
|
1215
|
+
onLine = (typedLine) => {
|
|
1216
|
+
// Up and Down start again from the newest request after every line.
|
|
1217
|
+
if (reader)
|
|
1218
|
+
reader.historyIndex = -1;
|
|
1219
|
+
const continued = keyboard.takeContinued();
|
|
567
1220
|
// An answer the agent is waiting for jumps the queue, because it is not a
|
|
568
1221
|
// request. Queued, it would sit behind the very job that is waiting on it.
|
|
569
|
-
if (takeAnswer(
|
|
1222
|
+
if (draft.length === 0 && takeAnswer(keyboard.expand(typedLine)))
|
|
570
1223
|
return;
|
|
1224
|
+
// A request on more than one line (manual 6.2). Held until a line ends
|
|
1225
|
+
// without a continuation, then sent once with its line breaks.
|
|
1226
|
+
if (continued || typedLine.endsWith('\\')) {
|
|
1227
|
+
draft.push(continued ? typedLine : typedLine.slice(0, -1));
|
|
1228
|
+
reader?.setPrompt(ui.c.dim(' ... '));
|
|
1229
|
+
reader?.prompt();
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const shown = [...draft, typedLine].join('\n');
|
|
1233
|
+
draft = [];
|
|
1234
|
+
// Paste labels swapped back for what was pasted (6.3).
|
|
1235
|
+
const line = keyboard.expand(shown);
|
|
1236
|
+
if (line.trim())
|
|
1237
|
+
earlier.add(shown, line);
|
|
571
1238
|
// Something said while a job is RUNNING goes to that job, not into the
|
|
572
1239
|
// queue behind it. A person who sees the agent going the wrong way at step
|
|
573
1240
|
// three had two options before this: stop it and lose everything, or watch
|
|
@@ -575,6 +1242,38 @@ async function runRepl() {
|
|
|
575
1242
|
// conversation before the next step, which is the earliest moment it can
|
|
576
1243
|
// change anything.
|
|
577
1244
|
const said = line.trim();
|
|
1245
|
+
// Paused with Esc: whatever comes next, even an empty Enter, is the answer
|
|
1246
|
+
// to "carry on how" (manual 6.7).
|
|
1247
|
+
if (job?.isPaused()) {
|
|
1248
|
+
job.carryOn(said);
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
// `queue`, `queue clear` and `transcript` while it runs are for the
|
|
1252
|
+
// session, not notes for the agent (6.8, 6.9).
|
|
1253
|
+
if (job && /^\/?queue(\s+clear)?$/i.test(said)) {
|
|
1254
|
+
job.queue(/clear$/i.test(said));
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (job && /^\/?transcript$/i.test(said)) {
|
|
1258
|
+
void showTranscript(job.jobId());
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
if (job && processWord(said))
|
|
1262
|
+
return;
|
|
1263
|
+
// `agents` and `stop h2` while helpers run (manual 5.25).
|
|
1264
|
+
if (job && /^\/?agents$/i.test(said)) {
|
|
1265
|
+
const running = job.helpers();
|
|
1266
|
+
if (!running.length)
|
|
1267
|
+
out(ui.c.dim(' No helpers are running.'));
|
|
1268
|
+
for (const h of running)
|
|
1269
|
+
out(` ${ui.c.bold(h.id)} ${h.agent ? `${h.agent}: ` : ''}${h.question}`);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const helperStop = /^\/?stop\s+(h\d+)$/i.exec(said);
|
|
1273
|
+
if (job && helperStop) {
|
|
1274
|
+
job.stopHelper(helperStop[1].toLowerCase());
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
578
1277
|
if (steerJob && said) {
|
|
579
1278
|
// The session says nothing back. `steer` already prints its own
|
|
580
1279
|
// acknowledgement, and two lines for one thing typed reads as a stutter.
|
|
@@ -588,13 +1287,319 @@ async function runRepl() {
|
|
|
588
1287
|
// existed printed nothing, and the session looked dead on arrival.
|
|
589
1288
|
takeTerminal();
|
|
590
1289
|
prompt();
|
|
1290
|
+
// Shift+Tab moves to the next permission mode (manual 4.5). Only at the
|
|
1291
|
+
// prompt: during a job the change would apply from the next job anyway, and
|
|
1292
|
+
// redrawing over a running job's output is unreadable.
|
|
1293
|
+
keys().on('keypress', (_s, key) => {
|
|
1294
|
+
if (!key || key.name !== 'tab' || !key.shift || working || answering || leaving)
|
|
1295
|
+
return;
|
|
1296
|
+
const next = MODES[(MODES.indexOf(steering_.mode) + 1) % MODES.length];
|
|
1297
|
+
steering_.mode = next;
|
|
1298
|
+
out();
|
|
1299
|
+
out(ui.c.dim(` mode ${ui.c.bold(next)}, from the next job`));
|
|
1300
|
+
prompt();
|
|
1301
|
+
});
|
|
591
1302
|
async function handle(line) {
|
|
592
1303
|
const typed = line.trim();
|
|
593
1304
|
if (!typed)
|
|
594
1305
|
return prompt();
|
|
595
|
-
const { word, slashed } = commandWord(typed);
|
|
1306
|
+
const { word, slashed: slashWord } = commandWord(typed);
|
|
1307
|
+
// A dragged file starting with `/` is a path, not a slashed command
|
|
1308
|
+
// (manual 3.3). Only when the whole first word is something on the disk.
|
|
1309
|
+
const slashed = slashWord && !attach.startsWithPath(typed);
|
|
1310
|
+
// Where the session works (manual 2.1 to 2.3). Read off the typed line, not
|
|
1311
|
+
// `word`, because a path keeps its case.
|
|
1312
|
+
const bare = typed.replace(/^\//, '').replace(/^comprism\s+/i, '').trim();
|
|
1313
|
+
// The instructions in force, and a note for this project (manual 2.8, 2.9).
|
|
1314
|
+
if (/^instructions$/i.test(bare)) {
|
|
1315
|
+
const { cmdInstructions } = await Promise.resolve().then(() => __importStar(require('./instructions')));
|
|
1316
|
+
await withTerminal(async () => { await cmdInstructions([]); });
|
|
1317
|
+
return prompt();
|
|
1318
|
+
}
|
|
1319
|
+
const remember = /^remember\s+(.+)$/i.exec(bare);
|
|
1320
|
+
if (remember) {
|
|
1321
|
+
const { addNote } = await Promise.resolve().then(() => __importStar(require('./memory')));
|
|
1322
|
+
await addNote(remember[1]);
|
|
1323
|
+
out();
|
|
1324
|
+
return prompt();
|
|
1325
|
+
}
|
|
1326
|
+
const where = /^(project|workspace)(?:\s+(.+))?$/i.exec(bare);
|
|
1327
|
+
const addFolder = /^folders\s+add\s+(.+)$/i.exec(bare);
|
|
1328
|
+
if (where || addFolder || /^folders$/i.test(bare)) {
|
|
1329
|
+
const { checkFolder, resolveProject } = await Promise.resolve().then(() => __importStar(require('../lib/project')));
|
|
1330
|
+
const named = where ? where[2] : addFolder ? addFolder[1] : undefined;
|
|
1331
|
+
if (named) {
|
|
1332
|
+
const found = checkFolder(named.trim());
|
|
1333
|
+
if (!found.ok) {
|
|
1334
|
+
out(ui.fail(found.why));
|
|
1335
|
+
out();
|
|
1336
|
+
return prompt();
|
|
1337
|
+
}
|
|
1338
|
+
if (where) {
|
|
1339
|
+
place.folder = found.path;
|
|
1340
|
+
(await Promise.resolve().then(() => __importStar(require('../lib/project-ops')))).useSessionFolder(found.path);
|
|
1341
|
+
}
|
|
1342
|
+
else if (!place.added.includes(found.path))
|
|
1343
|
+
place.added.push(found.path);
|
|
1344
|
+
}
|
|
1345
|
+
if (/^folders$/i.test(bare)) {
|
|
1346
|
+
const facts = resolveProject(await sessionFolder(), place.added);
|
|
1347
|
+
out();
|
|
1348
|
+
out(` ${ui.c.bold(facts.root)} ${ui.c.dim('project')}`);
|
|
1349
|
+
for (const f of facts.added)
|
|
1350
|
+
out(` ${ui.c.bold(f)} ${ui.c.dim('added for this session')}`);
|
|
1351
|
+
out();
|
|
1352
|
+
return prompt();
|
|
1353
|
+
}
|
|
1354
|
+
out();
|
|
1355
|
+
await showProjectLine();
|
|
1356
|
+
// Another folder has other skills.
|
|
1357
|
+
await refreshList();
|
|
1358
|
+
return prompt();
|
|
1359
|
+
}
|
|
1360
|
+
// `pin <model>` and `effort <level>` (manual 4.17, 4.18), kept by the
|
|
1361
|
+
// service for this session, so the browser shows the same pin.
|
|
1362
|
+
const pinTo = /^pin\s+(\S+)$/.exec(word);
|
|
1363
|
+
if (pinTo) {
|
|
1364
|
+
const said = await steer({ pin: pinTo[1] });
|
|
1365
|
+
if (said)
|
|
1366
|
+
out(ui.c.dim(` pinned ${ui.c.bold(said.pin_label ?? steering_.pin)} for the next requests. \`unpin\` hands the choice back.`));
|
|
1367
|
+
out();
|
|
1368
|
+
return prompt();
|
|
1369
|
+
}
|
|
1370
|
+
const effortTo = /^effort\s+(\S+)$/.exec(word);
|
|
1371
|
+
if (effortTo) {
|
|
1372
|
+
const level = effortTo[1];
|
|
1373
|
+
if (!['low', 'medium', 'normal', 'high'].includes(level))
|
|
1374
|
+
out(ui.fail('Effort is low, normal or high.'));
|
|
1375
|
+
else {
|
|
1376
|
+
const said = await steer({ effort: level === 'medium' ? 'normal' : level });
|
|
1377
|
+
if (said) {
|
|
1378
|
+
out(ui.c.dim(` effort ${ui.c.bold(said.effort)} for the next requests`));
|
|
1379
|
+
if (said.effort_note)
|
|
1380
|
+
out(ui.c.dim(` ${said.effort_note}`));
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
out();
|
|
1384
|
+
return prompt();
|
|
1385
|
+
}
|
|
1386
|
+
// `mode <name>` and `plan off` (manual 4.1, 4.5).
|
|
1387
|
+
const modeTo = /^mode\s+(\S+)$/.exec(word);
|
|
1388
|
+
if (modeTo) {
|
|
1389
|
+
const named = modeTo[1];
|
|
1390
|
+
if (!MODES.includes(named)) {
|
|
1391
|
+
out(ui.fail(`${named} is not a permission mode. One of: ${MODES.join(', ')}`));
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
steering_.mode = named;
|
|
1395
|
+
out(ui.c.dim(` mode ${ui.c.bold(named)}, from the next job`));
|
|
1396
|
+
}
|
|
1397
|
+
out();
|
|
1398
|
+
return prompt();
|
|
1399
|
+
}
|
|
1400
|
+
if (word === 'plan off') {
|
|
1401
|
+
steering_.plan = false;
|
|
1402
|
+
out(ui.c.dim(' planning off: the next requests are carried out'));
|
|
1403
|
+
out();
|
|
1404
|
+
return prompt();
|
|
1405
|
+
}
|
|
1406
|
+
if (processWord(word))
|
|
1407
|
+
return prompt();
|
|
1408
|
+
if (/^\/?agents$/i.test(word)) {
|
|
1409
|
+
await showHelpers();
|
|
1410
|
+
return prompt();
|
|
1411
|
+
}
|
|
1412
|
+
if (/^\/?stop\s+h\d+$/i.test(word)) {
|
|
1413
|
+
out(ui.c.dim(' No job is running, so there are no helpers to stop.'));
|
|
1414
|
+
out();
|
|
1415
|
+
return prompt();
|
|
1416
|
+
}
|
|
1417
|
+
// `output 3`: a command's whole output, kept on this machine (5.12).
|
|
1418
|
+
const outputOf = /^output(?:\s+(\d+))?$/.exec(word);
|
|
1419
|
+
if (outputOf) {
|
|
1420
|
+
const { keptOutput } = await Promise.resolve().then(() => __importStar(require('../executor/shell')));
|
|
1421
|
+
const kept = outputOf[1] ? keptOutput(Number(outputOf[1])) : undefined;
|
|
1422
|
+
if (!outputOf[1])
|
|
1423
|
+
out(ui.c.dim(' output N shows the whole output of command N, the number on its line.'));
|
|
1424
|
+
else if (!kept)
|
|
1425
|
+
out(ui.fail(`No output ${outputOf[1]} is kept in this session.`));
|
|
1426
|
+
else {
|
|
1427
|
+
out(ui.c.dim(` output ${outputOf[1]}: ${kept.command}`));
|
|
1428
|
+
out(kept.text.replace(/\n$/, '') || ui.c.dim(' (no output)'));
|
|
1429
|
+
}
|
|
1430
|
+
out();
|
|
1431
|
+
return prompt();
|
|
1432
|
+
}
|
|
1433
|
+
// `detach 2` takes the second waiting file off the list (manual 3.6).
|
|
1434
|
+
const detach = /^detach\s+(\d+)$/.exec(word);
|
|
1435
|
+
if (detach) {
|
|
1436
|
+
const n = Number(detach[1]);
|
|
1437
|
+
const gone = pending[n - 1];
|
|
1438
|
+
if (!gone) {
|
|
1439
|
+
out(ui.fail(pending.length === 0
|
|
1440
|
+
? 'Nothing is waiting to be attached.'
|
|
1441
|
+
: `There is no attachment ${n}. Type \`attached\` for the list.`));
|
|
1442
|
+
}
|
|
1443
|
+
else {
|
|
1444
|
+
pending = pending.filter((_, i) => i !== n - 1);
|
|
1445
|
+
out(ui.c.dim(` detached ${gone.filename}`));
|
|
1446
|
+
}
|
|
1447
|
+
out();
|
|
1448
|
+
return prompt();
|
|
1449
|
+
}
|
|
1450
|
+
// `changes`, `diff` and `rewind`, with what follows them (manual 7.1 to 7.6).
|
|
1451
|
+
const reviewWord = /^(changes|diff|rewind)(?:\s+(.*))?$/.exec(word);
|
|
1452
|
+
if (reviewWord) {
|
|
1453
|
+
const w = reviewWord[1];
|
|
1454
|
+
const rest = reviewWord[2] ?? '';
|
|
1455
|
+
// What the agent changed in this session, its diff, and putting it
|
|
1456
|
+
// back, files and conversation (manual 7.1 to 7.6).
|
|
1457
|
+
if (w === 'changes' || w === 'diff') {
|
|
1458
|
+
const done = session.jobs.filter((j) => j.jobId);
|
|
1459
|
+
if (!done.length) {
|
|
1460
|
+
out(ui.c.dim(' Nothing has run in this session yet.'));
|
|
1461
|
+
out();
|
|
1462
|
+
return prompt();
|
|
1463
|
+
}
|
|
1464
|
+
const { cmdChanges, cmdDiff } = await Promise.resolve().then(() => __importStar(require('./review')));
|
|
1465
|
+
for (const j of done) {
|
|
1466
|
+
if (w === 'changes')
|
|
1467
|
+
await cmdChanges([j.jobId]);
|
|
1468
|
+
else
|
|
1469
|
+
await cmdDiff([j.jobId, ...rest.split(/\s+/).filter(Boolean)]);
|
|
1470
|
+
}
|
|
1471
|
+
return prompt();
|
|
1472
|
+
}
|
|
1473
|
+
if (w === 'rewind') {
|
|
1474
|
+
const done = session.jobs.filter((j) => j.jobId);
|
|
1475
|
+
if (!done.length) {
|
|
1476
|
+
out(ui.c.dim(' Nothing has run in this session yet, so there is nothing to rewind.'));
|
|
1477
|
+
out();
|
|
1478
|
+
return prompt();
|
|
1479
|
+
}
|
|
1480
|
+
const conversation = /--conversation|--both/.test(rest);
|
|
1481
|
+
const files = !/--conversation/.test(rest) || /--both/.test(rest);
|
|
1482
|
+
out();
|
|
1483
|
+
out(` ${conversation ? 'Your requests in this session' : 'The steps of this session'}:`);
|
|
1484
|
+
done.forEach((j, i) => out(` ${i + 1} ${j.request.slice(0, 70)}`));
|
|
1485
|
+
const picked = await askLine(` Go back to before which request? [1 to ${done.length}, Enter for ${done.length}] `);
|
|
1486
|
+
if (picked === null) {
|
|
1487
|
+
out();
|
|
1488
|
+
return prompt();
|
|
1489
|
+
}
|
|
1490
|
+
const n = picked.trim() ? Number(picked.trim()) : done.length;
|
|
1491
|
+
if (!Number.isInteger(n) || n < 1 || n > done.length) {
|
|
1492
|
+
out(ui.fail(`Choose a number from 1 to ${done.length}.`));
|
|
1493
|
+
out();
|
|
1494
|
+
return prompt();
|
|
1495
|
+
}
|
|
1496
|
+
if (files) {
|
|
1497
|
+
// The files of every job from that request on, latest first.
|
|
1498
|
+
const { cmdRewind } = await Promise.resolve().then(() => __importStar(require('./review')));
|
|
1499
|
+
const yesNo = async (q) => /^y(es)?$/i.test(((await askLine(q)) ?? '').trim());
|
|
1500
|
+
for (const j of done.slice(n - 1).reverse())
|
|
1501
|
+
await cmdRewind([j.jobId], yesNo);
|
|
1502
|
+
}
|
|
1503
|
+
if (conversation) {
|
|
1504
|
+
const { sessions: ask8, failure } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
1505
|
+
const before = done[n - 2]?.jobId ?? '';
|
|
1506
|
+
const reply = await ask8('branch', { key: session.serverId, at_job: before });
|
|
1507
|
+
if (!reply.ok || !reply.sessions?.session_id) {
|
|
1508
|
+
out(ui.fail(failure(reply)));
|
|
1509
|
+
out();
|
|
1510
|
+
return prompt();
|
|
1511
|
+
}
|
|
1512
|
+
summarize(session);
|
|
1513
|
+
const kept = done.slice(0, n - 1);
|
|
1514
|
+
session = reopened(reply.sessions);
|
|
1515
|
+
session.jobs = kept.map((j) => ({ ...j }));
|
|
1516
|
+
history.push(session);
|
|
1517
|
+
out(ui.ok(`The conversation is back to before request ${n}, as a new branch.`));
|
|
1518
|
+
for (const line of reply.sessions.lines)
|
|
1519
|
+
out(line);
|
|
1520
|
+
out(ui.c.dim(' The original, with everything it cost, is kept and can be reopened.'));
|
|
1521
|
+
out();
|
|
1522
|
+
}
|
|
1523
|
+
return prompt();
|
|
1524
|
+
}
|
|
1525
|
+
return prompt();
|
|
1526
|
+
}
|
|
596
1527
|
if (COMMANDS.has(word)) {
|
|
597
1528
|
switch (word) {
|
|
1529
|
+
case 'plan':
|
|
1530
|
+
steering_.plan = true;
|
|
1531
|
+
out(ui.c.dim(' planning on: the next requests are planned first and change nothing. `plan off` stops.'));
|
|
1532
|
+
out();
|
|
1533
|
+
break;
|
|
1534
|
+
case 'mode':
|
|
1535
|
+
out(` ${ui.c.dim('mode')} ${ui.c.bold(steering_.mode)}${steering_.plan ? ui.c.dim(', planning first') : ''}`);
|
|
1536
|
+
out(ui.c.dim(` one of ${MODES.join(', ')}. \`mode <name>\` or Shift+Tab changes it.`));
|
|
1537
|
+
out();
|
|
1538
|
+
break;
|
|
1539
|
+
case 'pin': {
|
|
1540
|
+
const said = await steer({});
|
|
1541
|
+
out(said?.pin
|
|
1542
|
+
? ` ${ui.c.dim('pinned')} ${ui.c.bold(said.pin_label ?? said.pin)}`
|
|
1543
|
+
: ui.c.dim(' Nothing is pinned; the service chooses the model for each step. `pin <model>` pins one.'));
|
|
1544
|
+
out();
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
case 'unpin':
|
|
1548
|
+
if (await steer({ pin: '' }))
|
|
1549
|
+
out(ui.c.dim(' unpinned: the service chooses the model for each step again'));
|
|
1550
|
+
out();
|
|
1551
|
+
break;
|
|
1552
|
+
case 'effort': {
|
|
1553
|
+
const said = await steer({});
|
|
1554
|
+
out(ui.c.dim(said ? ` effort ${said.effort}` : ' effort not known.'));
|
|
1555
|
+
if (said?.effort_note)
|
|
1556
|
+
out(ui.c.dim(` ${said.effort_note}`));
|
|
1557
|
+
out();
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
case 'why':
|
|
1561
|
+
if (!session.why || session.why.length === 0)
|
|
1562
|
+
out(ui.c.dim(' No step has run in this session yet.'));
|
|
1563
|
+
else
|
|
1564
|
+
for (const line of session.why)
|
|
1565
|
+
out(line);
|
|
1566
|
+
out();
|
|
1567
|
+
break;
|
|
1568
|
+
case 'todo':
|
|
1569
|
+
if (!session.todos || session.todos.length === 0)
|
|
1570
|
+
out(ui.c.dim(' No checklist yet.'));
|
|
1571
|
+
else
|
|
1572
|
+
out((0, render_1.todos)(session.todos).trimEnd());
|
|
1573
|
+
out();
|
|
1574
|
+
break;
|
|
1575
|
+
case 'edit':
|
|
1576
|
+
// Returns without the redraw below: the editor puts the text back at
|
|
1577
|
+
// the prompt itself.
|
|
1578
|
+
return editRequest('');
|
|
1579
|
+
case 'context':
|
|
1580
|
+
await contextOf('context', session.lastJobId, '', session.serverId || session.id);
|
|
1581
|
+
break;
|
|
1582
|
+
case 'compact':
|
|
1583
|
+
await contextOf('compact', session.lastJobId);
|
|
1584
|
+
break;
|
|
1585
|
+
case 'queue':
|
|
1586
|
+
// Between jobs nothing can be waiting: a note typed now is a request.
|
|
1587
|
+
out(ui.c.dim(' No notes are waiting. Notes typed while a job runs join it at its next step.'));
|
|
1588
|
+
out();
|
|
1589
|
+
break;
|
|
1590
|
+
case 'transcript':
|
|
1591
|
+
await showTranscript(session.lastJobId);
|
|
1592
|
+
break;
|
|
1593
|
+
case 'paste':
|
|
1594
|
+
await pasteImage();
|
|
1595
|
+
break;
|
|
1596
|
+
case 'attached':
|
|
1597
|
+
if (pending.length === 0)
|
|
1598
|
+
out(ui.c.dim(' Nothing is waiting to be attached.'));
|
|
1599
|
+
for (const row of attach.listing(pending))
|
|
1600
|
+
out(` ${row}`);
|
|
1601
|
+
out();
|
|
1602
|
+
break;
|
|
598
1603
|
case 'exit':
|
|
599
1604
|
case 'quit':
|
|
600
1605
|
return leave();
|
|
@@ -602,13 +1607,43 @@ async function runRepl() {
|
|
|
602
1607
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
603
1608
|
break;
|
|
604
1609
|
case 'help':
|
|
1610
|
+
// Asked again, so a skill saved a minute ago is on the list.
|
|
1611
|
+
await refreshList();
|
|
605
1612
|
printHelp();
|
|
606
1613
|
break;
|
|
1614
|
+
// A new session from a copy of this one's conversation (manual 8.6).
|
|
1615
|
+
case 'branch': {
|
|
1616
|
+
if (session.jobs.length === 0) {
|
|
1617
|
+
out(ui.c.dim(' Nothing has run in this session yet, so there is nothing to branch.'));
|
|
1618
|
+
out();
|
|
1619
|
+
break;
|
|
1620
|
+
}
|
|
1621
|
+
const { sessions: ask8, failure } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
1622
|
+
const reply = await ask8('branch', { key: session.serverId });
|
|
1623
|
+
if (!reply.ok || !reply.sessions?.session_id) {
|
|
1624
|
+
out(ui.fail(failure(reply)));
|
|
1625
|
+
out();
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
summarize(session);
|
|
1629
|
+
session = reopened(reply.sessions);
|
|
1630
|
+
history.push(session);
|
|
1631
|
+
for (const line of reply.sessions.lines)
|
|
1632
|
+
out(line);
|
|
1633
|
+
out();
|
|
1634
|
+
break;
|
|
1635
|
+
}
|
|
1636
|
+
case 'remote': {
|
|
1637
|
+
await remoteOn(session);
|
|
1638
|
+
break;
|
|
1639
|
+
}
|
|
607
1640
|
case 'new': {
|
|
1641
|
+
await remoteOff(session, true);
|
|
608
1642
|
summarize(session);
|
|
609
1643
|
session = newSession();
|
|
610
1644
|
history.push(session);
|
|
611
|
-
|
|
1645
|
+
const line = await numbered(session);
|
|
1646
|
+
out(line ? ui.c.bold(line) : ui.ok('New session.'));
|
|
612
1647
|
out();
|
|
613
1648
|
break;
|
|
614
1649
|
}
|
|
@@ -616,20 +1651,22 @@ async function runRepl() {
|
|
|
616
1651
|
out();
|
|
617
1652
|
history.forEach((s, i) => {
|
|
618
1653
|
const here = s === session ? ui.c.green(' now') : ' ';
|
|
1654
|
+
// The NAME where our server has made one, and the id beside it
|
|
1655
|
+
// rather than instead of it: the id is what a person types back to
|
|
1656
|
+
// reopen the session, so it can never be replaced by the name.
|
|
1657
|
+
const named = s.title ? ` ${ui.c.text(s.title)}` : '';
|
|
619
1658
|
out(`${here} ${String(i + 1).padStart(2)}. ${ui.c.bold(s.id)}`
|
|
620
|
-
+ ` ${s.jobs.length} job(s) ${money(s.spentUsd)}`);
|
|
1659
|
+
+ ` ${s.jobs.length} job(s) ${money(s.spentUsd)}${named}`);
|
|
621
1660
|
});
|
|
622
|
-
// And what this account did before this window was opened
|
|
623
|
-
//
|
|
624
|
-
|
|
1661
|
+
// And what this account did before this window was opened, from
|
|
1662
|
+
// the service's record of sessions (manual 8.5). Kept numbered
|
|
1663
|
+
// separately so a number never means two different things.
|
|
1664
|
+
recent = await earlierSessions(history);
|
|
625
1665
|
if (recent.length > 0) {
|
|
626
1666
|
out();
|
|
627
1667
|
out(ui.c.dim(' earlier work on this account:'));
|
|
628
|
-
recent.forEach((
|
|
629
|
-
|
|
630
|
-
.replace(/\s+/g, ' ').slice(0, 58);
|
|
631
|
-
out(` ${ui.c.bold(`w${i + 1}`)}. ${said}`
|
|
632
|
-
+ ui.c.dim(` ${j.when.slice(0, 16).replace('T', ' ')} ${money(j.usd)}`));
|
|
1668
|
+
recent.forEach((r, i) => {
|
|
1669
|
+
out(` ${ui.c.bold(`w${i + 1}`.padStart(3))}. ${r.line.trimStart()}`);
|
|
633
1670
|
});
|
|
634
1671
|
}
|
|
635
1672
|
out();
|
|
@@ -643,6 +1680,24 @@ async function runRepl() {
|
|
|
643
1680
|
await cmdStatus();
|
|
644
1681
|
break;
|
|
645
1682
|
}
|
|
1683
|
+
// The session being stood in, unless an id is typed after it. Asking
|
|
1684
|
+
// the server for "the newest" here would answer about a different
|
|
1685
|
+
// session the moment two are open, and this one is on the screen.
|
|
1686
|
+
case 'cost':
|
|
1687
|
+
case 'spend': {
|
|
1688
|
+
// Nothing sent is nothing charged, and the service has no record of
|
|
1689
|
+
// a session before its first job, so it would answer "no such
|
|
1690
|
+
// session" to a person checking that a refusal cost them nothing
|
|
1691
|
+
// (manual 6.1).
|
|
1692
|
+
if (session.jobs.length === 0) {
|
|
1693
|
+
out(` ${ui.c.dim('session')} ${ui.c.bold(session.id)} ${ui.c.dim('nothing has run yet, so it has cost')} $0.00`);
|
|
1694
|
+
out();
|
|
1695
|
+
break;
|
|
1696
|
+
}
|
|
1697
|
+
const { cmdCost } = await Promise.resolve().then(() => __importStar(require('./cost')));
|
|
1698
|
+
await cmdCost([session.id]);
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
646
1701
|
case 'model': {
|
|
647
1702
|
const { cmdModel } = await Promise.resolve().then(() => __importStar(require('./settings')));
|
|
648
1703
|
await withTerminal(() => cmdModel([]));
|
|
@@ -660,11 +1715,6 @@ async function runRepl() {
|
|
|
660
1715
|
case 'keys':
|
|
661
1716
|
await withTerminal(() => (0, keys_1.cmdKeys)([]));
|
|
662
1717
|
break;
|
|
663
|
-
case 'workspace': {
|
|
664
|
-
const { cmdWorkspace } = await Promise.resolve().then(() => __importStar(require('./settings')));
|
|
665
|
-
await withTerminal(() => cmdWorkspace([]));
|
|
666
|
-
break;
|
|
667
|
-
}
|
|
668
1718
|
// Signing out ends the session, because everything after it would be
|
|
669
1719
|
// refused anyway. It used to answer "not available inside a session",
|
|
670
1720
|
// which left quitting and running a second command as the only way.
|
|
@@ -681,6 +1731,11 @@ async function runRepl() {
|
|
|
681
1731
|
}
|
|
682
1732
|
return prompt();
|
|
683
1733
|
}
|
|
1734
|
+
// `remote off` ends the link (manual 8.11).
|
|
1735
|
+
if (/^\/?remote\s+off$/.test(word)) {
|
|
1736
|
+
await remoteOff(session);
|
|
1737
|
+
return prompt();
|
|
1738
|
+
}
|
|
684
1739
|
// `sessions 2` reopens the second one from this run.
|
|
685
1740
|
const reopen = /^sessions\s+(\d+)$/.exec(word);
|
|
686
1741
|
if (reopen) {
|
|
@@ -696,6 +1751,22 @@ async function runRepl() {
|
|
|
696
1751
|
out();
|
|
697
1752
|
return prompt();
|
|
698
1753
|
}
|
|
1754
|
+
// `compact keep the requirement that ...` keeps those words exactly (6.15).
|
|
1755
|
+
// Read off `typed`, because what to keep keeps its case.
|
|
1756
|
+
const compactKeep = /^\/?compact\s+(.+)$/is.exec(typed);
|
|
1757
|
+
if (compactKeep) {
|
|
1758
|
+
await contextOf('compact', session.lastJobId, compactKeep[1].replace(/^keep\s+/i, '').trim());
|
|
1759
|
+
return prompt();
|
|
1760
|
+
}
|
|
1761
|
+
// `cost <session>` breaks down a session other than the one being stood in.
|
|
1762
|
+
// Read off `typed` rather than `word`, because `word` is lowercased so that
|
|
1763
|
+
// command matching is forgiving, and a session id is not a word.
|
|
1764
|
+
const costOf = /^\/?(?:cost|spend)\s+(\S+)$/i.exec(typed);
|
|
1765
|
+
if (costOf) {
|
|
1766
|
+
const { cmdCost } = await Promise.resolve().then(() => __importStar(require('./cost')));
|
|
1767
|
+
await cmdCost([costOf[1]]);
|
|
1768
|
+
return prompt();
|
|
1769
|
+
}
|
|
699
1770
|
// `sessions w1` picks up work from before this window was opened. A new
|
|
700
1771
|
// session is started rather than an old one edited: what it inherits is the
|
|
701
1772
|
// conversation, not the spending, and adding yesterday's cost to today's
|
|
@@ -703,27 +1774,37 @@ async function runRepl() {
|
|
|
703
1774
|
const carryOn = /^sessions\s+w(\d+)$/.exec(word);
|
|
704
1775
|
if (carryOn) {
|
|
705
1776
|
if (recent.length === 0)
|
|
706
|
-
recent = await
|
|
1777
|
+
recent = await earlierSessions(history);
|
|
707
1778
|
const target = recent[Number(carryOn[1]) - 1];
|
|
708
1779
|
if (!target) {
|
|
709
|
-
out(ui.fail(`There is no earlier
|
|
1780
|
+
out(ui.fail(`There is no earlier session w${carryOn[1]}. Type \`sessions\` for the list.`));
|
|
1781
|
+
out();
|
|
1782
|
+
return prompt();
|
|
710
1783
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1784
|
+
// The same session, resumed: its conversation and its running cost carry
|
|
1785
|
+
// on, and it never becomes a new session that has lost them (8.5).
|
|
1786
|
+
summarize(session);
|
|
1787
|
+
const found = await opening({ resume: target.number });
|
|
1788
|
+
if (found) {
|
|
1789
|
+
session = found;
|
|
715
1790
|
history.push(session);
|
|
716
|
-
out(ui.ok('Picking up where that left off.'));
|
|
717
|
-
out(ui.c.dim(` ${(target.name || target.request).replace(/\s+/g, ' ').slice(0, 70)}`));
|
|
718
|
-
out(ui.c.dim(' Ask your next question and it will know what was already said.'));
|
|
719
1791
|
}
|
|
720
|
-
out();
|
|
721
1792
|
return prompt();
|
|
722
1793
|
}
|
|
723
1794
|
// A slash means "this is a command", so an unknown one is a typo and not a
|
|
724
1795
|
// question. Sending it on would answer the typo and charge for the answer,
|
|
725
1796
|
// which is exactly what `/model` used to do.
|
|
726
|
-
|
|
1797
|
+
// A skill or saved command on the service's list is work for the agent,
|
|
1798
|
+
// slash and all: the service expands it (chapter 9).
|
|
1799
|
+
const isListed = () => Boolean(listed?.ok && listed.items.some((i) => i.source !== 'session' && i.name === word.split(/\s+/)[0]));
|
|
1800
|
+
let offered = isListed();
|
|
1801
|
+
if (slashed && !offered) {
|
|
1802
|
+
// A skill written since the list was read is found by asking again,
|
|
1803
|
+
// once, before the word is called a typo (manual 9.1, 9.2).
|
|
1804
|
+
await refreshList();
|
|
1805
|
+
offered = isListed();
|
|
1806
|
+
}
|
|
1807
|
+
if (slashed && !offered) {
|
|
727
1808
|
out(ui.fail(`\`${typed}\` is not a command, so nothing ran and nothing was charged.`));
|
|
728
1809
|
out(ui.c.dim(' Type `help` for the list, or ask without a slash to have work done.'));
|
|
729
1810
|
out();
|
|
@@ -738,14 +1819,22 @@ async function runRepl() {
|
|
|
738
1819
|
// act, so the second path was never worth the confusion it caused.
|
|
739
1820
|
const explicit = /^work\s+(.+)$/i.exec(typed);
|
|
740
1821
|
const asked = explicit ? explicit[1] : typed;
|
|
741
|
-
const { text, ids } = await gather(asked);
|
|
1822
|
+
const { text, ids, leftOut, send } = await gather(asked);
|
|
1823
|
+
if (!send)
|
|
1824
|
+
return prompt();
|
|
1825
|
+
// Sent with this request, so no longer waiting for the next one.
|
|
1826
|
+
pending = [];
|
|
742
1827
|
try {
|
|
743
|
-
await build(session, text || asked, confirm, (controls) => { cancelJob = controls.cancel; steerJob = controls.steer; }, ids);
|
|
1828
|
+
await build(session, text || asked, confirm, (controls) => { job = controls; cancelJob = controls.cancel; steerJob = controls.steer; }, ids, leftOut, askLine);
|
|
744
1829
|
}
|
|
745
1830
|
finally {
|
|
1831
|
+
job = null;
|
|
746
1832
|
cancelJob = null;
|
|
747
1833
|
steerJob = null;
|
|
748
1834
|
}
|
|
1835
|
+
// A note that arrived after the last step is the next request (6.8).
|
|
1836
|
+
queue.unshift(...view.late);
|
|
1837
|
+
view.late = [];
|
|
749
1838
|
prompt();
|
|
750
1839
|
}
|
|
751
1840
|
await finished;
|