@illuminis/comprism 0.1.4 → 0.1.6
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/out/agent/command.js +5 -5
- package/out/agent/render.d.ts +39 -6
- package/out/agent/render.js +287 -28
- package/out/agent/session.d.ts +21 -1
- package/out/agent/session.js +81 -17
- package/out/commands/agents.js +3 -3
- package/out/commands/ask.js +20 -11
- package/out/commands/codemap.d.ts +1 -1
- package/out/commands/codemap.js +3 -3
- package/out/commands/commands-thin.js +4 -4
- package/out/commands/config.js +1 -1
- package/out/commands/cost.js +4 -4
- package/out/commands/hooks.js +1 -1
- package/out/commands/install.d.ts +1 -1
- package/out/commands/install.js +3 -2
- package/out/commands/instructions.js +1 -1
- package/out/commands/integrations.js +4 -4
- package/out/commands/keys.js +6 -6
- package/out/commands/login.js +97 -27
- package/out/commands/permissions.js +2 -2
- package/out/commands/plugins.js +3 -3
- package/out/commands/privacy.js +2 -2
- package/out/commands/repl.d.ts +4 -0
- package/out/commands/repl.js +563 -101
- package/out/commands/review.js +6 -6
- package/out/commands/settings.js +30 -123
- package/out/commands/skills.js +2 -2
- package/out/commands/unattended.js +6 -6
- package/out/commands/update.js +1 -1
- package/out/commands/welcome.d.ts +25 -1
- package/out/commands/welcome.js +65 -38
- package/out/commands/worktrees.d.ts +1 -1
- package/out/graph/sync.d.ts +1 -1
- package/out/graph/sync.js +6 -6
- package/out/lib/attach.js +4 -3
- package/out/lib/commandlist.d.ts +1 -1
- package/out/lib/commandlist.js +2 -2
- package/out/lib/config.d.ts +3 -2
- package/out/lib/config.js +7 -8
- package/out/lib/connection.js +1 -1
- package/out/lib/gateway.d.ts +9 -5
- package/out/lib/keyboard.d.ts +3 -0
- package/out/lib/keyboard.js +7 -0
- package/out/lib/lastlogin.d.ts +14 -0
- package/out/lib/lastlogin.js +105 -0
- package/out/lib/machine.js +1 -1
- package/out/lib/picker.d.ts +1 -0
- package/out/lib/picker.js +158 -0
- package/out/lib/project-ops.d.ts +1 -1
- package/out/lib/project-ops.js +4 -4
- package/out/lib/prompt.d.ts +3 -0
- package/out/lib/prompt.js +21 -0
- package/out/lib/queue.js +1 -1
- package/out/lib/readiness.d.ts +3 -30
- package/out/lib/readiness.js +13 -82
- package/out/lib/servicecommand.d.ts +14 -0
- package/out/lib/servicecommand.js +74 -0
- package/out/lib/sessions.js +3 -3
- package/out/lib/startcache.d.ts +13 -0
- package/out/lib/startcache.js +78 -0
- package/out/lib/ui.d.ts +11 -0
- package/out/lib/ui.js +28 -0
- package/out/lib/voice.d.ts +36 -0
- package/out/lib/voice.js +137 -2
- package/out/lib/words.d.ts +20 -0
- package/out/lib/words.js +27 -0
- package/out/machine/codemap/build.d.ts +45 -0
- package/out/machine/codemap/build.js +91 -0
- package/out/machine/codemap/facts.d.ts +47 -0
- package/out/machine/codemap/facts.js +12 -0
- package/out/machine/codemap/files.d.ts +45 -0
- package/out/machine/codemap/files.js +207 -0
- package/out/machine/codemap/read-locales.d.ts +29 -0
- package/out/machine/codemap/read-locales.js +246 -0
- package/out/machine/codemap/read-python.d.ts +11 -0
- package/out/machine/codemap/read-python.js +116 -0
- package/out/machine/codemap/read-typescript.d.ts +16 -0
- package/out/machine/codemap/read-typescript.js +292 -0
- package/out/machine/executor/browser.d.ts +14 -0
- package/out/machine/executor/browser.js +270 -0
- package/out/machine/executor/diagnostics.d.ts +2 -0
- package/out/machine/executor/diagnostics.js +181 -0
- package/out/machine/executor/documents.d.ts +40 -0
- package/out/machine/executor/documents.js +170 -0
- package/out/machine/executor/files.d.ts +2 -0
- package/out/machine/executor/files.js +590 -0
- package/out/machine/executor/git.d.ts +48 -0
- package/out/machine/executor/git.js +145 -0
- package/out/machine/executor/hooks.d.ts +51 -0
- package/out/machine/executor/hooks.js +154 -0
- package/out/machine/executor/index.d.ts +45 -0
- package/out/machine/executor/index.js +367 -0
- package/out/machine/executor/notebook.d.ts +2 -0
- package/out/machine/executor/notebook.js +147 -0
- package/out/machine/executor/paths.d.ts +20 -0
- package/out/machine/executor/paths.js +154 -0
- package/out/machine/executor/sandbox.d.ts +40 -0
- package/out/machine/executor/sandbox.js +299 -0
- package/out/machine/executor/shell.d.ts +86 -0
- package/out/machine/executor/shell.js +582 -0
- package/out/machine/executor/toolservers.d.ts +20 -0
- package/out/machine/executor/toolservers.js +189 -0
- package/out/machine/executor/worktree.d.ts +9 -0
- package/out/machine/executor/worktree.js +119 -0
- package/out/machine/folder/project.d.ts +28 -0
- package/out/machine/folder/project.js +114 -0
- package/out/machine/runtime/home.d.ts +2 -0
- package/out/machine/runtime/home.js +48 -0
- package/out/machine/runtime/needs.d.ts +30 -0
- package/out/machine/runtime/needs.js +89 -0
- package/out/machine/runtime/self.d.ts +23 -0
- package/out/machine/runtime/self.js +124 -0
- package/out/native/dictate +0 -0
- package/out/providers/index.js +2 -1
- package/out/thin.js +13 -12
- package/package.json +5 -4
package/out/commands/repl.js
CHANGED
|
@@ -80,7 +80,7 @@ const keyboard_1 = require("../lib/keyboard");
|
|
|
80
80
|
const machine_1 = require("../lib/machine");
|
|
81
81
|
const ui = __importStar(require("../lib/ui"));
|
|
82
82
|
const render_1 = require("../agent/render");
|
|
83
|
-
const shell_1 = require("../executor/shell");
|
|
83
|
+
const shell_1 = require("../machine/executor/shell");
|
|
84
84
|
const voice = __importStar(require("../lib/voice"));
|
|
85
85
|
const out = (s = '') => process.stdout.write(s + '\n');
|
|
86
86
|
/**
|
|
@@ -127,6 +127,13 @@ function takeTerminal() {
|
|
|
127
127
|
completer: complete,
|
|
128
128
|
});
|
|
129
129
|
keyboard.bind(reader);
|
|
130
|
+
// Enter has already moved the cursor onto the first line of the live list,
|
|
131
|
+
// so clearing from here down removes it before anything else prints.
|
|
132
|
+
reader.on('line', () => {
|
|
133
|
+
if (popupShown)
|
|
134
|
+
process.stdout.write(`${CSI}J`);
|
|
135
|
+
popupShown = 0;
|
|
136
|
+
});
|
|
130
137
|
reader.on('line', onLine);
|
|
131
138
|
reader.on('SIGINT', () => interrupt());
|
|
132
139
|
}
|
|
@@ -136,6 +143,7 @@ function takeTerminal() {
|
|
|
136
143
|
* Anywhere else it offers nothing, so it never rewrites a sentence.
|
|
137
144
|
*/
|
|
138
145
|
function complete(line, done) {
|
|
146
|
+
clearPopup();
|
|
139
147
|
const slash = /^\/(\S*)$/.exec(line);
|
|
140
148
|
if (!slash) {
|
|
141
149
|
done(null, attach.completeAt(line, () => {
|
|
@@ -172,6 +180,103 @@ function complete(line, done) {
|
|
|
172
180
|
edit.cursor = edit.line.length;
|
|
173
181
|
reader.prompt(true);
|
|
174
182
|
}
|
|
183
|
+
const CSI = `${String.fromCharCode(27)}[`;
|
|
184
|
+
const SAVE = `${String.fromCharCode(27)}7`;
|
|
185
|
+
const RESTORE = `${String.fromCharCode(27)}8`;
|
|
186
|
+
/** How many lines the live list under the prompt holds now, 0 when none. */
|
|
187
|
+
let popupShown = 0;
|
|
188
|
+
const POPUP_ROWS = 8;
|
|
189
|
+
/** What the list under the prompt holds, and which of it is marked (-1: none). */
|
|
190
|
+
let popupHits = [];
|
|
191
|
+
let popupAt = -1;
|
|
192
|
+
/**
|
|
193
|
+
* Up and Down move through the list under the prompt; Enter runs the marked
|
|
194
|
+
* command, Tab fills it in to add to. With nothing marked, or no list showing,
|
|
195
|
+
* every key does what it always did (Up and Down recall earlier requests).
|
|
196
|
+
*/
|
|
197
|
+
keyboard.onMenuKey = (key) => {
|
|
198
|
+
if (!popupShown || !reader || popupHits.length === 0)
|
|
199
|
+
return false;
|
|
200
|
+
const edit = reader;
|
|
201
|
+
if (key === 'up' || key === 'down') {
|
|
202
|
+
popupAt = key === 'down' ? Math.min(popupHits.length - 1, popupAt + 1) : Math.max(-1, popupAt - 1);
|
|
203
|
+
updatePopup();
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (popupAt < 0)
|
|
207
|
+
return false;
|
|
208
|
+
const chosen = `/${popupHits[popupAt].name}`;
|
|
209
|
+
if (key === 'tab') {
|
|
210
|
+
edit.line = `${chosen} `;
|
|
211
|
+
edit.cursor = edit.line.length;
|
|
212
|
+
edit._refreshLine();
|
|
213
|
+
clearPopup();
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
// Enter: the marked command replaces what was typed, then Enter goes on.
|
|
217
|
+
edit.line = chosen;
|
|
218
|
+
edit.cursor = chosen.length;
|
|
219
|
+
popupAt = -1;
|
|
220
|
+
return false;
|
|
221
|
+
};
|
|
222
|
+
/** Takes the live list away, leaving the prompt and the cursor where they are. */
|
|
223
|
+
function clearPopup() {
|
|
224
|
+
if (!popupShown || !process.stdout.isTTY)
|
|
225
|
+
return;
|
|
226
|
+
// Each row the list used, blanked; never a clear to the end of the screen.
|
|
227
|
+
process.stdout.write(`${SAVE}${`${CSI}1E${CSI}2K`.repeat(popupShown)}${RESTORE}`);
|
|
228
|
+
popupShown = 0;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The commands and skills that match what is typed, shown under the prompt
|
|
232
|
+
* while it is typed (manual 6.4). The same list Tab and `help` offer, from the
|
|
233
|
+
* service, so a skill added on the server appears here with no new build.
|
|
234
|
+
* Drawn only while the line is `/` and a word; anything else clears it.
|
|
235
|
+
*/
|
|
236
|
+
function updatePopup() {
|
|
237
|
+
if (!reader || !process.stdout.isTTY)
|
|
238
|
+
return;
|
|
239
|
+
const line = reader.line ?? '';
|
|
240
|
+
const slash = /^\/(\S*)$/.exec(line);
|
|
241
|
+
const typed = slash ? slash[1].toLowerCase() : '';
|
|
242
|
+
const hits = slash ? listedItems().filter((i) => i.name.startsWith(typed)) : [];
|
|
243
|
+
// A different set of matches starts with nothing marked, so Enter still
|
|
244
|
+
// sends what was typed until Down is pressed.
|
|
245
|
+
if (hits.map((h) => h.name).join() !== popupHits.map((h) => h.name).join())
|
|
246
|
+
popupAt = -1;
|
|
247
|
+
popupHits = hits;
|
|
248
|
+
// Shown for a whole word too, so the list stays in sight while it is chosen.
|
|
249
|
+
if (!hits.length) {
|
|
250
|
+
clearPopup();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const first = Math.max(0, Math.min(popupAt - POPUP_ROWS + 1, hits.length - POPUP_ROWS));
|
|
254
|
+
const shown = hits.slice(first, first + POPUP_ROWS);
|
|
255
|
+
const width = Math.min(18, Math.max(...shown.map((i) => i.name.length)) + 1);
|
|
256
|
+
const room = Math.max(20, (process.stdout.columns || 100) - width - 8);
|
|
257
|
+
const rows = shown.map((i, k) => {
|
|
258
|
+
const from = i.source === 'session' ? '' : ` ${i.source}`;
|
|
259
|
+
const words = `${i.description}${from}`;
|
|
260
|
+
const name = `/${i.name}`.padEnd(width + 1);
|
|
261
|
+
const marked = first + k === popupAt;
|
|
262
|
+
return `${marked ? ui.c.blue('\u25B8 ') : ' '}${marked ? ui.c.bold(ui.c.blue(name)) : ui.c.bold(name)} `
|
|
263
|
+
+ ui.c.dim(words.length > room ? `${words.slice(0, room - 3)}...` : words);
|
|
264
|
+
});
|
|
265
|
+
if (hits.length > shown.length)
|
|
266
|
+
rows.push(ui.c.dim(` ${hits.length - shown.length} more, Down to see them`));
|
|
267
|
+
const col = reader.getCursorPos().cols;
|
|
268
|
+
// A shorter list than last time blanks the rows it no longer uses, rather
|
|
269
|
+
// than clearing to the end of the screen: on the bottom row there is no line
|
|
270
|
+
// below to move to, and that clear wiped the list's own last row.
|
|
271
|
+
const height = rows.length;
|
|
272
|
+
while (rows.length < popupShown)
|
|
273
|
+
rows.push('');
|
|
274
|
+
// Room is made first, so a prompt on the bottom row scrolls up rather than
|
|
275
|
+
// the list running off the screen, and the cursor comes back to its column.
|
|
276
|
+
process.stdout.write(`${'\n'.repeat(rows.length)}${CSI}${rows.length}A${CSI}${col + 1}G${SAVE}`
|
|
277
|
+
+ rows.map((r) => `${CSI}1E${CSI}2K${r}`).join('') + RESTORE);
|
|
278
|
+
popupShown = Math.max(height, 1);
|
|
279
|
+
}
|
|
175
280
|
/** The service's list for this session, once fetched (manual 6.4). */
|
|
176
281
|
let listed = null;
|
|
177
282
|
/** The session words this tool runs. Sent to the service, which lists only
|
|
@@ -180,7 +285,7 @@ function sessionWords() {
|
|
|
180
285
|
return [...COMMANDS, 'project', 'folders', 'instructions', 'remember', 'detach', 'output', 'processes', 'stop', 'agents'];
|
|
181
286
|
}
|
|
182
287
|
async function refreshList() {
|
|
183
|
-
const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../
|
|
288
|
+
const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../machine/folder/project')));
|
|
184
289
|
const root = resolveProject(await sessionFolder(), place.added).root;
|
|
185
290
|
const got = await (0, commandlist_1.fetchCommandList)(root, sessionWords());
|
|
186
291
|
// A failed refresh keeps the list already held rather than emptying it.
|
|
@@ -214,9 +319,10 @@ let interrupt = () => leaveSession();
|
|
|
214
319
|
/** Words that are commands. Anything else typed is work. */
|
|
215
320
|
const COMMANDS = new Set([
|
|
216
321
|
'new', 'sessions', 'branch', 'remote', 'model', 'providers', 'keys', 'status',
|
|
217
|
-
'voice', 'help', 'exit', 'quit', 'clear', 'login', 'logout', 'signout',
|
|
322
|
+
'voice', 'help', 'exit', 'quit', 'clear', 'login', 'logout', 'signout', 'history',
|
|
218
323
|
'disconnect', 'cost', 'spend', 'attached', 'paste', 'plan', 'todo', 'mode',
|
|
219
324
|
'queue', 'transcript', 'edit', 'context', 'compact', 'why', 'unpin', 'pin', 'effort',
|
|
325
|
+
'optimize',
|
|
220
326
|
]);
|
|
221
327
|
/** `processes` and `stop pN` (manual 5.13): what this session started, and
|
|
222
328
|
* stopping one by its number. Works while a job runs, too. True if handled. */
|
|
@@ -250,11 +356,12 @@ async function showHelpers() {
|
|
|
250
356
|
out();
|
|
251
357
|
}
|
|
252
358
|
/** The permission modes, in the order Shift+Tab moves through them (4.5). */
|
|
359
|
+
/** The modes Shift+Tab steps through, as the service last listed them. */
|
|
253
360
|
const MODES = ['read_only', 'approve_writes', 'auto_edit', 'full_auto'];
|
|
254
361
|
/** How the next job in this session runs: its mode and whether it plans first
|
|
255
362
|
* (manual 4.1, 4.5). Changed by `mode`, Shift+Tab, `plan` and `plan off`, and
|
|
256
363
|
* applied from the next job; the service still decides what each mode allows. */
|
|
257
|
-
const steering_ = { mode: 'approve_writes', plan: false, pin: '', effort: '' };
|
|
364
|
+
const steering_ = { mode: 'approve_writes', plan: false, pin: '', effort: '', modes: MODES };
|
|
258
365
|
/**
|
|
259
366
|
* The line as a command, with a leading slash taken off.
|
|
260
367
|
*
|
|
@@ -308,9 +415,9 @@ async function withTerminal(run) {
|
|
|
308
415
|
* request. Returns nothing when the workspace cannot be reached: a list is not
|
|
309
416
|
* worth stopping a session over.
|
|
310
417
|
*/
|
|
311
|
-
async function earlierSessions(run) {
|
|
418
|
+
async function earlierSessions(run, limit = 10) {
|
|
312
419
|
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
313
|
-
const reply = await sessions('list', { limit
|
|
420
|
+
const reply = await sessions('list', { limit });
|
|
314
421
|
if (!reply.ok || !reply.sessions?.sessions)
|
|
315
422
|
return [];
|
|
316
423
|
const rows = reply.sessions.sessions;
|
|
@@ -324,7 +431,7 @@ async function earlierSessions(run) {
|
|
|
324
431
|
here.id = r.number;
|
|
325
432
|
return;
|
|
326
433
|
}
|
|
327
|
-
found.push({ number: r.number, line: lines[i] ?? r.number });
|
|
434
|
+
found.push({ number: r.number, line: lines[i] ?? r.number, card: r.card });
|
|
328
435
|
});
|
|
329
436
|
return found;
|
|
330
437
|
}
|
|
@@ -416,8 +523,100 @@ async function opening(how) {
|
|
|
416
523
|
for (const line of reply.sessions.lines)
|
|
417
524
|
out(line);
|
|
418
525
|
out();
|
|
526
|
+
// What the session was about, before the next question goes in.
|
|
527
|
+
await showConversation(reply.sessions.session_id, false);
|
|
419
528
|
return reopened(reply.sessions);
|
|
420
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* A session's earlier questions and answers, read back (manual 8.4).
|
|
532
|
+
*
|
|
533
|
+
* On reopening, the last three exchanges, each answer cut to its first lines,
|
|
534
|
+
* so the screen says what the session was about without filling it. `history`
|
|
535
|
+
* shows every turn in full. Answers are drawn the way a new answer is: tables
|
|
536
|
+
* as tables, emphasis as emphasis.
|
|
537
|
+
*/
|
|
538
|
+
async function showConversation(key, full) {
|
|
539
|
+
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
540
|
+
const { plain } = await Promise.resolve().then(() => __importStar(require('../agent/render')));
|
|
541
|
+
const reply = await sessions('read', { key });
|
|
542
|
+
let turns = (reply.sessions?.turns) ?? [];
|
|
543
|
+
// A service released before it sent the turns still hands over the whole
|
|
544
|
+
// session on export, every request and answer in full. Read from there, so
|
|
545
|
+
// the answers are drawn with their headings and tables rather than as the
|
|
546
|
+
// one line summaries an older service lists.
|
|
547
|
+
if (reply.ok && turns.length === 0)
|
|
548
|
+
turns = await exportedTurns(key);
|
|
549
|
+
if (!reply.ok || turns.length === 0) {
|
|
550
|
+
if (full) {
|
|
551
|
+
out(ui.c.dim(' Nothing earlier in this session was kept to show.'));
|
|
552
|
+
out();
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const exchanges = turns.filter((t) => t.role === 'you').length;
|
|
557
|
+
let shown = turns;
|
|
558
|
+
if (!full) {
|
|
559
|
+
// From the third question from the end, so what is shown is whole exchanges.
|
|
560
|
+
let asked = 0;
|
|
561
|
+
let from = turns.length;
|
|
562
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
563
|
+
if (turns[i].role === 'you' && ++asked === 3) {
|
|
564
|
+
from = i;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
if (i === 0)
|
|
568
|
+
from = 0;
|
|
569
|
+
}
|
|
570
|
+
shown = turns.slice(from);
|
|
571
|
+
}
|
|
572
|
+
out(ui.c.dim(full ? ` The whole session, ${exchanges} question(s):`
|
|
573
|
+
: ` Earlier in this session${exchanges > 3 ? `, the last 3 of ${exchanges} questions` : ''}:`));
|
|
574
|
+
out();
|
|
575
|
+
for (const t of shown) {
|
|
576
|
+
if (t.role === 'you') {
|
|
577
|
+
out(` ${ui.c.bold(ui.c.blue('you'))} ${ui.c.bold(t.text.trim())}`);
|
|
578
|
+
out();
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
// Drawn exactly as a new answer is, rail and all, never indented further.
|
|
582
|
+
const lines = plain(t.text.trim()).split('\n');
|
|
583
|
+
const keep = full ? lines : lines.slice(0, openingCut(lines));
|
|
584
|
+
for (const l of keep)
|
|
585
|
+
out(l);
|
|
586
|
+
if (keep.length < lines.length)
|
|
587
|
+
out(ui.c.dim(` ... ${lines.length - keep.length} more lines. Type /history to read it all.`));
|
|
588
|
+
out();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Where to cut an earlier answer on reopening: the last paragraph break in its
|
|
593
|
+
* first dozen lines, so a table or a list is never shown half drawn. An answer
|
|
594
|
+
* with no break that early is cut at eight lines, as before.
|
|
595
|
+
*/
|
|
596
|
+
function openingCut(lines) {
|
|
597
|
+
if (lines.length <= 12)
|
|
598
|
+
return lines.length;
|
|
599
|
+
// A break is the rail on its own; a blank row inside a table has more rules.
|
|
600
|
+
const isBreak = (l) => /^│?$/.test(l.replace(/\x1b\[[0-9;]*m/g, '').trim());
|
|
601
|
+
for (let i = 12; i >= 3; i--)
|
|
602
|
+
if (isBreak(lines[i]))
|
|
603
|
+
return i;
|
|
604
|
+
return 8;
|
|
605
|
+
}
|
|
606
|
+
/** A session's questions and answers in full, from the service's export. */
|
|
607
|
+
async function exportedTurns(key) {
|
|
608
|
+
const { sessions } = await Promise.resolve().then(() => __importStar(require('../lib/sessions')));
|
|
609
|
+
const reply = await sessions('export', { key, format: 'json' });
|
|
610
|
+
const doc = reply.sessions?.document;
|
|
611
|
+
const turns = [];
|
|
612
|
+
for (const job of doc?.work ?? []) {
|
|
613
|
+
if (job.request)
|
|
614
|
+
turns.push({ role: 'you', text: job.request });
|
|
615
|
+
if (job.answer)
|
|
616
|
+
turns.push({ role: 'agent', text: job.answer });
|
|
617
|
+
}
|
|
618
|
+
return turns;
|
|
619
|
+
}
|
|
421
620
|
/** The command and skill list, as the service sent it (manual 6.4). */
|
|
422
621
|
function printHelp() {
|
|
423
622
|
out();
|
|
@@ -425,7 +624,7 @@ function printHelp() {
|
|
|
425
624
|
out();
|
|
426
625
|
drawList(listedItems());
|
|
427
626
|
if (!listed?.ok) {
|
|
428
|
-
out(ui.c.amber(' Your
|
|
627
|
+
out(ui.c.amber(' Your organization could not be asked for its skills, so only this tool\'s own words are shown.'));
|
|
429
628
|
out();
|
|
430
629
|
}
|
|
431
630
|
else if (listed.needsUpdate.length > 0) {
|
|
@@ -447,9 +646,42 @@ function printHelp() {
|
|
|
447
646
|
/**
|
|
448
647
|
* The folders this session works in (manual 2.1 and 2.3). `project <path>`
|
|
449
648
|
* changes the folder for the rest of the session; `folders add <path>` allows
|
|
450
|
-
* another. Neither changes the saved default, which is `comprism
|
|
649
|
+
* another. Neither changes the saved default, which is `comprism workspace`.
|
|
650
|
+
*/
|
|
651
|
+
const place = { folder: null, added: [], workspace: '' };
|
|
652
|
+
/**
|
|
653
|
+
* One command the service runs itself (web manual 13.2, 13.3). The service says
|
|
654
|
+
* what it means and writes every line; this answers whether a folder exists on
|
|
655
|
+
* this computer when asked, prints the lines, and carries out the one action it
|
|
656
|
+
* names. True when the session's workspace or folders changed; the request to
|
|
657
|
+
* run when a next step was chosen.
|
|
451
658
|
*/
|
|
452
|
-
|
|
659
|
+
async function serviceCommand(words) {
|
|
660
|
+
const { runServiceCommand } = await Promise.resolve().then(() => __importStar(require('../lib/servicecommand')));
|
|
661
|
+
const said = await runServiceCommand(words, { cwd: await sessionFolder(), current: place.workspace });
|
|
662
|
+
if (!said.reached) {
|
|
663
|
+
out(ui.fail(said.message || 'Your organization could not answer that.'));
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
for (const line of said.lines)
|
|
667
|
+
out(line);
|
|
668
|
+
const a = said.action;
|
|
669
|
+
if (!a)
|
|
670
|
+
return false;
|
|
671
|
+
if (a.run)
|
|
672
|
+
return a.run;
|
|
673
|
+
if (a.open_workspace)
|
|
674
|
+
place.workspace = a.open_workspace;
|
|
675
|
+
if (a.use_folder) {
|
|
676
|
+
place.folder = a.use_folder;
|
|
677
|
+
(await Promise.resolve().then(() => __importStar(require('../lib/project-ops')))).useSessionFolder(a.use_folder);
|
|
678
|
+
}
|
|
679
|
+
if (a.add_folder && !place.added.includes(a.add_folder))
|
|
680
|
+
place.added.push(a.add_folder);
|
|
681
|
+
if (a.remove_folder)
|
|
682
|
+
place.added = place.added.filter((f) => f !== a.remove_folder);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
453
685
|
async function sessionFolder() {
|
|
454
686
|
if (place.folder)
|
|
455
687
|
return place.folder;
|
|
@@ -457,14 +689,18 @@ async function sessionFolder() {
|
|
|
457
689
|
return currentWorkspace().path;
|
|
458
690
|
}
|
|
459
691
|
/** The line this session begins with, as the service wrote it (manual 2.2). */
|
|
460
|
-
async function
|
|
461
|
-
const { resolveProject, asWire } = await Promise.resolve().then(() => __importStar(require('../
|
|
692
|
+
async function projectLine() {
|
|
693
|
+
const { resolveProject, asWire } = await Promise.resolve().then(() => __importStar(require('../machine/folder/project')));
|
|
462
694
|
const { ask } = await Promise.resolve().then(() => __importStar(require('../lib/gateway')));
|
|
463
695
|
const facts = resolveProject(await sessionFolder(), place.added);
|
|
464
696
|
const reply = await ask({ intent: 'project', prompt: '', project: asWire(facts, 'approve_writes') });
|
|
465
697
|
// Drawn as sent. When the service cannot answer, the folder alone, so the
|
|
466
698
|
// person still knows where they are.
|
|
467
|
-
|
|
699
|
+
return reply.ok && reply.project_line ? reply.project_line : ` ${facts.root}`;
|
|
700
|
+
}
|
|
701
|
+
/** `asked` is the same line already on its way, when startup asked early. */
|
|
702
|
+
async function showProjectLine(asked) {
|
|
703
|
+
out(ui.c.bold(await (asked ?? projectLine())));
|
|
468
704
|
out();
|
|
469
705
|
}
|
|
470
706
|
/** Ctrl+O's choice, kept for the next job too, and notes left over from the
|
|
@@ -483,7 +719,7 @@ async function contextOf(intent, jobId, keep = '', sessionKey) {
|
|
|
483
719
|
intent, prompt: keep, ...(jobId ? { jobId } : {}), ...(sessionKey ? { spendSession: sessionKey } : {}),
|
|
484
720
|
});
|
|
485
721
|
if (!reply.ok || !reply.context) {
|
|
486
|
-
out(ui.fail(reply.message || 'Your
|
|
722
|
+
out(ui.fail(reply.message || 'Your organization could not answer that.'));
|
|
487
723
|
}
|
|
488
724
|
else {
|
|
489
725
|
for (const line of reply.context.lines)
|
|
@@ -502,7 +738,7 @@ async function showTranscript(jobId) {
|
|
|
502
738
|
const { ask } = await Promise.resolve().then(() => __importStar(require('../lib/gateway')));
|
|
503
739
|
const reply = await ask({ intent: 'transcript', prompt: '', jobId });
|
|
504
740
|
if (!reply.ok || !reply.transcript) {
|
|
505
|
-
out(ui.fail(reply.message || 'The transcript could not be read from your
|
|
741
|
+
out(ui.fail(reply.message || 'The transcript could not be read from your organization.'));
|
|
506
742
|
}
|
|
507
743
|
else {
|
|
508
744
|
for (const line of reply.transcript.lines)
|
|
@@ -758,10 +994,48 @@ async function runRepl(how = {}) {
|
|
|
758
994
|
const finished = new Promise((resolve) => { done = resolve; });
|
|
759
995
|
const conn = (0, connection_1.readConnection)();
|
|
760
996
|
if (!conn?.url || !conn.workspaceKey) {
|
|
761
|
-
await (0, welcome_1.showWelcome)();
|
|
997
|
+
await (0, welcome_1.showWelcome)(undefined, process.stdin.isTTY === true && !how.noSignIn);
|
|
998
|
+
// Signed out: sign in right here rather than naming another command to
|
|
999
|
+
// type. The last person is offered, so it is usually Enter and a password.
|
|
1000
|
+
if (process.stdin.isTTY === true && !how.noSignIn) {
|
|
1001
|
+
const { cmdLogin } = await Promise.resolve().then(() => __importStar(require('./login')));
|
|
1002
|
+
await cmdLogin(['--into-session']);
|
|
1003
|
+
const { closePrompt } = await Promise.resolve().then(() => __importStar(require('../lib/prompt')));
|
|
1004
|
+
closePrompt();
|
|
1005
|
+
if ((0, connection_1.readConnection)()?.workspaceKey)
|
|
1006
|
+
return runRepl({ ...how, noSignIn: true, noMark: true });
|
|
1007
|
+
}
|
|
762
1008
|
return;
|
|
763
1009
|
}
|
|
764
|
-
|
|
1010
|
+
// Everything the opening screen needs is asked for AT ONCE, then drawn in
|
|
1011
|
+
// order as it arrives. Asked one after another it was five round trips end
|
|
1012
|
+
// to end, about fifteen seconds on a laptop before the prompt appeared.
|
|
1013
|
+
//
|
|
1014
|
+
// And what the service said LAST time is drawn at once, so the prompt is
|
|
1015
|
+
// ready in well under a second. The same questions are still asked; their
|
|
1016
|
+
// answers are kept for next time, and a change that matters is said as soon
|
|
1017
|
+
// as it arrives. A first start, or another workspace, waits as before.
|
|
1018
|
+
const { readStart, saveStart } = await Promise.resolve().then(() => __importStar(require('../lib/startcache')));
|
|
1019
|
+
const { session: whoAmI } = await Promise.resolve().then(() => __importStar(require('./welcome')));
|
|
1020
|
+
const known = readStart(conn.url);
|
|
1021
|
+
const here = await sessionFolder();
|
|
1022
|
+
const meAsked = whoAmI().then((me) => { if (me)
|
|
1023
|
+
saveStart(conn.url, { me }); return me; });
|
|
1024
|
+
const stateAsked = (0, keys_1.keyState)().then((s) => {
|
|
1025
|
+
if (s.reached)
|
|
1026
|
+
saveStart(conn.url, { hasKey: s.hasKey });
|
|
1027
|
+
return s;
|
|
1028
|
+
});
|
|
1029
|
+
const lineAsked = projectLine()
|
|
1030
|
+
.then((l) => { saveStart(conn.url, { lines: { [here]: l } }); return l; })
|
|
1031
|
+
.catch(() => '');
|
|
1032
|
+
const listAsked = refreshList()
|
|
1033
|
+
.then(() => { if (listed?.ok)
|
|
1034
|
+
saveStart(conn.url, { list: listed }); })
|
|
1035
|
+
.catch(() => undefined);
|
|
1036
|
+
if (known.list?.ok && !listed)
|
|
1037
|
+
listed = known.list;
|
|
1038
|
+
await (0, welcome_1.showWelcome)(known.me ? known.me : await meAsked, false, !how.noMark);
|
|
765
1039
|
// A key, before anything else. With none there is nothing to bill and nothing
|
|
766
1040
|
// to measure, so a prompt accepted here could only ever be refused later.
|
|
767
1041
|
//
|
|
@@ -769,9 +1043,20 @@ async function runRepl(how = {}) {
|
|
|
769
1043
|
// could not ask your account" is the whole point: on 17 September a person
|
|
770
1044
|
// whose workspace held four working keys was shown the first message and sent
|
|
771
1045
|
// to a form, because an unanswered question was read as an answer of none.
|
|
772
|
-
const state =
|
|
1046
|
+
const state = known.hasKey === true ? { reached: true, hasKey: true } : await stateAsked;
|
|
1047
|
+
if (known.hasKey === true) {
|
|
1048
|
+
// Drawn from the last answer; the fresh one says so if the key has gone.
|
|
1049
|
+
void stateAsked.then((fresh) => {
|
|
1050
|
+
if (fresh.reached && !fresh.hasKey) {
|
|
1051
|
+
out();
|
|
1052
|
+
out(ui.c.amber(' Your account no longer has a provider key stored, so requests will be refused.'));
|
|
1053
|
+
out(` ${ui.c.dim('run')} ${ui.c.bold('keys')} ${ui.c.dim('to add one')}`);
|
|
1054
|
+
reader?.prompt(true);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
773
1058
|
if (!state.reached) {
|
|
774
|
-
out(ui.fail(`Your
|
|
1059
|
+
out(ui.fail(`Your organization could not be reached, so ${ui.c.bold('nothing is known')} about your keys.`));
|
|
775
1060
|
out(` ${ui.c.dim(state.why)}`);
|
|
776
1061
|
out(` ${ui.c.dim('check the address with')} ${ui.c.bold('comprism status')}${ui.c.dim(', or sign in again with')} ${ui.c.bold('comprism login')}`);
|
|
777
1062
|
out();
|
|
@@ -808,6 +1093,14 @@ async function runRepl(how = {}) {
|
|
|
808
1093
|
}
|
|
809
1094
|
steering_.pin = said.pin ?? '';
|
|
810
1095
|
steering_.effort = said.effort === 'normal' ? '' : said.effort;
|
|
1096
|
+
// Mode and plan are the service's too (CLI manual 4.1, 4.5); kept here only
|
|
1097
|
+
// to send with the next job and to draw the prompt.
|
|
1098
|
+
if (said.mode)
|
|
1099
|
+
steering_.mode = said.mode;
|
|
1100
|
+
if (typeof said.plan === 'boolean')
|
|
1101
|
+
steering_.plan = said.plan;
|
|
1102
|
+
if (said.modes?.length)
|
|
1103
|
+
steering_.modes = said.modes;
|
|
811
1104
|
return said;
|
|
812
1105
|
};
|
|
813
1106
|
if (how.latest || how.resume || how.pick) {
|
|
@@ -821,7 +1114,8 @@ async function runRepl(how = {}) {
|
|
|
821
1114
|
session = found;
|
|
822
1115
|
}
|
|
823
1116
|
else {
|
|
824
|
-
|
|
1117
|
+
// The session's number arrives while the person is already typing.
|
|
1118
|
+
void numbered(session);
|
|
825
1119
|
}
|
|
826
1120
|
history.push(session);
|
|
827
1121
|
// Ctrl-C leaves, and says what the session cost on the way out. A product
|
|
@@ -909,6 +1203,52 @@ async function runRepl(how = {}) {
|
|
|
909
1203
|
* the person saw their sentence twice and could edit only the second copy.
|
|
910
1204
|
*/
|
|
911
1205
|
const speak = async () => {
|
|
1206
|
+
// On a Mac, the words appear as they are spoken, the way the web app shows
|
|
1207
|
+
// them, from the Mac's own recogniser. Anywhere it cannot listen (no
|
|
1208
|
+
// helper, or permission not given) this falls through to recording and
|
|
1209
|
+
// having the server write it down when Enter is pressed.
|
|
1210
|
+
if (process.stdout.isTTY) {
|
|
1211
|
+
const live = voice.listenLive();
|
|
1212
|
+
if (live) {
|
|
1213
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1214
|
+
if (live.problem() && !live.words()) {
|
|
1215
|
+
await live.stop();
|
|
1216
|
+
out(ui.c.dim(` ${live.problem()}`));
|
|
1217
|
+
}
|
|
1218
|
+
else {
|
|
1219
|
+
out();
|
|
1220
|
+
out(ui.c.green(' listening...') + ui.c.dim(' say it, then press enter'));
|
|
1221
|
+
const draw = () => {
|
|
1222
|
+
const bars = Math.round(live.level() * 12);
|
|
1223
|
+
const meter = ui.c.green('█'.repeat(bars)) + ui.c.dim('░'.repeat(12 - bars));
|
|
1224
|
+
const room = Math.max(20, (process.stdout.columns || 100) - 20);
|
|
1225
|
+
const heard = live.words();
|
|
1226
|
+
const shown = heard.length > room ? `...${heard.slice(-(room - 3))}` : heard;
|
|
1227
|
+
process.stdout.write(`\r${CSI}2K ${meter} ${shown}`);
|
|
1228
|
+
};
|
|
1229
|
+
const ticking = setInterval(draw, 80);
|
|
1230
|
+
await nextLine();
|
|
1231
|
+
clearInterval(ticking);
|
|
1232
|
+
process.stdout.write(`${CSI}1A\r${CSI}2K`);
|
|
1233
|
+
const said = await live.stop();
|
|
1234
|
+
if (said === null) {
|
|
1235
|
+
out(ui.c.dim(` ${live.problem() || 'dictation stopped'}`));
|
|
1236
|
+
out();
|
|
1237
|
+
return prompt();
|
|
1238
|
+
}
|
|
1239
|
+
if (!said) {
|
|
1240
|
+
out(ui.c.dim(' no words in that one'));
|
|
1241
|
+
out();
|
|
1242
|
+
return prompt();
|
|
1243
|
+
}
|
|
1244
|
+
// Into the prompt, not sent: the person reads it, fixes what was
|
|
1245
|
+
// misheard, and presses Enter. Nothing is asked or charged until then.
|
|
1246
|
+
prompt();
|
|
1247
|
+
reader?.write(said);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
912
1252
|
const short = voice.missing();
|
|
913
1253
|
if (short) {
|
|
914
1254
|
out();
|
|
@@ -927,7 +1267,45 @@ async function runRepl(how = {}) {
|
|
|
927
1267
|
}
|
|
928
1268
|
out();
|
|
929
1269
|
out(ui.c.green(' listening...') + ui.c.dim(' say it, then press enter'));
|
|
1270
|
+
// What it hears, while it hears it: a meter that moves with the voice at
|
|
1271
|
+
// once, and the words so far, refreshed every few seconds. Without these
|
|
1272
|
+
// a person talks into a blank line with no idea whether anything is
|
|
1273
|
+
// arriving. The words so far are a preview; the whole recording is
|
|
1274
|
+
// written down again when they press Enter, and that is what they get.
|
|
1275
|
+
const tty = process.stdout.isTTY === true;
|
|
1276
|
+
let heard = '';
|
|
1277
|
+
let asking = false;
|
|
1278
|
+
let lastAsked = 0;
|
|
1279
|
+
const draw = () => {
|
|
1280
|
+
const bars = Math.round(voice.level(live.file) * 12);
|
|
1281
|
+
const meter = ui.c.green('█'.repeat(bars)) + ui.c.dim('░'.repeat(12 - bars));
|
|
1282
|
+
const room = Math.max(20, (process.stdout.columns || 100) - 20);
|
|
1283
|
+
const words = heard.length > room ? `...${heard.slice(-(room - 3))}` : heard;
|
|
1284
|
+
process.stdout.write(`\r${CSI}2K ${meter} ${words}`);
|
|
1285
|
+
};
|
|
1286
|
+
const preview = async () => {
|
|
1287
|
+
if (asking || Date.now() - lastAsked < 2500)
|
|
1288
|
+
return;
|
|
1289
|
+
const part = voice.soFar(live.file);
|
|
1290
|
+
if (!part)
|
|
1291
|
+
return;
|
|
1292
|
+
asking = true;
|
|
1293
|
+
lastAsked = Date.now();
|
|
1294
|
+
try {
|
|
1295
|
+
const got = await voice.transcribe(conn.url, conn.workspaceKey, part);
|
|
1296
|
+
if (got.text)
|
|
1297
|
+
heard = got.text.trim();
|
|
1298
|
+
}
|
|
1299
|
+
catch { /* a preview that fails leaves the last one showing */ }
|
|
1300
|
+
asking = false;
|
|
1301
|
+
};
|
|
1302
|
+
const ticking = tty ? setInterval(() => { draw(); void preview(); }, 120) : null;
|
|
930
1303
|
await nextLine();
|
|
1304
|
+
if (ticking)
|
|
1305
|
+
clearInterval(ticking);
|
|
1306
|
+
// The line the typed Enter left behind is the meter's; clear it.
|
|
1307
|
+
if (tty)
|
|
1308
|
+
process.stdout.write(`${CSI}1A\r${CSI}2K`);
|
|
931
1309
|
const { file, bytes } = await live.stop();
|
|
932
1310
|
try {
|
|
933
1311
|
if (bytes < 2000) {
|
|
@@ -1010,7 +1388,7 @@ async function runRepl(how = {}) {
|
|
|
1010
1388
|
* a drag and drop, and the files wait for the question that follows.
|
|
1011
1389
|
*/
|
|
1012
1390
|
const gather = async (typed) => {
|
|
1013
|
-
const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../
|
|
1391
|
+
const { resolveProject } = await Promise.resolve().then(() => __importStar(require('../machine/folder/project')));
|
|
1014
1392
|
const here = await sessionFolder();
|
|
1015
1393
|
const root = resolveProject(here, place.added).root;
|
|
1016
1394
|
completion.root = root;
|
|
@@ -1168,17 +1546,20 @@ async function runRepl(how = {}) {
|
|
|
1168
1546
|
reader.setPrompt(`${ui.c.bold(ui.c.blue('comprism'))} ${ui.c.dim('>')} `);
|
|
1169
1547
|
reader.prompt();
|
|
1170
1548
|
};
|
|
1171
|
-
|
|
1549
|
+
const lastLine = known.lines?.[here];
|
|
1550
|
+
await showProjectLine(lastLine ? Promise.resolve(lastLine) : lineAsked);
|
|
1172
1551
|
// The command and skill list, and whether this company lets typed requests
|
|
1173
1552
|
// be kept on this machine (manual 6.4, 6.5). Unknown keeps them for this
|
|
1174
|
-
// session only and leaves any file alone.
|
|
1175
|
-
|
|
1553
|
+
// session only and leaves any file alone. The last list serves until the
|
|
1554
|
+
// fresh one lands.
|
|
1555
|
+
if (!known.list?.ok)
|
|
1556
|
+
await listAsked;
|
|
1176
1557
|
// `config set history off` keeps nothing at all, not even for this session.
|
|
1177
1558
|
const historyOff = (0, machine_1.machineSettings)().history === 'off';
|
|
1178
1559
|
earlier.start(historyOff ? false : listed?.ok ? listed.keepHistory : null);
|
|
1179
1560
|
if (historyOff)
|
|
1180
1561
|
earlier.disable();
|
|
1181
|
-
out(ui.c.dim(' Type what you want done,
|
|
1562
|
+
out(ui.c.dim(' Type what you want done. / lists commands and skills, voice dictates, Ctrl+C leaves.'));
|
|
1182
1563
|
out();
|
|
1183
1564
|
// One line at a time, in the order they arrived.
|
|
1184
1565
|
//
|
|
@@ -1290,14 +1671,25 @@ async function runRepl(how = {}) {
|
|
|
1290
1671
|
// Shift+Tab moves to the next permission mode (manual 4.5). Only at the
|
|
1291
1672
|
// prompt: during a job the change would apply from the next job anyway, and
|
|
1292
1673
|
// redrawing over a running job's output is unreadable.
|
|
1674
|
+
// The live list of commands and skills, redrawn after each key at the prompt,
|
|
1675
|
+
// once the reader has applied the key to the line.
|
|
1676
|
+
keys().on('keypress', (_s, key) => {
|
|
1677
|
+
if (working || answering || leaving || key?.name === 'return' || key?.name === 'tab')
|
|
1678
|
+
return;
|
|
1679
|
+
setImmediate(updatePopup);
|
|
1680
|
+
});
|
|
1293
1681
|
keys().on('keypress', (_s, key) => {
|
|
1294
1682
|
if (!key || key.name !== 'tab' || !key.shift || working || answering || leaving)
|
|
1295
1683
|
return;
|
|
1296
|
-
|
|
1297
|
-
|
|
1684
|
+
// The next mode in the service's own list, set by the service.
|
|
1685
|
+
const modes = steering_.modes;
|
|
1686
|
+
const next = modes[(modes.indexOf(steering_.mode) + 1) % modes.length];
|
|
1298
1687
|
out();
|
|
1299
|
-
|
|
1300
|
-
|
|
1688
|
+
void steer({ mode: next }).then((said) => {
|
|
1689
|
+
for (const line of said?.lines ?? [])
|
|
1690
|
+
out(ui.c.dim(line));
|
|
1691
|
+
prompt();
|
|
1692
|
+
});
|
|
1301
1693
|
});
|
|
1302
1694
|
async function handle(line) {
|
|
1303
1695
|
const typed = line.trim();
|
|
@@ -1323,38 +1715,27 @@ async function runRepl(how = {}) {
|
|
|
1323
1715
|
out();
|
|
1324
1716
|
return prompt();
|
|
1325
1717
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
-
}
|
|
1718
|
+
// Commands the service runs itself (web manual 13.2, 13.3): any word the
|
|
1719
|
+
// service lists as its own, and `project`, the old name for `workspace`.
|
|
1720
|
+
// What they mean and every line printed are the service's; this sends what
|
|
1721
|
+
// was typed, answers whether a folder exists, and carries out the action.
|
|
1722
|
+
const head = (bare.split(/\s+/)[0] ?? '').toLowerCase();
|
|
1723
|
+
const { waitingNextSteps } = await Promise.resolve().then(() => __importStar(require('../agent/session')));
|
|
1724
|
+
const serviceWord = head === 'project'
|
|
1725
|
+
|| (/^\d+$/.test(bare) && waitingNextSteps > 0)
|
|
1726
|
+
|| (listed?.items ?? []).some((i) => i.source === 'service' && i.name === head);
|
|
1727
|
+
if (serviceWord) {
|
|
1728
|
+
const words = head === 'project' ? bare.replace(/^project\b/i, 'workspace') : bare;
|
|
1729
|
+
const acted = await serviceCommand(words);
|
|
1354
1730
|
out();
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1731
|
+
// A next step chosen: it runs as the request it stands for.
|
|
1732
|
+
if (typeof acted === 'string')
|
|
1733
|
+
return handle(`work ${acted}`);
|
|
1734
|
+
if (acted) {
|
|
1735
|
+
await showProjectLine();
|
|
1736
|
+
// Another folder has other skills.
|
|
1737
|
+
await refreshList();
|
|
1738
|
+
}
|
|
1358
1739
|
return prompt();
|
|
1359
1740
|
}
|
|
1360
1741
|
// `pin <model>` and `effort <level>` (manual 4.17, 4.18), kept by the
|
|
@@ -1367,18 +1748,35 @@ async function runRepl(how = {}) {
|
|
|
1367
1748
|
out();
|
|
1368
1749
|
return prompt();
|
|
1369
1750
|
}
|
|
1751
|
+
// `optimize on` and `optimize off <model>` (manual 4.17): the same switch
|
|
1752
|
+
// as beside the model in the browser and VS Code. Off is the session pin.
|
|
1753
|
+
const optimizeTo = /^optimize\s+(on|off)(?:\s+(\S+))?$/i.exec(word);
|
|
1754
|
+
if (optimizeTo) {
|
|
1755
|
+
const on = optimizeTo[1].toLowerCase() === 'on';
|
|
1756
|
+
const hold = optimizeTo[2] ?? steering_.pin;
|
|
1757
|
+
if (on) {
|
|
1758
|
+
if (await steer({ pin: '' }))
|
|
1759
|
+
out(ui.c.dim(' Optimize is on: CompletionPrism picks the model for each step'));
|
|
1760
|
+
}
|
|
1761
|
+
else if (!hold) {
|
|
1762
|
+
out(ui.fail('Name the model to keep, such as `optimize off sonnet`.'));
|
|
1763
|
+
}
|
|
1764
|
+
else {
|
|
1765
|
+
const said = await steer({ pin: hold });
|
|
1766
|
+
if (said)
|
|
1767
|
+
out(ui.c.dim(` Optimize is off: ${ui.c.bold(said.pin_label ?? hold)} answers every request. Work is not optimized. \`optimize on\` hands the choice back.`));
|
|
1768
|
+
}
|
|
1769
|
+
out();
|
|
1770
|
+
return prompt();
|
|
1771
|
+
}
|
|
1370
1772
|
const effortTo = /^effort\s+(\S+)$/.exec(word);
|
|
1371
1773
|
if (effortTo) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
-
}
|
|
1774
|
+
// The service checks the word and says what is allowed.
|
|
1775
|
+
const said = await steer({ effort: effortTo[1] });
|
|
1776
|
+
if (said) {
|
|
1777
|
+
out(ui.c.dim(` effort ${ui.c.bold(said.effort)} for the next requests`));
|
|
1778
|
+
if (said.effort_note)
|
|
1779
|
+
out(ui.c.dim(` ${said.effort_note}`));
|
|
1382
1780
|
}
|
|
1383
1781
|
out();
|
|
1384
1782
|
return prompt();
|
|
@@ -1386,20 +1784,16 @@ async function runRepl(how = {}) {
|
|
|
1386
1784
|
// `mode <name>` and `plan off` (manual 4.1, 4.5).
|
|
1387
1785
|
const modeTo = /^mode\s+(\S+)$/.exec(word);
|
|
1388
1786
|
if (modeTo) {
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
out(ui.
|
|
1392
|
-
}
|
|
1393
|
-
else {
|
|
1394
|
-
steering_.mode = named;
|
|
1395
|
-
out(ui.c.dim(` mode ${ui.c.bold(named)}, from the next job`));
|
|
1396
|
-
}
|
|
1787
|
+
const said = await steer({ mode: modeTo[1] });
|
|
1788
|
+
for (const line of said?.lines ?? [])
|
|
1789
|
+
out(ui.c.dim(line));
|
|
1397
1790
|
out();
|
|
1398
1791
|
return prompt();
|
|
1399
1792
|
}
|
|
1400
1793
|
if (word === 'plan off') {
|
|
1401
|
-
|
|
1402
|
-
|
|
1794
|
+
const said = await steer({ plan: false });
|
|
1795
|
+
for (const line of said?.lines ?? [])
|
|
1796
|
+
out(ui.c.dim(line));
|
|
1403
1797
|
out();
|
|
1404
1798
|
return prompt();
|
|
1405
1799
|
}
|
|
@@ -1417,7 +1811,7 @@ async function runRepl(how = {}) {
|
|
|
1417
1811
|
// `output 3`: a command's whole output, kept on this machine (5.12).
|
|
1418
1812
|
const outputOf = /^output(?:\s+(\d+))?$/.exec(word);
|
|
1419
1813
|
if (outputOf) {
|
|
1420
|
-
const { keptOutput } = await Promise.resolve().then(() => __importStar(require('../executor/shell')));
|
|
1814
|
+
const { keptOutput } = await Promise.resolve().then(() => __importStar(require('../machine/executor/shell')));
|
|
1421
1815
|
const kept = outputOf[1] ? keptOutput(Number(outputOf[1])) : undefined;
|
|
1422
1816
|
if (!outputOf[1])
|
|
1423
1817
|
out(ui.c.dim(' output N shows the whole output of command N, the number on its line.'));
|
|
@@ -1526,16 +1920,20 @@ async function runRepl(how = {}) {
|
|
|
1526
1920
|
}
|
|
1527
1921
|
if (COMMANDS.has(word)) {
|
|
1528
1922
|
switch (word) {
|
|
1529
|
-
case 'plan':
|
|
1530
|
-
|
|
1531
|
-
|
|
1923
|
+
case 'plan': {
|
|
1924
|
+
const said = await steer({ plan: true });
|
|
1925
|
+
for (const line of said?.lines ?? [])
|
|
1926
|
+
out(ui.c.dim(line));
|
|
1532
1927
|
out();
|
|
1533
1928
|
break;
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1929
|
+
}
|
|
1930
|
+
case 'mode': {
|
|
1931
|
+
const said = await steer({});
|
|
1932
|
+
for (const line of said?.lines ?? [])
|
|
1933
|
+
out(ui.c.dim(line));
|
|
1537
1934
|
out();
|
|
1538
1935
|
break;
|
|
1936
|
+
}
|
|
1539
1937
|
case 'pin': {
|
|
1540
1938
|
const said = await steer({});
|
|
1541
1939
|
out(said?.pin
|
|
@@ -1544,6 +1942,14 @@ async function runRepl(how = {}) {
|
|
|
1544
1942
|
out();
|
|
1545
1943
|
break;
|
|
1546
1944
|
}
|
|
1945
|
+
case 'optimize': {
|
|
1946
|
+
const said = await steer({});
|
|
1947
|
+
out(said?.pin
|
|
1948
|
+
? ` ${ui.c.dim('Optimize is off:')} ${ui.c.bold(said.pin_label ?? said.pin)} ${ui.c.dim('answers every request. `optimize on` turns it back on.')}`
|
|
1949
|
+
: ui.c.dim(' Optimize is on: CompletionPrism picks the model for each step. `optimize off <model>` keeps one.'));
|
|
1950
|
+
out();
|
|
1951
|
+
break;
|
|
1952
|
+
}
|
|
1547
1953
|
case 'unpin':
|
|
1548
1954
|
if (await steer({ pin: '' }))
|
|
1549
1955
|
out(ui.c.dim(' unpinned: the service chooses the model for each step again'));
|
|
@@ -1649,6 +2055,46 @@ async function runRepl(how = {}) {
|
|
|
1649
2055
|
}
|
|
1650
2056
|
case 'sessions': {
|
|
1651
2057
|
out();
|
|
2058
|
+
// What this account did before this window was opened, from the
|
|
2059
|
+
// service's record of sessions (manual 8.5). Kept numbered
|
|
2060
|
+
// separately so a number never means two different things.
|
|
2061
|
+
// Every session the service keeps a record of for this person, up to
|
|
2062
|
+
// its limit, when they can be moved through and picked by number; a
|
|
2063
|
+
// typed list stays short enough to read.
|
|
2064
|
+
const onTerminal = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2065
|
+
recent = await earlierSessions(history, onTerminal ? 200 : 10);
|
|
2066
|
+
// On a terminal, the list is picked from with the arrow keys; the
|
|
2067
|
+
// typed forms below still work everywhere, and in a pipe are the
|
|
2068
|
+
// only way.
|
|
2069
|
+
if (onTerminal && history.length + recent.length > 1) {
|
|
2070
|
+
// Names, dates and cost only. The ids are for typing a session
|
|
2071
|
+
// back, and nobody types in a list they move through with arrows.
|
|
2072
|
+
const noId = (line) => line.trimStart().replace(/^(?:srv|s)_[0-9a-f]+\s+/i, '');
|
|
2073
|
+
const rows = [
|
|
2074
|
+
...history.map((s) => [
|
|
2075
|
+
`${s.title ?? 'this session'}${s === session ? ' (now)' : ''}`,
|
|
2076
|
+
`this window \u00b7 ${s.jobs.length} job(s) \u00b7 ${money(s.spentUsd)}`,
|
|
2077
|
+
]),
|
|
2078
|
+
// The service's two lines where it sent them; its one line otherwise.
|
|
2079
|
+
...recent.map((r) => (r.card?.length ? r.card : [noId(r.line)])),
|
|
2080
|
+
];
|
|
2081
|
+
const { pick } = await Promise.resolve().then(() => __importStar(require('../lib/picker')));
|
|
2082
|
+
let choice = null;
|
|
2083
|
+
out(ui.c.dim(' choose a session:'));
|
|
2084
|
+
await withTerminal(async () => {
|
|
2085
|
+
choice = await pick(rows, 'Up and Down to choose, or type its number, Enter to open, Esc to stay here', history.indexOf(session), true);
|
|
2086
|
+
});
|
|
2087
|
+
out();
|
|
2088
|
+
if (choice === null)
|
|
2089
|
+
return prompt();
|
|
2090
|
+
const n = choice;
|
|
2091
|
+
if (n < history.length) {
|
|
2092
|
+
if (history[n] === session)
|
|
2093
|
+
return prompt();
|
|
2094
|
+
return handle(`sessions ${n + 1}`);
|
|
2095
|
+
}
|
|
2096
|
+
return handle(`sessions w${n - history.length + 1}`);
|
|
2097
|
+
}
|
|
1652
2098
|
history.forEach((s, i) => {
|
|
1653
2099
|
const here = s === session ? ui.c.green(' now') : ' ';
|
|
1654
2100
|
// The NAME where our server has made one, and the id beside it
|
|
@@ -1658,10 +2104,6 @@ async function runRepl(how = {}) {
|
|
|
1658
2104
|
out(`${here} ${String(i + 1).padStart(2)}. ${ui.c.bold(s.id)}`
|
|
1659
2105
|
+ ` ${s.jobs.length} job(s) ${money(s.spentUsd)}${named}`);
|
|
1660
2106
|
});
|
|
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);
|
|
1665
2107
|
if (recent.length > 0) {
|
|
1666
2108
|
out();
|
|
1667
2109
|
out(ui.c.dim(' earlier work on this account:'));
|
|
@@ -1685,17 +2127,10 @@ async function runRepl(how = {}) {
|
|
|
1685
2127
|
// session the moment two are open, and this one is on the screen.
|
|
1686
2128
|
case 'cost':
|
|
1687
2129
|
case 'spend': {
|
|
1688
|
-
// Nothing sent is nothing charged
|
|
1689
|
-
//
|
|
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
|
-
}
|
|
2130
|
+
// Nothing sent is nothing charged: the service answers even before
|
|
2131
|
+
// the first job, so a refusal can be checked as costing nothing (6.1).
|
|
1697
2132
|
const { cmdCost } = await Promise.resolve().then(() => __importStar(require('./cost')));
|
|
1698
|
-
await cmdCost([session.id]);
|
|
2133
|
+
await cmdCost([session.serverId || session.id]);
|
|
1699
2134
|
break;
|
|
1700
2135
|
}
|
|
1701
2136
|
case 'model': {
|
|
@@ -1718,6 +2153,33 @@ async function runRepl(how = {}) {
|
|
|
1718
2153
|
// Signing out ends the session, because everything after it would be
|
|
1719
2154
|
// refused anyway. It used to answer "not available inside a session",
|
|
1720
2155
|
// which left quitting and running a second command as the only way.
|
|
2156
|
+
case 'history':
|
|
2157
|
+
// Every earlier question and answer in the session being stood in.
|
|
2158
|
+
out();
|
|
2159
|
+
await showConversation(session.serverId, true);
|
|
2160
|
+
break;
|
|
2161
|
+
case 'login': {
|
|
2162
|
+
// Signing in again, or as somebody else, from inside a session. The
|
|
2163
|
+
// session was opened with the old sign in, so it ends and the next
|
|
2164
|
+
// `comprism` opens with the new one.
|
|
2165
|
+
const { cmdLogin } = await Promise.resolve().then(() => __importStar(require('./login')));
|
|
2166
|
+
const before = (0, connection_1.readConnection)()?.token;
|
|
2167
|
+
await withTerminal(async () => {
|
|
2168
|
+
// The Enter that sent `/login` is still in the terminal's input.
|
|
2169
|
+
// Given a moment it goes to the session, not to the email question.
|
|
2170
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
2171
|
+
// Anything already typed but not yet read is thrown away, so the
|
|
2172
|
+
// first question waits for a real answer.
|
|
2173
|
+
while (process.stdin.read() !== null) { /* discard */ }
|
|
2174
|
+
await cmdLogin([]);
|
|
2175
|
+
});
|
|
2176
|
+
// Canceled or refused: nothing changed, so the session carries on.
|
|
2177
|
+
if ((0, connection_1.readConnection)()?.token === before)
|
|
2178
|
+
break;
|
|
2179
|
+
out(ui.c.dim(` type ${ui.c.bold('comprism')} to start a session with this sign in`));
|
|
2180
|
+
out();
|
|
2181
|
+
return leave();
|
|
2182
|
+
}
|
|
1721
2183
|
case 'logout':
|
|
1722
2184
|
case 'signout':
|
|
1723
2185
|
case 'disconnect': {
|