@illuminis/comprism 0.1.3 → 0.1.4

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 (131) hide show
  1. package/README.md +61 -2
  2. package/out/agent/command.d.ts +75 -4
  3. package/out/agent/command.js +220 -25
  4. package/out/agent/render.d.ts +17 -2
  5. package/out/agent/render.js +157 -14
  6. package/out/agent/session.d.ts +141 -2
  7. package/out/agent/session.js +735 -149
  8. package/out/commands/agents.d.ts +2 -0
  9. package/out/commands/agents.js +79 -0
  10. package/out/commands/ask.d.ts +1 -1
  11. package/out/commands/ask.js +78 -11
  12. package/out/commands/commands-thin.js +79 -17
  13. package/out/commands/config.d.ts +1 -0
  14. package/out/commands/config.js +138 -0
  15. package/out/commands/cost.d.ts +1 -0
  16. package/out/commands/cost.js +167 -0
  17. package/out/commands/hooks.d.ts +1 -0
  18. package/out/commands/hooks.js +83 -0
  19. package/out/commands/install.d.ts +44 -1
  20. package/out/commands/install.js +198 -4
  21. package/out/commands/instructions.d.ts +1 -0
  22. package/out/commands/instructions.js +113 -0
  23. package/out/commands/integrations.d.ts +3 -0
  24. package/out/commands/integrations.js +215 -0
  25. package/out/commands/jobs.d.ts +5 -0
  26. package/out/commands/jobs.js +157 -0
  27. package/out/commands/login.js +188 -36
  28. package/out/commands/memory.d.ts +3 -0
  29. package/out/commands/memory.js +113 -0
  30. package/out/commands/permissions.d.ts +1 -0
  31. package/out/commands/permissions.js +94 -0
  32. package/out/commands/plugins.d.ts +4 -0
  33. package/out/commands/plugins.js +192 -0
  34. package/out/commands/privacy.d.ts +1 -0
  35. package/out/commands/privacy.js +57 -0
  36. package/out/commands/providerKey.d.ts +32 -0
  37. package/out/commands/providerKey.js +108 -0
  38. package/out/commands/repl.d.ts +8 -1
  39. package/out/commands/repl.js +1207 -118
  40. package/out/commands/report.d.ts +39 -0
  41. package/out/commands/report.js +115 -0
  42. package/out/commands/review.d.ts +5 -0
  43. package/out/commands/review.js +223 -0
  44. package/out/commands/sessions.d.ts +23 -0
  45. package/out/commands/sessions.js +115 -0
  46. package/out/commands/settings.d.ts +3 -1
  47. package/out/commands/settings.js +18 -16
  48. package/out/commands/skills.d.ts +21 -0
  49. package/out/commands/skills.js +207 -0
  50. package/out/commands/unattended.d.ts +7 -0
  51. package/out/commands/unattended.js +351 -0
  52. package/out/commands/update.d.ts +1 -0
  53. package/out/commands/update.js +123 -0
  54. package/out/commands/worktrees.d.ts +5 -0
  55. package/out/commands/worktrees.js +186 -0
  56. package/out/executor/browser.d.ts +14 -0
  57. package/out/executor/browser.js +270 -0
  58. package/out/executor/diagnostics.d.ts +2 -0
  59. package/out/executor/diagnostics.js +181 -0
  60. package/out/executor/files.js +270 -40
  61. package/out/executor/git.js +42 -29
  62. package/out/executor/hooks.d.ts +42 -58
  63. package/out/executor/hooks.js +89 -182
  64. package/out/executor/index.d.ts +21 -5
  65. package/out/executor/index.js +160 -14
  66. package/out/executor/paths.d.ts +6 -1
  67. package/out/executor/paths.js +34 -6
  68. package/out/executor/sandbox.d.ts +40 -0
  69. package/out/executor/sandbox.js +299 -0
  70. package/out/executor/shell.d.ts +49 -4
  71. package/out/executor/shell.js +302 -56
  72. package/out/executor/toolservers.d.ts +20 -0
  73. package/out/executor/toolservers.js +189 -0
  74. package/out/executor/worktree.d.ts +9 -0
  75. package/out/executor/worktree.js +119 -0
  76. package/out/graph/read-python.js +2 -1
  77. package/out/lib/attach.d.ts +56 -12
  78. package/out/lib/attach.js +230 -63
  79. package/out/lib/clipboard.d.ts +23 -0
  80. package/out/lib/clipboard.js +182 -0
  81. package/out/lib/commandlist.d.ts +20 -0
  82. package/out/lib/commandlist.js +58 -0
  83. package/out/lib/decision.d.ts +22 -0
  84. package/out/lib/decision.js +50 -0
  85. package/out/lib/fingerprint.d.ts +25 -0
  86. package/out/lib/fingerprint.js +58 -0
  87. package/out/lib/gateway.d.ts +186 -2
  88. package/out/lib/gateway.js +59 -4
  89. package/out/lib/history.d.ts +24 -0
  90. package/out/lib/history.js +137 -0
  91. package/out/lib/ide.d.ts +19 -0
  92. package/out/lib/ide.js +131 -0
  93. package/out/lib/keyboard.d.ts +95 -0
  94. package/out/lib/keyboard.js +383 -0
  95. package/out/lib/machine.d.ts +21 -0
  96. package/out/lib/machine.js +91 -0
  97. package/out/lib/notify.d.ts +4 -0
  98. package/out/lib/notify.js +52 -0
  99. package/out/lib/output.d.ts +48 -0
  100. package/out/lib/output.js +108 -0
  101. package/out/lib/project-ops.d.ts +19 -0
  102. package/out/lib/project-ops.js +146 -0
  103. package/out/lib/project.d.ts +28 -0
  104. package/out/lib/project.js +114 -0
  105. package/out/lib/prompt.js +15 -2
  106. package/out/lib/queue.d.ts +13 -0
  107. package/out/lib/queue.js +116 -0
  108. package/out/lib/readiness.d.ts +19 -0
  109. package/out/lib/readiness.js +170 -1
  110. package/out/lib/self.d.ts +23 -0
  111. package/out/lib/self.js +124 -0
  112. package/out/lib/sessions.d.ts +19 -0
  113. package/out/lib/sessions.js +221 -0
  114. package/out/lib/stdin.d.ts +32 -0
  115. package/out/lib/stdin.js +117 -0
  116. package/out/lib/store.d.ts +40 -0
  117. package/out/lib/store.js +138 -0
  118. package/out/lib/sync.d.ts +18 -0
  119. package/out/lib/sync.js +81 -0
  120. package/out/lib/ui.d.ts +2 -4
  121. package/out/lib/ui.js +31 -25
  122. package/out/lib/voice.js +24 -0
  123. package/out/postinstall.js +42 -17
  124. package/out/providers/anthropic.d.ts +22 -0
  125. package/out/providers/anthropic.js +80 -0
  126. package/out/providers/index.d.ts +8 -0
  127. package/out/providers/index.js +83 -0
  128. package/out/providers/openai.d.ts +11 -0
  129. package/out/providers/openai.js +57 -0
  130. package/out/thin.js +594 -32
  131. package/package.json +9 -49
@@ -0,0 +1,117 @@
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.STDIN_NAME = void 0;
37
+ exports.readPiped = readPiped;
38
+ exports.attachPiped = attachPiped;
39
+ exports.openTerminal = openTerminal;
40
+ /**
41
+ * Text piped in, and the keyboard behind the pipe (manual 3.9).
42
+ *
43
+ * `git diff | comprism work "write a commit message"` hands the tool two
44
+ * things through two doors: the diff on standard input and the person at the
45
+ * terminal. They are never confused. Standard input is read to its end and
46
+ * attached as a file called `standard input`; it is material to read, never
47
+ * an answer. Approval questions go to the terminal itself (`/dev/tty`, or the
48
+ * Windows console), so a `y` in the piped text can never approve anything.
49
+ * With no terminal at all, the job is unattended (manual 10.1).
50
+ */
51
+ const fs = __importStar(require("node:fs"));
52
+ const os = __importStar(require("node:os"));
53
+ const path = __importStar(require("node:path"));
54
+ const tty = __importStar(require("node:tty"));
55
+ const attach = __importStar(require("./attach"));
56
+ /** The name piped text is attached under. */
57
+ exports.STDIN_NAME = 'standard input';
58
+ /** How long a pipe may stay silent before the person is told it is waited on. */
59
+ const QUIET_MS = 2000;
60
+ /**
61
+ * Everything piped in, read to the end. Null when standard input is a
62
+ * terminal, and empty when the pipe closed with nothing on it.
63
+ *
64
+ * A pipe that stays open with nothing coming (a command still running, or one
65
+ * that never closes) is announced after two seconds on standard error, so the
66
+ * person is not left looking at a still screen. Ctrl+C ends it as usual.
67
+ */
68
+ function readPiped(input = process.stdin, say = (l) => { process.stderr.write(`${l}\n`); }) {
69
+ if (input.isTTY)
70
+ return Promise.resolve(null);
71
+ return new Promise((resolve) => {
72
+ const chunks = [];
73
+ const quiet = setTimeout(() => say(' waiting for standard input (Ctrl+C to stop)'), QUIET_MS);
74
+ input.on('data', (c) => {
75
+ chunks.push(typeof c === 'string' ? Buffer.from(c) : c);
76
+ });
77
+ const done = () => {
78
+ clearTimeout(quiet);
79
+ resolve(Buffer.concat(chunks).toString('utf8'));
80
+ };
81
+ input.on('end', done);
82
+ input.on('error', done);
83
+ });
84
+ }
85
+ /**
86
+ * The piped text, uploaded as `standard input`. Written to a temporary file so
87
+ * it travels exactly like any attachment, and the file removed straight after.
88
+ */
89
+ async function attachPiped(text, baseUrl, credential) {
90
+ const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'comprism-stdin-'));
91
+ const file = path.join(folder, exports.STDIN_NAME);
92
+ try {
93
+ fs.writeFileSync(file, text);
94
+ return await attach.upload(baseUrl, credential, [file]);
95
+ }
96
+ finally {
97
+ fs.rmSync(folder, { recursive: true, force: true });
98
+ }
99
+ }
100
+ /**
101
+ * The terminal the person is sitting at, when standard input is a pipe.
102
+ * Null when there is none (a script, a scheduler, a container), which makes
103
+ * the job unattended.
104
+ */
105
+ function openTerminal(platform = process.platform) {
106
+ try {
107
+ const fd = fs.openSync(platform === 'win32' ? 'CONIN$' : '/dev/tty', 'r');
108
+ if (!tty.isatty(fd)) {
109
+ fs.closeSync(fd);
110
+ return null;
111
+ }
112
+ return new tty.ReadStream(fd);
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
@@ -0,0 +1,40 @@
1
+ export declare const LEDGER_FILE = "ledger.jsonl";
2
+ export declare const DECISIONS_FILE = "decisions.jsonl";
3
+ export declare const SESSIONS_FILE = "sessions.json";
4
+ /**
5
+ * Files that may never carry a supersede row and may never be resolved through
6
+ * `latestByKey`. Adding a file here is a promise that its rows are frozen.
7
+ */
8
+ export declare const IMMUTABLE_FILES: ReadonlySet<string>;
9
+ export declare function storePath(file: string): string;
10
+ /**
11
+ * Append to a supersedable store. Refuses the immutable ones by name.
12
+ */
13
+ export declare function appendRow(file: string, row: object): void;
14
+ /**
15
+ * Append to an immutable store. The only write path to `decisions.jsonl`.
16
+ *
17
+ * Refuses any row carrying `supersedes`. A decision record that supersedes
18
+ * another is not a correction, it is a second decision, and it belongs in the
19
+ * file as its own row with its own id.
20
+ */
21
+ export declare function appendImmutable(file: string, row: object): void;
22
+ /**
23
+ * Read every parseable row.
24
+ *
25
+ * A truncated final line - the process died mid-write on a machine without the
26
+ * fsync guarantee, or the file was copied while being written - is skipped
27
+ * rather than thrown. One lost row is a gap in the evidence; an exception here
28
+ * would make every row after it unreadable, which is a far worse failure.
29
+ */
30
+ export declare function readRows<T>(file: string): T[];
31
+ /**
32
+ * Resolve supersede chains: the last row wins per key.
33
+ *
34
+ * Takes the file name rather than just the rows, so that calling it on an
35
+ * immutable store is an error at the call site rather than a review comment
36
+ * somebody has to catch.
37
+ */
38
+ export declare function latestByKey<T>(file: string, rows: T[], key: (row: T) => string): Map<string, T>;
39
+ /** Row count without parsing, for reports and the gate. */
40
+ export declare function countRows(file: string): number;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.IMMUTABLE_FILES = exports.SESSIONS_FILE = exports.DECISIONS_FILE = exports.LEDGER_FILE = void 0;
7
+ exports.storePath = storePath;
8
+ exports.appendRow = appendRow;
9
+ exports.appendImmutable = appendImmutable;
10
+ exports.readRows = readRows;
11
+ exports.latestByKey = latestByKey;
12
+ exports.countRows = countRows;
13
+ /**
14
+ * The append-only store.
15
+ *
16
+ * One JSON object per line. A row is written once and never edited. There is no
17
+ * update path, no delete path and no rewrite path in this file, and that is the
18
+ * point: the discipline is structural rather than a convention someone has to
19
+ * remember at three in the morning.
20
+ *
21
+ * Two grades of immutability, and the difference matters:
22
+ *
23
+ * - Ordinary stores (the ledger) are append-only with supersession. A
24
+ * correction is a NEW row carrying `supersedes`, and `latestByKey` resolves
25
+ * the chain at read time. Nothing is lost; the earlier belief stays on the
26
+ * record.
27
+ * - `decisions.jsonl` is stricter. It is never superseded, for any reason,
28
+ * because it means exactly one thing forever: what the system knew and
29
+ * chose at decision time. A supersede row would make it a claim about the
30
+ * present instead. The fences below make that unenforceable by accident:
31
+ * `appendRow` refuses the file, `appendImmutable` refuses a `supersedes`
32
+ * field, and `latestByKey` refuses to run against it at all.
33
+ */
34
+ const node_fs_1 = __importDefault(require("node:fs"));
35
+ const node_path_1 = __importDefault(require("node:path"));
36
+ const config_1 = require("./config");
37
+ exports.LEDGER_FILE = 'ledger.jsonl';
38
+ exports.DECISIONS_FILE = 'decisions.jsonl';
39
+ exports.SESSIONS_FILE = 'sessions.json';
40
+ /**
41
+ * Files that may never carry a supersede row and may never be resolved through
42
+ * `latestByKey`. Adding a file here is a promise that its rows are frozen.
43
+ */
44
+ exports.IMMUTABLE_FILES = new Set([exports.DECISIONS_FILE]);
45
+ function storePath(file) {
46
+ return node_path_1.default.join((0, config_1.homeDir)(), file);
47
+ }
48
+ /**
49
+ * The single write primitive. Everything that appends goes through here.
50
+ *
51
+ * `fsync` after every row is the reason a kill mid-session leaves a parseable
52
+ * file: the bytes are on the disk, not in a buffer that dies with the process.
53
+ * It costs a syscall per call, against a network round trip to a model, so the
54
+ * cost is not measurable and the guarantee is absolute.
55
+ */
56
+ function writeLine(file, row) {
57
+ (0, config_1.ensureHome)();
58
+ const line = `${JSON.stringify(row)}\n`;
59
+ const fd = node_fs_1.default.openSync(storePath(file), 'a', 0o600);
60
+ try {
61
+ node_fs_1.default.writeSync(fd, line, null, 'utf8');
62
+ node_fs_1.default.fsyncSync(fd);
63
+ }
64
+ finally {
65
+ node_fs_1.default.closeSync(fd);
66
+ }
67
+ }
68
+ /**
69
+ * Append to a supersedable store. Refuses the immutable ones by name.
70
+ */
71
+ function appendRow(file, row) {
72
+ if (exports.IMMUTABLE_FILES.has(file)) {
73
+ throw new Error(`${file} is immutable: append through appendImmutable, which forbids supersession.`);
74
+ }
75
+ writeLine(file, row);
76
+ }
77
+ /**
78
+ * Append to an immutable store. The only write path to `decisions.jsonl`.
79
+ *
80
+ * Refuses any row carrying `supersedes`. A decision record that supersedes
81
+ * another is not a correction, it is a second decision, and it belongs in the
82
+ * file as its own row with its own id.
83
+ */
84
+ function appendImmutable(file, row) {
85
+ if (!exports.IMMUTABLE_FILES.has(file)) {
86
+ throw new Error(`${file} is not an immutable store: append through appendRow.`);
87
+ }
88
+ if (Object.prototype.hasOwnProperty.call(row, 'supersedes')) {
89
+ throw new Error(`${file} rows are never superseded. A later decision is a new record, not a correction.`);
90
+ }
91
+ writeLine(file, row);
92
+ }
93
+ /**
94
+ * Read every parseable row.
95
+ *
96
+ * A truncated final line - the process died mid-write on a machine without the
97
+ * fsync guarantee, or the file was copied while being written - is skipped
98
+ * rather than thrown. One lost row is a gap in the evidence; an exception here
99
+ * would make every row after it unreadable, which is a far worse failure.
100
+ */
101
+ function readRows(file) {
102
+ const p = storePath(file);
103
+ if (!node_fs_1.default.existsSync(p))
104
+ return [];
105
+ const out = [];
106
+ for (const line of node_fs_1.default.readFileSync(p, 'utf8').split('\n')) {
107
+ const trimmed = line.trim();
108
+ if (!trimmed)
109
+ continue;
110
+ try {
111
+ out.push(JSON.parse(trimmed));
112
+ }
113
+ catch {
114
+ continue;
115
+ }
116
+ }
117
+ return out;
118
+ }
119
+ /**
120
+ * Resolve supersede chains: the last row wins per key.
121
+ *
122
+ * Takes the file name rather than just the rows, so that calling it on an
123
+ * immutable store is an error at the call site rather than a review comment
124
+ * somebody has to catch.
125
+ */
126
+ function latestByKey(file, rows, key) {
127
+ if (exports.IMMUTABLE_FILES.has(file)) {
128
+ throw new Error(`${file} is never superseded, so resolving it through latestByKey would imply it can be.`);
129
+ }
130
+ const map = new Map();
131
+ for (const row of rows)
132
+ map.set(key(row), row);
133
+ return map;
134
+ }
135
+ /** Row count without parsing, for reports and the gate. */
136
+ function countRows(file) {
137
+ return readRows(file).length;
138
+ }
@@ -0,0 +1,18 @@
1
+ export interface SyncTarget {
2
+ /** Tenant base URL, e.g. https://acme.completionprism.ai */
3
+ url: string;
4
+ /** A tenant session token. Never written to any file by this command. */
5
+ token: string;
6
+ /** Only needed on localhost, where there is no subdomain to resolve. */
7
+ tenant?: string;
8
+ }
9
+ export interface SyncOutcome {
10
+ accepted: Record<string, number>;
11
+ duplicates: Record<string, number>;
12
+ rejected: Record<string, string[]>;
13
+ reconciliation: Record<string, unknown>;
14
+ sent: Record<string, number>;
15
+ }
16
+ /** Everything the local stores hold, in the shape the tenant expects. */
17
+ export declare function collectBatch(): Record<string, unknown>;
18
+ export declare function sync(target: SyncTarget): Promise<SyncOutcome>;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectBatch = collectBatch;
4
+ exports.sync = sync;
5
+ /**
6
+ * Sync: the one thin contract between the Companion and the tenant.
7
+ *
8
+ * The Companion works standalone with no service to call - that is the wedge,
9
+ * and nothing here changes it. Sync is opt-in, additive, and one direction: the
10
+ * local stores are read, posted, and left exactly as they were. Nothing is
11
+ * deleted locally on success, because the local record is the primary evidence
12
+ * and a server round trip is not a reason to destroy it.
13
+ *
14
+ * Two properties make it safe to run on a cron:
15
+ *
16
+ * **Idempotent.** Every record is keyed by the id the Companion assigned it,
17
+ * and the server refuses a duplicate rather than inserting a second copy. A
18
+ * retry after a dropped connection cannot double-count anyone's spend.
19
+ *
20
+ * **Reconciled.** The response carries what the batch claimed and what the
21
+ * tenant now holds. If the two disagree, that is visible here, in the command
22
+ * the person just ran - not three weeks later in a figure someone is trying to
23
+ * defend to a CFO.
24
+ */
25
+ const chains_1 = require("../internal/chains");
26
+ const config_1 = require("./config");
27
+ const evidence_1 = require("../internal/evidence");
28
+ const links_1 = require("../internal/links");
29
+ const scan_1 = require("../internal/scan");
30
+ const sessions_1 = require("../commands/sessions");
31
+ const snapshot_1 = require("../internal/snapshot");
32
+ const store_1 = require("./store");
33
+ const version_1 = require("./version");
34
+ /** Everything the local stores hold, in the shape the tenant expects. */
35
+ function collectBatch() {
36
+ const sessions = (0, sessions_1.listSessions)();
37
+ const chains = (0, chains_1.buildChainsFromStore)(sessions);
38
+ return {
39
+ companion_version: version_1.COMPRISM_VERSION,
40
+ sessions,
41
+ ledger: (0, store_1.readRows)(store_1.LEDGER_FILE),
42
+ decisions: (0, snapshot_1.readSnapshots)(),
43
+ chains,
44
+ links: (0, links_1.readLinks)(),
45
+ executed_pairs: (0, evidence_1.readExecutedPairs)(),
46
+ policy_rejections: (0, evidence_1.readRejections)(),
47
+ exploration: (0, evidence_1.readExploration)(),
48
+ imported_ledger: (0, scan_1.readImportedLedger)(),
49
+ scan_findings: (0, scan_1.readFindings)(),
50
+ };
51
+ }
52
+ async function sync(target) {
53
+ const batch = collectBatch();
54
+ const cfg = (0, config_1.loadConfig)();
55
+ const headers = {
56
+ 'content-type': 'application/json',
57
+ authorization: `Bearer ${target.token}`,
58
+ };
59
+ // On localhost there is no subdomain, so the tenant travels in a header.
60
+ // Hyphenated in the header, underscored in the schema: the single most common
61
+ // cause of "it cannot find the tenant".
62
+ if (target.tenant)
63
+ headers['X-Tenant-ID'] = target.tenant;
64
+ const res = await fetch(`${target.url.replace(/\/+$/, '')}/api/v1/completion/sync`, {
65
+ method: 'POST',
66
+ headers,
67
+ body: JSON.stringify(batch),
68
+ });
69
+ if (!res.ok) {
70
+ // The status line, never the body: a server error body can echo the payload,
71
+ // and the payload is the customer's record.
72
+ throw new Error(`sync failed: ${res.status} ${res.statusText}`);
73
+ }
74
+ const result = (await res.json());
75
+ const sent = {};
76
+ for (const [key, value] of Object.entries(batch)) {
77
+ if (Array.isArray(value))
78
+ sent[key] = value.length;
79
+ }
80
+ return { ...result, sent, ...(cfg.mode ? {} : {}) };
81
+ }
package/out/lib/ui.d.ts CHANGED
@@ -79,12 +79,10 @@ export declare function table(headers: string[], rows: string[][]): string;
79
79
  * out of the way, leaving the answer and a one-line receipt.
80
80
  */
81
81
  export declare class LiveIndicator {
82
- private timer;
83
- private frame;
84
- private label;
85
- private readonly frames;
82
+ private shown;
86
83
  start(label: string): void;
87
84
  update(label: string): void;
85
+ private draw;
88
86
  stop(): void;
89
87
  }
90
88
  export interface ReceiptData {
package/out/lib/ui.js CHANGED
@@ -35,15 +35,20 @@ exports.warn = warn;
35
35
  exports.fail = fail;
36
36
  exports.info = info;
37
37
  const isTty = process.stdout.isTTY === true;
38
- const noColor = !!process.env.NO_COLOR || process.env.TERM === 'dumb';
39
- const useColor = isTty && !noColor;
38
+ /** Decided when drawn, not when loaded, so `--plain`, `plain true` and
39
+ * `theme plain` applied at start up take effect (manual 6.10, 6.12). */
40
+ const noColor = () => !!process.env.NO_COLOR || process.env.TERM === 'dumb';
41
+ const useColorNow = () => isTty && !noColor();
42
+ /** Plain mode: no color and nothing redrawn; every update is a new line. */
43
+ const plainNow = () => process.env.COMPRISM_PLAIN === '1' || !isTty;
44
+ const light = () => process.env.COMPRISM_THEME === 'light';
40
45
  function width() {
41
46
  return Math.min(process.stdout.columns || 88, 100);
42
47
  }
43
48
  // ── palette ─────────────────────────────────────────────────────────────────
44
49
  // illuminis brand, from branding/brand.toml. Deep navy is unreadable on a dark
45
50
  // 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;
51
+ const rgb = (r, g, b) => (s) => useColorNow() ? `\x1b[38;2;${r};${g};${b}m${s}\x1b[0m` : s;
47
52
  exports.c = {
48
53
  blue: rgb(59, 111, 181), // primary_blue #3B6FB5
49
54
  deep: rgb(31, 77, 120), // heading_blue #1F4D78
@@ -52,9 +57,10 @@ exports.c = {
52
57
  amber: rgb(217, 119, 6), // warning #D97706
53
58
  red: rgb(220, 38, 38), // error #DC2626
54
59
  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),
60
+ // Light on a dark terminal, dark on a light one (manual 6.10).
61
+ text: (s) => (light() ? rgb(40, 44, 52)(s) : rgb(214, 219, 228)(s)),
62
+ bold: (s) => (useColorNow() ? `\x1b[1m${s}\x1b[0m` : s),
63
+ dim: (s) => (useColorNow() ? `\x1b[2m${s}\x1b[0m` : s),
58
64
  };
59
65
  /** The prism: white light entering, a spectrum leaving. The brand in one line. */
60
66
  const SPECTRUM = [
@@ -75,13 +81,13 @@ const SPECTRUM = [
75
81
  * and the brand is the one thing a customer notices before the engine.
76
82
  */
77
83
  function spectrumAt(index, glyph) {
78
- if (!useColor)
84
+ if (!useColorNow())
79
85
  return glyph;
80
86
  const [r, g, b] = SPECTRUM[Math.abs(index) % SPECTRUM.length];
81
87
  return `\x1b[38;2;${r};${g};${b}m${glyph}\x1b[0m`;
82
88
  }
83
89
  function prismRule(w = width()) {
84
- if (!useColor)
90
+ if (!useColorNow())
85
91
  return '-'.repeat(w);
86
92
  const seg = Math.max(1, Math.floor(w / SPECTRUM.length));
87
93
  let out = '';
@@ -197,29 +203,29 @@ function table(headers, rows) {
197
203
  * out of the way, leaving the answer and a one-line receipt.
198
204
  */
199
205
  class LiveIndicator {
200
- timer = null;
201
- frame = 0;
202
- label = '';
203
- frames = ['◜', '◠', '◝', '◞', '◡', '◟'];
206
+ shown = false;
207
+ // No animation of its own. A glyph turning on a timer looks like work
208
+ // whether or not anything is happening; the line changes only when the
209
+ // caller has something new to say, and in plain mode each change is a new
210
+ // line rather than a redraw (manual 6.12).
204
211
  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);
212
+ this.draw(label);
213
213
  }
214
214
  update(label) {
215
- this.label = label;
215
+ this.draw(label);
216
+ }
217
+ draw(label) {
218
+ if (plainNow()) {
219
+ process.stdout.write(` ${label}\n`);
220
+ return;
221
+ }
222
+ process.stdout.write(`\r ${exports.c.purple('◠')} ${exports.c.dim(label)} `);
223
+ this.shown = true;
216
224
  }
217
225
  stop() {
218
- if (this.timer)
219
- clearInterval(this.timer);
220
- this.timer = null;
221
- if (isTty)
226
+ if (this.shown && !plainNow())
222
227
  process.stdout.write(`\r${' '.repeat(width())}\r`);
228
+ this.shown = false;
223
229
  }
224
230
  }
225
231
  exports.LiveIndicator = LiveIndicator;
package/out/lib/voice.js CHANGED
@@ -143,6 +143,30 @@ function start() {
143
143
  stdio: ['ignore', 'ignore', 'ignore'],
144
144
  });
145
145
  let stopped = null;
146
+ // The recorder never outlives the session. Closing the window or ending the
147
+ // process while it listens would otherwise leave a microphone recording to a
148
+ // file nobody knows about, the worst thing this tool could do.
149
+ const abandon = () => {
150
+ try {
151
+ child.kill('SIGKILL');
152
+ }
153
+ catch { /* already gone */ }
154
+ discard(file);
155
+ };
156
+ const onSignal = (signal) => {
157
+ abandon();
158
+ release();
159
+ process.kill(process.pid, signal);
160
+ };
161
+ const release = () => {
162
+ process.off('exit', abandon);
163
+ process.off('SIGHUP', onSignal);
164
+ process.off('SIGTERM', onSignal);
165
+ };
166
+ process.once('exit', abandon);
167
+ process.once('SIGHUP', onSignal);
168
+ process.once('SIGTERM', onSignal);
169
+ child.once('close', release);
146
170
  const stop = () => {
147
171
  if (stopped)
148
172
  return stopped;
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
3
6
  Object.defineProperty(exports, "__esModule", { value: true });
4
7
  /**
5
8
  * Runs on `npm install -g @illuminis/comprism`. Rule R1.
@@ -20,8 +23,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
20
23
  * swallowing its own errors. A package that fails to install because its
21
24
  * telemetry could not configure itself deserves to be uninstalled.
22
25
  */
26
+ const node_fs_1 = __importDefault(require("node:fs"));
23
27
  const install_1 = require("./commands/install");
24
28
  const readiness_1 = require("./lib/readiness");
29
+ /**
30
+ * Written to the terminal itself, not to npm. npm hides everything an install
31
+ * script prints unless the install fails, so the next steps a person needs
32
+ * right now never reached the screen (owner, 27 September 2026). Falls back to
33
+ * standard output where there is no terminal, such as a build server's log.
34
+ */
35
+ function say(text) {
36
+ const tty = process.platform === 'win32' ? 'CONOUT$' : '/dev/tty';
37
+ try {
38
+ const fd = node_fs_1.default.openSync(tty, 'w');
39
+ node_fs_1.default.writeSync(fd, text);
40
+ node_fs_1.default.closeSync(fd);
41
+ }
42
+ catch {
43
+ process.stdout.write(text);
44
+ }
45
+ }
25
46
  async function main() {
26
47
  // Local development installs (`npm install` inside the repo) should not touch
27
48
  // the developer's shell profile. Only a global install means "set me up".
@@ -44,29 +65,33 @@ async function main() {
44
65
  if (report.desktop.staleRemoved) {
45
66
  desktopLines.push(' · removed an old CompletionPrism entry from Claude Desktop (it could not answer)');
46
67
  }
47
- if (report.desktop.note) {
68
+ // Only when Claude Desktop is on this machine: a line about an app somebody
69
+ // does not have is noise in a list of next steps.
70
+ if (report.desktop.note && report.desktop.present) {
48
71
  desktopLines.push(` · ${report.desktop.note}`);
49
72
  }
50
73
  else if (report.desktop.present && report.baseUrl) {
51
74
  desktopLines.push(` · Claude Desktop: add ${report.baseUrl}/mcp under Settings > Connectors`);
52
75
  }
53
- process.stdout.write([
76
+ // A short numbered list of what to do next, and nothing a person has to
77
+ // decode. The setup detail (which profile got a line) is one quiet line.
78
+ const signIn = !(report.enrolled && report.baseUrl);
79
+ const steps = [
80
+ 'Open a new terminal window.',
81
+ ...(signIn ? ['Sign in: comprism login'] : []),
82
+ 'Start working: comprism (or claude, already measured)',
83
+ ...(report.baseUrl ? [`See what it saved: ${report.baseUrl}`] : []),
84
+ ];
85
+ say([
54
86
  '',
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,
87
+ ' CompletionPrism is installed.',
60
88
  '',
61
- ' Open a new terminal and just work:',
62
- ' claude your tools, instrumented, unchanged',
63
- ' comprism our own session',
64
- ' comprism check what this machine still needs',
89
+ ' Next steps',
90
+ ...steps.map((line, i) => ` ${i + 1}. ${line}`),
91
+ ...desktopLines,
65
92
  '',
66
- ...(report.baseUrl
67
- ? [` What your work cost: ${report.baseUrl}`, '']
68
- : []),
69
- ' If our recorder is ever down your tools are unaffected. Undo with `comprism uninstall`.',
93
+ ...report.profilesTouched.map((p) => ` Set up in ${p}.`),
94
+ ' To remove it later, one command: comprism uninstall',
70
95
  '',
71
96
  ].join('\n'));
72
97
  // What this machine still needs, said at the only moment somebody is
@@ -76,7 +101,7 @@ async function main() {
76
101
  // already fine is noise on a screen somebody is about to close.
77
102
  const machine = (0, readiness_1.check)();
78
103
  if (!machine.ready) {
79
- process.stdout.write([
104
+ say([
80
105
  ' Two things the work itself may need on this machine:',
81
106
  ...machine.lines,
82
107
  '',
@@ -89,7 +114,7 @@ async function main() {
89
114
  }
90
115
  catch {
91
116
  // Never fail an install over setup. The user can run `comprism install`.
92
- process.stdout.write('\n CompletionPrism installed. Run `comprism install` to finish setup.\n\n');
117
+ say('\n CompletionPrism is installed. Next step: open a new terminal and run `comprism install`.\n\n');
93
118
  }
94
119
  }
95
120
  void main();
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Anthropic adapter.
3
+ *
4
+ * Deliberately dependency-free: the Messages API over `fetch`, nothing else.
5
+ * The Companion has to install into someone else's tool without dragging a
6
+ * dependency tree behind it, and a proxy that sits in front of a provider is a
7
+ * poor place to inherit somebody's transitive supply chain.
8
+ */
9
+ import type { Message, ProviderResult } from '../lib/types';
10
+ export interface AnthropicCall {
11
+ model: string;
12
+ messages: Message[];
13
+ maxTokens: number;
14
+ apiKey: string;
15
+ }
16
+ export declare function callAnthropic(call: AnthropicCall): Promise<ProviderResult>;
17
+ /** Carries the status code so the shared retry policy can read it. */
18
+ export declare class ProviderHttpError extends Error {
19
+ status: number;
20
+ latencyMs: number;
21
+ constructor(status: number, message: string);
22
+ }