@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.
Files changed (80) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +281 -0
  3. package/out/agent/command.d.ts +86 -0
  4. package/out/agent/command.js +259 -0
  5. package/out/agent/render.d.ts +97 -0
  6. package/out/agent/render.js +255 -0
  7. package/out/agent/session.d.ts +175 -0
  8. package/out/agent/session.js +573 -0
  9. package/out/commands/ask.d.ts +1 -0
  10. package/out/commands/ask.js +146 -0
  11. package/out/commands/codemap.d.ts +2 -0
  12. package/out/commands/codemap.js +151 -0
  13. package/out/commands/commands-thin.d.ts +39 -0
  14. package/out/commands/commands-thin.js +182 -0
  15. package/out/commands/install.d.ts +163 -0
  16. package/out/commands/install.js +543 -0
  17. package/out/commands/keys.d.ts +55 -0
  18. package/out/commands/keys.js +344 -0
  19. package/out/commands/login.d.ts +9 -0
  20. package/out/commands/login.js +384 -0
  21. package/out/commands/repl.d.ts +1 -0
  22. package/out/commands/repl.js +752 -0
  23. package/out/commands/settings.d.ts +21 -0
  24. package/out/commands/settings.js +244 -0
  25. package/out/commands/welcome.d.ts +1 -0
  26. package/out/commands/welcome.js +196 -0
  27. package/out/executor/documents.d.ts +40 -0
  28. package/out/executor/documents.js +170 -0
  29. package/out/executor/files.d.ts +2 -0
  30. package/out/executor/files.js +360 -0
  31. package/out/executor/git.d.ts +48 -0
  32. package/out/executor/git.js +132 -0
  33. package/out/executor/hooks.d.ts +67 -0
  34. package/out/executor/hooks.js +247 -0
  35. package/out/executor/index.d.ts +29 -0
  36. package/out/executor/index.js +221 -0
  37. package/out/executor/notebook.d.ts +2 -0
  38. package/out/executor/notebook.js +147 -0
  39. package/out/executor/paths.d.ts +15 -0
  40. package/out/executor/paths.js +126 -0
  41. package/out/executor/shell.d.ts +41 -0
  42. package/out/executor/shell.js +336 -0
  43. package/out/graph/build.d.ts +45 -0
  44. package/out/graph/build.js +91 -0
  45. package/out/graph/facts.d.ts +47 -0
  46. package/out/graph/facts.js +12 -0
  47. package/out/graph/files.d.ts +45 -0
  48. package/out/graph/files.js +207 -0
  49. package/out/graph/read-locales.d.ts +29 -0
  50. package/out/graph/read-locales.js +246 -0
  51. package/out/graph/read-python.d.ts +11 -0
  52. package/out/graph/read-python.js +115 -0
  53. package/out/graph/read-typescript.d.ts +16 -0
  54. package/out/graph/read-typescript.js +292 -0
  55. package/out/graph/sync.d.ts +66 -0
  56. package/out/graph/sync.js +242 -0
  57. package/out/lib/attach.d.ts +62 -0
  58. package/out/lib/attach.js +228 -0
  59. package/out/lib/config.d.ts +93 -0
  60. package/out/lib/config.js +198 -0
  61. package/out/lib/connection.d.ts +73 -0
  62. package/out/lib/connection.js +188 -0
  63. package/out/lib/gateway.d.ts +239 -0
  64. package/out/lib/gateway.js +171 -0
  65. package/out/lib/prompt.d.ts +34 -0
  66. package/out/lib/prompt.js +108 -0
  67. package/out/lib/types.d.ts +417 -0
  68. package/out/lib/types.js +21 -0
  69. package/out/lib/ui.d.ts +114 -0
  70. package/out/lib/ui.js +265 -0
  71. package/out/lib/version.d.ts +24 -0
  72. package/out/lib/version.js +27 -0
  73. package/out/lib/voice.d.ts +50 -0
  74. package/out/lib/voice.js +218 -0
  75. package/out/postinstall.d.ts +2 -0
  76. package/out/postinstall.js +92 -0
  77. package/out/thin.d.ts +2 -0
  78. package/out/thin.js +259 -0
  79. package/package.json +101 -0
  80. 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
+ `;