@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
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The folder a command should act on.
3
+ *
4
+ * The folder you are standing in, unless you named one. That is what every
5
+ * other tool in a terminal does, and a saved folder that silently overrode the
6
+ * current one would be a trap.
7
+ */
8
+ export declare function currentWorkspace(): {
9
+ path: string;
10
+ source: 'saved' | 'cwd';
11
+ };
12
+ export declare function cmdProviders(): Promise<void>;
13
+ /**
14
+ * Set the model you are compared against.
15
+ *
16
+ * This is NOT the model that runs your work: the product picks that, and the
17
+ * whole point is that it often picks a cheaper one. This is the model you WOULD
18
+ * have used, which is the only thing a saving can honestly be measured against.
19
+ */
20
+ export declare function cmdModel(args: string[]): Promise<void>;
21
+ export declare function cmdWorkspace(args: string[]): Promise<void>;
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.currentWorkspace = currentWorkspace;
40
+ exports.cmdProviders = cmdProviders;
41
+ exports.cmdModel = cmdModel;
42
+ exports.cmdWorkspace = cmdWorkspace;
43
+ /**
44
+ * The three things a person sets once and then forgets: which model they are
45
+ * compared against, which vendors they can reach, and which folder they work in.
46
+ *
47
+ * ── What is stored here, and what is not ──────────────────────────────────
48
+ *
49
+ * A PREFERENCE lives on this machine: the model you would otherwise have used,
50
+ * and the folder you work in. Neither is a secret and neither is model logic;
51
+ * they are two facts about you, and a round trip to ask us for them would be
52
+ * slower and no safer.
53
+ *
54
+ * THE MODEL LIST does not live here. It comes from our server on every call,
55
+ * because the models and their prices are ours, and because a copy on a
56
+ * customer's laptop goes stale the day a vendor changes a price. A stale price
57
+ * silently corrupts every figure downstream of it, which is the exact failure
58
+ * this product exists to replace.
59
+ */
60
+ const node_fs_1 = __importDefault(require("node:fs"));
61
+ const node_path_1 = __importDefault(require("node:path"));
62
+ const prompt_1 = require("../lib/prompt");
63
+ const gateway_1 = require("../lib/gateway");
64
+ const config_1 = require("../lib/config");
65
+ const ui = __importStar(require("../lib/ui"));
66
+ const out = (s = '') => process.stdout.write(s + '\n');
67
+ /** Where the chosen folder is remembered. Beside the rest of the settings. */
68
+ function workspaceFile() {
69
+ return node_path_1.default.join((0, config_1.homeDir)(), 'workspace.json');
70
+ }
71
+ /**
72
+ * The folder a command should act on.
73
+ *
74
+ * The folder you are standing in, unless you named one. That is what every
75
+ * other tool in a terminal does, and a saved folder that silently overrode the
76
+ * current one would be a trap.
77
+ */
78
+ function currentWorkspace() {
79
+ try {
80
+ const saved = JSON.parse(node_fs_1.default.readFileSync(workspaceFile(), 'utf8'));
81
+ if (saved.path && node_fs_1.default.existsSync(saved.path))
82
+ return { path: saved.path, source: 'saved' };
83
+ }
84
+ catch { /* nothing saved, which is the normal case */ }
85
+ return { path: process.cwd(), source: 'cwd' };
86
+ }
87
+ /**
88
+ * How a vendor writes its own name.
89
+ *
90
+ * The workspace returns a machine name and title-cases it, which turns `openai`
91
+ * into "Openai" and `xai` into "Xai". Neither company spells itself that way,
92
+ * and two screens in the same product showing the same vendors must spell them
93
+ * identically. Kept in step with the same list in `keys.ts`.
94
+ */
95
+ const VENDOR_LABEL = {
96
+ anthropic: 'Anthropic', openai: 'OpenAI', google: 'Google', xai: 'xAI',
97
+ };
98
+ const vendorLabel = (key, fallback) => VENDOR_LABEL[key.toLowerCase()] || fallback;
99
+ async function cmdProviders() {
100
+ out(ui.banner('AI providers'));
101
+ out();
102
+ const reply = await (0, gateway_1.ask)({ intent: 'catalog', prompt: '' });
103
+ // Four ways this can fail to be an answer, and none of them is "your account
104
+ // has no vendors". Saying otherwise sends somebody to add a key they already
105
+ // have, which is the fault reported on 17 September 2026.
106
+ if (!reply.ok || reply.served === false || reply.complete === false
107
+ || !reply.providers || reply.providers.length === 0) {
108
+ out(ui.fail('Your workspace could not tell us which vendors it can reach.'));
109
+ out(` ${ui.c.dim(reply.message || reply.decision.rationale || 'no answer from the workspace')}`);
110
+ out(` ${ui.c.dim('check the address with')} ${ui.c.bold('comprism status')}`);
111
+ out();
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ for (const p of reply.providers) {
116
+ const state = p.has_key ? ui.c.green('ready') : ui.c.amber('no key stored');
117
+ out(` ${ui.c.bold(vendorLabel(p.provider, p.label))} ${state}`);
118
+ for (const m of p.models) {
119
+ const mark = m.reachable ? ui.c.green(' ok ') : ui.c.dim(' -- ');
120
+ out(` ${mark} ${m.label.padEnd(24)} ${ui.c.dim(m.tier)}`);
121
+ }
122
+ out();
123
+ }
124
+ const total = reply.providers.reduce((n, p) => n + p.models.length, 0);
125
+ out(ui.c.dim(` ${total} models across ${reply.providers.length} vendors, every one priced from our own study.`));
126
+ out(ui.c.dim(' A vendor with no key stored can still be called by sending your key with the request.'));
127
+ out();
128
+ }
129
+ /**
130
+ * Set the model you are compared against.
131
+ *
132
+ * This is NOT the model that runs your work: the product picks that, and the
133
+ * whole point is that it often picks a cheaper one. This is the model you WOULD
134
+ * have used, which is the only thing a saving can honestly be measured against.
135
+ */
136
+ async function cmdModel(args) {
137
+ const cfg = (0, config_1.loadConfig)();
138
+ const named = args.find((a) => !a.startsWith('--'));
139
+ out(ui.banner('Your comparison model'));
140
+ out();
141
+ out(ui.c.dim(' The model you would have used anyway. Savings are measured against it.'));
142
+ out(` ${ui.c.dim('Now:')} ${ui.c.bold(cfg.defaultModel || 'not set')}`);
143
+ out();
144
+ const reply = await (0, gateway_1.ask)({ intent: 'catalog', prompt: '' });
145
+ const models = (reply.providers || []).flatMap((p) => p.models);
146
+ // Same rule as every other reader: an answer we did not get is not an answer
147
+ // of none.
148
+ if (!reply.ok || reply.served === false || reply.complete === false
149
+ || models.length === 0) {
150
+ out(ui.fail('Your workspace could not tell us which models it can reach.'));
151
+ out(` ${ui.c.dim(reply.message || reply.decision.rationale || 'no answer from the workspace')}`);
152
+ out();
153
+ process.exitCode = 1;
154
+ return;
155
+ }
156
+ const match = (value) => models.find((m) => m.id === value || m.key === value
157
+ || m.label.toLowerCase() === value.toLowerCase());
158
+ let chosen = named ? match(named) : undefined;
159
+ if (named && !chosen) {
160
+ out(ui.fail(`No model called "${named}".`));
161
+ out(` ${ui.c.dim('see the list with')} ${ui.c.bold('comprism providers')}`);
162
+ out();
163
+ process.exitCode = 1;
164
+ return;
165
+ }
166
+ if (!chosen) {
167
+ models.forEach((m, i) => {
168
+ const n = String(i + 1).padStart(2);
169
+ out(` ${n}. ${m.label.padEnd(24)} ${ui.c.dim(`${m.provider} ${m.tier}`)}`);
170
+ });
171
+ out();
172
+ const answer = await (0, prompt_1.ask)(' number, or a model name: ');
173
+ (0, prompt_1.closePrompt)();
174
+ if (!answer) {
175
+ out(ui.info('Left as it was.'));
176
+ out();
177
+ return;
178
+ }
179
+ const asNumber = Number(answer);
180
+ chosen = Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= models.length
181
+ ? models[asNumber - 1]
182
+ : match(answer);
183
+ if (!chosen) {
184
+ out(ui.fail(`"${answer}" is not on the list.`));
185
+ out();
186
+ process.exitCode = 1;
187
+ return;
188
+ }
189
+ }
190
+ (0, config_1.saveConfig)({ ...cfg, defaultModel: chosen.id });
191
+ out();
192
+ out(ui.ok(`Comparing against ${ui.c.bold(chosen.label)}.`));
193
+ out(` ${ui.c.dim('every receipt from now on prices your work against this model')}`);
194
+ out();
195
+ }
196
+ async function cmdWorkspace(args) {
197
+ const named = args.find((a) => !a.startsWith('--'));
198
+ const current = currentWorkspace();
199
+ out(ui.banner('Working folder'));
200
+ out();
201
+ out(` ${ui.c.bold(current.path)}`);
202
+ out(` ${ui.c.dim(current.source === 'cwd'
203
+ ? 'the folder you are standing in'
204
+ : 'saved, so it is used wherever you run the command from')}`);
205
+ out();
206
+ if (args.includes('--clear')) {
207
+ try {
208
+ node_fs_1.default.unlinkSync(workspaceFile());
209
+ }
210
+ catch { /* nothing was saved */ }
211
+ out(ui.ok('Cleared. The folder you are standing in is used again.'));
212
+ out();
213
+ return;
214
+ }
215
+ let target = named;
216
+ if (!target) {
217
+ const answer = await (0, prompt_1.ask)(' new folder, or enter to keep it: ');
218
+ (0, prompt_1.closePrompt)();
219
+ if (!answer) {
220
+ out(ui.info('Left as it was.'));
221
+ out();
222
+ return;
223
+ }
224
+ target = answer;
225
+ }
226
+ const resolved = node_path_1.default.resolve(target.replace(/^~/, process.env.HOME || '~'));
227
+ if (!node_fs_1.default.existsSync(resolved)) {
228
+ out(ui.fail(`There is no folder at ${resolved}.`));
229
+ out();
230
+ process.exitCode = 1;
231
+ return;
232
+ }
233
+ if (!node_fs_1.default.statSync(resolved).isDirectory()) {
234
+ out(ui.fail(`${resolved} is a file, not a folder.`));
235
+ out();
236
+ process.exitCode = 1;
237
+ return;
238
+ }
239
+ node_fs_1.default.mkdirSync((0, config_1.homeDir)(), { recursive: true });
240
+ node_fs_1.default.writeFileSync(workspaceFile(), JSON.stringify({ path: resolved }, null, 2), { mode: 0o600 });
241
+ out(ui.ok(`Working folder set to ${ui.c.bold(resolved)}.`));
242
+ out(` ${ui.c.dim('clear it with')} ${ui.c.bold('comprism workspace --clear')}`);
243
+ out();
244
+ }
@@ -0,0 +1 @@
1
+ export declare function showWelcome(): Promise<void>;
@@ -0,0 +1,196 @@
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.showWelcome = showWelcome;
37
+ /**
38
+ * The first thing anybody sees.
39
+ *
40
+ * ── Why a banner is not decoration ────────────────────────────────────────
41
+ *
42
+ * Typing the bare command used to print a list of other commands. That tells a
43
+ * person what they COULD do and nothing about where they are. In a product
44
+ * whose whole claim is a number, four facts make the number mean anything: who
45
+ * you are signed in as, whose workspace you are in, which model your savings
46
+ * are measured against, and which folder you are working in.
47
+ *
48
+ * A person who does not know the comparison model cannot read a saving, and a
49
+ * person who has to run three commands to find it will not.
50
+ *
51
+ * The prism spectrum is the brand: white light in, a spectrum out. It is drawn
52
+ * from the same eight colors as every other illuminis screen, through the same
53
+ * helper, so a terminal and the portal read as one product.
54
+ *
55
+ * Everything here is either already on this machine or comes back in ONE call.
56
+ * A welcome screen costing three round trips is one people learn to skip.
57
+ */
58
+ const connection_1 = require("../lib/connection");
59
+ const gateway_1 = require("../lib/gateway");
60
+ const config_1 = require("../lib/config");
61
+ const settings_1 = require("./settings");
62
+ const version_1 = require("../lib/version");
63
+ const ui = __importStar(require("../lib/ui"));
64
+ const out = (s = '') => process.stdout.write(s + '\n');
65
+ /**
66
+ * Who this machine is signed in as, and everything else the header prints.
67
+ *
68
+ * ── One call, to the one service ──────────────────────────────────────────
69
+ *
70
+ * This used to call the platform's own `whoami` for the identity, and the
71
+ * header then had no way at all to say how many vendor keys the account held.
72
+ * Two questions, two addresses, and the header could show a person facts that
73
+ * disagreed with each other. On 17 September 2026 it did exactly that: "User
74
+ * unknown" beside a company name, and a session that then announced no provider
75
+ * key was stored while four were stored and active.
76
+ *
77
+ * Every line below now comes from one request to the single service, so the
78
+ * header either shows a consistent picture or says plainly that it could not
79
+ * reach the workspace. There is no third state in which half of it is true.
80
+ *
81
+ * Null rather than an error when it cannot be reached. A header is the wrong
82
+ * place to fail: somebody on a plane still gets the local facts, and the lines
83
+ * needing the server say so.
84
+ */
85
+ async function session() {
86
+ const conn = (0, connection_1.readConnection)();
87
+ if (!conn?.url || !conn.workspaceKey)
88
+ return null;
89
+ const reply = await (0, gateway_1.ask)({ intent: 'session', prompt: '' });
90
+ // `ok` false means nobody answered. `served` false means the workspace is
91
+ // running a build without this question. Neither is an answer about the
92
+ // account, and neither may be drawn as one.
93
+ if (!reply.ok || reply.served === false || !reply.session)
94
+ return null;
95
+ return reply.session;
96
+ }
97
+ /**
98
+ * The mark: a prism splitting light.
99
+ *
100
+ * A four row pyramid, drawn only in the full block glyph. The first attempt
101
+ * used quarter blocks and came out looking like a rendering fault, because
102
+ * those glyphs sit differently in every terminal font and a mark cannot depend
103
+ * on a font. A full block renders identically everywhere, so the only thing
104
+ * carrying the design is the color, which is the point of a prism.
105
+ *
106
+ * Color is taken by COLUMN, not by row. The apex sits on the middle of the
107
+ * spectrum and each row down widens to one more color on either side, so the
108
+ * base carries all eight. That is a prism doing its job: one input at the top,
109
+ * the whole spectrum at the bottom. Banding it by row would have shown four of
110
+ * the eight and read as a striped triangle instead.
111
+ *
112
+ * The eight values come from the same helper as the rule beneath it, so the
113
+ * mark and the rule can never drift apart.
114
+ */
115
+ const MARK_BASE = 8;
116
+ function mark() {
117
+ const rows = [];
118
+ for (let r = 1; r <= 4; r++) {
119
+ const blocks = r * 2;
120
+ const left = (MARK_BASE - blocks) / 2;
121
+ let line = ' '.repeat(left);
122
+ for (let i = 0; i < blocks; i++)
123
+ line += ui.spectrumAt(left + i, '\u2588');
124
+ rows.push(line + ' '.repeat(left));
125
+ }
126
+ return rows;
127
+ }
128
+ /** A label and its value, aligned so the values form one column. */
129
+ function row(label, value) {
130
+ return ` ${ui.c.dim(label.padEnd(11))}${value}`;
131
+ }
132
+ /** "base-tenant" is a machine name. A person reads "Base Tenant". */
133
+ function readable(name) {
134
+ return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase());
135
+ }
136
+ async function showWelcome() {
137
+ const conn = (0, connection_1.readConnection)();
138
+ const cfg = (0, config_1.loadConfig)();
139
+ const folder = (0, settings_1.currentWorkspace)();
140
+ const me = await session();
141
+ const [m1, m2, m3, m4] = mark();
142
+ out();
143
+ out(` ${m1} ${ui.c.bold('CompletionPrism')} ${ui.c.dim(`v${version_1.COMPRISM_VERSION}`)}`);
144
+ out(` ${m2} ${ui.c.muted('Optimize AI completion economics by optimizing work.')}`);
145
+ out(` ${m3}`);
146
+ out(` ${m4}`);
147
+ out(ui.prismRule());
148
+ out();
149
+ if (!conn?.url) {
150
+ // Nothing else here is knowable yet, and the one useful thing to say is the
151
+ // single command that changes that.
152
+ out(row('Signed in', ui.c.amber('not yet')));
153
+ out(row('Comparing', cfg.defaultModel || 'not set'));
154
+ out(row('Workspace', `${folder.path} ${ui.c.dim('(where you are)')}`));
155
+ out();
156
+ out(` ${ui.c.dim('start with')} ${ui.c.bold('comprism login')}`);
157
+ out();
158
+ return;
159
+ }
160
+ const company = me?.company || readable(conn.tenant || 'unknown');
161
+ const who = me?.user_email || ui.c.dim('unknown');
162
+ const plan = me?.plan || '';
163
+ const allowance = me?.requests_per_month || 0;
164
+ // The address the SERVER says it answers on, in preference to the one this
165
+ // machine happens to hold. A client that can only echo its own stored address
166
+ // can never tell a person that the address is the thing that is wrong, which
167
+ // is exactly what happened when a machine sat pointed at a dead laptop port.
168
+ const address = (me?.workspace_url || conn.url).replace(/^https?:\/\//, '');
169
+ out(row('User', who));
170
+ out(row('Company', company));
171
+ out(row('Server', ui.c.dim(address)));
172
+ out(row('Comparing', `${cfg.defaultModel || ui.c.amber('not set')} ${ui.c.dim('change with `comprism model`')}`));
173
+ out(row('Workspace', `${folder.path} ${ui.c.dim(folder.source === 'cwd' ? '(where you are)' : '(saved)')}`));
174
+ if (plan) {
175
+ out(row('Plan', allowance
176
+ ? `${plan} ${ui.c.dim(`${allowance.toLocaleString()} requests a month`)}`
177
+ : plan));
178
+ }
179
+ if (!me) {
180
+ out();
181
+ out(` ${ui.c.amber('your workspace could not be reached, so only local settings are shown')}`);
182
+ out(` ${ui.c.dim('nothing here is known about your account, including your provider keys')}`);
183
+ }
184
+ else if (me.vendors_known === false) {
185
+ // It answered, but could not establish this one line. Said plainly, because
186
+ // an unchecked vendor list shown as a number is the fault this whole change
187
+ // exists to end.
188
+ out();
189
+ out(` ${ui.c.amber('your vendor keys could not be checked, so that is unknown rather than none')}`);
190
+ }
191
+ out();
192
+ out(` ${ui.c.dim('ask a question')} ${ui.c.bold('comprism ask')}`);
193
+ out(` ${ui.c.dim('have work done')} ${ui.c.bold('comprism work')}`);
194
+ out(` ${ui.c.dim('every command')} ${ui.c.bold('comprism help')}`);
195
+ out();
196
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Applying a patch, and reading a PDF.
3
+ *
4
+ * Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
5
+ * items D15 (apply a patch) and D3 (read a PDF).
6
+ *
7
+ * Both exist for the same reason: the thing the agent needs is already in a
8
+ * format somebody else chose. A patch arrives from a review or a bug report, and
9
+ * a specification arrives as a PDF because that is how standards bodies publish.
10
+ * Neither is worth re-typing into a form we prefer.
11
+ */
12
+ import { RunResult, ShellOptions } from "./shell";
13
+ /**
14
+ * Apply a unified diff.
15
+ *
16
+ * Through `git apply` rather than a parser of our own. Patch formats have
17
+ * decades of edge cases in them - renames, mode changes, binary hunks, fuzzy
18
+ * context - and a hand-rolled applier gets the common case right and the
19
+ * interesting ones wrong, silently, in somebody's repository.
20
+ *
21
+ * **Checked before it is applied.** `--check` runs the whole thing and changes
22
+ * nothing, so a patch that does not fit is refused whole rather than leaving
23
+ * half of itself behind. That is the same property `edit_file` has and for the
24
+ * same reason: a file left half changed is worse than one not changed, because
25
+ * the model believes the whole change landed.
26
+ */
27
+ export declare function applyPatch(patch: string, opts: ShellOptions): Promise<RunResult>;
28
+ /**
29
+ * Read a PDF as text.
30
+ *
31
+ * Through whichever extractor the machine has, in order of how good the output
32
+ * is: `pdftotext` keeps the layout, `mutool` is close, and Python's pypdf is
33
+ * usually present on a machine that has Python at all. If none of them is
34
+ * there, it says so and names one to install rather than returning nothing.
35
+ *
36
+ * A PDF full of scanned images produces no text, and that is reported as what it
37
+ * is. Silently returning an empty document would have the model conclude the
38
+ * specification is blank.
39
+ */
40
+ export declare function readPdf(root: string, rel: string, pages: string | undefined, opts: ShellOptions): Promise<RunResult>;
@@ -0,0 +1,170 @@
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.applyPatch = applyPatch;
37
+ exports.readPdf = readPdf;
38
+ /**
39
+ * Applying a patch, and reading a PDF.
40
+ *
41
+ * Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
42
+ * items D15 (apply a patch) and D3 (read a PDF).
43
+ *
44
+ * Both exist for the same reason: the thing the agent needs is already in a
45
+ * format somebody else chose. A patch arrives from a review or a bug report, and
46
+ * a specification arrives as a PDF because that is how standards bodies publish.
47
+ * Neither is worth re-typing into a form we prefer.
48
+ */
49
+ const shell_1 = require("./shell");
50
+ const paths_1 = require("./paths");
51
+ const fs = __importStar(require("fs"));
52
+ const os = __importStar(require("os"));
53
+ const path = __importStar(require("path"));
54
+ /**
55
+ * Apply a unified diff.
56
+ *
57
+ * Through `git apply` rather than a parser of our own. Patch formats have
58
+ * decades of edge cases in them - renames, mode changes, binary hunks, fuzzy
59
+ * context - and a hand-rolled applier gets the common case right and the
60
+ * interesting ones wrong, silently, in somebody's repository.
61
+ *
62
+ * **Checked before it is applied.** `--check` runs the whole thing and changes
63
+ * nothing, so a patch that does not fit is refused whole rather than leaving
64
+ * half of itself behind. That is the same property `edit_file` has and for the
65
+ * same reason: a file left half changed is worse than one not changed, because
66
+ * the model believes the whole change landed.
67
+ */
68
+ async function applyPatch(patch, opts) {
69
+ if (!String(patch || "").trim()) {
70
+ return { isError: true, content: "Refused: there is no patch here." };
71
+ }
72
+ // Written to a temporary file rather than piped, so the patch cannot be
73
+ // mangled by shell quoting. A diff is full of characters a shell cares about.
74
+ const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "patch-")), "change.patch");
75
+ fs.writeFileSync(file, patch.endsWith("\n") ? patch : `${patch}\n`, "utf8");
76
+ try {
77
+ const check = await (0, shell_1.runCommand)(`git apply --check --verbose ${file}`, opts);
78
+ if (!/^exit 0/.test(check.content)) {
79
+ return {
80
+ isError: true,
81
+ content: "Refused, and nothing was changed: this patch does not apply to the "
82
+ + `project as it stands.\n\n${check.content}\n\n`
83
+ + "Read the files it touches and make the change with edit_file "
84
+ + "instead, or ask for a patch against the current state.",
85
+ };
86
+ }
87
+ const applied = await (0, shell_1.runCommand)(`git apply --stat --apply ${file}`, opts);
88
+ if (!/^exit 0/.test(applied.content)) {
89
+ return { ...applied, isError: true };
90
+ }
91
+ return { content: `Applied the patch.\n\n${applied.content}`, summary: "patch applied" };
92
+ }
93
+ finally {
94
+ try {
95
+ fs.rmSync(path.dirname(file), { recursive: true, force: true });
96
+ }
97
+ catch { /* nothing to do */ }
98
+ }
99
+ }
100
+ /**
101
+ * Read a PDF as text.
102
+ *
103
+ * Through whichever extractor the machine has, in order of how good the output
104
+ * is: `pdftotext` keeps the layout, `mutool` is close, and Python's pypdf is
105
+ * usually present on a machine that has Python at all. If none of them is
106
+ * there, it says so and names one to install rather than returning nothing.
107
+ *
108
+ * A PDF full of scanned images produces no text, and that is reported as what it
109
+ * is. Silently returning an empty document would have the model conclude the
110
+ * specification is blank.
111
+ */
112
+ async function readPdf(root, rel, pages, opts) {
113
+ let file;
114
+ try {
115
+ file = (0, paths_1.resolveInside)(root, rel, true);
116
+ }
117
+ catch (err) {
118
+ return { isError: true, content: err.message };
119
+ }
120
+ const range = pages ? pageRange(pages) : null;
121
+ const attempts = [
122
+ `pdftotext -layout ${range ? `-f ${range.from} -l ${range.to} ` : ""}${quote(file)} -`,
123
+ `mutool draw -F txt ${quote(file)} ${range ? `${range.from}-${range.to}` : ""}`,
124
+ `python3 -c ${quote(pythonExtractor(file, range))}`,
125
+ ];
126
+ for (const command of attempts) {
127
+ const result = await (0, shell_1.runCommand)(command, { ...opts, timeoutMs: 120_000 });
128
+ if (!/^exit 0/.test(result.content))
129
+ continue;
130
+ const body = result.content.replace(/^exit 0\n\n?/, "").trim();
131
+ if (!body) {
132
+ return {
133
+ isError: true,
134
+ content: `${rel} produced no text. It is probably scanned images rather than `
135
+ + "text, so there is nothing to read without character recognition.",
136
+ };
137
+ }
138
+ const cut = body.length > 60_000
139
+ ? `${body.slice(0, 60_000)}\n\n[... the rest was not read. Ask for a page range.]`
140
+ : body;
141
+ return { content: cut, summary: `read ${rel}` };
142
+ }
143
+ return {
144
+ isError: true,
145
+ content: `Nothing on this machine can read a PDF. Install one of them and try `
146
+ + "again: `brew install poppler` on a Mac, `apt install poppler-utils` on "
147
+ + "Linux, or `pip install pypdf`.",
148
+ };
149
+ }
150
+ function pageRange(pages) {
151
+ const match = /^(\d+)\s*(?:-\s*(\d+))?$/.exec(pages.trim());
152
+ if (!match)
153
+ return null;
154
+ const from = Number(match[1]);
155
+ return { from, to: match[2] ? Number(match[2]) : from };
156
+ }
157
+ function quote(value) {
158
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
159
+ }
160
+ function pythonExtractor(file, range) {
161
+ const slice = range ? `[${range.from - 1}:${range.to}]` : "";
162
+ return ("import sys\n"
163
+ + "try:\n"
164
+ + " from pypdf import PdfReader\n"
165
+ + "except ImportError:\n"
166
+ + " sys.exit(1)\n"
167
+ + `r = PdfReader(${JSON.stringify(file)})\n`
168
+ + `for page in r.pages${slice}:\n`
169
+ + " print(page.extract_text() or '')\n");
170
+ }
@@ -0,0 +1,2 @@
1
+ import { RunResult } from "./shell";
2
+ export declare function performFileAction(root: string, name: string, a: Record<string, unknown>): RunResult;