@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
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Copyright (c) 2026 illuminis AI, LLC. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software is proprietary and confidential. It is distributed so that
|
|
4
|
+
authorized users may install and run it as published. No license is granted to
|
|
5
|
+
copy, modify, distribute, sublicense, sell, or create derivative works from it,
|
|
6
|
+
and no rights are granted by implication, estoppel, or otherwise.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
9
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
10
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
11
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
12
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
13
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
14
|
+
|
|
15
|
+
Contact: legal@illuminis.ai
|
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# `@illuminis/comprism` - the CompletionPrism Companion
|
|
2
|
+
|
|
3
|
+
The half of CompletionPrism that runs on your machine. It sits between the tool
|
|
4
|
+
you already use and the AI provider, records what finishing your work actually
|
|
5
|
+
cost, and changes nothing about what your tool does.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install && npm run build
|
|
9
|
+
|
|
10
|
+
export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY
|
|
11
|
+
node out/cli.js setup
|
|
12
|
+
node out/cli.js session
|
|
13
|
+
node out/cli.js report
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## The coding agent
|
|
17
|
+
|
|
18
|
+
`comprism agent` is a coding agent in your terminal. It reads your project,
|
|
19
|
+
changes files, runs commands, runs your tests, and shows you every change before
|
|
20
|
+
it makes it.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g @illuminis/comprism
|
|
24
|
+
comprism login # once, per machine
|
|
25
|
+
cd ~/your-project
|
|
26
|
+
comprism agent "the tests are failing, work out why and fix it"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**What makes it different from the others.** Every step is routed to whichever
|
|
30
|
+
model finishes the work for least, and it can change its mind between one step
|
|
31
|
+
and the next. The receipt at the end says what the job actually cost.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
/Users/you/your-project 23 actions available
|
|
35
|
+
step 1 claude-sonnet-5 $0.02 5.0s
|
|
36
|
+
ok run_tests 425ms
|
|
37
|
+
step 2 claude-sonnet-5 $0.02 3.0s
|
|
38
|
+
ok read_file parser.js
|
|
39
|
+
ok edit_file parser.js
|
|
40
|
+
step 3 claude-sonnet-5 $0.02 3.3s
|
|
41
|
+
ok run_tests 262ms
|
|
42
|
+
|
|
43
|
+
Done
|
|
44
|
+
6 steps . 6 actions . $0.14 . tests passed
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### How much it is allowed to do
|
|
48
|
+
|
|
49
|
+
Four settings, and the safe one is the default. Nothing is automatic unless you
|
|
50
|
+
choose it.
|
|
51
|
+
|
|
52
|
+
| `--mode` | What it may do |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `read_only` | Look at the project and answer questions. Change nothing |
|
|
55
|
+
| `approve_writes` | **The default.** Show you every change before making it |
|
|
56
|
+
| `auto_edit` | Change files freely. Ask before running anything |
|
|
57
|
+
| `full_auto` | Get on with it. Publishing and installing packages are still confirmed |
|
|
58
|
+
|
|
59
|
+
Some things no setting unlocks: force pushing, rewriting history, downloading a
|
|
60
|
+
script and running it, anything that touches your credentials, and deploying to
|
|
61
|
+
production. Those are yours to do.
|
|
62
|
+
|
|
63
|
+
### Before you let it change anything
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
comprism agent --plan "rename the parser module and update every caller"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
It reads what it needs, tells you what it would do, and changes nothing. The
|
|
70
|
+
cheapest moment to disagree with an agent is before it starts.
|
|
71
|
+
|
|
72
|
+
### While it is running
|
|
73
|
+
|
|
74
|
+
- **Ctrl+C** stops the job. It does not kill the command line tool, and it does
|
|
75
|
+
not leave the job running on the server.
|
|
76
|
+
- **Type anything** and it joins the next step, so you can correct course without
|
|
77
|
+
starting again.
|
|
78
|
+
|
|
79
|
+
### Taking a job back
|
|
80
|
+
|
|
81
|
+
Every change is copied before it is made. Open the job in the portal and undo it,
|
|
82
|
+
and the files go back to exactly the bytes they started with. Where a job also
|
|
83
|
+
ran a command, the undo says so plainly rather than claiming to have reversed it.
|
|
84
|
+
|
|
85
|
+
### Carrying on tomorrow
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
comprism agent --resume <job-id> "also add a test for the empty case"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
It continues the conversation rather than starting a new one, so it already knows
|
|
92
|
+
what it read and decided.
|
|
93
|
+
|
|
94
|
+
### In a script, or in continuous integration
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
comprism agent --yes --mode auto_edit "run the linter and fix what it reports"
|
|
98
|
+
echo $? # 0 if it finished, non-zero if it did not
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
With no terminal attached nobody can be asked anything, so anything that would
|
|
102
|
+
need an approval is **refused** rather than assumed. A script that needs to change
|
|
103
|
+
files says so by choosing a mode that allows it.
|
|
104
|
+
|
|
105
|
+
### Which model
|
|
106
|
+
|
|
107
|
+
By default, whichever finishes each step for least. Pin one when you want to
|
|
108
|
+
decide yourself:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
comprism agent --model gpt-5 "..." # or any model your account can reach
|
|
112
|
+
comprism agent --max-spend 2.00 "..." # stop here rather than running on
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Four providers are wired: Anthropic, OpenAI, Google and xAI. Which ones you can
|
|
116
|
+
reach depends on the keys on your account.
|
|
117
|
+
|
|
118
|
+
### Requirements
|
|
119
|
+
|
|
120
|
+
Node 22 or newer, and a workspace you have signed in to with `comprism login`.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Instrument a tool you already use
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
node out/cli.js proxy
|
|
128
|
+
export ANTHROPIC_BASE_URL=http://127.0.0.1:8787
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Requests are forwarded to the provider byte for byte. Your key is forwarded,
|
|
132
|
+
never read or stored. Unset the variable and we are gone, with nothing to undo.
|
|
133
|
+
|
|
134
|
+
## Whose Anthropic account pays
|
|
135
|
+
|
|
136
|
+
Yours, on every surface. How it gets there differs by one thing only: whether the
|
|
137
|
+
surface can carry a credential.
|
|
138
|
+
|
|
139
|
+
| How you work | Where the key comes from | Stored anywhere? |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| `comprism session`, `comprism proxy`, or an IDE pointed at your workspace | this machine, per request | no |
|
|
142
|
+
| The developer API and the SDKs | the `X-Anthropic-Key` header, per request | no |
|
|
143
|
+
| Claude Desktop and claude.ai | your workspace's own stored key | yes, encrypted, and only this one |
|
|
144
|
+
|
|
145
|
+
The third row is the exception and it is a protocol limit rather than a choice:
|
|
146
|
+
an MCP connector reaches the server with no channel for a provider credential, so
|
|
147
|
+
answering those prompts at all means the workspace holding a key. A workspace
|
|
148
|
+
that holds none gets its connector answers refused - never billed to illuminis.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
node out/cli.js key # whose account pays, here and in your workspace
|
|
152
|
+
node out/cli.js key push # send this machine's key, for the connector surface
|
|
153
|
+
node out/cli.js key clear # take it back out
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`push` is the one command that deliberately sends a provider key anywhere. It
|
|
157
|
+
sends the key this machine already resolves, over TLS, to the workspace you are
|
|
158
|
+
signed in to; Anthropic validates it before it is stored; nothing is written to
|
|
159
|
+
disk here either way. An administrator can do the same in the app, under Policy
|
|
160
|
+
and Data Controls.
|
|
161
|
+
|
|
162
|
+
## The stats footer
|
|
163
|
+
|
|
164
|
+
Through the proxy there is no result object and no widget area: the client
|
|
165
|
+
renders the message body and nothing else. So a finished Anthropic answer comes
|
|
166
|
+
back with two extra lines under it.
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
The GCD of 1071 and 462 is 21.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
**$0.038 saved** vs Claude Opus 5 · *Claude Haiku 4.5 · 12,614 tokens · 0.0s*
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**How it works.** On the way out, the footer is wrapped in an HTML comment pair
|
|
176
|
+
(`<!--cp-meta-->` … `<!--/cp-meta-->`) that renders as nothing. On a streamed
|
|
177
|
+
response it arrives as one more text content block - `content_block_start`,
|
|
178
|
+
`content_block_delta`, `content_block_stop` at the next free index - emitted
|
|
179
|
+
after the provider's last block and before the terminating `message_delta`. On a
|
|
180
|
+
non-streamed one it is spliced in as a text block after the last existing text
|
|
181
|
+
block. Either way the provider's own bytes are forwarded unchanged; the footer is
|
|
182
|
+
added, never merged.
|
|
183
|
+
|
|
184
|
+
On the way back in, every previously injected footer is cut out of the request by
|
|
185
|
+
sentinel before it reaches the provider. So the model never sees one, never
|
|
186
|
+
learns to imitate the format, and nobody pays context for the same footer twice.
|
|
187
|
+
Stripping is idempotent and runs whether or not injection is switched on.
|
|
188
|
+
|
|
189
|
+
**It is never allowed to cost you an answer.** If anything in the footer path
|
|
190
|
+
throws, the provider's response passes through completely unmodified and the
|
|
191
|
+
failure is logged rather than shown. Nothing is added at all when the turn ends
|
|
192
|
+
in a tool call, a refusal, a truncation, an error event, an empty response, or a
|
|
193
|
+
model with no published price.
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
node out/cli.js footer # what it is doing now
|
|
197
|
+
node out/cli.js footer off # byte-faithful in both directions
|
|
198
|
+
node out/cli.js footer on
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Configuring pricing, the baseline and the template
|
|
202
|
+
|
|
203
|
+
Everything lives under `footer` in `~/.comprism/config.json`:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"footer": {
|
|
208
|
+
"enabled": true,
|
|
209
|
+
"template": "compact",
|
|
210
|
+
"baselineModel": "claude-opus-5",
|
|
211
|
+
"pricing": {
|
|
212
|
+
"claude-haiku-4-5": { "inPerMTok": 0.8, "outPerMTok": 4.0 }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`baselineModel` is what the saving is measured against: the one capable model a
|
|
219
|
+
company would have mandated for everything. It is named in the footer itself,
|
|
220
|
+
because "$0.038 saved" is a claim nobody can check and "$0.038 saved vs Claude
|
|
221
|
+
Opus 5" is one they can. When the routed model costs the same or more, no
|
|
222
|
+
dollar figure is shown at all - never a negative one.
|
|
223
|
+
|
|
224
|
+
`pricing` overrides the published catalog per model, for tenants whose
|
|
225
|
+
contracted rate is not list price. `inPerMTok` and `outPerMTok` are required;
|
|
226
|
+
`cacheReadPerMTok` and `cacheWritePerMTok` are optional and default to the
|
|
227
|
+
provider's published multipliers of the input price (0.1x and 1.25x). A model in
|
|
228
|
+
neither the catalog nor this table is unpriced, and an unpriced turn gets no
|
|
229
|
+
footer rather than a guessed one.
|
|
230
|
+
|
|
231
|
+
`template` is one of:
|
|
232
|
+
|
|
233
|
+
| Template | Hero |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `compact` | this turn's saving, with the baseline named |
|
|
236
|
+
| `cumulative` | everything saved since the recorder started, then this turn |
|
|
237
|
+
| `detailed` | this turn's saving, plus the in / out / cached token split |
|
|
238
|
+
|
|
239
|
+
To add another, extend `renderFooter` in `src/footer.ts` and the `FooterTemplate`
|
|
240
|
+
union in `src/types.ts`. Keep it to a divider and one line: the footer exists to
|
|
241
|
+
punctuate an answer, not to compete with it.
|
|
242
|
+
|
|
243
|
+
### The known constraint
|
|
244
|
+
|
|
245
|
+
The client renders markdown in the message body and offers no other surface, so
|
|
246
|
+
that is the whole design space. No panel, no badge, no hover, no color beyond
|
|
247
|
+
what bold and italic give. Anything richer belongs in the portal, which is why
|
|
248
|
+
the footer stays deliberately small.
|
|
249
|
+
|
|
250
|
+
## What it records, and what it refuses to
|
|
251
|
+
|
|
252
|
+
Under `~/.comprism/`, mode 700, append-only:
|
|
253
|
+
|
|
254
|
+
| File | One row per |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `ledger.jsonl` | provider call: model, tokens, cost, latency, finish reason |
|
|
257
|
+
| `decisions.jsonl` | decision point: what was known and chosen, **never edited** |
|
|
258
|
+
| `sessions.json` | working session: the shape of each turn, not its words |
|
|
259
|
+
|
|
260
|
+
Never written, anywhere: your prompts, the responses, or your API keys. A request
|
|
261
|
+
is identified by the sha256 of its normalized text and nothing else. Turning text
|
|
262
|
+
storage on is an explicit tenant decision; everything here works without it.
|
|
263
|
+
|
|
264
|
+
## The estimator is present and inert
|
|
265
|
+
|
|
266
|
+
With no evidence, the estimator returns "no recommendation, insufficient
|
|
267
|
+
evidence", records why, and the request proceeds untouched. It never invents a
|
|
268
|
+
number. Selection arrives with the model program, through the same interface,
|
|
269
|
+
and nothing upstream of it changes.
|
|
270
|
+
|
|
271
|
+
## The gate
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
npm run gate
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Runs the frozen spec's Phase 1 acceptance list: one ledger row per call, decision
|
|
278
|
+
ids resolving, remediation on all three weak conditions, a caller override
|
|
279
|
+
dispatching exactly one model, the canaries for request text and keys, ledger
|
|
280
|
+
durability under a mid-write kill, and `report` reconciling to the ledger.
|
|
281
|
+
Eighteen checks. All of them must pass before anything here is committed.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type JobResult } from "./session";
|
|
2
|
+
export interface AgentArgs {
|
|
3
|
+
request: string[];
|
|
4
|
+
/** Start it and hand the terminal back. See `runDetached` for what this can
|
|
5
|
+
* and cannot do. */
|
|
6
|
+
background?: boolean;
|
|
7
|
+
mode?: string;
|
|
8
|
+
plan?: boolean;
|
|
9
|
+
/** The person read the plan and said build it. Changes to files in this
|
|
10
|
+
* project then happen without being asked about one at a time. Commands and
|
|
11
|
+
* publishing are still asked about. */
|
|
12
|
+
approvePlan?: boolean;
|
|
13
|
+
model?: string;
|
|
14
|
+
/** Run `model` and nothing else, whatever the router would prefer. */
|
|
15
|
+
pin?: boolean;
|
|
16
|
+
maxSpend?: number;
|
|
17
|
+
/** `--allow "python3 grade.py"`, repeatable. Exact commands, pre approved. */
|
|
18
|
+
allow?: string[];
|
|
19
|
+
yes?: boolean;
|
|
20
|
+
resume?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Go without the code map for this job, whatever the company setting says.
|
|
23
|
+
*
|
|
24
|
+
* Two people need it and both are legitimate. The corpus study, because a run
|
|
25
|
+
* with a map and a run without one are two different quantities and averaging
|
|
26
|
+
* them would flatter whichever arm happened to have one. And anybody who
|
|
27
|
+
* suspects the map and wants the files read directly.
|
|
28
|
+
*/
|
|
29
|
+
noMap?: boolean;
|
|
30
|
+
/** The job this question follows on from, so the agent knows what was
|
|
31
|
+
* already said. Not the same as `resume`, which re-enters a job that stopped
|
|
32
|
+
* part way; this is the ordinary next question in a conversation. */
|
|
33
|
+
continuesJob?: string;
|
|
34
|
+
cwd?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Ask the person a yes or no question.
|
|
37
|
+
*
|
|
38
|
+
* Supplied by a caller that already owns the keyboard, so a session and the
|
|
39
|
+
* agent never open two readers on it. Two readers divide the typing between
|
|
40
|
+
* them and the second one reads nothing, which is how a request to run a
|
|
41
|
+
* command inside a session came back refused with nobody having declined it.
|
|
42
|
+
*
|
|
43
|
+
* Absent, the agent opens its own reader exactly as it always has, which is
|
|
44
|
+
* what the standalone command wants.
|
|
45
|
+
*/
|
|
46
|
+
confirm?: (question: string) => Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Files already uploaded, to be put in front of this question.
|
|
49
|
+
*
|
|
50
|
+
* Ids, never contents: the file was read by the server when it was attached,
|
|
51
|
+
* and what it holds is text with a half-hour life. Sending the bytes again
|
|
52
|
+
* here would be sending them twice and storing them nowhere useful.
|
|
53
|
+
*/
|
|
54
|
+
attachments?: string[];
|
|
55
|
+
/**
|
|
56
|
+
* Handed a way to stop the job, as soon as there is one to stop.
|
|
57
|
+
*
|
|
58
|
+
* For a caller that owns the keyboard and therefore also owns Ctrl+C. This
|
|
59
|
+
* command stops a job on its own SIGINT handler, and a caller holding a
|
|
60
|
+
* readline interface never lets that handler fire, so without this the job
|
|
61
|
+
* carried on after the person asked to leave and the session closed reporting
|
|
62
|
+
* a cost of nothing on work it had already paid for.
|
|
63
|
+
*/
|
|
64
|
+
/** Handed the controls the moment the job starts: stop it, or say something
|
|
65
|
+
* to it while it runs. A caller that owns the keyboard needs both, because
|
|
66
|
+
* a person watching a job go the wrong way at step three should be able to
|
|
67
|
+
* correct it rather than stop it and start again. */
|
|
68
|
+
onStarted?: (controls: {
|
|
69
|
+
cancel: () => void;
|
|
70
|
+
steer: (text: string) => void;
|
|
71
|
+
}) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Told what the job cost, once it has finished.
|
|
74
|
+
*
|
|
75
|
+
* The command itself only needs an exit code, so that is still what this
|
|
76
|
+
* returns. A session running several jobs needs the figures as well, to keep
|
|
77
|
+
* a running total: without them it showed every session as zero spent and
|
|
78
|
+
* zero saved, which is the one number this product cannot get wrong.
|
|
79
|
+
*
|
|
80
|
+
* Not called when the job never started, because there is nothing to add.
|
|
81
|
+
*/
|
|
82
|
+
onFinished?: (result: JobResult) => void;
|
|
83
|
+
}
|
|
84
|
+
export declare function runAgent(args: AgentArgs): Promise<number>;
|
|
85
|
+
/** The help text. Written to be read by somebody who has not read anything else. */
|
|
86
|
+
export declare const AGENT_HELP: string;
|
|
@@ -0,0 +1,259 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AGENT_HELP = void 0;
|
|
37
|
+
exports.runAgent = runAgent;
|
|
38
|
+
/**
|
|
39
|
+
* `comprism work` — the coding agent, from a terminal.
|
|
40
|
+
*
|
|
41
|
+
* Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
|
|
42
|
+
* items A6 (interactive), A7 (scriptable), C14 (resume) and L2 (how to install
|
|
43
|
+
* it).
|
|
44
|
+
*
|
|
45
|
+
* ## Two shapes, one session
|
|
46
|
+
*
|
|
47
|
+
* With a person at the keyboard, approvals are asked, Ctrl+C stops the run
|
|
48
|
+
* rather than killing the process, and anything typed while it works joins the
|
|
49
|
+
* next step.
|
|
50
|
+
*
|
|
51
|
+
* With `--yes` or no terminal attached, nobody is asked anything. The
|
|
52
|
+
* permission mode has to have settled it in advance, and an action that would
|
|
53
|
+
* need an approval is refused rather than assumed. **Silence is never consent**:
|
|
54
|
+
* a script that needs to change files says so by choosing a mode that allows it.
|
|
55
|
+
*/
|
|
56
|
+
const fs = __importStar(require("fs"));
|
|
57
|
+
const readline = __importStar(require("readline"));
|
|
58
|
+
const connection_1 = require("../lib/connection");
|
|
59
|
+
const session_1 = require("./session");
|
|
60
|
+
const ui = __importStar(require("./render"));
|
|
61
|
+
/** The WebSocket the session needs is a global from Node 22.
|
|
62
|
+
*
|
|
63
|
+
* Checked here with a message somebody can act on. Without it the failure is
|
|
64
|
+
* `WebSocket is not defined` from inside a library, which reads as a broken
|
|
65
|
+
* install rather than an old runtime. */
|
|
66
|
+
function requireWebSocket() {
|
|
67
|
+
if (typeof globalThis.WebSocket === "function")
|
|
68
|
+
return true;
|
|
69
|
+
process.stderr.write(ui.red(" This needs Node 22 or newer.") +
|
|
70
|
+
ui.dim(` You are on ${process.version}. Upgrade Node and try again.\n`));
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const MODES = ["read_only", "approve_writes", "auto_edit", "full_auto"];
|
|
74
|
+
/**
|
|
75
|
+
* Start a job, hand the terminal back, and keep working.
|
|
76
|
+
*
|
|
77
|
+
* Specification register item C15.
|
|
78
|
+
*
|
|
79
|
+
* **What this honestly is, and is not.** The job continues without somebody
|
|
80
|
+
* watching it, and it writes to a log they can follow or ignore. The machine has
|
|
81
|
+
* to stay on, because the actions run HERE: reading and changing files is done
|
|
82
|
+
* by this process on this disk, and no amount of server-side cleverness moves
|
|
83
|
+
* that. A laptop that goes to sleep pauses the job, and the job says so when it
|
|
84
|
+
* wakes.
|
|
85
|
+
*
|
|
86
|
+
* Anything needing an approval is refused rather than waited on, for the same
|
|
87
|
+
* reason `--yes` refuses: nobody is there, and treating silence as consent would
|
|
88
|
+
* let a job change files nobody ever saw. So a background job is only useful in
|
|
89
|
+
* a permission mode that settles it in advance, and the command says so rather
|
|
90
|
+
* than letting somebody find out an hour later.
|
|
91
|
+
*/
|
|
92
|
+
async function runDetached(args) {
|
|
93
|
+
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
94
|
+
const os = await Promise.resolve().then(() => __importStar(require("os")));
|
|
95
|
+
const pathMod = await Promise.resolve().then(() => __importStar(require("path")));
|
|
96
|
+
const mode = args.mode ?? "approve_writes";
|
|
97
|
+
if (mode === "approve_writes" || mode === "read_only") {
|
|
98
|
+
process.stderr.write(ui.red(" A background job cannot ask you anything, so in this mode it "
|
|
99
|
+
+ "would refuse every change.\n")
|
|
100
|
+
+ ui.dim(" Use --mode auto_edit or --mode full_auto, or run it in the "
|
|
101
|
+
+ "foreground.\n"));
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
const logPath = pathMod.join(os.tmpdir(), `comprism-agent-${Date.now()}.log`);
|
|
105
|
+
const log = fs.openSync(logPath, "a");
|
|
106
|
+
// Detached, with its own process group, so closing the terminal does not take
|
|
107
|
+
// the job with it. `unref` is what actually lets this process exit.
|
|
108
|
+
const child = spawn(process.execPath, [
|
|
109
|
+
process.argv[1] ?? "", "agent",
|
|
110
|
+
...args.request,
|
|
111
|
+
"--mode", mode, "--yes",
|
|
112
|
+
...(args.model ? ["--model", args.model] : []),
|
|
113
|
+
...(args.pin ? ["--pin"] : []),
|
|
114
|
+
...(args.maxSpend ? ["--max-spend", String(args.maxSpend)] : []),
|
|
115
|
+
...(args.allow ?? []).flatMap((c) => ["--allow", c]),
|
|
116
|
+
...(args.cwd ? ["--cwd", args.cwd] : []),
|
|
117
|
+
], { detached: true, stdio: ["ignore", log, log], env: { ...process.env, NO_COLOR: "1" } });
|
|
118
|
+
child.unref();
|
|
119
|
+
process.stdout.write(ui.green(` Started in the background as process ${child.pid}.\n`)
|
|
120
|
+
+ ui.dim(` Follow it: tail -f ${logPath}\n`)
|
|
121
|
+
+ ui.dim(" It keeps working while you do something else. This machine has "
|
|
122
|
+
+ "to stay awake: the files are here, so the work is too.\n"));
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
async function runAgent(args) {
|
|
126
|
+
if (!requireWebSocket())
|
|
127
|
+
return 1;
|
|
128
|
+
if (args.background)
|
|
129
|
+
return runDetached(args);
|
|
130
|
+
const connection = (0, connection_1.readConnection)();
|
|
131
|
+
if (!connection?.url) {
|
|
132
|
+
process.stderr.write(
|
|
133
|
+
// The command named here MUST be one this tool actually has. It said
|
|
134
|
+
// `comprism login`, which has never existed, so the one instruction a
|
|
135
|
+
// brand new user is given was a dead end.
|
|
136
|
+
ui.red(" Not signed in.") +
|
|
137
|
+
ui.dim(" Run `comprism login` first.\n"));
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
// The machine credential, not the session token. The session token belongs to
|
|
141
|
+
// a browser sign-in and is short lived; this is the long-lived credential
|
|
142
|
+
// minted for this laptop, listed in the portal beside every other machine and
|
|
143
|
+
// revocable on its own. A job running for twenty minutes must not stop
|
|
144
|
+
// because a browser session expired somewhere else.
|
|
145
|
+
const credential = connection.workspaceKey;
|
|
146
|
+
if (!credential) {
|
|
147
|
+
process.stderr.write(ui.red(" This machine has no workspace key.") +
|
|
148
|
+
ui.dim(" Run `comprism connect` again to mint one.\n"));
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
const root = fs.realpathSync(args.cwd ?? process.cwd());
|
|
152
|
+
const mode = args.mode ?? "approve_writes";
|
|
153
|
+
if (!MODES.includes(mode)) {
|
|
154
|
+
process.stderr.write(ui.red(` ${mode} is not a permission mode.`) +
|
|
155
|
+
ui.dim(` One of: ${MODES.join(", ")}\n`));
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
const request = args.request.join(" ").trim();
|
|
159
|
+
if (!request && !args.resume) {
|
|
160
|
+
process.stderr.write(ui.dim(" Say what you want done.\n"));
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
// Headless when asked for, and also when there is no terminal at all: piped
|
|
164
|
+
// into a script, nobody is there to answer an approval, and pretending
|
|
165
|
+
// otherwise would hang the pipeline until it timed out.
|
|
166
|
+
const headless = Boolean(args.yes) || !process.stdin.isTTY;
|
|
167
|
+
const session = new session_1.TerminalSession({
|
|
168
|
+
baseUrl: connection.url,
|
|
169
|
+
credential,
|
|
170
|
+
root,
|
|
171
|
+
permissionMode: mode,
|
|
172
|
+
planOnly: Boolean(args.plan),
|
|
173
|
+
planApproved: Boolean(args.approvePlan),
|
|
174
|
+
model: args.model,
|
|
175
|
+
pinModel: Boolean(args.pin),
|
|
176
|
+
maxSpendUsd: args.maxSpend,
|
|
177
|
+
approvedCommands: args.allow ?? [],
|
|
178
|
+
attachmentIds: args.attachments ?? [],
|
|
179
|
+
headless,
|
|
180
|
+
confirm: args.confirm,
|
|
181
|
+
resumeJobId: args.resume,
|
|
182
|
+
continuesJob: args.continuesJob,
|
|
183
|
+
withoutMap: Boolean(args.noMap),
|
|
184
|
+
});
|
|
185
|
+
// Ctrl+C stops the JOB, not the process. Killing the process would leave the
|
|
186
|
+
// job running on the server with nothing watching it, and whatever the agent
|
|
187
|
+
// had started on this machine would carry on unsupervised.
|
|
188
|
+
const onInterrupt = () => session.cancel();
|
|
189
|
+
process.on("SIGINT", onInterrupt);
|
|
190
|
+
args.onStarted?.({
|
|
191
|
+
cancel: () => session.cancel(),
|
|
192
|
+
steer: (text) => session.steer(text),
|
|
193
|
+
});
|
|
194
|
+
// Anything typed while it works. Not a prompt, deliberately: the run is
|
|
195
|
+
// printing, and a prompt competing with it for the same lines is unreadable.
|
|
196
|
+
//
|
|
197
|
+
// Skipped entirely when the caller lent us a keyboard. The whole point of
|
|
198
|
+
// `confirm` is that one component owns the terminal; opening a reader here as
|
|
199
|
+
// well would put the same two owners back on it by another door, and the
|
|
200
|
+
// caller is the one already reading what gets typed.
|
|
201
|
+
let steering = null;
|
|
202
|
+
if (!headless && !args.confirm) {
|
|
203
|
+
steering = readline.createInterface({ input: process.stdin, terminal: false });
|
|
204
|
+
steering.on("line", (line) => {
|
|
205
|
+
const text = line.trim();
|
|
206
|
+
if (text)
|
|
207
|
+
session.steer(text);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const result = await session.run(request);
|
|
212
|
+
args.onFinished?.(result);
|
|
213
|
+
// A shell needs to know whether it worked. Anything other than finishing
|
|
214
|
+
// cleanly is a non-zero exit, so `comprism work ... && next-thing` behaves
|
|
215
|
+
// the way a person expects.
|
|
216
|
+
return result.outcome === "finished" ? 0 : 1;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
process.stderr.write(ui.red(` ${err.message}\n`));
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
process.off("SIGINT", onInterrupt);
|
|
224
|
+
steering?.close();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** The help text. Written to be read by somebody who has not read anything else. */
|
|
228
|
+
exports.AGENT_HELP = `
|
|
229
|
+
${ui.bold("comprism work")} — a coding agent in your terminal
|
|
230
|
+
|
|
231
|
+
comprism work "add a test for the parser and make it pass"
|
|
232
|
+
|
|
233
|
+
${ui.bold("What it does")}
|
|
234
|
+
Reads your project, changes files, runs commands and runs your tests, and
|
|
235
|
+
shows you every change before it makes it. Each step is routed to whichever
|
|
236
|
+
model finishes the work for least, and the receipt at the end says what it
|
|
237
|
+
cost.
|
|
238
|
+
|
|
239
|
+
${ui.bold("Options")}
|
|
240
|
+
--mode <mode> read_only, approve_writes (default), auto_edit, full_auto
|
|
241
|
+
--plan work out what it would do and change nothing
|
|
242
|
+
--approve-plan you read the plan and want it built. Files change without
|
|
243
|
+
being asked about one at a time; commands still ask
|
|
244
|
+
--model <id> tell it which model you are running, so the receipt can
|
|
245
|
+
price the saving against it. It may still move you up
|
|
246
|
+
--pin run --model and nothing else, whatever it would prefer
|
|
247
|
+
--max-spend <usd> stop at this much rather than running on
|
|
248
|
+
--allow "<cmd>" approve one exact command up front, repeatable. The only
|
|
249
|
+
way an unattended run can verify its own work
|
|
250
|
+
--yes never ask; anything needing approval is refused instead
|
|
251
|
+
--background start it and get your terminal back; it keeps working
|
|
252
|
+
--resume <job> carry on from a job you already started
|
|
253
|
+
--cwd <path> work in this folder rather than the current one
|
|
254
|
+
--no-map do not use the code map for this job; read the files
|
|
255
|
+
|
|
256
|
+
${ui.bold("While it runs")}
|
|
257
|
+
Ctrl+C stops the job. Anything you type joins the next step, so you can
|
|
258
|
+
correct it without starting again.
|
|
259
|
+
`;
|