@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.
- package/out/agent/render.d.ts +24 -0
- package/out/agent/render.js +35 -3
- package/out/agent/session.js +9 -0
- package/out/commands/decisionEngine.d.ts +1 -0
- package/out/commands/decisionEngine.js +66 -0
- package/out/lib/gateway.d.ts +2 -2
- package/out/lib/voice.d.ts +2 -39
- package/out/lib/voice.js +6 -147
- package/out/machine/voice/record.d.ts +38 -0
- package/out/machine/voice/record.js +208 -0
- package/out/thin.js +7 -0
- package/package.json +1 -1
package/out/agent/render.d.ts
CHANGED
|
@@ -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.
|
package/out/agent/render.js
CHANGED
|
@@ -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
|
|
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) {
|
package/out/agent/session.js
CHANGED
|
@@ -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
|
+
}
|
package/out/lib/gateway.d.ts
CHANGED
|
@@ -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;
|
package/out/lib/voice.d.ts
CHANGED
|
@@ -1,39 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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 =
|
|
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
|
-
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
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,
|