@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/out/lib/ui.js ADDED
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ /**
3
+ * Terminal presentation.
4
+ *
5
+ * This is a developer product, so the terminal IS the interface, and it gets the
6
+ * same care a screen would. Three rules hold it together:
7
+ *
8
+ * 1. **The brand shows up here.** The prism spectrum and the illuminis palette
9
+ * are the same ones the portal and the documents use, rendered in 24-bit
10
+ * color where the terminal supports it.
11
+ * 2. **Color is never the only signal.** Every state that matters also has a
12
+ * glyph and a word, so the output survives `NO_COLOR`, a pipe, a CI log and
13
+ * a color-blind reader unchanged.
14
+ * 3. **Nothing here is decoration for its own sake.** Every panel answers a
15
+ * question the person actually has: what did that cost, what was wasted,
16
+ * and how sure are we of the number.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.LiveIndicator = exports.c = void 0;
20
+ exports.width = width;
21
+ exports.spectrumAt = spectrumAt;
22
+ exports.prismRule = prismRule;
23
+ exports.banner = banner;
24
+ exports.money = money;
25
+ exports.clip = clip;
26
+ exports.pad = pad;
27
+ exports.padLeft = padLeft;
28
+ exports.stripAnsi = stripAnsi;
29
+ exports.kpiCards = kpiCards;
30
+ exports.paretoBars = paretoBars;
31
+ exports.table = table;
32
+ exports.receipt = receipt;
33
+ exports.ok = ok;
34
+ exports.warn = warn;
35
+ exports.fail = fail;
36
+ exports.info = info;
37
+ const isTty = process.stdout.isTTY === true;
38
+ const noColor = !!process.env.NO_COLOR || process.env.TERM === 'dumb';
39
+ const useColor = isTty && !noColor;
40
+ function width() {
41
+ return Math.min(process.stdout.columns || 88, 100);
42
+ }
43
+ // ── palette ─────────────────────────────────────────────────────────────────
44
+ // illuminis brand, from branding/brand.toml. Deep navy is unreadable on a dark
45
+ // terminal, so the terminal palette leans on the two blues and the purple.
46
+ const rgb = (r, g, b) => (s) => useColor ? `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m` : s;
47
+ exports.c = {
48
+ blue: rgb(59, 111, 181), // primary_blue #3B6FB5
49
+ deep: rgb(31, 77, 120), // heading_blue #1F4D78
50
+ purple: rgb(107, 91, 167), // purple_accent #6B5BA7
51
+ green: rgb(22, 163, 74), // success #16A34A
52
+ amber: rgb(217, 119, 6), // warning #D97706
53
+ red: rgb(220, 38, 38), // error #DC2626
54
+ muted: rgb(158, 158, 158), // muted_text #9E9E9E
55
+ text: rgb(214, 219, 228),
56
+ bold: (s) => (useColor ? `\x1b[1m${s}\x1b[0m` : s),
57
+ dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s),
58
+ };
59
+ /** The prism: white light entering, a spectrum leaving. The brand in one line. */
60
+ const SPECTRUM = [
61
+ [59, 111, 181],
62
+ [79, 105, 186],
63
+ [99, 98, 184],
64
+ [123, 92, 175],
65
+ [150, 90, 160],
66
+ [176, 92, 138],
67
+ [201, 101, 112],
68
+ [217, 119, 6],
69
+ ];
70
+ /**
71
+ * Any glyph, in one of the eight spectrum colors.
72
+ *
73
+ * Exported so the logo and the rule under it are drawn from the SAME eight
74
+ * values. Two hand-picked palettes in one product drift apart within a release,
75
+ * and the brand is the one thing a customer notices before the engine.
76
+ */
77
+ function spectrumAt(index, glyph) {
78
+ if (!useColor)
79
+ return glyph;
80
+ const [r, g, b] = SPECTRUM[Math.abs(index) % SPECTRUM.length];
81
+ return `\x1b[38;2;${r};${g};${b}m${glyph}\x1b[0m`;
82
+ }
83
+ function prismRule(w = width()) {
84
+ if (!useColor)
85
+ return '-'.repeat(w);
86
+ const seg = Math.max(1, Math.floor(w / SPECTRUM.length));
87
+ let out = '';
88
+ for (let i = 0; i < SPECTRUM.length; i++) {
89
+ const [r, g, b] = SPECTRUM[i];
90
+ const len = i === SPECTRUM.length - 1 ? w - seg * (SPECTRUM.length - 1) : seg;
91
+ out += `\x1b[38;2;${r};${g};${b}m${'━'.repeat(Math.max(0, len))}`;
92
+ }
93
+ return `${out}\x1b[0m`;
94
+ }
95
+ function banner(subtitle) {
96
+ const lines = [
97
+ '',
98
+ ` ${exports.c.bold(exports.c.blue('◣'))}${exports.c.bold(exports.c.purple('◥'))} ${exports.c.bold('CompletionPrism')} ${exports.c.dim('comprism')}`,
99
+ subtitle ? ` ${exports.c.muted(subtitle)}` : '',
100
+ prismRule(),
101
+ ].filter(Boolean);
102
+ return lines.join('\n');
103
+ }
104
+ // ── primitives ──────────────────────────────────────────────────────────────
105
+ function money(v) {
106
+ if (v === null)
107
+ return 'unpriced';
108
+ if (v === 0)
109
+ return '$0.00';
110
+ if (v < 0.01)
111
+ return `$${v.toFixed(5)}`;
112
+ if (v < 1)
113
+ return `$${v.toFixed(4)}`;
114
+ return `$${v.toFixed(2)}`;
115
+ }
116
+ /** Clip to a visible width, with an ellipsis, so a card never bleeds. */
117
+ function clip(s, n) {
118
+ const visible = stripAnsi(s);
119
+ if (visible.length <= n)
120
+ return s;
121
+ return `${visible.slice(0, Math.max(0, n - 1))}\u2026`;
122
+ }
123
+ function pad(s, n) {
124
+ const visible = stripAnsi(s);
125
+ return visible.length >= n ? s : s + ' '.repeat(n - visible.length);
126
+ }
127
+ function padLeft(s, n) {
128
+ const visible = stripAnsi(s);
129
+ return visible.length >= n ? s : ' '.repeat(n - visible.length) + s;
130
+ }
131
+ function stripAnsi(s) {
132
+ // eslint-disable-next-line no-control-regex
133
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
134
+ }
135
+ /**
136
+ * A row of KPI cards - the same shape the portal's summary tab uses, so the two
137
+ * surfaces read as one product rather than two tools that happen to share a name.
138
+ */
139
+ function kpiCards(cards) {
140
+ const w = width();
141
+ const cardW = Math.max(18, Math.floor((w - (cards.length - 1) * 2) / cards.length));
142
+ const tone = (t) => t === 'good' ? exports.c.green : t === 'warn' ? exports.c.amber : exports.c.text;
143
+ const top = cards.map(() => `┌${'─'.repeat(cardW - 2)}┐`).join(' ');
144
+ const inner = cardW - 4;
145
+ const label = cards
146
+ .map((k) => `│ ${pad(exports.c.muted(clip(k.label.toUpperCase(), inner)), inner)} │`)
147
+ .join(' ');
148
+ const value = cards
149
+ .map((k) => `│ ${pad(exports.c.bold(tone(k.tone)(clip(k.value, inner))), inner)} │`)
150
+ .join(' ');
151
+ const note = cards
152
+ .map((k) => `│ ${pad(exports.c.dim(clip(k.note || '', inner)), inner)} │`)
153
+ .join(' ');
154
+ const bottom = cards.map(() => `└${'─'.repeat(cardW - 2)}┘`).join(' ');
155
+ return [top, label, value, note, bottom].join('\n');
156
+ }
157
+ /**
158
+ * A Pareto bar: sorted descending with a running cumulative share.
159
+ *
160
+ * Pareto rather than a plain bar chart because the question a spend screen has
161
+ * to answer is not "how much did each cost" but "how few of these carry most of
162
+ * the bill" - the answer is usually two or three, and that is the whole insight.
163
+ */
164
+ function paretoBars(rows, opts = {}) {
165
+ const unit = opts.unit || ((v) => v.toFixed(2));
166
+ const sorted = [...rows].sort((a, b) => b.value - a.value);
167
+ const total = sorted.reduce((s, r) => s + r.value, 0) || 1;
168
+ const labelW = Math.max(...sorted.map((r) => r.label.length), 8);
169
+ const barW = opts.barWidth || Math.max(12, Math.min(34, width() - labelW - 30));
170
+ const max = sorted[0]?.value || 1;
171
+ let cumulative = 0;
172
+ const out = [];
173
+ for (const r of sorted) {
174
+ cumulative += r.value;
175
+ const share = (cumulative / total) * 100;
176
+ const filled = Math.max(r.value > 0 ? 1 : 0, Math.round((r.value / max) * barW));
177
+ const bar = exports.c.blue('█'.repeat(filled)) + exports.c.dim('·'.repeat(Math.max(0, barW - filled)));
178
+ out.push(` ${pad(exports.c.text(r.label), labelW)} ${bar} ${padLeft(unit(r.value), 10)} ${exports.c.dim(`${padLeft(share.toFixed(0), 3)}% cum`)}${r.sub ? ` ${exports.c.dim(r.sub)}` : ''}`);
179
+ }
180
+ return out.join('\n');
181
+ }
182
+ function table(headers, rows) {
183
+ const widths = headers.map((h, i) => Math.max(stripAnsi(h).length, ...rows.map((r) => stripAnsi(r[i] || '').length)));
184
+ const head = ` ${headers.map((h, i) => pad(exports.c.muted(h.toUpperCase()), widths[i])).join(' ')}`;
185
+ const sep = ` ${widths.map((w) => exports.c.dim('─'.repeat(w))).join(' ')}`;
186
+ const body = rows.map((r) => ` ${r.map((cell, i) => pad(cell, widths[i])).join(' ')}`);
187
+ return [head, sep, ...body].join('\n');
188
+ }
189
+ // ── the live indicator ──────────────────────────────────────────────────────
190
+ /**
191
+ * The thing that makes it visible that something is between the person and the
192
+ * provider.
193
+ *
194
+ * Without this, an instrumented session looks exactly like an uninstrumented
195
+ * one, and a product nobody can see is a product nobody renews. It shows the
196
+ * work as it happens - characterising, dispatching, recording - and then gets
197
+ * out of the way, leaving the answer and a one-line receipt.
198
+ */
199
+ class LiveIndicator {
200
+ timer = null;
201
+ frame = 0;
202
+ label = '';
203
+ frames = ['◜', '◠', '◝', '◞', '◡', '◟'];
204
+ start(label) {
205
+ if (!isTty)
206
+ return;
207
+ this.label = label;
208
+ this.timer = setInterval(() => {
209
+ const glyph = this.frames[this.frame % this.frames.length];
210
+ this.frame += 1;
211
+ process.stdout.write(`\r ${exports.c.purple(glyph)} ${exports.c.dim(this.label)} `);
212
+ }, 90);
213
+ }
214
+ update(label) {
215
+ this.label = label;
216
+ }
217
+ stop() {
218
+ if (this.timer)
219
+ clearInterval(this.timer);
220
+ this.timer = null;
221
+ if (isTty)
222
+ process.stdout.write(`\r${' '.repeat(width())}\r`);
223
+ }
224
+ }
225
+ exports.LiveIndicator = LiveIndicator;
226
+ /**
227
+ * The receipt, rendered by the client and never injected into the conversation.
228
+ *
229
+ * Injected text is re-sent on every later turn, so a receipt in the message
230
+ * array is a line item the customer pays for again on every turn for the rest of
231
+ * the session. It belongs on the screen, not in the context window.
232
+ */
233
+ function receipt(r) {
234
+ const parts = [];
235
+ parts.push(`${exports.c.purple('◈')} ${exports.c.bold(r.model)}`);
236
+ parts.push(exports.c.dim('·'));
237
+ parts.push(`${r.inputTokens ?? '?'}${exports.c.dim(' in')} ${r.outputTokens ?? '?'}${exports.c.dim(' out')}`);
238
+ parts.push(exports.c.dim('·'));
239
+ parts.push(exports.c.bold(money(r.costUsd)));
240
+ parts.push(exports.c.dim('·'));
241
+ parts.push(exports.c.dim(`${(r.latencyMs / 1000).toFixed(1)}s`));
242
+ parts.push(exports.c.dim('·'));
243
+ parts.push(exports.c.dim(`${r.tier}`));
244
+ const lines = [` ${parts.join(' ')}`];
245
+ if (r.remediated) {
246
+ lines.push(` ${exports.c.amber('↑')} ${exports.c.amber('weak answer repaired')} ${exports.c.dim(`- ${r.attempts} attempts, ${money(r.wastedUsd)} spent on the attempt that failed`)}`);
247
+ }
248
+ lines.push(` ${exports.c.green('✓')} ${exports.c.dim('recorded:')} ${exports.c.dim('ledger row, decision record, fingerprint only - no request text stored')}`);
249
+ if (r.withheld) {
250
+ lines.push(` ${exports.c.muted('○')} ${exports.c.dim(`no recommendation (${r.withheld.replace(/_/g, ' ')}) - request ran untouched`)}`);
251
+ }
252
+ return lines.join('\n');
253
+ }
254
+ function ok(msg) {
255
+ return ` ${exports.c.green('✓')} ${msg}`;
256
+ }
257
+ function warn(msg) {
258
+ return ` ${exports.c.amber('!')} ${msg}`;
259
+ }
260
+ function fail(msg) {
261
+ return ` ${exports.c.red('✗')} ${msg}`;
262
+ }
263
+ function info(msg) {
264
+ return ` ${exports.c.blue('▸')} ${msg}`;
265
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Every version string the Companion stamps onto a record.
3
+ *
4
+ * Ground rule 3: a derived record without the version of the code that produced
5
+ * it cannot be restated and cannot serve as evidence. These are bumped when the
6
+ * behavior behind them changes, never on a whim, because consumers select by
7
+ * version and old records keep meaning what they meant.
8
+ */
9
+ /** The package version. Stamped on every row the Companion writes. */
10
+ export declare const COMPRISM_VERSION = "0.1.0";
11
+ /** The structural classifier. Bump when a signal is added, removed or retuned. */
12
+ export declare const CLASSIFIER_VERSION = "structural-v1";
13
+ /**
14
+ * The dispatch policy.
15
+ *
16
+ * `naive` is the honest name for what this release does: the first eligible
17
+ * candidate for the structural band, with no estimate consulted. When the model
18
+ * program delivers tables, decisions made under this version stay permanently
19
+ * distinguishable from decisions made under a learned one - which is exactly
20
+ * why the version is on the record rather than in a changelog.
21
+ */
22
+ export declare const POLICY_VERSION = "naive-v1";
23
+ /** The estimator interface. Inert in this release: no tables, no recommendation. */
24
+ export declare const ESTIMATOR_VERSION = "inert-v1";
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * Every version string the Companion stamps onto a record.
4
+ *
5
+ * Ground rule 3: a derived record without the version of the code that produced
6
+ * it cannot be restated and cannot serve as evidence. These are bumped when the
7
+ * behavior behind them changes, never on a whim, because consumers select by
8
+ * version and old records keep meaning what they meant.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ESTIMATOR_VERSION = exports.POLICY_VERSION = exports.CLASSIFIER_VERSION = exports.COMPRISM_VERSION = void 0;
12
+ /** The package version. Stamped on every row the Companion writes. */
13
+ exports.COMPRISM_VERSION = '0.1.0';
14
+ /** The structural classifier. Bump when a signal is added, removed or retuned. */
15
+ exports.CLASSIFIER_VERSION = 'structural-v1';
16
+ /**
17
+ * The dispatch policy.
18
+ *
19
+ * `naive` is the honest name for what this release does: the first eligible
20
+ * candidate for the structural band, with no estimate consulted. When the model
21
+ * program delivers tables, decisions made under this version stay permanently
22
+ * distinguishable from decisions made under a learned one - which is exactly
23
+ * why the version is on the record rather than in a changelog.
24
+ */
25
+ exports.POLICY_VERSION = 'naive-v1';
26
+ /** The estimator interface. Inert in this release: no tables, no recommendation. */
27
+ exports.ESTIMATOR_VERSION = 'inert-v1';
@@ -0,0 +1,50 @@
1
+ /** One recording tool, as this file needs to drive it. */
2
+ interface Recorder {
3
+ /** What to tell somebody who wants to know what is listening. */
4
+ name: string;
5
+ command: string;
6
+ args: (out: string) => string[];
7
+ }
8
+ /** The first recording tool this machine has, or null. */
9
+ export declare function recorder(): Recorder | null;
10
+ /**
11
+ * What to tell somebody who has no recording tool, or null when they do.
12
+ *
13
+ * One sentence and one command. Not a list of three options with trade-offs:
14
+ * somebody who just wanted to say a sentence out loud is not shopping for audio
15
+ * software.
16
+ */
17
+ export declare function missing(): string | null;
18
+ /** A recording in progress. */
19
+ export interface Recording {
20
+ /** Where the audio is being written. */
21
+ file: string;
22
+ /** What is doing the listening, for the line on screen. */
23
+ by: string;
24
+ /** Stop, wait for the file to be finished properly, and hand it back. */
25
+ stop: () => Promise<{
26
+ file: string;
27
+ bytes: number;
28
+ }>;
29
+ }
30
+ /**
31
+ * Start listening.
32
+ *
33
+ * Throws when there is nothing to listen with, which the caller should have
34
+ * headed off with `missing` so the person gets a sentence rather than an error.
35
+ */
36
+ export declare function start(): Recording;
37
+ /** Delete the recording and the folder holding it. Always, and immediately. */
38
+ export declare function discard(file: string): void;
39
+ /**
40
+ * The words in a recording, according to our server.
41
+ *
42
+ * Every refusal comes back as a sentence somebody can act on, because the
43
+ * commonest one by far is a tenant holding only an Anthropic key, and
44
+ * "transcription failed" would send that person looking at their microphone.
45
+ */
46
+ export declare function transcribe(baseUrl: string, credential: string, file: string): Promise<{
47
+ text: string;
48
+ provider: string;
49
+ }>;
50
+ export {};
@@ -0,0 +1,218 @@
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.recorder = recorder;
37
+ exports.missing = missing;
38
+ exports.start = start;
39
+ exports.discard = discard;
40
+ exports.transcribe = transcribe;
41
+ /**
42
+ * Saying a request instead of typing it.
43
+ *
44
+ * ── The split, and why it is where it is ──────────────────────────────────
45
+ *
46
+ * This file records. It does not transcribe, and it never will: the published
47
+ * package holds no model logic, no vendor address and no provider key, and a
48
+ * ship check enforces that. The recording goes to our server, the server asks a
49
+ * vendor, and the words come back. See `services/transcription.py`.
50
+ *
51
+ * ── Recording without adding a dependency ─────────────────────────────────
52
+ *
53
+ * This package has no dependencies at all, deliberately, and a microphone
54
+ * library would be the first - a compiled one, on every customer's laptop, for
55
+ * a feature most of them will use occasionally. So it drives a recording tool
56
+ * the machine already has.
57
+ *
58
+ * That is an honest trade and it has a cost worth stating: macOS ships no
59
+ * command line recorder, so on a Mac with neither ffmpeg nor sox this does not
60
+ * work. The answer to that is to say so in one sentence naming the one command
61
+ * that fixes it, which is what `missing` returns. What it must never do is fail
62
+ * quietly and leave somebody holding a key wondering whether we heard them.
63
+ *
64
+ * ── Stopping ──────────────────────────────────────────────────────────────
65
+ *
66
+ * Every one of these tools writes a WAV header that is only correct once the
67
+ * recording ends properly, so stopping means an interrupt and then WAITING for
68
+ * the tool to finish writing. Killing it outright leaves a file whose header
69
+ * claims a length it does not have, which every transcriber reads as silence.
70
+ */
71
+ const node_child_process_1 = require("node:child_process");
72
+ const fs = __importStar(require("node:fs"));
73
+ const os = __importStar(require("node:os"));
74
+ const path = __importStar(require("node:path"));
75
+ const gateway_1 = require("./gateway");
76
+ /** Is this on the PATH at all. */
77
+ function present(command) {
78
+ try {
79
+ return (0, node_child_process_1.spawnSync)('which', [command], { stdio: 'ignore' }).status === 0;
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ /**
86
+ * Sixteen kilohertz, one channel, everywhere.
87
+ *
88
+ * What every speech model wants and nothing more. Recording in studio quality
89
+ * would make a file several times the size, take longer to upload, and be
90
+ * downsampled by the vendor on arrival anyway.
91
+ */
92
+ const RATE = '16000';
93
+ const RECORDERS = [
94
+ // sox first. It is the one built for this, it needs no device name, and it
95
+ // stops cleanly.
96
+ { name: 'sox', command: 'rec', args: (out) => ['-q', '-c', '1', '-r', RATE, out] },
97
+ {
98
+ name: 'ffmpeg',
99
+ command: 'ffmpeg',
100
+ args: (out) => [
101
+ '-loglevel', 'quiet', '-nostdin',
102
+ ...(process.platform === 'darwin'
103
+ ? ['-f', 'avfoundation', '-i', ':default']
104
+ : ['-f', 'alsa', '-i', 'default']),
105
+ '-ac', '1', '-ar', RATE, '-y', out,
106
+ ],
107
+ },
108
+ // Linux's own, present on most desktop installs.
109
+ { name: 'arecord', command: 'arecord', args: (out) => ['-q', '-f', 'S16_LE', '-c', '1', '-r', RATE, out] },
110
+ ];
111
+ /** The first recording tool this machine has, or null. */
112
+ function recorder() {
113
+ return RECORDERS.find((r) => present(r.command)) ?? null;
114
+ }
115
+ /**
116
+ * What to tell somebody who has no recording tool, or null when they do.
117
+ *
118
+ * One sentence and one command. Not a list of three options with trade-offs:
119
+ * somebody who just wanted to say a sentence out loud is not shopping for audio
120
+ * software.
121
+ */
122
+ function missing() {
123
+ if (recorder())
124
+ return null;
125
+ return process.platform === 'darwin'
126
+ ? 'Speaking needs a recording tool, and macOS does not come with one. '
127
+ + 'Install it once with: brew install sox'
128
+ : 'Speaking needs a recording tool. Install it once with: '
129
+ + 'sudo apt install sox (or your system\'s package manager)';
130
+ }
131
+ /**
132
+ * Start listening.
133
+ *
134
+ * Throws when there is nothing to listen with, which the caller should have
135
+ * headed off with `missing` so the person gets a sentence rather than an error.
136
+ */
137
+ function start() {
138
+ const tool = recorder();
139
+ if (!tool)
140
+ throw new Error(missing() ?? 'No recording tool.');
141
+ const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'comprism-voice-')), 'said.wav');
142
+ const child = (0, node_child_process_1.spawn)(tool.command, tool.args(file), {
143
+ stdio: ['ignore', 'ignore', 'ignore'],
144
+ });
145
+ let stopped = null;
146
+ const stop = () => {
147
+ if (stopped)
148
+ return stopped;
149
+ stopped = new Promise((resolve) => {
150
+ const done = () => {
151
+ let bytes = 0;
152
+ try {
153
+ bytes = fs.statSync(file).size;
154
+ }
155
+ catch {
156
+ bytes = 0;
157
+ }
158
+ resolve({ file, bytes });
159
+ };
160
+ // Interrupt, not kill. Every one of these tools finishes writing the WAV
161
+ // header on an interrupt and none of them does on a kill, and a header
162
+ // that was never finished reads as silence to whoever transcribes it.
163
+ child.once('close', done);
164
+ try {
165
+ child.kill('SIGINT');
166
+ }
167
+ catch {
168
+ done();
169
+ }
170
+ // A tool that has not closed in two seconds is not going to. Taking the
171
+ // file as it stands beats leaving somebody at a prompt that has stopped
172
+ // answering them.
173
+ setTimeout(() => {
174
+ try {
175
+ child.kill('SIGKILL');
176
+ }
177
+ catch { /* already gone */ }
178
+ done();
179
+ }, 2000).unref();
180
+ });
181
+ return stopped;
182
+ };
183
+ return { file, by: tool.name, stop };
184
+ }
185
+ /** Delete the recording and the folder holding it. Always, and immediately. */
186
+ function discard(file) {
187
+ try {
188
+ fs.rmSync(path.dirname(file), { recursive: true, force: true });
189
+ }
190
+ catch { /* a temp file we could not remove is not worth a word to anybody */ }
191
+ }
192
+ /**
193
+ * The words in a recording, according to our server.
194
+ *
195
+ * Every refusal comes back as a sentence somebody can act on, because the
196
+ * commonest one by far is a tenant holding only an Anthropic key, and
197
+ * "transcription failed" would send that person looking at their microphone.
198
+ */
199
+ async function transcribe(baseUrl, credential, file) {
200
+ const form = new FormData();
201
+ form.append('audio', new Blob([fs.readFileSync(file)], { type: 'audio/wav' }), 'said.wav');
202
+ // Permission first, from the one service, which also names the address. The
203
+ // recording itself does not travel through that door: a JSON request is the
204
+ // wrong shape for audio. The decision is made there and nowhere else.
205
+ const granted = await (0, gateway_1.ask)({ intent: 'transcribe', prompt: '' });
206
+ const pass = granted.ok && granted.served !== false ? granted.upload : null;
207
+ if (granted.ok && granted.served !== false && !pass) {
208
+ throw new Error(granted.message || 'your workspace would not allow that recording');
209
+ }
210
+ const where = pass?.upload_path || '/api/v1/agent/transcribe';
211
+ const res = await fetch(`${baseUrl.replace(/\/+$/, '')}${where}${pass?.ticket ? `?ticket=${encodeURIComponent(pass.ticket)}` : ''}`, { method: 'POST', headers: { Authorization: `Bearer ${credential}` }, body: form });
212
+ if (!res.ok) {
213
+ const body = await res.json().catch(() => ({}));
214
+ throw new Error(body.detail || `The server said ${res.status}.`);
215
+ }
216
+ const row = await res.json();
217
+ return { text: row.text, provider: row.provider };
218
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * Runs on `npm install -g @illuminis/comprism`. Rule R1.
6
+ *
7
+ * The whole point is that the user types one command and is finished. So the
8
+ * install configures interception itself rather than printing instructions.
9
+ *
10
+ * Three things make that safe to do to somebody's machine automatically:
11
+ *
12
+ * - It is fenced and reversible. One marked block in the shell profile, and
13
+ * `comprism uninstall` removes exactly it.
14
+ * - It says out loud what it touched. No silent edits.
15
+ * - It cannot break anything. The script it installs exports nothing unless a
16
+ * recorder is actually answering, so a broken or missing proxy leaves every
17
+ * AI tool on the machine talking straight to its provider, as before.
18
+ *
19
+ * And it never fails the install: `|| true` in the npm script, plus this file
20
+ * swallowing its own errors. A package that fails to install because its
21
+ * telemetry could not configure itself deserves to be uninstalled.
22
+ */
23
+ const install_1 = require("./commands/install");
24
+ const readiness_1 = require("./lib/readiness");
25
+ async function main() {
26
+ // Local development installs (`npm install` inside the repo) should not touch
27
+ // the developer's shell profile. Only a global install means "set me up".
28
+ const isGlobal = process.env.npm_config_global === 'true';
29
+ if (!isGlobal)
30
+ return;
31
+ try {
32
+ const report = (0, install_1.install)();
33
+ // Claude Desktop is not something this script can finish. A remote
34
+ // connector is authorized by a consent screen, so the honest output is the
35
+ // address to paste - never a claim that it is live.
36
+ //
37
+ // This path is the one that used to get it wrong twice over: npm runs this
38
+ // script, so anything derived from the running file is derived from
39
+ // POSTINSTALL rather than the CLI, and the entry it wrote looked perfectly
40
+ // reasonable in the config file while being a server that could only
41
+ // observe. It reported "registered and answering" and the user then saw no
42
+ // receipt and an empty Sessions screen, with nothing anywhere saying why.
43
+ const desktopLines = [];
44
+ if (report.desktop.staleRemoved) {
45
+ desktopLines.push(' · removed an old CompletionPrism entry from Claude Desktop (it could not answer)');
46
+ }
47
+ if (report.desktop.note) {
48
+ desktopLines.push(` · ${report.desktop.note}`);
49
+ }
50
+ else if (report.desktop.present && report.baseUrl) {
51
+ desktopLines.push(` · Claude Desktop: add ${report.baseUrl}/mcp under Settings > Connectors`);
52
+ }
53
+ process.stdout.write([
54
+ '',
55
+ ' CompletionPrism is set up.',
56
+ ` · interception configured against ${report.baseUrl || 'no tenant yet - run `comprism connect`'}`,
57
+ ...report.profilesTouched.map((p) => ` · added one line to ${p}`),
58
+ ...report.profilesAlreadyHad.map((p) => ` · already configured in ${p}`),
59
+ ...desktopLines,
60
+ '',
61
+ ' Open a new terminal and just work:',
62
+ ' claude your tools, instrumented, unchanged',
63
+ ' comprism our own session',
64
+ ' comprism report what finishing your work cost',
65
+ '',
66
+ ' If our recorder is ever down your tools are unaffected. Undo with `comprism uninstall`.',
67
+ '',
68
+ ].join('\n'));
69
+ // What this machine still needs, said at the only moment somebody is
70
+ // thinking about setup. Finding out three days later, mid job, that a PDF
71
+ // cannot be read is the same information delivered at the worst possible
72
+ // time. Said only when something IS missing: a list of things that are
73
+ // already fine is noise on a screen somebody is about to close.
74
+ const machine = (0, readiness_1.check)();
75
+ if (!machine.ready) {
76
+ process.stdout.write([
77
+ ' Two things the work itself may need on this machine:',
78
+ ...machine.lines,
79
+ '',
80
+ ` To add what is missing: ${machine.fix}`,
81
+ ' Everything else, including Word, PowerPoint and PDF documents, is',
82
+ ' built on our servers and needs nothing installed here.',
83
+ '',
84
+ ].join('\n'));
85
+ }
86
+ }
87
+ catch {
88
+ // Never fail an install over setup. The user can run `comprism install`.
89
+ process.stdout.write('\n CompletionPrism installed. Run `comprism install` to finish setup.\n\n');
90
+ }
91
+ }
92
+ void main();