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