@illuminis/comprism 0.1.6 → 0.1.8

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.
@@ -113,6 +113,30 @@ export interface ReceiptCard {
113
113
  details?: string | null;
114
114
  badge: string | null;
115
115
  warning: string | null;
116
+ /** Where the saving came from, worded by the server for every client. */
117
+ savings?: ReceiptSavings | null;
118
+ /** The decision engine's itemized cost, named by the server (web manual 11.4). */
119
+ engine?: {
120
+ label: string;
121
+ name: string;
122
+ value: string;
123
+ line: string;
124
+ } | null;
125
+ }
126
+ /** The receipt's savings table: headings, one row per cost factor, the total. */
127
+ export interface ReceiptSavings {
128
+ columns: string[];
129
+ rows: Array<{
130
+ key: string;
131
+ label: string;
132
+ user: string;
133
+ chosen: string;
134
+ saving: string;
135
+ }>;
136
+ total: {
137
+ label: string;
138
+ value: string;
139
+ };
116
140
  }
117
141
  /**
118
142
  * The receipt under a finished job.
@@ -238,8 +238,10 @@ function ending(line, detail) {
238
238
  }
239
239
  /** How the job ended, in the words a person reads. */
240
240
  function outcome(o, detail) {
241
+ // A clean finish prints no word: the answer above says what was done.
242
+ if (o === "finished")
243
+ return detail ? `\n${(0, exports.dim)(detail)}\n` : "";
241
244
  const word = {
242
- finished: (0, exports.green)("Done"),
243
245
  cancelled: (0, exports.yellow)("Stopped"),
244
246
  step_limit: (0, exports.yellow)("Stopped after too many steps"),
245
247
  spend_limit: (0, exports.yellow)("Stopped at the spending limit"),
@@ -276,6 +278,33 @@ function savedFile(name, fullPath, bytes) {
276
278
  return `${(0, exports.green)(" saved ")}${link(name, href)}${(0, exports.dim)(` ${bytes} bytes`)}\n`
277
279
  + `${(0, exports.gray)(` ${fullPath}`)}\n`;
278
280
  }
281
+ /**
282
+ * The savings table as aligned columns: names left, money right, the saving
283
+ * column bright and the rest quiet. Every word and figure is the server's.
284
+ * Too narrow for the headings, it keeps the rows and drops the heading line
285
+ * rather than cutting a word.
286
+ */
287
+ function savingsRows(savings, room) {
288
+ if (!savings || !savings.rows.length)
289
+ return [];
290
+ const body = savings.rows.map((r) => [r.label, r.user, r.chosen, r.saving]);
291
+ const [c0 = "", c1 = "", c2 = "", c3 = ""] = savings.columns;
292
+ const heading = [c0, c1, c2, c3];
293
+ const fit = (rows) => [0, 1, 2, 3].map((i) => " ".repeat(Math.max(...rows.map((r) => r[i].length))));
294
+ let w = fit([...body, heading]).map((x) => x.length);
295
+ const head = w.reduce((a, b) => a + b, 0) + 6 <= room;
296
+ if (!head)
297
+ w = fit(body).map((x) => x.length);
298
+ const [w0 = 0, w1 = 0, w2 = 0, w3 = 0] = w;
299
+ const left = (r) => `${r[0].padEnd(w0)} ${r[1].padStart(w1)} ${r[2].padStart(w2)}`;
300
+ const out = [""];
301
+ if (head)
302
+ out.push((0, exports.dim)(`${left(heading)} ${heading[3].padStart(w3)}`));
303
+ for (const r of body)
304
+ out.push(`${(0, exports.dim)(left(r))} ${(0, exports.bold)(r[3].padStart(w3))}`);
305
+ out.push((0, exports.bold)(`${savings.total.label.padEnd(w0 + w1 + w2 + 4)} ${savings.total.value.padStart(w3)}`));
306
+ return out;
307
+ }
279
308
  /** The card's two lines, as the approved look draws them. */
280
309
  function cardRows(card) {
281
310
  const rows = [];
@@ -285,6 +314,8 @@ function cardRows(card) {
285
314
  rows.push(head);
286
315
  if (card.amount)
287
316
  rows.push(card.saved ? (0, exports.bold)((0, exports.green)(card.amount)) : (0, exports.dim)(card.amount));
317
+ if (card.engine)
318
+ rows.push((0, exports.dim)(card.engine.line));
288
319
  if (card.warning)
289
320
  rows.push((0, exports.dim)(card.warning));
290
321
  return rows;
@@ -306,7 +337,8 @@ function cardRows(card) {
306
337
  * formatting, which belongs to the client, and it is the only part that does.
307
338
  */
308
339
  function receipt(text, models = [], testsPassed, card) {
309
- const lines = card ? cardRows(card).map((r) => ` ${r}`) : [];
340
+ const lines = card
341
+ ? [...cardRows(card), ...savingsRows(card.savings, width() - 4)].map((r) => ` ${r}`) : [];
310
342
  for (const line of card ? [] : String(text || "").split("\n")) {
311
343
  if (!line.trim())
312
344
  continue;
@@ -370,7 +402,7 @@ function wrap(text, room) {
370
402
  function boxed(text, models, testsPassed, card) {
371
403
  // Room for the border, the padding and the `receipt` label on the first line.
372
404
  const inner = width() - 16;
373
- const rows = card ? cardRows(card) : [];
405
+ const rows = card ? [...cardRows(card), ...savingsRows(card.savings, inner)] : [];
374
406
  const links = [];
375
407
  for (const raw of String(text || "").split("\n")) {
376
408
  if (card) {
@@ -1010,6 +1010,11 @@ class TerminalSession {
1010
1010
  }
1011
1011
  case "action_finished":
1012
1012
  this.flushScreen();
1013
+ // A step that failed is the agent's to read and retry, not the
1014
+ // person's: it is not printed, outside the full view (6.9) that a
1015
+ // person turns on to see everything (owner, 7 October 2026).
1016
+ if (frame.ok === false && !this.full)
1017
+ break;
1013
1018
  this.out(ui.action(String(frame.name ?? ""), this.targetOf(frame), frame.ok !== false, frame.latency_ms) + "\n");
1014
1019
  // The change itself, as it is made, unless it was just shown for
1015
1020
  // approval (manual 5.6 to 5.8). A change that changed nothing has
@@ -1030,6 +1035,10 @@ class TerminalSession {
1030
1035
  }
1031
1036
  break;
1032
1037
  case "action_refused":
1038
+ // The same: the agent is told why and carries on. Only the full view
1039
+ // shows it.
1040
+ if (!this.full)
1041
+ break;
1033
1042
  this.out(ui.red(` refused ${String(frame.name ?? "")}`)
1034
1043
  + ui.dim(` ${String(frame.reason ?? "")}\n`));
1035
1044
  break;
@@ -0,0 +1 @@
1
+ export declare function cmdDecisionEngine(args: string[]): Promise<number>;
@@ -0,0 +1,66 @@
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.cmdDecisionEngine = cmdDecisionEngine;
37
+ /**
38
+ * `comprism decision-engine [none | <engine>]` (web manual 11.4): whether your
39
+ * work uses the decision engine your company has turned on. With no argument it
40
+ * shows your pick; with one it changes it. The service keeps the pick and words
41
+ * every line, so this matches the drop-down in Work and in VS Code.
42
+ */
43
+ const gateway_1 = require("../lib/gateway");
44
+ const ui = __importStar(require("../lib/ui"));
45
+ const out = (s = "") => process.stdout.write(s + "\n");
46
+ async function cmdDecisionEngine(args) {
47
+ const choice = args[0];
48
+ const reply = await (0, gateway_1.ask)({
49
+ intent: "decision_engine", prompt: "",
50
+ op: choice ? "set" : "show",
51
+ ...(choice ? { payload: { engine: choice === "none" ? null : choice } } : {}),
52
+ });
53
+ if (!reply.ok || reply.served === false) {
54
+ out(ui.fail(reply.served === false
55
+ ? "Your organization is running an older build that does not answer this yet."
56
+ : `Your organization could not be reached: ${reply.message || "no answer"}`));
57
+ return 1;
58
+ }
59
+ out();
60
+ if (reply.message)
61
+ out(ui.fail(reply.message));
62
+ for (const line of reply.lines ?? [])
63
+ out(line);
64
+ out();
65
+ return reply.message ? 1 : 0;
66
+ }
@@ -62,7 +62,7 @@ export interface SpendReport {
62
62
  export interface GatewayReply {
63
63
  /** Local transport metadata, not an authentication decision. */
64
64
  httpStatus?: number;
65
- intent: 'decide' | 'answer' | 'catalog' | 'store_key' | 'session' | 'job_start' | 'resolve_workspace' | 'sign_in' | 'sign_out' | 'attach' | 'transcribe' | 'graph' | 'spend' | 'project' | 'commands' | 'transcript' | 'context' | 'compact' | 'sessions' | 'permissions' | 'jobs' | 'agents' | 'integrations' | 'skills' | 'plugins' | 'schema' | 'usage' | 'instructions' | 'config' | 'memory' | 'worktrees' | 'privacy' | 'hooks' | 'queue' | 'review' | 'release' | 'command';
65
+ intent: 'decide' | 'answer' | 'catalog' | 'store_key' | 'session' | 'job_start' | 'resolve_workspace' | 'sign_in' | 'sign_out' | 'attach' | 'transcribe' | 'graph' | 'spend' | 'project' | 'commands' | 'transcript' | 'context' | 'compact' | 'sessions' | 'permissions' | 'jobs' | 'agents' | 'integrations' | 'skills' | 'plugins' | 'schema' | 'usage' | 'instructions' | 'config' | 'memory' | 'worktrees' | 'privacy' | 'hooks' | 'queue' | 'review' | 'release' | 'decision_engine' | 'command';
66
66
  client: string;
67
67
  decision: Decision;
68
68
  answer: string | null;
@@ -342,7 +342,7 @@ export interface SessionOut {
342
342
  can_write: boolean;
343
343
  }
344
344
  export interface Ask {
345
- intent: 'decide' | 'answer' | 'catalog' | 'store_key' | 'session' | 'job_start' | 'resolve_workspace' | 'sign_in' | 'sign_out' | 'attach' | 'transcribe' | 'graph' | 'spend' | 'project' | 'commands' | 'transcript' | 'context' | 'compact' | 'sessions' | 'permissions' | 'jobs' | 'agents' | 'integrations' | 'skills' | 'plugins' | 'schema' | 'usage' | 'instructions' | 'config' | 'memory' | 'worktrees' | 'privacy' | 'hooks' | 'queue' | 'review' | 'release' | 'command';
345
+ intent: 'decide' | 'answer' | 'catalog' | 'store_key' | 'session' | 'job_start' | 'resolve_workspace' | 'sign_in' | 'sign_out' | 'attach' | 'transcribe' | 'graph' | 'spend' | 'project' | 'commands' | 'transcript' | 'context' | 'compact' | 'sessions' | 'permissions' | 'jobs' | 'agents' | 'integrations' | 'skills' | 'plugins' | 'schema' | 'usage' | 'instructions' | 'config' | 'memory' | 'worktrees' | 'privacy' | 'hooks' | 'queue' | 'review' | 'release' | 'decision_engine' | 'command';
346
346
  prompt: string;
347
347
  callingModel?: string;
348
348
  contextTokens?: number;
@@ -1,39 +1,5 @@
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;
1
+ export { discard, missing, recorder, start } from '../machine/voice/record';
2
+ export type { Recorder, Recording } from '../machine/voice/record';
37
3
  /** Live dictation in progress, on the Mac's own recogniser. */
38
4
  export interface LiveDictation {
39
5
  /** The loudness now, 0 to 1. */
@@ -70,8 +36,6 @@ export declare function level(file: string): number;
70
36
  * actually there. Returns null until there is at least half a second to send.
71
37
  */
72
38
  export declare function soFar(file: string): string | null;
73
- /** Delete the recording and the folder holding it. Always, and immediately. */
74
- export declare function discard(file: string): void;
75
39
  /**
76
40
  * The words in a recording, according to our server.
77
41
  *
@@ -83,4 +47,3 @@ export declare function transcribe(baseUrl: string, credential: string, file: st
83
47
  text: string;
84
48
  provider: string;
85
49
  }>;
86
- export {};
package/out/lib/voice.js CHANGED
@@ -33,13 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.recorder = recorder;
37
- exports.missing = missing;
38
- exports.start = start;
36
+ exports.start = exports.recorder = exports.missing = exports.discard = void 0;
39
37
  exports.listenLive = listenLive;
40
38
  exports.level = level;
41
39
  exports.soFar = soFar;
42
- exports.discard = discard;
43
40
  exports.transcribe = transcribe;
44
41
  /**
45
42
  * Saying a request instead of typing it.
@@ -73,144 +70,13 @@ exports.transcribe = transcribe;
73
70
  */
74
71
  const node_child_process_1 = require("node:child_process");
75
72
  const fs = __importStar(require("node:fs"));
76
- const os = __importStar(require("node:os"));
77
73
  const path = __importStar(require("node:path"));
78
74
  const gateway_1 = require("./gateway");
79
- /** Is this on the PATH at all. */
80
- function present(command) {
81
- try {
82
- return (0, node_child_process_1.spawnSync)('which', [command], { stdio: 'ignore' }).status === 0;
83
- }
84
- catch {
85
- return false;
86
- }
87
- }
88
- /**
89
- * Sixteen kilohertz, one channel, everywhere.
90
- *
91
- * What every speech model wants and nothing more. Recording in studio quality
92
- * would make a file several times the size, take longer to upload, and be
93
- * downsampled by the vendor on arrival anyway.
94
- */
95
- const RATE = '16000';
96
- const RECORDERS = [
97
- // sox first. It is the one built for this, it needs no device name, and it
98
- // stops cleanly.
99
- { name: 'sox', command: 'rec', args: (out) => ['-q', '-c', '1', '-r', RATE, out] },
100
- {
101
- name: 'ffmpeg',
102
- command: 'ffmpeg',
103
- args: (out) => [
104
- '-loglevel', 'quiet', '-nostdin',
105
- ...(process.platform === 'darwin'
106
- ? ['-f', 'avfoundation', '-i', ':default']
107
- : ['-f', 'alsa', '-i', 'default']),
108
- // Written as it is heard, not held until the end, so the meter and the
109
- // words so far have something to read while the person is speaking.
110
- '-ac', '1', '-ar', RATE, '-flush_packets', '1', '-y', out,
111
- ],
112
- },
113
- // Linux's own, present on most desktop installs.
114
- { name: 'arecord', command: 'arecord', args: (out) => ['-q', '-f', 'S16_LE', '-c', '1', '-r', RATE, out] },
115
- ];
116
- /** The first recording tool this machine has, or null. */
117
- function recorder() {
118
- return RECORDERS.find((r) => present(r.command)) ?? null;
119
- }
120
- /**
121
- * What to tell somebody who has no recording tool, or null when they do.
122
- *
123
- * One sentence and one command. Not a list of three options with trade-offs:
124
- * somebody who just wanted to say a sentence out loud is not shopping for audio
125
- * software.
126
- */
127
- function missing() {
128
- if (recorder())
129
- return null;
130
- return process.platform === 'darwin'
131
- ? 'Speaking needs a recording tool, and macOS does not come with one. '
132
- + 'Install it once with: brew install sox'
133
- : 'Speaking needs a recording tool. Install it once with: '
134
- + 'sudo apt install sox (or your system\'s package manager)';
135
- }
136
- /**
137
- * Start listening.
138
- *
139
- * Throws when there is nothing to listen with, which the caller should have
140
- * headed off with `missing` so the person gets a sentence rather than an error.
141
- */
142
- function start() {
143
- const tool = recorder();
144
- if (!tool)
145
- throw new Error(missing() ?? 'No recording tool.');
146
- const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'comprism-voice-')), 'said.wav');
147
- const child = (0, node_child_process_1.spawn)(tool.command, tool.args(file), {
148
- stdio: ['ignore', 'ignore', 'ignore'],
149
- });
150
- let stopped = null;
151
- // The recorder never outlives the session. Closing the window or ending the
152
- // process while it listens would otherwise leave a microphone recording to a
153
- // file nobody knows about, the worst thing this tool could do.
154
- const abandon = () => {
155
- try {
156
- child.kill('SIGKILL');
157
- }
158
- catch { /* already gone */ }
159
- discard(file);
160
- };
161
- const onSignal = (signal) => {
162
- abandon();
163
- release();
164
- process.kill(process.pid, signal);
165
- };
166
- const release = () => {
167
- process.off('exit', abandon);
168
- process.off('SIGHUP', onSignal);
169
- process.off('SIGTERM', onSignal);
170
- };
171
- process.once('exit', abandon);
172
- process.once('SIGHUP', onSignal);
173
- process.once('SIGTERM', onSignal);
174
- child.once('close', release);
175
- const stop = () => {
176
- if (stopped)
177
- return stopped;
178
- stopped = new Promise((resolve) => {
179
- const done = () => {
180
- let bytes = 0;
181
- try {
182
- bytes = fs.statSync(file).size;
183
- }
184
- catch {
185
- bytes = 0;
186
- }
187
- resolve({ file, bytes });
188
- };
189
- // Interrupt, not kill. Every one of these tools finishes writing the WAV
190
- // header on an interrupt and none of them does on a kill, and a header
191
- // that was never finished reads as silence to whoever transcribes it.
192
- child.once('close', done);
193
- try {
194
- child.kill('SIGINT');
195
- }
196
- catch {
197
- done();
198
- }
199
- // A tool that has not closed in two seconds is not going to. Taking the
200
- // file as it stands beats leaving somebody at a prompt that has stopped
201
- // answering them.
202
- setTimeout(() => {
203
- try {
204
- child.kill('SIGKILL');
205
- }
206
- catch { /* already gone */ }
207
- done();
208
- }, 2000).unref();
209
- });
210
- return stopped;
211
- };
212
- return { file, by: tool.name, stop };
213
- }
75
+ var record_1 = require("../machine/voice/record");
76
+ Object.defineProperty(exports, "discard", { enumerable: true, get: function () { return record_1.discard; } });
77
+ Object.defineProperty(exports, "missing", { enumerable: true, get: function () { return record_1.missing; } });
78
+ Object.defineProperty(exports, "recorder", { enumerable: true, get: function () { return record_1.recorder; } });
79
+ Object.defineProperty(exports, "start", { enumerable: true, get: function () { return record_1.start; } });
214
80
  /** The helper built beside this file on a Mac, when there is one. */
215
81
  function helper() {
216
82
  if (process.platform !== 'darwin')
@@ -341,13 +207,6 @@ function soFar(file) {
341
207
  return null;
342
208
  }
343
209
  }
344
- /** Delete the recording and the folder holding it. Always, and immediately. */
345
- function discard(file) {
346
- try {
347
- fs.rmSync(path.dirname(file), { recursive: true, force: true });
348
- }
349
- catch { /* a temp file we could not remove is not worth a word to anybody */ }
350
- }
351
210
  /**
352
211
  * The words in a recording, according to our server.
353
212
  *
@@ -0,0 +1,38 @@
1
+ /** One recording tool, as this file needs to drive it. */
2
+ export 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;
@@ -0,0 +1,208 @@
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
+ /**
41
+ * Recording what a person says, on their own computer, for every client that
42
+ * offers a microphone: the command line and the editor extension.
43
+ *
44
+ * Physical work on the person's machine, so it lives here once rather than in
45
+ * each client (rule 17). Nothing here transcribes: the recording goes to our
46
+ * server, which asks a vendor, and the words come back. See the terminal's
47
+ * `lib/voice.ts` and the service's `transcription.py`.
48
+ *
49
+ * ── Recording without adding a dependency ─────────────────────────────────
50
+ *
51
+ * This drives a recording tool the machine already has rather than a compiled
52
+ * microphone library on every customer's laptop. macOS ships no command line
53
+ * recorder, so on a Mac with neither ffmpeg nor sox this does not work, and
54
+ * `missing` says so in one sentence naming the one command that fixes it.
55
+ *
56
+ * ── Stopping ──────────────────────────────────────────────────────────────
57
+ *
58
+ * Every one of these tools writes a WAV header that is only correct once the
59
+ * recording ends properly, so stopping means an interrupt and then WAITING for
60
+ * the tool to finish writing. Killing it outright leaves a file whose header
61
+ * claims a length it does not have, which every transcriber reads as silence.
62
+ */
63
+ const node_child_process_1 = require("node:child_process");
64
+ const fs = __importStar(require("node:fs"));
65
+ const os = __importStar(require("node:os"));
66
+ const path = __importStar(require("node:path"));
67
+ /** Is this on the PATH at all. */
68
+ function present(command) {
69
+ try {
70
+ return (0, node_child_process_1.spawnSync)('which', [command], { stdio: 'ignore' }).status === 0;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /**
77
+ * Sixteen kilohertz, one channel, everywhere.
78
+ *
79
+ * What every speech model wants and nothing more. Recording in studio quality
80
+ * would make a file several times the size, take longer to upload, and be
81
+ * downsampled by the vendor on arrival anyway.
82
+ */
83
+ const RATE = '16000';
84
+ const RECORDERS = [
85
+ // sox first. It is the one built for this, it needs no device name, and it
86
+ // stops cleanly.
87
+ { name: 'sox', command: 'rec', args: (out) => ['-q', '-c', '1', '-r', RATE, out] },
88
+ {
89
+ name: 'ffmpeg',
90
+ command: 'ffmpeg',
91
+ args: (out) => [
92
+ '-loglevel', 'quiet', '-nostdin',
93
+ ...(process.platform === 'darwin'
94
+ ? ['-f', 'avfoundation', '-i', ':default']
95
+ : ['-f', 'alsa', '-i', 'default']),
96
+ // Written as it is heard, not held until the end, so the meter and the
97
+ // words so far have something to read while the person is speaking.
98
+ '-ac', '1', '-ar', RATE, '-flush_packets', '1', '-y', out,
99
+ ],
100
+ },
101
+ // Linux's own, present on most desktop installs.
102
+ { name: 'arecord', command: 'arecord', args: (out) => ['-q', '-f', 'S16_LE', '-c', '1', '-r', RATE, out] },
103
+ ];
104
+ /** The first recording tool this machine has, or null. */
105
+ function recorder() {
106
+ return RECORDERS.find((r) => present(r.command)) ?? null;
107
+ }
108
+ /**
109
+ * What to tell somebody who has no recording tool, or null when they do.
110
+ *
111
+ * One sentence and one command. Not a list of three options with trade-offs:
112
+ * somebody who just wanted to say a sentence out loud is not shopping for audio
113
+ * software.
114
+ */
115
+ function missing() {
116
+ if (recorder())
117
+ return null;
118
+ return process.platform === 'darwin'
119
+ ? 'Speaking needs a recording tool, and macOS does not come with one. '
120
+ + 'Install it once with: brew install sox'
121
+ : 'Speaking needs a recording tool. Install it once with: '
122
+ + 'sudo apt install sox (or your system\'s package manager)';
123
+ }
124
+ /**
125
+ * Start listening.
126
+ *
127
+ * Throws when there is nothing to listen with, which the caller should have
128
+ * headed off with `missing` so the person gets a sentence rather than an error.
129
+ */
130
+ function start() {
131
+ const tool = recorder();
132
+ if (!tool)
133
+ throw new Error(missing() ?? 'No recording tool.');
134
+ const file = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'comprism-voice-')), 'said.wav');
135
+ const child = (0, node_child_process_1.spawn)(tool.command, tool.args(file), {
136
+ stdio: ['ignore', 'ignore', 'ignore'],
137
+ });
138
+ let stopped = null;
139
+ // The recorder never outlives the session. Closing the window or ending the
140
+ // process while it listens would otherwise leave a microphone recording to a
141
+ // file nobody knows about, the worst thing this tool could do.
142
+ const abandon = () => {
143
+ try {
144
+ child.kill('SIGKILL');
145
+ }
146
+ catch { /* already gone */ }
147
+ discard(file);
148
+ };
149
+ const onSignal = (signal) => {
150
+ abandon();
151
+ release();
152
+ process.kill(process.pid, signal);
153
+ };
154
+ const release = () => {
155
+ process.off('exit', abandon);
156
+ process.off('SIGHUP', onSignal);
157
+ process.off('SIGTERM', onSignal);
158
+ };
159
+ process.once('exit', abandon);
160
+ process.once('SIGHUP', onSignal);
161
+ process.once('SIGTERM', onSignal);
162
+ child.once('close', release);
163
+ const stop = () => {
164
+ if (stopped)
165
+ return stopped;
166
+ stopped = new Promise((resolve) => {
167
+ const done = () => {
168
+ let bytes = 0;
169
+ try {
170
+ bytes = fs.statSync(file).size;
171
+ }
172
+ catch {
173
+ bytes = 0;
174
+ }
175
+ resolve({ file, bytes });
176
+ };
177
+ // Interrupt, not kill. Every one of these tools finishes writing the WAV
178
+ // header on an interrupt and none of them does on a kill, and a header
179
+ // that was never finished reads as silence to whoever transcribes it.
180
+ child.once('close', done);
181
+ try {
182
+ child.kill('SIGINT');
183
+ }
184
+ catch {
185
+ done();
186
+ }
187
+ // A tool that has not closed in two seconds is not going to. Taking the
188
+ // file as it stands beats leaving somebody at a prompt that has stopped
189
+ // answering them.
190
+ setTimeout(() => {
191
+ try {
192
+ child.kill('SIGKILL');
193
+ }
194
+ catch { /* already gone */ }
195
+ done();
196
+ }, 2000).unref();
197
+ });
198
+ return stopped;
199
+ };
200
+ return { file, by: tool.name, stop };
201
+ }
202
+ /** Delete the recording and the folder holding it. Always, and immediately. */
203
+ function discard(file) {
204
+ try {
205
+ fs.rmSync(path.dirname(file), { recursive: true, force: true });
206
+ }
207
+ catch { /* a temp file we could not remove is not worth a word to anybody */ }
208
+ }
package/out/thin.js CHANGED
@@ -134,6 +134,7 @@ function usage() {
134
134
  ['comprism plugins', 'packs of skills, helpers, hooks and connections'],
135
135
  ['comprism worktrees', 'separate copies of the repository'],
136
136
  ['comprism privacy', 'what your company keeps about your work'],
137
+ ['comprism decision-engine', 'whether your work uses a decision engine; add none or jev to change it'],
137
138
  ['comprism permissions', 'the rules in force here; permissions why <action> "<target>"'],
138
139
  ['comprism status', 'is this machine signed in, and to what'],
139
140
  ['comprism check', 'what this machine still needs, and how to add it'],
@@ -308,6 +309,12 @@ async function main() {
308
309
  process.exitCode = await cmdPrivacy();
309
310
  return;
310
311
  }
312
+ // Whether your work uses a decision engine (web manual 11.4).
313
+ case 'decision-engine': {
314
+ const { cmdDecisionEngine } = await Promise.resolve().then(() => __importStar(require('./commands/decisionEngine')));
315
+ process.exitCode = await cmdDecisionEngine(args);
316
+ return;
317
+ }
311
318
  // Separate copies of the repository (manual 2.11).
312
319
  case 'worktrees': {
313
320
  const { cmdWorktrees } = await Promise.resolve().then(() => __importStar(require('./commands/worktrees')));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@illuminis/comprism",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "CompletionPrism Companion. Records what finishing a piece of work actually costs - every attempt, and the human time spent noticing and repairing the ones that failed.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,