@illuminis/comprism 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +281 -0
- package/out/agent/command.d.ts +86 -0
- package/out/agent/command.js +259 -0
- package/out/agent/render.d.ts +97 -0
- package/out/agent/render.js +255 -0
- package/out/agent/session.d.ts +175 -0
- package/out/agent/session.js +573 -0
- package/out/commands/ask.d.ts +1 -0
- package/out/commands/ask.js +146 -0
- package/out/commands/codemap.d.ts +2 -0
- package/out/commands/codemap.js +151 -0
- package/out/commands/commands-thin.d.ts +39 -0
- package/out/commands/commands-thin.js +182 -0
- package/out/commands/install.d.ts +163 -0
- package/out/commands/install.js +543 -0
- package/out/commands/keys.d.ts +55 -0
- package/out/commands/keys.js +344 -0
- package/out/commands/login.d.ts +9 -0
- package/out/commands/login.js +384 -0
- package/out/commands/repl.d.ts +1 -0
- package/out/commands/repl.js +752 -0
- package/out/commands/settings.d.ts +21 -0
- package/out/commands/settings.js +244 -0
- package/out/commands/welcome.d.ts +1 -0
- package/out/commands/welcome.js +196 -0
- package/out/executor/documents.d.ts +40 -0
- package/out/executor/documents.js +170 -0
- package/out/executor/files.d.ts +2 -0
- package/out/executor/files.js +360 -0
- package/out/executor/git.d.ts +48 -0
- package/out/executor/git.js +132 -0
- package/out/executor/hooks.d.ts +67 -0
- package/out/executor/hooks.js +247 -0
- package/out/executor/index.d.ts +29 -0
- package/out/executor/index.js +221 -0
- package/out/executor/notebook.d.ts +2 -0
- package/out/executor/notebook.js +147 -0
- package/out/executor/paths.d.ts +15 -0
- package/out/executor/paths.js +126 -0
- package/out/executor/shell.d.ts +41 -0
- package/out/executor/shell.js +336 -0
- package/out/graph/build.d.ts +45 -0
- package/out/graph/build.js +91 -0
- package/out/graph/facts.d.ts +47 -0
- package/out/graph/facts.js +12 -0
- package/out/graph/files.d.ts +45 -0
- package/out/graph/files.js +207 -0
- package/out/graph/read-locales.d.ts +29 -0
- package/out/graph/read-locales.js +246 -0
- package/out/graph/read-python.d.ts +11 -0
- package/out/graph/read-python.js +115 -0
- package/out/graph/read-typescript.d.ts +16 -0
- package/out/graph/read-typescript.js +292 -0
- package/out/graph/sync.d.ts +66 -0
- package/out/graph/sync.js +242 -0
- package/out/lib/attach.d.ts +62 -0
- package/out/lib/attach.js +228 -0
- package/out/lib/config.d.ts +93 -0
- package/out/lib/config.js +198 -0
- package/out/lib/connection.d.ts +73 -0
- package/out/lib/connection.js +188 -0
- package/out/lib/gateway.d.ts +239 -0
- package/out/lib/gateway.js +171 -0
- package/out/lib/prompt.d.ts +34 -0
- package/out/lib/prompt.js +108 -0
- package/out/lib/types.d.ts +417 -0
- package/out/lib/types.js +21 -0
- package/out/lib/ui.d.ts +114 -0
- package/out/lib/ui.js +265 -0
- package/out/lib/version.d.ts +24 -0
- package/out/lib/version.js +27 -0
- package/out/lib/voice.d.ts +50 -0
- package/out/lib/voice.js +218 -0
- package/out/postinstall.d.ts +2 -0
- package/out/postinstall.js +92 -0
- package/out/thin.d.ts +2 -0
- package/out/thin.js +259 -0
- package/package.json +101 -0
- package/scripts/read_python.py +270 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.runRepl = runRepl;
|
|
40
|
+
/**
|
|
41
|
+
* The product, running.
|
|
42
|
+
*
|
|
43
|
+
* ── What changed and why ──────────────────────────────────────────────────
|
|
44
|
+
*
|
|
45
|
+
* Typing `comprism` used to print a screen and exit, so every question meant
|
|
46
|
+
* typing the program's name again. Nobody works that way, and it makes a
|
|
47
|
+
* conversation look like a series of unrelated commands when the product's
|
|
48
|
+
* whole value is the thread between them.
|
|
49
|
+
*
|
|
50
|
+
* So `comprism` STARTS. It opens a session, shows where you are, and then
|
|
51
|
+
* waits for work. You type a question and get an answer with its receipt. You
|
|
52
|
+
* type the next one and it continues the same session.
|
|
53
|
+
*
|
|
54
|
+
* A bare word is a command; anything else is work. That is the only rule a
|
|
55
|
+
* person has to hold, and it is the rule every shell already taught them.
|
|
56
|
+
*
|
|
57
|
+
* comprism
|
|
58
|
+
* > what is the capital of Canada? ← work
|
|
59
|
+
* > sessions ← a command
|
|
60
|
+
* > new ← a command
|
|
61
|
+
* > exit ← or Ctrl-C
|
|
62
|
+
*
|
|
63
|
+
* ── A session with no key cannot do anything ──────────────────────────────
|
|
64
|
+
*
|
|
65
|
+
* Every answer is billed to the customer's own provider account, so with no key
|
|
66
|
+
* there is nothing to run. Rather than accept a prompt and refuse it, the
|
|
67
|
+
* session sends the person to `keys` first and will not start until one is
|
|
68
|
+
* stored. Being stopped early and told why beats being stopped late.
|
|
69
|
+
*/
|
|
70
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
71
|
+
const connection_1 = require("../lib/connection");
|
|
72
|
+
const keys_1 = require("./keys");
|
|
73
|
+
const welcome_1 = require("./welcome");
|
|
74
|
+
const attach = __importStar(require("../lib/attach"));
|
|
75
|
+
const ui = __importStar(require("../lib/ui"));
|
|
76
|
+
const voice = __importStar(require("../lib/voice"));
|
|
77
|
+
const out = (s = '') => process.stdout.write(s + '\n');
|
|
78
|
+
/**
|
|
79
|
+
* The session's reader, and the two calls that lend the keyboard out.
|
|
80
|
+
*
|
|
81
|
+
* Lending it out is for the handful of COMMANDS that open a reader of their own
|
|
82
|
+
* (`keys`, `model`, `workspace`). A job no longer needs it: the agent is handed
|
|
83
|
+
* this reader instead of opening a second one, so the session keeps the
|
|
84
|
+
* keyboard for the whole of its life. See `build`.
|
|
85
|
+
*/
|
|
86
|
+
let reader = null;
|
|
87
|
+
let onLine = null;
|
|
88
|
+
function releaseTerminal() {
|
|
89
|
+
reader?.close();
|
|
90
|
+
reader = null;
|
|
91
|
+
}
|
|
92
|
+
function takeTerminal() {
|
|
93
|
+
if (reader || !onLine)
|
|
94
|
+
return;
|
|
95
|
+
reader = node_readline_1.default.createInterface({
|
|
96
|
+
input: process.stdin,
|
|
97
|
+
output: process.stdout,
|
|
98
|
+
terminal: process.stdin.isTTY === true,
|
|
99
|
+
});
|
|
100
|
+
reader.on('line', onLine);
|
|
101
|
+
reader.on('SIGINT', () => leaveSession());
|
|
102
|
+
}
|
|
103
|
+
/** Set by the session so the handover can end it too. */
|
|
104
|
+
let leaveSession = () => { };
|
|
105
|
+
/** Words that are commands. Anything else typed is work. */
|
|
106
|
+
const COMMANDS = new Set([
|
|
107
|
+
'new', 'sessions', 'model', 'providers', 'keys', 'workspace', 'status',
|
|
108
|
+
'voice', 'help', 'exit', 'quit', 'clear', 'login', 'logout', 'signout',
|
|
109
|
+
'disconnect',
|
|
110
|
+
]);
|
|
111
|
+
/**
|
|
112
|
+
* The line as a command, with a leading slash taken off.
|
|
113
|
+
*
|
|
114
|
+
* Every other tool in this space teaches a slash, so people type `/model` here.
|
|
115
|
+
* That used to miss the command list entirely and go to the agent as a piece of
|
|
116
|
+
* work: it answered the question and the answer was charged to the person's own
|
|
117
|
+
* provider account. A command is the same command whether or not it is
|
|
118
|
+
* announced with a slash, and a slashed word that is NOT a command is refused
|
|
119
|
+
* rather than billed.
|
|
120
|
+
*/
|
|
121
|
+
function commandWord(typed) {
|
|
122
|
+
const slashed = typed.startsWith('/');
|
|
123
|
+
// `comprism sessions`, typed at the comprism prompt. Everybody does it, and
|
|
124
|
+
// it used to be sent to the model as work: the agent looked around the
|
|
125
|
+
// project for something called "comprism sessions", found nothing, and asked
|
|
126
|
+
// what was meant. Reading the program's own name as part of a question is a
|
|
127
|
+
// product refusing to recognize itself.
|
|
128
|
+
const bare = (slashed ? typed.slice(1) : typed).trim().replace(/^comprism\s+/i, '');
|
|
129
|
+
return { word: bare.toLowerCase(), slashed };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Run a command that asks its own questions.
|
|
133
|
+
*
|
|
134
|
+
* The session holds the keyboard so it can read what is typed at its prompt. A
|
|
135
|
+
* command like `keys` opens its own reader to ask a question, and two readers
|
|
136
|
+
* on one keyboard divide the typing between them: the answer either reached the
|
|
137
|
+
* wrong reader or arrived nowhere at all and the session sat there waiting.
|
|
138
|
+
*
|
|
139
|
+
* So the session stands down for the length of the command, the command's own
|
|
140
|
+
* reader is closed behind it whatever happened, and the session picks the
|
|
141
|
+
* keyboard back up. The same handover a job gets, for the same reason.
|
|
142
|
+
*/
|
|
143
|
+
async function withTerminal(run) {
|
|
144
|
+
releaseTerminal();
|
|
145
|
+
try {
|
|
146
|
+
await run();
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
const { closePrompt } = await Promise.resolve().then(() => __importStar(require('../lib/prompt')));
|
|
150
|
+
closePrompt();
|
|
151
|
+
takeTerminal();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* What this account has actually done, newest first.
|
|
156
|
+
*
|
|
157
|
+
* The in-memory list only ever knew about the current run, so closing the
|
|
158
|
+
* window lost every thread: a person who came back the next morning and typed
|
|
159
|
+
* `sessions` was shown one line, the session they were standing in, and had no
|
|
160
|
+
* way to pick up yesterday's work. The record has held all of it the whole
|
|
161
|
+
* time. Reading it is the difference between a session and a scratchpad.
|
|
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.
|
|
165
|
+
*/
|
|
166
|
+
async function pastWork(limit = 10) {
|
|
167
|
+
const conn = (0, connection_1.readConnection)();
|
|
168
|
+
// The SESSION token, not the workspace key. The workspace key is a machine
|
|
169
|
+
// credential for intercepted requests and the portal's own routes refuse it,
|
|
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 {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function newSession() {
|
|
197
|
+
return {
|
|
198
|
+
// Short and readable, because a person reads it in a list and types it back.
|
|
199
|
+
id: new Date().toISOString().slice(11, 19).replace(/:/g, ''),
|
|
200
|
+
startedAt: new Date().toISOString(),
|
|
201
|
+
jobs: [],
|
|
202
|
+
spentUsd: 0,
|
|
203
|
+
savedUsd: 0,
|
|
204
|
+
compared: 0,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const money = (n) => `$${n.toFixed(4)}`;
|
|
208
|
+
function printHelp() {
|
|
209
|
+
out();
|
|
210
|
+
out(ui.c.dim(' Type a question and press enter. Or one of these:'));
|
|
211
|
+
out();
|
|
212
|
+
const rows = [
|
|
213
|
+
['new', 'start a fresh session'],
|
|
214
|
+
['sessions', 'this run and earlier work, and pick one up'],
|
|
215
|
+
['model', 'the model your savings are measured against'],
|
|
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)}`);
|
|
227
|
+
}
|
|
228
|
+
out();
|
|
229
|
+
out(ui.c.dim(' A slash is fine as well, so `/model` and `model` do the same thing.'));
|
|
230
|
+
out(ui.c.dim(' Drag a file onto this window, or type @ and a path, to have it read.'));
|
|
231
|
+
out();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Hand a request to the coding agent, in the folder this session is working in.
|
|
235
|
+
*
|
|
236
|
+
* The agent reads the project, proposes every change before making it, and runs
|
|
237
|
+
* the tests. It is the same agent the standalone command runs; there is one
|
|
238
|
+
* implementation, so the session and the command cannot behave differently.
|
|
239
|
+
*/
|
|
240
|
+
async function build(session, request, confirm, onStarted, attachmentIds = []) {
|
|
241
|
+
const { runAgent } = await Promise.resolve().then(() => __importStar(require('../agent/command')));
|
|
242
|
+
const { currentWorkspace } = await Promise.resolve().then(() => __importStar(require('./settings')));
|
|
243
|
+
const folder = currentWorkspace();
|
|
244
|
+
out();
|
|
245
|
+
out(ui.c.dim(` working in ${folder.path}`));
|
|
246
|
+
out();
|
|
247
|
+
// Counted the moment it starts, not when it returns. A person who leaves
|
|
248
|
+
// part-way through a job was told "nothing asked in that session" over work
|
|
249
|
+
// that had already been paid for, which is the one number this product cannot
|
|
250
|
+
// get wrong.
|
|
251
|
+
session.jobs.push({ request });
|
|
252
|
+
const code = await runAgent({
|
|
253
|
+
request: [request],
|
|
254
|
+
cwd: folder.path,
|
|
255
|
+
// Carry on from the last question rather than starting again. `new` clears
|
|
256
|
+
// it, which is what `new` is for.
|
|
257
|
+
continuesJob: session.lastJobId,
|
|
258
|
+
// The session's own keyboard, handed to the agent.
|
|
259
|
+
//
|
|
260
|
+
// The agent has to put a change in front of the person and wait. It used to
|
|
261
|
+
// open a reader of its own to do that, and with the session already reading
|
|
262
|
+
// the same keyboard the question landed nowhere: every command came back
|
|
263
|
+
// "needs approval and nobody is watching", refused by the product rather
|
|
264
|
+
// than by the person. One reader, owned by whoever started the process,
|
|
265
|
+
// passed down.
|
|
266
|
+
confirm,
|
|
267
|
+
onStarted,
|
|
268
|
+
// Files the person attached, already read and waiting on the server.
|
|
269
|
+
attachments: attachmentIds,
|
|
270
|
+
// Ask before a change and before a command, because now the person can
|
|
271
|
+
// actually answer. This was auto_edit only because approving a write was
|
|
272
|
+
// impossible from inside a session; it is the mode the standalone command
|
|
273
|
+
// uses, so the two behave the same way.
|
|
274
|
+
mode: 'approve_writes',
|
|
275
|
+
// The running total is the whole point of a session, so it is added up
|
|
276
|
+
// from what each job actually cost. It used to be declared and never
|
|
277
|
+
// touched, so a session ended by reporting zero spent and zero saved
|
|
278
|
+
// directly underneath a job that had just charged three cents.
|
|
279
|
+
onFinished: (r) => {
|
|
280
|
+
// Recorded even when the job did not finish. A run that was stopped or
|
|
281
|
+
// ran out of steps still said things, and the next question is usually
|
|
282
|
+
// about exactly that.
|
|
283
|
+
if (r.jobId)
|
|
284
|
+
session.lastJobId = r.jobId;
|
|
285
|
+
session.spentUsd += r.totalUsd;
|
|
286
|
+
if (r.savedUsd !== null) {
|
|
287
|
+
session.savedUsd += r.savedUsd;
|
|
288
|
+
session.compared += 1;
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
out();
|
|
293
|
+
if (code !== 0)
|
|
294
|
+
out(ui.c.amber(' the job did not finish'));
|
|
295
|
+
}
|
|
296
|
+
/** The running total, shown when a session ends or another is opened. */
|
|
297
|
+
function summarize(s) {
|
|
298
|
+
if (s.jobs.length === 0) {
|
|
299
|
+
out(ui.c.dim(' nothing asked in that session'));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
// A saving is only shown when at least one job could be priced against the
|
|
303
|
+
// comparison model. Otherwise there is nothing to compare, and printing
|
|
304
|
+
// "$0.0000 saved" would state a figure nobody measured.
|
|
305
|
+
const saving = s.compared > 0
|
|
306
|
+
? ` ${ui.c.green(money(s.savedUsd))} ${ui.c.dim('saved')}`
|
|
307
|
+
: ` ${ui.c.dim('nothing to compare against yet')}`;
|
|
308
|
+
out(` ${ui.c.dim('session')} ${ui.c.bold(s.id)} ${s.jobs.length} job(s)`
|
|
309
|
+
+ ` ${money(s.spentUsd)} ${ui.c.dim('spent')}${saving}`);
|
|
310
|
+
}
|
|
311
|
+
async function runRepl() {
|
|
312
|
+
// Resolved when the person leaves. The reader is closed and reopened around
|
|
313
|
+
// every job, so waiting on it closing would end the session at the first one.
|
|
314
|
+
let done = () => { };
|
|
315
|
+
const finished = new Promise((resolve) => { done = resolve; });
|
|
316
|
+
const conn = (0, connection_1.readConnection)();
|
|
317
|
+
if (!conn?.url || !conn.workspaceKey) {
|
|
318
|
+
await (0, welcome_1.showWelcome)();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
await (0, welcome_1.showWelcome)();
|
|
322
|
+
// A key, before anything else. With none there is nothing to bill and nothing
|
|
323
|
+
// to measure, so a prompt accepted here could only ever be refused later.
|
|
324
|
+
//
|
|
325
|
+
// THREE outcomes, not two. Telling "your account holds no key" apart from "we
|
|
326
|
+
// could not ask your account" is the whole point: on 17 September a person
|
|
327
|
+
// whose workspace held four working keys was shown the first message and sent
|
|
328
|
+
// to a form, because an unanswered question was read as an answer of none.
|
|
329
|
+
const state = await (0, keys_1.keyState)();
|
|
330
|
+
if (!state.reached) {
|
|
331
|
+
out(ui.fail(`Your workspace could not be reached, so ${ui.c.bold('nothing is known')} about your keys.`));
|
|
332
|
+
out(` ${ui.c.dim(state.why)}`);
|
|
333
|
+
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')}`);
|
|
334
|
+
out();
|
|
335
|
+
process.exitCode = 1;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!state.hasKey) {
|
|
339
|
+
out(ui.c.amber(' Your account has no provider key stored, so nothing can run yet.'));
|
|
340
|
+
out(ui.c.dim(' Every answer is billed to your own provider account.'));
|
|
341
|
+
out();
|
|
342
|
+
await (0, keys_1.cmdKeys)([]);
|
|
343
|
+
const after = await (0, keys_1.keyState)();
|
|
344
|
+
if (!(after.reached && after.hasKey)) {
|
|
345
|
+
out(ui.fail('Still no key stored on your account, so the session cannot start.'));
|
|
346
|
+
out(` ${ui.c.dim('run')} ${ui.c.bold('comprism keys')} ${ui.c.dim('when you have one')}`);
|
|
347
|
+
out();
|
|
348
|
+
process.exitCode = 1;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
out();
|
|
352
|
+
}
|
|
353
|
+
const history = [];
|
|
354
|
+
let session = newSession();
|
|
355
|
+
history.push(session);
|
|
356
|
+
// Ctrl-C leaves, and says what the session cost on the way out. A product
|
|
357
|
+
// that vanishes without telling you what it spent is the thing this one
|
|
358
|
+
// exists to replace.
|
|
359
|
+
// Set while the agent is waiting on a yes or no. The next line typed is that
|
|
360
|
+
// answer, not the next request, so it is handed straight to whoever is
|
|
361
|
+
// waiting instead of joining the queue.
|
|
362
|
+
let answering = null;
|
|
363
|
+
const takeAnswer = (line) => {
|
|
364
|
+
if (!answering)
|
|
365
|
+
return false;
|
|
366
|
+
const waiting = answering;
|
|
367
|
+
answering = null;
|
|
368
|
+
waiting(line);
|
|
369
|
+
return true;
|
|
370
|
+
};
|
|
371
|
+
/**
|
|
372
|
+
* The keyboard, lent to the agent for one question.
|
|
373
|
+
*
|
|
374
|
+
* The session keeps its reader. The agent is given a way to use it, and gets
|
|
375
|
+
* back a plain yes or no. Anything other than yes is no, which is the same
|
|
376
|
+
* rule the standalone command applies, so an approval is never assumed from a
|
|
377
|
+
* stray keystroke.
|
|
378
|
+
*/
|
|
379
|
+
const confirm = (question) => new Promise((resolve) => {
|
|
380
|
+
if (leaving || !reader) {
|
|
381
|
+
resolve(false);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
answering = (line) => resolve(/^y(es)?$/i.test(line.trim()));
|
|
385
|
+
reader.setPrompt(` ${question} `);
|
|
386
|
+
reader.prompt();
|
|
387
|
+
});
|
|
388
|
+
/** The next line typed, whatever it says. The same lent keyboard, used to
|
|
389
|
+
* wait rather than to ask: nothing is read here, the press of enter is the
|
|
390
|
+
* whole signal. */
|
|
391
|
+
const nextLine = () => new Promise((resolve) => {
|
|
392
|
+
if (leaving || !reader) {
|
|
393
|
+
resolve('');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
answering = resolve;
|
|
397
|
+
reader.setPrompt('');
|
|
398
|
+
reader.prompt();
|
|
399
|
+
});
|
|
400
|
+
/**
|
|
401
|
+
* Say it instead of typing it.
|
|
402
|
+
*
|
|
403
|
+
* Recorded here, transcribed on the server, and then put INTO the prompt
|
|
404
|
+
* rather than sent. Speech is misheard often enough that sending it
|
|
405
|
+
* unseen would eventually spend somebody's money on a question they never
|
|
406
|
+
* asked, and the fix costs one glance.
|
|
407
|
+
*
|
|
408
|
+
* This draws its own prompt at the end, which is why the caller must not.
|
|
409
|
+
* The words go into the line buffer, and a buffer written before the prompt
|
|
410
|
+
* exists is echoed once by readline and again when the prompt redraws, so
|
|
411
|
+
* the person saw their sentence twice and could edit only the second copy.
|
|
412
|
+
*/
|
|
413
|
+
const speak = async () => {
|
|
414
|
+
const short = voice.missing();
|
|
415
|
+
if (short) {
|
|
416
|
+
out();
|
|
417
|
+
out(ui.c.amber(` ${short}`));
|
|
418
|
+
out();
|
|
419
|
+
return prompt();
|
|
420
|
+
}
|
|
421
|
+
let live;
|
|
422
|
+
try {
|
|
423
|
+
live = voice.start();
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
out(ui.fail(err.message));
|
|
427
|
+
out();
|
|
428
|
+
return prompt();
|
|
429
|
+
}
|
|
430
|
+
out();
|
|
431
|
+
out(ui.c.green(' listening...') + ui.c.dim(' say it, then press enter'));
|
|
432
|
+
await nextLine();
|
|
433
|
+
const { file, bytes } = await live.stop();
|
|
434
|
+
try {
|
|
435
|
+
if (bytes < 2000) {
|
|
436
|
+
out(ui.c.dim(' nothing was recorded'));
|
|
437
|
+
out();
|
|
438
|
+
return prompt();
|
|
439
|
+
}
|
|
440
|
+
out(ui.c.dim(' writing that down...'));
|
|
441
|
+
const said = await voice.transcribe(conn.url, conn.workspaceKey, file);
|
|
442
|
+
if (!said.text) {
|
|
443
|
+
out(ui.c.dim(' no words in that one'));
|
|
444
|
+
out();
|
|
445
|
+
return prompt();
|
|
446
|
+
}
|
|
447
|
+
out();
|
|
448
|
+
// The prompt first, THEN the words into it. The person sees their own
|
|
449
|
+
// sentence at the prompt, edits it where the microphone misheard, and
|
|
450
|
+
// presses enter. Nothing has been asked or charged until they do.
|
|
451
|
+
prompt();
|
|
452
|
+
reader?.write(said.text);
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
out(ui.fail(err.message));
|
|
456
|
+
out();
|
|
457
|
+
prompt();
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
// The recording is deleted here, always. It is the most personal thing
|
|
461
|
+
// this product ever touches and there is no reason on earth to keep it.
|
|
462
|
+
voice.discard(file);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* Files the person pointed at, read and ready, and the question without the
|
|
467
|
+
* long paths in it.
|
|
468
|
+
*
|
|
469
|
+
* Uploading happens before the job starts and is reported before anything is
|
|
470
|
+
* charged, because the commonest complaint about any assistant is that it
|
|
471
|
+
* answered without looking at the thing you gave it.
|
|
472
|
+
*/
|
|
473
|
+
const gather = async (typed) => {
|
|
474
|
+
const { currentWorkspace } = await Promise.resolve().then(() => __importStar(require('./settings')));
|
|
475
|
+
const found = attach.mentions(typed, currentWorkspace().path);
|
|
476
|
+
if (found.length === 0)
|
|
477
|
+
return { text: typed, ids: [] };
|
|
478
|
+
out();
|
|
479
|
+
out(ui.c.dim(` reading ${found.length} file(s)...`));
|
|
480
|
+
const { attached, refused } = await attach.upload(conn.url, conn.workspaceKey, found.map((m) => m.file));
|
|
481
|
+
for (const a of attached)
|
|
482
|
+
out(ui.c.dim(` ${attach.describe(a)}`));
|
|
483
|
+
for (const r of refused)
|
|
484
|
+
out(ui.c.amber(` ${r.file} was not read: ${r.why}`));
|
|
485
|
+
// Only the files that actually arrived come out of the sentence. One that
|
|
486
|
+
// was refused stays written where the person put it, so the question still
|
|
487
|
+
// says what it was about rather than silently losing it.
|
|
488
|
+
const landed = found.filter((m) => !refused.some((r) => r.file === m.file));
|
|
489
|
+
return { text: attach.withoutPaths(typed, landed), ids: attached.map((a) => a.id) };
|
|
490
|
+
};
|
|
491
|
+
// A way to stop the job that is running, while it is running. Null between
|
|
492
|
+
// jobs, which is also the answer to "is anything running".
|
|
493
|
+
/** The earlier jobs shown by the last `sessions`, so `sessions w2` means the
|
|
494
|
+
* same thing the person just read. */
|
|
495
|
+
let recent = [];
|
|
496
|
+
let cancelJob = null;
|
|
497
|
+
/** Set while a job is running, so a line typed mid job reaches it instead of
|
|
498
|
+
* queueing up behind it as the next question. */
|
|
499
|
+
let steerJob = null;
|
|
500
|
+
let leaving = false;
|
|
501
|
+
/**
|
|
502
|
+
* Ctrl-C, or `exit`.
|
|
503
|
+
*
|
|
504
|
+
* Three things in order, and the order is the point. The question on screen
|
|
505
|
+
* is answered no, because nobody said yes. The job is stopped, because the
|
|
506
|
+
* person asked to leave and a job left running spends money nobody is
|
|
507
|
+
* watching. Then the session waits for the job to report before it says what
|
|
508
|
+
* it cost, since a figure printed before the job replies is a figure of zero
|
|
509
|
+
* over work already paid for.
|
|
510
|
+
*
|
|
511
|
+
* The wait is capped. A server that never answers must not turn Ctrl-C into
|
|
512
|
+
* something a person has to press twice.
|
|
513
|
+
*/
|
|
514
|
+
const leave = () => {
|
|
515
|
+
if (leaving)
|
|
516
|
+
return;
|
|
517
|
+
leaving = true;
|
|
518
|
+
takeAnswer('n');
|
|
519
|
+
cancelJob?.();
|
|
520
|
+
const close = () => {
|
|
521
|
+
out();
|
|
522
|
+
summarize(session);
|
|
523
|
+
out();
|
|
524
|
+
releaseTerminal();
|
|
525
|
+
done();
|
|
526
|
+
};
|
|
527
|
+
if (!working)
|
|
528
|
+
return close();
|
|
529
|
+
const waitedFrom = Date.now();
|
|
530
|
+
const settle = setInterval(() => {
|
|
531
|
+
if (!working || Date.now() - waitedFrom > 5000) {
|
|
532
|
+
clearInterval(settle);
|
|
533
|
+
close();
|
|
534
|
+
}
|
|
535
|
+
}, 50);
|
|
536
|
+
};
|
|
537
|
+
leaveSession = leave;
|
|
538
|
+
const prompt = () => {
|
|
539
|
+
if (leaving || !reader)
|
|
540
|
+
return;
|
|
541
|
+
reader.setPrompt(`${ui.c.bold(ui.c.blue('comprism'))} ${ui.c.dim('>')} `);
|
|
542
|
+
reader.prompt();
|
|
543
|
+
};
|
|
544
|
+
out(ui.c.dim(' Type what you want done, or `help`. Ctrl-C to leave.'));
|
|
545
|
+
out();
|
|
546
|
+
// One line at a time, in the order they arrived.
|
|
547
|
+
//
|
|
548
|
+
// Readline keeps reading while an answer is still coming back, so two
|
|
549
|
+
// questions typed quickly ran at once and the second finished first. Pausing
|
|
550
|
+
// the reader fixed the order and THREW AWAY anything already typed, which is
|
|
551
|
+
// worse: a person who types ahead loses what they typed.
|
|
552
|
+
//
|
|
553
|
+
// So lines are queued and worked through one at a time. Nothing is dropped
|
|
554
|
+
// and nothing overtakes.
|
|
555
|
+
const queue = [];
|
|
556
|
+
let working = false;
|
|
557
|
+
const drain = async () => {
|
|
558
|
+
if (working)
|
|
559
|
+
return;
|
|
560
|
+
working = true;
|
|
561
|
+
while (queue.length > 0 && !leaving) {
|
|
562
|
+
await handle(queue.shift());
|
|
563
|
+
}
|
|
564
|
+
working = false;
|
|
565
|
+
};
|
|
566
|
+
onLine = (line) => {
|
|
567
|
+
// An answer the agent is waiting for jumps the queue, because it is not a
|
|
568
|
+
// request. Queued, it would sit behind the very job that is waiting on it.
|
|
569
|
+
if (takeAnswer(line))
|
|
570
|
+
return;
|
|
571
|
+
// Something said while a job is RUNNING goes to that job, not into the
|
|
572
|
+
// queue behind it. A person who sees the agent going the wrong way at step
|
|
573
|
+
// three had two options before this: stop it and lose everything, or watch
|
|
574
|
+
// it finish doing the wrong thing and pay for the privilege. It joins the
|
|
575
|
+
// conversation before the next step, which is the earliest moment it can
|
|
576
|
+
// change anything.
|
|
577
|
+
const said = line.trim();
|
|
578
|
+
if (steerJob && said) {
|
|
579
|
+
// The session says nothing back. `steer` already prints its own
|
|
580
|
+
// acknowledgement, and two lines for one thing typed reads as a stutter.
|
|
581
|
+
steerJob(said);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
queue.push(line);
|
|
585
|
+
void drain();
|
|
586
|
+
};
|
|
587
|
+
// The keyboard first, THEN the prompt. Drawing the prompt before the reader
|
|
588
|
+
// existed printed nothing, and the session looked dead on arrival.
|
|
589
|
+
takeTerminal();
|
|
590
|
+
prompt();
|
|
591
|
+
async function handle(line) {
|
|
592
|
+
const typed = line.trim();
|
|
593
|
+
if (!typed)
|
|
594
|
+
return prompt();
|
|
595
|
+
const { word, slashed } = commandWord(typed);
|
|
596
|
+
if (COMMANDS.has(word)) {
|
|
597
|
+
switch (word) {
|
|
598
|
+
case 'exit':
|
|
599
|
+
case 'quit':
|
|
600
|
+
return leave();
|
|
601
|
+
case 'clear':
|
|
602
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
603
|
+
break;
|
|
604
|
+
case 'help':
|
|
605
|
+
printHelp();
|
|
606
|
+
break;
|
|
607
|
+
case 'new': {
|
|
608
|
+
summarize(session);
|
|
609
|
+
session = newSession();
|
|
610
|
+
history.push(session);
|
|
611
|
+
out(ui.ok(`New session ${ui.c.bold(session.id)}.`));
|
|
612
|
+
out();
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
case 'sessions': {
|
|
616
|
+
out();
|
|
617
|
+
history.forEach((s, i) => {
|
|
618
|
+
const here = s === session ? ui.c.green(' now') : ' ';
|
|
619
|
+
out(`${here} ${String(i + 1).padStart(2)}. ${ui.c.bold(s.id)}`
|
|
620
|
+
+ ` ${s.jobs.length} job(s) ${money(s.spentUsd)}`);
|
|
621
|
+
});
|
|
622
|
+
// And what this account did before this window was opened. Kept
|
|
623
|
+
// numbered separately so a number never means two different things.
|
|
624
|
+
recent = await pastWork();
|
|
625
|
+
if (recent.length > 0) {
|
|
626
|
+
out();
|
|
627
|
+
out(ui.c.dim(' earlier work on this account:'));
|
|
628
|
+
recent.forEach((j, i) => {
|
|
629
|
+
const said = (j.name || j.request || '(no request recorded)')
|
|
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)}`));
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
out();
|
|
636
|
+
out(ui.c.dim(' `sessions 2` reopens one from this run.'
|
|
637
|
+
+ (recent.length ? ' `sessions w1` picks up earlier work.' : '')));
|
|
638
|
+
out();
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case 'status': {
|
|
642
|
+
const { cmdStatus } = await Promise.resolve().then(() => __importStar(require('./commands-thin')));
|
|
643
|
+
await cmdStatus();
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
case 'model': {
|
|
647
|
+
const { cmdModel } = await Promise.resolve().then(() => __importStar(require('./settings')));
|
|
648
|
+
await withTerminal(() => cmdModel([]));
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case 'providers': {
|
|
652
|
+
const { cmdProviders } = await Promise.resolve().then(() => __importStar(require('./settings')));
|
|
653
|
+
await cmdProviders();
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case 'voice':
|
|
657
|
+
// Returns without the redraw below: `speak` has drawn the prompt
|
|
658
|
+
// already, and has put the dictated words into it.
|
|
659
|
+
return speak();
|
|
660
|
+
case 'keys':
|
|
661
|
+
await withTerminal(() => (0, keys_1.cmdKeys)([]));
|
|
662
|
+
break;
|
|
663
|
+
case 'workspace': {
|
|
664
|
+
const { cmdWorkspace } = await Promise.resolve().then(() => __importStar(require('./settings')));
|
|
665
|
+
await withTerminal(() => cmdWorkspace([]));
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
// Signing out ends the session, because everything after it would be
|
|
669
|
+
// refused anyway. It used to answer "not available inside a session",
|
|
670
|
+
// which left quitting and running a second command as the only way.
|
|
671
|
+
case 'logout':
|
|
672
|
+
case 'signout':
|
|
673
|
+
case 'disconnect': {
|
|
674
|
+
const { cmdDisconnect } = await Promise.resolve().then(() => __importStar(require('./commands-thin')));
|
|
675
|
+
await withTerminal(() => cmdDisconnect());
|
|
676
|
+
return leave();
|
|
677
|
+
}
|
|
678
|
+
default:
|
|
679
|
+
out(ui.c.dim(` \`${word}\` is not available inside a session.`));
|
|
680
|
+
out();
|
|
681
|
+
}
|
|
682
|
+
return prompt();
|
|
683
|
+
}
|
|
684
|
+
// `sessions 2` reopens the second one from this run.
|
|
685
|
+
const reopen = /^sessions\s+(\d+)$/.exec(word);
|
|
686
|
+
if (reopen) {
|
|
687
|
+
const target = history[Number(reopen[1]) - 1];
|
|
688
|
+
if (!target) {
|
|
689
|
+
out(ui.fail(`There is no session ${reopen[1]}.`));
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
summarize(session);
|
|
693
|
+
session = target;
|
|
694
|
+
out(ui.ok(`Back in session ${ui.c.bold(session.id)}, ${session.jobs.length} job(s) so far.`));
|
|
695
|
+
}
|
|
696
|
+
out();
|
|
697
|
+
return prompt();
|
|
698
|
+
}
|
|
699
|
+
// `sessions w1` picks up work from before this window was opened. A new
|
|
700
|
+
// session is started rather than an old one edited: what it inherits is the
|
|
701
|
+
// conversation, not the spending, and adding yesterday's cost to today's
|
|
702
|
+
// running total would make the session summary a wrong number.
|
|
703
|
+
const carryOn = /^sessions\s+w(\d+)$/.exec(word);
|
|
704
|
+
if (carryOn) {
|
|
705
|
+
if (recent.length === 0)
|
|
706
|
+
recent = await pastWork();
|
|
707
|
+
const target = recent[Number(carryOn[1]) - 1];
|
|
708
|
+
if (!target) {
|
|
709
|
+
out(ui.fail(`There is no earlier job w${carryOn[1]}. Type \`sessions\` for the list.`));
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
summarize(session);
|
|
713
|
+
session = newSession();
|
|
714
|
+
session.lastJobId = target.id;
|
|
715
|
+
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
|
+
}
|
|
720
|
+
out();
|
|
721
|
+
return prompt();
|
|
722
|
+
}
|
|
723
|
+
// A slash means "this is a command", so an unknown one is a typo and not a
|
|
724
|
+
// question. Sending it on would answer the typo and charge for the answer,
|
|
725
|
+
// which is exactly what `/model` used to do.
|
|
726
|
+
if (slashed) {
|
|
727
|
+
out(ui.fail(`\`${typed}\` is not a command, so nothing ran and nothing was charged.`));
|
|
728
|
+
out(ui.c.dim(' Type `help` for the list, or ask without a slash to have work done.'));
|
|
729
|
+
out();
|
|
730
|
+
return prompt();
|
|
731
|
+
}
|
|
732
|
+
// EVERYTHING is work, and work means the agent.
|
|
733
|
+
//
|
|
734
|
+
// There used to be two paths: a question was answered by a model, and only
|
|
735
|
+
// an explicit build request reached the agent. A person who asked it to
|
|
736
|
+
// build them an application got a polite description of how they might do
|
|
737
|
+
// it themselves. The agent answers questions perfectly well and can also
|
|
738
|
+
// act, so the second path was never worth the confusion it caused.
|
|
739
|
+
const explicit = /^work\s+(.+)$/i.exec(typed);
|
|
740
|
+
const asked = explicit ? explicit[1] : typed;
|
|
741
|
+
const { text, ids } = await gather(asked);
|
|
742
|
+
try {
|
|
743
|
+
await build(session, text || asked, confirm, (controls) => { cancelJob = controls.cancel; steerJob = controls.steer; }, ids);
|
|
744
|
+
}
|
|
745
|
+
finally {
|
|
746
|
+
cancelJob = null;
|
|
747
|
+
steerJob = null;
|
|
748
|
+
}
|
|
749
|
+
prompt();
|
|
750
|
+
}
|
|
751
|
+
await finished;
|
|
752
|
+
}
|