@illuminis/comprism 0.1.3 → 0.1.5

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 (142) hide show
  1. package/README.md +61 -2
  2. package/out/agent/command.d.ts +75 -4
  3. package/out/agent/command.js +221 -26
  4. package/out/agent/render.d.ts +56 -8
  5. package/out/agent/render.js +441 -39
  6. package/out/agent/session.d.ts +156 -2
  7. package/out/agent/session.js +795 -154
  8. package/out/commands/agents.d.ts +2 -0
  9. package/out/commands/agents.js +79 -0
  10. package/out/commands/ask.d.ts +1 -1
  11. package/out/commands/ask.js +88 -12
  12. package/out/commands/commands-thin.js +79 -17
  13. package/out/commands/config.d.ts +1 -0
  14. package/out/commands/config.js +138 -0
  15. package/out/commands/cost.d.ts +1 -0
  16. package/out/commands/cost.js +167 -0
  17. package/out/commands/hooks.d.ts +1 -0
  18. package/out/commands/hooks.js +83 -0
  19. package/out/commands/install.d.ts +44 -1
  20. package/out/commands/install.js +198 -4
  21. package/out/commands/instructions.d.ts +1 -0
  22. package/out/commands/instructions.js +113 -0
  23. package/out/commands/integrations.d.ts +3 -0
  24. package/out/commands/integrations.js +215 -0
  25. package/out/commands/jobs.d.ts +5 -0
  26. package/out/commands/jobs.js +157 -0
  27. package/out/commands/login.js +249 -43
  28. package/out/commands/memory.d.ts +3 -0
  29. package/out/commands/memory.js +113 -0
  30. package/out/commands/permissions.d.ts +1 -0
  31. package/out/commands/permissions.js +94 -0
  32. package/out/commands/plugins.d.ts +4 -0
  33. package/out/commands/plugins.js +192 -0
  34. package/out/commands/privacy.d.ts +1 -0
  35. package/out/commands/privacy.js +57 -0
  36. package/out/commands/providerKey.d.ts +32 -0
  37. package/out/commands/providerKey.js +108 -0
  38. package/out/commands/repl.d.ts +12 -1
  39. package/out/commands/repl.js +1649 -122
  40. package/out/commands/report.d.ts +39 -0
  41. package/out/commands/report.js +115 -0
  42. package/out/commands/review.d.ts +5 -0
  43. package/out/commands/review.js +223 -0
  44. package/out/commands/sessions.d.ts +23 -0
  45. package/out/commands/sessions.js +115 -0
  46. package/out/commands/settings.d.ts +3 -1
  47. package/out/commands/settings.js +18 -16
  48. package/out/commands/skills.d.ts +21 -0
  49. package/out/commands/skills.js +207 -0
  50. package/out/commands/unattended.d.ts +7 -0
  51. package/out/commands/unattended.js +351 -0
  52. package/out/commands/update.d.ts +1 -0
  53. package/out/commands/update.js +123 -0
  54. package/out/commands/welcome.d.ts +25 -1
  55. package/out/commands/welcome.js +71 -35
  56. package/out/commands/worktrees.d.ts +5 -0
  57. package/out/commands/worktrees.js +186 -0
  58. package/out/executor/browser.d.ts +14 -0
  59. package/out/executor/browser.js +270 -0
  60. package/out/executor/diagnostics.d.ts +2 -0
  61. package/out/executor/diagnostics.js +181 -0
  62. package/out/executor/files.js +270 -40
  63. package/out/executor/git.js +42 -29
  64. package/out/executor/hooks.d.ts +42 -58
  65. package/out/executor/hooks.js +89 -182
  66. package/out/executor/index.d.ts +21 -5
  67. package/out/executor/index.js +160 -14
  68. package/out/executor/paths.d.ts +6 -1
  69. package/out/executor/paths.js +34 -6
  70. package/out/executor/sandbox.d.ts +40 -0
  71. package/out/executor/sandbox.js +299 -0
  72. package/out/executor/shell.d.ts +49 -4
  73. package/out/executor/shell.js +302 -56
  74. package/out/executor/toolservers.d.ts +20 -0
  75. package/out/executor/toolservers.js +189 -0
  76. package/out/executor/worktree.d.ts +9 -0
  77. package/out/executor/worktree.js +119 -0
  78. package/out/graph/read-python.js +2 -1
  79. package/out/lib/attach.d.ts +56 -12
  80. package/out/lib/attach.js +230 -63
  81. package/out/lib/clipboard.d.ts +23 -0
  82. package/out/lib/clipboard.js +182 -0
  83. package/out/lib/commandlist.d.ts +20 -0
  84. package/out/lib/commandlist.js +58 -0
  85. package/out/lib/decision.d.ts +22 -0
  86. package/out/lib/decision.js +50 -0
  87. package/out/lib/fingerprint.d.ts +25 -0
  88. package/out/lib/fingerprint.js +58 -0
  89. package/out/lib/gateway.d.ts +190 -2
  90. package/out/lib/gateway.js +59 -4
  91. package/out/lib/history.d.ts +24 -0
  92. package/out/lib/history.js +137 -0
  93. package/out/lib/ide.d.ts +19 -0
  94. package/out/lib/ide.js +131 -0
  95. package/out/lib/keyboard.d.ts +98 -0
  96. package/out/lib/keyboard.js +390 -0
  97. package/out/lib/lastlogin.d.ts +14 -0
  98. package/out/lib/lastlogin.js +105 -0
  99. package/out/lib/machine.d.ts +21 -0
  100. package/out/lib/machine.js +91 -0
  101. package/out/lib/notify.d.ts +4 -0
  102. package/out/lib/notify.js +52 -0
  103. package/out/lib/output.d.ts +48 -0
  104. package/out/lib/output.js +108 -0
  105. package/out/lib/picker.d.ts +1 -0
  106. package/out/lib/picker.js +158 -0
  107. package/out/lib/project-ops.d.ts +19 -0
  108. package/out/lib/project-ops.js +146 -0
  109. package/out/lib/project.d.ts +28 -0
  110. package/out/lib/project.js +114 -0
  111. package/out/lib/prompt.d.ts +3 -0
  112. package/out/lib/prompt.js +36 -2
  113. package/out/lib/queue.d.ts +13 -0
  114. package/out/lib/queue.js +116 -0
  115. package/out/lib/readiness.d.ts +19 -0
  116. package/out/lib/readiness.js +170 -1
  117. package/out/lib/self.d.ts +23 -0
  118. package/out/lib/self.js +124 -0
  119. package/out/lib/sessions.d.ts +19 -0
  120. package/out/lib/sessions.js +221 -0
  121. package/out/lib/startcache.d.ts +13 -0
  122. package/out/lib/startcache.js +78 -0
  123. package/out/lib/stdin.d.ts +32 -0
  124. package/out/lib/stdin.js +117 -0
  125. package/out/lib/store.d.ts +40 -0
  126. package/out/lib/store.js +138 -0
  127. package/out/lib/sync.d.ts +18 -0
  128. package/out/lib/sync.js +81 -0
  129. package/out/lib/ui.d.ts +10 -4
  130. package/out/lib/ui.js +65 -25
  131. package/out/lib/voice.d.ts +36 -0
  132. package/out/lib/voice.js +160 -1
  133. package/out/native/dictate +0 -0
  134. package/out/postinstall.js +42 -17
  135. package/out/providers/anthropic.d.ts +22 -0
  136. package/out/providers/anthropic.js +80 -0
  137. package/out/providers/index.d.ts +8 -0
  138. package/out/providers/index.js +83 -0
  139. package/out/providers/openai.d.ts +11 -0
  140. package/out/providers/openai.js +57 -0
  141. package/out/thin.js +595 -33
  142. package/package.json +11 -50
package/out/thin.js CHANGED
@@ -56,7 +56,46 @@ Object.defineProperty(exports, "__esModule", { value: true });
56
56
  */
57
57
  const ui = __importStar(require("./lib/ui"));
58
58
  const version_1 = require("./lib/version");
59
+ const self_1 = require("./lib/self");
59
60
  const out = (s = '') => process.stdout.write(s + '\n');
61
+ /** A refusal a script reads: always on standard error (Appendix B). */
62
+ const err = (s = '') => process.stderr.write(s + '\n');
63
+ /**
64
+ * Piped text, attached as `standard input`, and the terminal to ask approval
65
+ * questions at (manual 3.9). The attachment line goes to standard error when
66
+ * standard output is not a terminal, so a script reads only the result.
67
+ * Null when the text was piped and could not be attached: the request is not
68
+ * sent without the material it was about.
69
+ */
70
+ async function attachStdin(piped) {
71
+ const { attachPiped, openTerminal } = await Promise.resolve().then(() => __importStar(require('./lib/stdin')));
72
+ const keyboard = process.stdin.isTTY ? null : openTerminal();
73
+ if (!piped?.trim())
74
+ return { ids: [], keyboard };
75
+ const say = process.stdout.isTTY ? out : err;
76
+ const { readConnection } = await Promise.resolve().then(() => __importStar(require('./lib/connection')));
77
+ const conn = readConnection();
78
+ if (!conn?.url || !conn.workspaceKey)
79
+ return { ids: [], keyboard };
80
+ const attach = await Promise.resolve().then(() => __importStar(require('./lib/attach')));
81
+ const { attached, refused } = await attachPiped(piped, conn.url, conn.workspaceKey);
82
+ for (const line of attach.describe(attached))
83
+ say(ui.c.dim(` ${line}`));
84
+ for (const r of refused)
85
+ err(ui.c.amber(` ${r.why}`));
86
+ if (refused.length > 0) {
87
+ keyboard?.destroy();
88
+ err(ui.c.dim(' Nothing was sent.'));
89
+ return null;
90
+ }
91
+ return { ids: attached.map((a) => a.id), keyboard };
92
+ }
93
+ /** Flags `work` takes with a value, and flags it takes on their own. */
94
+ const WORK_VALUES = ['--mode', '--model', '--max-spend', '--max-steps', '--effort', '--resume',
95
+ '--cwd', '--allow', '--add-folder', '--timeout', '--issue', '--team', '--output', '--worktree',
96
+ '--schema', '--config', '--max-minutes', '--queued'];
97
+ const WORK_SWITCHES = ['--plan', '--approve-plan', '--pin', '--no-optimize', '--sandbox', '--yes', '--background',
98
+ '--no-map', '--no-graph', '--quiet', '--no-personal', '--cloud'];
60
99
  /**
61
100
  * The value after a flag, or undefined when the flag was not given.
62
101
  *
@@ -79,25 +118,296 @@ function usage() {
79
118
  ['comprism work', 'have it do the work: reads your project, edits, runs your tests'],
80
119
  ['comprism login', 'sign in with your email and password'],
81
120
  ['comprism', 'start a session and just type your questions'],
121
+ ['comprism --continue', 'carry on the most recent session in this project'],
122
+ ['comprism --resume [number]', 'carry on a session; with no number, choose from a list'],
123
+ ['comprism sessions', 'your recent sessions, newest first; --search to find one'],
82
124
  ['comprism keys', 'add a provider key, or point at a file of them'],
83
125
  ['comprism model', 'the model your savings are measured against'],
84
126
  ['comprism providers', 'every AI vendor and model, and what you can reach'],
85
- ['comprism workspace', 'the folder to work in, default is where you are'],
127
+ ['comprism project', 'the folder to work in, default is where you are'],
128
+ ['comprism config', 'settings in force and where each came from'],
129
+ ['comprism instructions', 'the instructions the next job receives'],
130
+ ['comprism memory', 'this project\'s notes'],
131
+ ['comprism skills', 'the skills here and where each came from; /name runs one'],
132
+ ['comprism integrations', 'outside tools the agent can call, and their sign in'],
133
+ ['comprism plugins', 'packs of skills, helpers, hooks and connections'],
134
+ ['comprism worktrees', 'separate copies of the repository'],
135
+ ['comprism privacy', 'what your company keeps about your work'],
136
+ ['comprism permissions', 'the rules in force here; permissions why <action> "<target>"'],
86
137
  ['comprism status', 'is this machine signed in, and to what'],
87
138
  ['comprism check', 'what this machine still needs, and how to add it'],
88
139
  ['comprism logout', 'sign out, and revoke this machine\'s key'],
89
- ['comprism uninstall', 'undo the setup this machine got at install'],
140
+ ['comprism cost', 'what a session cost, and which factors drove it'],
141
+ ['comprism schema output', 'the shape of --output json, for scripts'],
142
+ ['comprism usage', 'your coding allowance left this month, and provider limits'],
143
+ ['comprism hooks', 'the hooks in force here; hooks test <when> <action> <file> runs them'],
144
+ ['comprism update', 'check for a newer version and install it; --check only says'],
145
+ ['comprism changes', 'what the agent changed in a job, apart from your own changes'],
146
+ ['comprism diff', 'the agent\'s changes, line by line'],
147
+ ['comprism rewind <job>', 'put the files back; --to-step N or one file'],
148
+ ['comprism schedule', 'requests that run by themselves; schedule add "<when>" "<request>"'],
149
+ ['comprism triggers', 'outside events that start a job; triggers add "<request>"'],
150
+ ['comprism worker', 'run the jobs queued for this machine, or --cloud for any worker'],
151
+ ['comprism install', 'set up this machine again, if the install could not'],
152
+ ['comprism uninstall', 'remove comprism from this machine, everything it set up included'],
90
153
  ]));
91
154
  out();
92
155
  out(ui.c.dim(` v${version_1.COMPRISM_VERSION} · every decision is made on our servers`));
93
156
  out();
94
157
  }
158
+ /**
159
+ * Behind a company proxy (manual 1.11): Node uses HTTPS_PROXY and NO_PROXY for
160
+ * its own requests only when NODE_USE_ENV_PROXY is set as it starts, so the
161
+ * tool starts itself once more with it set. Nothing changes without a proxy.
162
+ */
163
+ function throughProxy() {
164
+ const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
165
+ if (!proxy || process.env.NODE_USE_ENV_PROXY)
166
+ return false;
167
+ const { spawnSync } = require('child_process');
168
+ // Node 22 calls its proxy support experimental and says so on every start;
169
+ // that one notice is not the person's concern.
170
+ const [cmd, argv] = (0, self_1.selfLaunch)(process.argv.slice(2), ['--disable-warning=UNDICI-EHPA']);
171
+ const r = spawnSync(cmd, argv, {
172
+ stdio: 'inherit', env: { ...process.env, NODE_USE_ENV_PROXY: '1', ...((0, self_1.isStandalone)() ? { NODE_NO_WARNINGS: '1' } : {}) },
173
+ });
174
+ process.exitCode = r.status ?? (r.signal === 'SIGINT' ? 130 : 1);
175
+ return true;
176
+ }
95
177
  async function main() {
178
+ // The sandbox's network relay (manual 4.12). Under Node it runs with `-e`;
179
+ // the ready to run program has no `-e`, so it asks for it by name.
180
+ if (process.argv[2] === '__relay' && (0, self_1.isStandalone)() && process.env.COMPRISM_RELAY) {
181
+ new Function('require', 'process', process.env.COMPRISM_RELAY)(require, process);
182
+ return;
183
+ }
184
+ if (throughProxy())
185
+ return;
186
+ // `--plain` anywhere means no color and no redrawn lines (manual 6.12). Taken
187
+ // off the line so no command reads it as a word of its request.
188
+ const plainAt = process.argv.indexOf('--plain', 2);
189
+ if (plainAt !== -1)
190
+ process.argv.splice(plainAt, 1);
191
+ const { applyLook } = await Promise.resolve().then(() => __importStar(require('./lib/machine')));
192
+ applyLook(plainAt !== -1);
96
193
  const [, , command, ...args] = process.argv;
194
+ // Carrying on a session (manual 8.4, 8.5). Flags rather than words, so
195
+ // `comprism --resume` reads as the same product starting, not a command.
196
+ if (command === '--continue' || command === '-c') {
197
+ const { runRepl } = await Promise.resolve().then(() => __importStar(require('./commands/repl')));
198
+ return runRepl({ latest: true });
199
+ }
200
+ if (command === '--resume' || command === '-r') {
201
+ const { runRepl } = await Promise.resolve().then(() => __importStar(require('./commands/repl')));
202
+ // `--resume --pr 42`: the latest session that worked on that pull request (8.10).
203
+ const prAt = args.indexOf('--pr');
204
+ if (prAt !== -1) {
205
+ const { sessions: ask8, failure } = await Promise.resolve().then(() => __importStar(require('./lib/sessions')));
206
+ const found = await ask8('by_pr', { pr: args[prAt + 1] ?? '' });
207
+ const first = found.sessions?.sessions?.[0]?.number;
208
+ if (!first) {
209
+ out(found.ok ? (found.sessions?.lines?.[0] ?? '') : ui.fail(failure(found)));
210
+ process.exitCode = 1;
211
+ return;
212
+ }
213
+ return runRepl({ resume: first });
214
+ }
215
+ const id = args.find((a) => a && !a.startsWith('-'));
216
+ if (id) {
217
+ // A session a cloud job worked on: its changes come here first (10.18).
218
+ const { sessions: ask8 } = await Promise.resolve().then(() => __importStar(require('./lib/sessions')));
219
+ const found = await ask8('resume', { key: id });
220
+ if (found.ok && found.sessions?.session_id) {
221
+ const { handBack } = await Promise.resolve().then(() => __importStar(require('./commands/unattended')));
222
+ await handBack(found.sessions.session_id, process.cwd());
223
+ }
224
+ }
225
+ return runRepl(id ? { resume: id } : { pick: true });
226
+ }
97
227
  switch (command) {
228
+ case 'sessions': {
229
+ const { cmdSessions } = await Promise.resolve().then(() => __importStar(require('./lib/sessions')));
230
+ process.exitCode = await cmdSessions(args);
231
+ return;
232
+ }
233
+ case 'review': {
234
+ // A read only job whose instruction is held on the service (manual 5.19).
235
+ const pr = flag(args, '--pr');
236
+ if (args.includes('--pr') && !/^\d+$/.test(pr ?? '')) {
237
+ out(ui.fail('--pr takes a pull request number, for example --pr 42.'));
238
+ process.exitCode = 2;
239
+ return;
240
+ }
241
+ if (args.includes('--post') && !pr) {
242
+ out(ui.fail('--post needs --pr: findings are posted to a pull request.'));
243
+ process.exitCode = 2;
244
+ return;
245
+ }
246
+ const { runAgent } = await Promise.resolve().then(() => __importStar(require('./agent/command')));
247
+ const { randomUUID } = await Promise.resolve().then(() => __importStar(require('node:crypto')));
248
+ process.exitCode = await runAgent({
249
+ request: [pr ? `review pull request #${pr}` : 'review my uncommitted changes'],
250
+ mode: 'read_only',
251
+ review: { ...(pr ? { pr: Number(pr) } : {}), post: args.includes('--post') },
252
+ conversationId: randomUUID(),
253
+ cwd: flag(args, '--cwd') ?? (await Promise.resolve().then(() => __importStar(require('./commands/settings')))).currentWorkspace().path,
254
+ });
255
+ return;
256
+ }
257
+ case 'integrations': {
258
+ const { cmdIntegrations } = await Promise.resolve().then(() => __importStar(require('./commands/integrations')));
259
+ process.exitCode = await cmdIntegrations(args);
260
+ return;
261
+ }
262
+ case 'plugins': {
263
+ const { cmdPlugins } = await Promise.resolve().then(() => __importStar(require('./commands/plugins')));
264
+ process.exitCode = await cmdPlugins(args);
265
+ return;
266
+ }
267
+ case 'skills': {
268
+ const { cmdSkills } = await Promise.resolve().then(() => __importStar(require('./commands/skills')));
269
+ process.exitCode = await cmdSkills(args);
270
+ return;
271
+ }
272
+ case 'agents': {
273
+ const { cmdAgents } = await Promise.resolve().then(() => __importStar(require('./commands/agents')));
274
+ process.exitCode = await cmdAgents(args);
275
+ return;
276
+ }
277
+ case 'jobs': {
278
+ const { cmdJobs } = await Promise.resolve().then(() => __importStar(require('./commands/jobs')));
279
+ process.exitCode = await cmdJobs(args);
280
+ return;
281
+ }
282
+ case 'permissions': {
283
+ const { cmdPermissions } = await Promise.resolve().then(() => __importStar(require('./commands/permissions')));
284
+ process.exitCode = await cmdPermissions(args);
285
+ return;
286
+ }
287
+ case 'config': {
288
+ const { cmdConfig } = await Promise.resolve().then(() => __importStar(require('./commands/config')));
289
+ process.exitCode = await cmdConfig(args);
290
+ return;
291
+ }
292
+ // Every instruction source in force, and your personal ones (manual 2.5 to 2.8).
293
+ case 'instructions': {
294
+ const { cmdInstructions } = await Promise.resolve().then(() => __importStar(require('./commands/instructions')));
295
+ process.exitCode = await cmdInstructions(args);
296
+ return;
297
+ }
298
+ // This project's notes (manual 2.9).
299
+ case 'memory': {
300
+ const { cmdMemory } = await Promise.resolve().then(() => __importStar(require('./commands/memory')));
301
+ process.exitCode = await cmdMemory(args);
302
+ return;
303
+ }
304
+ // What your company keeps about your work (manual 2.13).
305
+ case 'privacy': {
306
+ const { cmdPrivacy } = await Promise.resolve().then(() => __importStar(require('./commands/privacy')));
307
+ process.exitCode = await cmdPrivacy();
308
+ return;
309
+ }
310
+ // Separate copies of the repository (manual 2.11).
311
+ case 'worktrees': {
312
+ const { cmdWorktrees } = await Promise.resolve().then(() => __importStar(require('./commands/worktrees')));
313
+ process.exitCode = await cmdWorktrees(args);
314
+ return;
315
+ }
98
316
  case 'ask': {
99
317
  const { cmdAsk } = await Promise.resolve().then(() => __importStar(require('./commands/ask')));
100
- return cmdAsk(args);
318
+ process.exitCode = await cmdAsk(args);
319
+ return;
320
+ }
321
+ // The newest version, and installing it when asked (manual 1.7).
322
+ case 'update': {
323
+ const { cmdUpdate } = await Promise.resolve().then(() => __importStar(require('./commands/update')));
324
+ process.exitCode = await cmdUpdate(args);
325
+ return;
326
+ }
327
+ // What the agent changed, the diff, and putting files back (manual 7).
328
+ case 'changes': {
329
+ const { cmdChanges } = await Promise.resolve().then(() => __importStar(require('./commands/review')));
330
+ process.exitCode = await cmdChanges(args);
331
+ return;
332
+ }
333
+ case 'diff': {
334
+ const { cmdDiff } = await Promise.resolve().then(() => __importStar(require('./commands/review')));
335
+ process.exitCode = await cmdDiff(args);
336
+ return;
337
+ }
338
+ case 'rewind': {
339
+ const { cmdRewind } = await Promise.resolve().then(() => __importStar(require('./commands/review')));
340
+ process.exitCode = await cmdRewind(args);
341
+ return;
342
+ }
343
+ // Jobs nobody is sitting at (manual 10.17 to 10.19).
344
+ case 'schedule': {
345
+ const { cmdSchedule } = await Promise.resolve().then(() => __importStar(require('./commands/unattended')));
346
+ process.exitCode = await cmdSchedule(args);
347
+ return;
348
+ }
349
+ case 'triggers': {
350
+ const { cmdTriggers } = await Promise.resolve().then(() => __importStar(require('./commands/unattended')));
351
+ process.exitCode = await cmdTriggers(args);
352
+ return;
353
+ }
354
+ case 'worker': {
355
+ const { cmdWorker } = await Promise.resolve().then(() => __importStar(require('./commands/unattended')));
356
+ process.exitCode = await cmdWorker(args);
357
+ return;
358
+ }
359
+ // Hooks in force here, and running them now to see what they do (10.10).
360
+ case 'hooks': {
361
+ const { cmdHooks } = await Promise.resolve().then(() => __importStar(require('./commands/hooks')));
362
+ process.exitCode = await cmdHooks(args);
363
+ return;
364
+ }
365
+ // The coding allowance and provider limits, from the service (manual 10.12).
366
+ case 'ide': {
367
+ // Which VS Code window this folder is joined to (VS Code manual 9.12).
368
+ const { findEditor } = await Promise.resolve().then(() => __importStar(require('./lib/ide')));
369
+ const link = findEditor(process.cwd());
370
+ out(link
371
+ ? ` Connected to VS Code for ${link.folder}. Proposed changes open in its diff view.`
372
+ : ' No VS Code window with CompletionPrism has this folder open. Open the folder in VS Code and run this again.');
373
+ return;
374
+ }
375
+ case 'usage': {
376
+ const { ask: askGateway } = await Promise.resolve().then(() => __importStar(require('./lib/gateway')));
377
+ const { exitFor } = await Promise.resolve().then(() => __importStar(require('./lib/output')));
378
+ const reply = await askGateway({ intent: 'usage', prompt: '', tzMinutes: -new Date().getTimezoneOffset() });
379
+ const found = reply.usage;
380
+ if (!reply.ok || reply.served === false || !found?.lines) {
381
+ err(ui.fail(reply.message || 'Usage could not be read from your workspace.'));
382
+ process.exitCode = exitFor(reply.httpStatus === 401 ? 'signed_out'
383
+ : reply.ok || reply.httpStatus ? 'incomplete' : 'unreachable');
384
+ return;
385
+ }
386
+ out();
387
+ for (const line of found.lines)
388
+ out(` ${line}`);
389
+ out();
390
+ return;
391
+ }
392
+ // The published shape of `--output json`, from the service (manual 10.3).
393
+ case 'schema': {
394
+ if (args[0] !== 'output') {
395
+ err(ui.fail('comprism schema output prints the shape of --output json.'));
396
+ process.exitCode = 2;
397
+ return;
398
+ }
399
+ const { ask: askGateway } = await Promise.resolve().then(() => __importStar(require('./lib/gateway')));
400
+ const reply = await askGateway({ intent: 'schema', prompt: '' });
401
+ const doc = reply.schema_doc;
402
+ if (!reply.ok || reply.served === false || !doc?.output) {
403
+ err(ui.fail(reply.message || 'The schema could not be read from your workspace.'));
404
+ const { exitFor } = await Promise.resolve().then(() => __importStar(require('./lib/output')));
405
+ process.exitCode = exitFor(reply.httpStatus === 401 ? 'signed_out'
406
+ : reply.ok || reply.httpStatus ? 'incomplete' : 'unreachable');
407
+ return;
408
+ }
409
+ out(JSON.stringify(doc.output, null, 2));
410
+ return;
101
411
  }
102
412
  // `work` is the word. `agent` still answers to it, because anybody who
103
413
  // learned the old name should not meet "unknown command".
@@ -108,39 +418,205 @@ async function main() {
108
418
  out(AGENT_HELP);
109
419
  return;
110
420
  }
421
+ // Every typing mistake is refused here, before anything is sent, asked
422
+ // or charged (manual 10.6, exit 2 in Appendix B). A reason goes to
423
+ // standard error, so a script reading standard output reads nothing.
424
+ const { outputChoice, readJsonFile, unknownFlag } = await Promise.resolve().then(() => __importStar(require('./lib/output')));
425
+ const { MODES } = await Promise.resolve().then(() => __importStar(require('./agent/command')));
426
+ const typo = (why) => {
427
+ err(ui.fail(why) + ui.c.dim(' Nothing ran and nothing was charged.'));
428
+ process.exitCode = 2;
429
+ };
430
+ const output = outputChoice(args);
431
+ if ('bad' in output)
432
+ return typo(output.bad);
433
+ const stray = unknownFlag(args, WORK_VALUES, WORK_SWITCHES);
434
+ if (stray)
435
+ return typo(`${stray} is not an option of comprism work. comprism work --help lists them.`);
436
+ // `--schema` and `--config` name files read here and judged by the
437
+ // service (manual 10.5, 10.8). A missing or unreadable file is a typing
438
+ // mistake.
439
+ let answerSchema;
440
+ if (args.includes('--schema')) {
441
+ const got = readJsonFile(String(flag(args, '--schema') ?? ''), '--schema');
442
+ if ('bad' in got)
443
+ return typo(got.bad);
444
+ answerSchema = got.value;
445
+ }
446
+ let settingsConfig;
447
+ if (args.includes('--config')) {
448
+ const got = readJsonFile(String(flag(args, '--config') ?? ''), '--config');
449
+ if ('bad' in got)
450
+ return typo(got.bad);
451
+ settingsConfig = got.value;
452
+ }
453
+ const wantedMode = flag(args, '--mode');
454
+ if (args.includes('--mode') && !MODES.includes(String(wantedMode))) {
455
+ return typo(`${wantedMode ?? '(nothing)'} is not a permission mode. One of: ${MODES.join(', ')}.`);
456
+ }
457
+ const spend = flag(args, '--max-spend');
458
+ if (args.includes('--max-spend') && !(Number(spend) > 0)) {
459
+ return typo('--max-spend takes an amount in dollars, for example --max-spend 2.00.');
460
+ }
461
+ // A step limit that is not a whole number is a typing mistake (4.13).
462
+ if (args.includes('--max-steps')) {
463
+ const steps = Number(flag(args, '--max-steps'));
464
+ if (!Number.isInteger(steps) || steps <= 0) {
465
+ return typo('--max-steps takes a whole number, for example --max-steps 40.');
466
+ }
467
+ }
468
+ // A time limit is a number of minutes (manual 10.7).
469
+ if (args.includes('--max-minutes') && !(Number(flag(args, '--max-minutes')) > 0)) {
470
+ return typo('--max-minutes takes a number of minutes, for example --max-minutes 15.');
471
+ }
472
+ // Effort is one of three words (manual 4.18).
473
+ if (args.includes('--effort') && !['low', 'medium', 'high'].includes(String(flag(args, '--effort')))) {
474
+ return typo('--effort takes low, medium or high.');
475
+ }
476
+ // A time limit that is not a number of seconds is a typing mistake:
477
+ // nothing runs and nothing is charged (manual 5.12, exit 2).
478
+ if (args.includes('--issue') && !/^\d+$/.test(flag(args, '--issue') ?? '')) {
479
+ return typo('--issue takes an issue number, for example --issue 42.');
480
+ }
481
+ if (args.includes('--timeout')) {
482
+ const secs = Number(flag(args, '--timeout'));
483
+ if (!Number.isInteger(secs) || secs <= 0) {
484
+ return typo('--timeout takes a whole number of seconds, for example --timeout 600.');
485
+ }
486
+ }
487
+ // Text piped in is read to its end first: it is material for the job,
488
+ // never the request and never an answer (manual 3.9).
489
+ const { readPiped } = await Promise.resolve().then(() => __importStar(require('./lib/stdin')));
490
+ const piped = await readPiped();
111
491
  const { subject, closePrompt } = await Promise.resolve().then(() => __importStar(require('./lib/prompt')));
112
492
  // Asked for when it was not given, same as every other command.
113
- const request = await subject(args, ' what would you like done? ', ['--mode', '--model', '--max-spend', '--resume', '--cwd', '--allow']);
493
+ const request = await subject(args, ' what would you like done? ', WORK_VALUES);
114
494
  closePrompt();
115
495
  if (!request) {
116
- out(ui.fail('Nothing was asked for.'));
117
- out();
496
+ if (piped?.trim()) {
497
+ return typo('Piped text needs a request with it, for example: '
498
+ + 'git diff | comprism work "write a commit message for this"');
499
+ }
500
+ // Standard input closed with nothing on it is a missing request.
501
+ return typo('Nothing was asked for.');
502
+ }
503
+ // On a CompletionPrism worker with a copy of the repository (10.18).
504
+ if (args.includes('--cloud')) {
505
+ const { submitCloud } = await Promise.resolve().then(() => __importStar(require('./commands/unattended')));
506
+ process.exitCode = await submitCloud(request, flag(args, '--cwd') ?? process.cwd(), args);
118
507
  return;
119
508
  }
120
- const spend = flag(args, '--max-spend');
509
+ // `comprism work /name the rest` runs that skill (manual 9.2). A slash
510
+ // word the service lists as neither a skill nor a saved command is a
511
+ // typo, refused before anything is sent or charged, as in a session.
512
+ const slashWord = /^\/([a-z][a-z0-9-]*)(?:\s|$)/i.exec(request.trim());
513
+ if (slashWord && !(await Promise.resolve().then(() => __importStar(require('./lib/attach')))).startsWithPath(request.trim())) {
514
+ const { fetchCommandList } = await Promise.resolve().then(() => __importStar(require('./lib/commandlist')));
515
+ const { resolveProject } = await Promise.resolve().then(() => __importStar(require('./lib/project')));
516
+ const where = flag(args, '--cwd') ?? (await Promise.resolve().then(() => __importStar(require('./commands/settings')))).currentWorkspace().path;
517
+ const list = await fetchCommandList(resolveProject(where).root, []);
518
+ const name = slashWord[1].toLowerCase();
519
+ if (list.ok && !list.items.some((i) => i.source !== 'session' && i.name === name)) {
520
+ return typo(`\`/${name}\` is neither a skill nor a saved command here. comprism skills lists the skills.`);
521
+ }
522
+ }
523
+ const fromPipe = await attachStdin(piped);
524
+ if (fromPipe === null) {
525
+ process.exitCode = 1;
526
+ return;
527
+ }
528
+ // `--resume` takes a session number or a job number and carries on that
529
+ // session's conversation (manual 8.4). The service resolves it; a job
530
+ // that belongs to no session is rejoined as before.
531
+ let resume = flag(args, '--resume');
532
+ let carry = {};
533
+ if (resume) {
534
+ const { sessions } = await Promise.resolve().then(() => __importStar(require('./lib/sessions')));
535
+ const found = await sessions('resume', { key: resume });
536
+ if (found.ok && found.sessions?.session_id) {
537
+ for (const line of found.sessions.lines)
538
+ out(line);
539
+ out();
540
+ carry = {
541
+ conversationId: found.sessions.session_id,
542
+ ...(found.sessions.continues_job ? { continuesJob: found.sessions.continues_job } : {}),
543
+ };
544
+ resume = undefined;
545
+ }
546
+ else if (resume.startsWith('s_')) {
547
+ out(ui.fail(found.message || 'There is no session with that number you can open.'));
548
+ out();
549
+ process.exitCode = 1;
550
+ return;
551
+ }
552
+ }
553
+ // Every job belongs to a session, so it has a number, is listed and can
554
+ // be resumed (manual 8.1). A job on its own is a session of one job.
555
+ if (!carry.conversationId && !resume) {
556
+ const { randomUUID } = await Promise.resolve().then(() => __importStar(require('node:crypto')));
557
+ carry.conversationId = randomUUID();
558
+ }
559
+ // A separate copy of the repository (manual 2.11): made the first time,
560
+ // and the job runs in it.
561
+ let workFolder = flag(args, '--cwd')
562
+ ?? (await Promise.resolve().then(() => __importStar(require('./commands/settings')))).currentWorkspace().path;
563
+ const worktree = flag(args, '--worktree');
564
+ if (worktree) {
565
+ const { checkFolder, resolveProject } = await Promise.resolve().then(() => __importStar(require('./lib/project')));
566
+ const start = checkFolder(workFolder);
567
+ if (!start.ok) {
568
+ out(ui.fail(start.why));
569
+ process.exitCode = 2;
570
+ return;
571
+ }
572
+ const { ensureWorktree } = await Promise.resolve().then(() => __importStar(require('./commands/worktrees')));
573
+ const made = await ensureWorktree(resolveProject(start.path), worktree, carry.conversationId ?? 'cli');
574
+ if (!made) {
575
+ process.exitCode = 1;
576
+ return;
577
+ }
578
+ workFolder = made;
579
+ }
121
580
  process.exitCode = await runAgent({
581
+ ...carry,
122
582
  request: [request],
123
583
  mode: flag(args, '--mode'),
124
584
  plan: args.includes('--plan'),
125
585
  approvePlan: args.includes('--approve-plan'),
126
586
  model: flag(args, '--model'),
127
- pin: args.includes('--pin'),
587
+ pin: args.includes('--pin') || args.includes('--no-optimize'),
128
588
  ...(args.includes('--max-spend') ? { maxSpend: Number(spend) } : {}),
589
+ ...(args.includes('--max-steps') ? { maxSteps: Number(flag(args, '--max-steps')) } : {}),
590
+ ...(args.includes('--max-minutes') ? { maxMinutes: Number(flag(args, '--max-minutes')) } : {}),
591
+ ...(args.includes('--effort') ? { effort: flag(args, '--effort') } : {}),
592
+ sandbox: args.includes('--sandbox'),
593
+ ...(args.includes('--timeout') ? { timeout: Number(flag(args, '--timeout')) } : {}),
594
+ ...(args.includes('--issue') ? { issue: Number(flag(args, '--issue')) } : {}),
595
+ ...(args.includes('--team') ? { team: flag(args, '--team') } : {}),
129
596
  allow: args.reduce((acc, a, i) => (args[i - 1] === '--allow' ? [...acc, a] : acc), []),
597
+ // Repeatable, and the only way a second folder is allowed (manual 2.3).
598
+ addFolders: args.reduce((acc, a, i) => (args[i - 1] === '--add-folder' ? [...acc, a] : acc), []),
130
599
  yes: args.includes('--yes'),
131
600
  background: args.includes('--background'),
132
- resume: flag(args, '--resume'),
601
+ output,
602
+ ...(args.includes('--queued') ? { queuedJob: flag(args, '--queued') } : {}),
603
+ ...(answerSchema ? { answerSchema } : {}),
604
+ ...(settingsConfig ? { settingsConfig } : {}),
605
+ noPersonal: args.includes('--no-personal'),
606
+ resume,
133
607
  // Go without the code map for this one job. Two reasons somebody uses
134
608
  // it: they are measuring, and a run with a map and a run without one
135
609
  // are two different quantities; or they suspect the map and want the
136
610
  // files read directly. Both are legitimate and neither should require
137
611
  // an administrator.
138
612
  noMap: args.includes('--no-map') || args.includes('--no-graph'),
613
+ attachments: fromPipe.ids,
614
+ keyboard: fromPipe.keyboard,
139
615
  // The saved working folder when there is one, so `comprism workspace`
140
616
  // is a setting the agent honors rather than a stored string nothing reads.
141
- cwd: flag(args, '--cwd')
142
- ?? (await Promise.resolve().then(() => __importStar(require('./commands/settings')))).currentWorkspace().path,
617
+ cwd: workFolder,
143
618
  });
619
+ fromPipe.keyboard?.destroy();
144
620
  return;
145
621
  }
146
622
  // The map of the project you are standing in: what it holds, and a way to
@@ -193,9 +669,12 @@ async function main() {
193
669
  const { cmdProviders } = await Promise.resolve().then(() => __importStar(require('./commands/settings')));
194
670
  return cmdProviders();
195
671
  }
672
+ // `project` is the word (manual 2.1). `workspace` stays as another name,
673
+ // because it also means the company account and people learned it first.
674
+ case 'project':
196
675
  case 'workspace': {
197
- const { cmdWorkspace } = await Promise.resolve().then(() => __importStar(require('./commands/settings')));
198
- return cmdWorkspace(args);
676
+ const { cmdProject } = await Promise.resolve().then(() => __importStar(require('./commands/settings')));
677
+ return cmdProject(args);
199
678
  }
200
679
  case 'keys': {
201
680
  const { cmdKeys } = await Promise.resolve().then(() => __importStar(require('./commands/keys')));
@@ -208,25 +687,59 @@ async function main() {
208
687
  // What this machine still needs. Its own word because somebody whose PDF
209
688
  // would not read has a question about THIS machine, and the answer should
210
689
  // not be buried inside a setup command they have already run.
690
+ // What a session cost, and what drove it. Its own word because "where did
691
+ // the money go" is a different question from "what did this answer cost",
692
+ // and the receipt under an answer only ever answers the second.
693
+ case 'cost':
694
+ case 'spend': {
695
+ const { cmdCost } = await Promise.resolve().then(() => __importStar(require('./commands/cost')));
696
+ process.exitCode = await cmdCost(args);
697
+ return;
698
+ }
211
699
  case 'check':
212
700
  case 'doctor': {
213
- const { check } = await Promise.resolve().then(() => __importStar(require('./lib/readiness')));
214
- const machine = check();
701
+ const { diagnose } = await Promise.resolve().then(() => __importStar(require('./lib/readiness')));
702
+ const found = await diagnose();
215
703
  out();
216
704
  out(ui.banner('This machine'));
217
705
  out();
218
- for (const line of machine.lines)
219
- out(line);
220
- out();
221
- if (machine.ready) {
222
- out(ui.ok('Everything the work needs is here.'));
223
- }
224
- else {
225
- out(` ${ui.c.dim('to add what is missing:')} ${ui.c.bold(machine.fix)}`);
706
+ for (const f of found) {
707
+ const mark = f.ok ? ui.c.dim('ok ') : f.required ? 'no ' : ui.c.dim('-- ');
708
+ out(` ${mark} ${f.line}${!f.ok && f.fix ? `. ${f.fix}` : ''}`);
226
709
  }
227
710
  out();
228
- out(ui.c.dim(' Word, PowerPoint and PDF documents are built on our servers,'));
229
- out(ui.c.dim(' so nothing has to be installed here for those.'));
711
+ const fixes = [...new Set(found.filter((f) => !f.ok && f.fix).map((f) => f.fix))];
712
+ if (fixes.length)
713
+ out(` ${ui.c.dim('to add what is missing:')} ${ui.c.bold(fixes.join(', then '))}`);
714
+ out(` ${ui.c.dim('to check the account and the service:')} ${ui.c.bold('comprism status')}`);
715
+ out();
716
+ // Only a required item decides the exit status (manual 1.6).
717
+ if (found.some((f) => f.required && !f.ok))
718
+ process.exitCode = 1;
719
+ return;
720
+ }
721
+ // The install's own message, the postinstall fallback and the generated
722
+ // shell script all tell a person to run this, so it has to exist as a word.
723
+ case 'install': {
724
+ const { install } = await Promise.resolve().then(() => __importStar(require('./commands/install')));
725
+ const report = install();
726
+ // `--quiet` is the install scripts' form (manual 1.1, Option 1): only the
727
+ // lines that say what changed, under the script's own heading.
728
+ const quiet = args.includes('--quiet');
729
+ if (!quiet) {
730
+ out();
731
+ out(ui.banner('Set up this machine'));
732
+ out();
733
+ }
734
+ for (const profile of report.profilesTouched)
735
+ out(` added one line to ${profile}`);
736
+ for (const profile of report.profilesAlreadyHad)
737
+ out(` already set up in ${profile}`);
738
+ if (quiet)
739
+ return;
740
+ out(report.enrolled && report.baseUrl
741
+ ? ` AI tools in a new terminal are measured against ${report.baseUrl}`
742
+ : ` nothing is measured until this machine signs in: ${ui.c.bold('comprism login')}`);
230
743
  out();
231
744
  return;
232
745
  }
@@ -236,22 +749,56 @@ async function main() {
236
749
  // word, so a customer following our own instruction got "there is no
237
750
  // command called uninstall" and was left to edit their profile by hand.
238
751
  case 'uninstall': {
752
+ // One command takes everything back out, as one command put it in
753
+ // (owner, 27 September 2026): the machine's key is revoked, the shell
754
+ // line and generated script go, the saved settings go, and the program
755
+ // removes itself. `--keep-settings` keeps the sign in for a reinstall.
756
+ const keep = args.includes('--keep-settings');
757
+ const fs = await Promise.resolve().then(() => __importStar(require('node:fs')));
758
+ const { spawnSync, spawn } = await Promise.resolve().then(() => __importStar(require('node:child_process')));
239
759
  const { uninstall } = await Promise.resolve().then(() => __importStar(require('./commands/install')));
760
+ if (!keep) {
761
+ const { cmdDisconnect } = await Promise.resolve().then(() => __importStar(require('./commands/commands-thin')));
762
+ await cmdDisconnect().catch(() => undefined);
763
+ }
240
764
  const report = uninstall();
241
- out();
242
765
  out(ui.banner('Removed from this machine'));
243
766
  out();
244
- if (report.profilesCleaned.length === 0) {
245
- out(ui.c.dim(' Nothing to take out: no shell profile carried our line.'));
767
+ for (const profile of report.profilesCleaned)
768
+ out(` removed our line from ${profile}`);
769
+ if (report.scriptRemoved)
770
+ out(' removed the generated shell script');
771
+ if (report.programRemoved)
772
+ out(' removed the program from ~/.comprism/bin');
773
+ if (!keep) {
774
+ fs.rmSync(report.kept, { recursive: true, force: true });
775
+ out(` removed the saved settings in ${report.kept}`);
246
776
  }
247
- else {
248
- for (const profile of report.profilesCleaned) {
249
- out(` removed our line from ${profile}`);
250
- }
777
+ // The program itself, the way it was installed. On Windows a running
778
+ // program cannot delete itself, so the removal runs just after we exit.
779
+ const remover = report.method === 'npm'
780
+ ? [process.platform === 'win32' ? 'npm.cmd' : 'npm', ['uninstall', '-g', '@illuminis/comprism']]
781
+ : report.method === 'python'
782
+ ? (spawnSync('pipx', ['--version'], { stdio: 'ignore' }).status === 0
783
+ ? ['pipx', ['uninstall', 'comprism-cli']]
784
+ : [process.platform === 'win32' ? 'python' : 'python3', ['-m', 'pip', 'uninstall', '-y', 'comprism-cli']])
785
+ : null;
786
+ if (remover && process.platform === 'win32') {
787
+ // Through cmd.exe, which is how npm.cmd and pip run on Windows, with
788
+ // every value its own argument and no shell string built from them.
789
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- a fixed removal command, no shell
790
+ spawn('cmd.exe', ['/d', '/c', remover[0], ...remover[1]], { detached: true, stdio: 'ignore' }).unref();
791
+ out(' removing the comprism command');
792
+ }
793
+ else if (remover) {
794
+ const done = spawnSync(remover[0], remover[1], { stdio: 'ignore' });
795
+ out(done.status === 0
796
+ ? ' removed the comprism command'
797
+ : ui.warn(` could not remove the command itself; run: ${remover[0]} ${remover[1].join(' ')}`));
251
798
  }
252
799
  out();
253
800
  out(ui.c.dim(' Every AI tool on this machine now talks straight to its provider.'));
254
- out(ui.c.dim(' To remove the command itself: npm uninstall -g @illuminis/comprism'));
801
+ out(ui.c.dim(' Open a new terminal window to finish.'));
255
802
  out();
256
803
  return;
257
804
  }
@@ -271,8 +818,23 @@ async function main() {
271
818
  if (command) {
272
819
  out(ui.fail(`There is no command called "${command}".`));
273
820
  out();
821
+ // A script that mistypes a command must not read it as success. Two is
822
+ // the manual's number for a wrongly typed command (Appendix B).
823
+ process.exitCode = 2;
274
824
  return usage();
275
825
  }
826
+ // Text piped into the bare command has nothing to go with (manual 3.9).
827
+ if (!process.stdin.isTTY) {
828
+ const { readPiped } = await Promise.resolve().then(() => __importStar(require('./lib/stdin')));
829
+ const piped = await readPiped();
830
+ if (piped?.trim()) {
831
+ err(ui.fail('Piped text needs a request with it, for example: '
832
+ + '... | comprism ask "why is this failing"'));
833
+ err(ui.c.dim(' Nothing ran and nothing was charged.'));
834
+ process.exitCode = 2;
835
+ return;
836
+ }
837
+ }
276
838
  // The bare command STARTS the product rather than describing it.
277
839
  const { runRepl } = await Promise.resolve().then(() => __importStar(require('./commands/repl')));
278
840
  return runRepl();