@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,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.status = status;
|
|
4
|
+
exports.refresh = refresh;
|
|
5
|
+
exports.coverageSentence = coverageSentence;
|
|
6
|
+
exports.readerReport = readerReport;
|
|
7
|
+
exports.pushFile = pushFile;
|
|
8
|
+
/**
|
|
9
|
+
* Keep the service's map of this project in step with the files on disk.
|
|
10
|
+
*
|
|
11
|
+
* A map that lies is worse than no map, because the agent trusts it. So the
|
|
12
|
+
* rule here is simple and it is never bent: before the map is used, the files
|
|
13
|
+
* are checked; what moved is read again; and if it cannot be brought up to
|
|
14
|
+
* date, the agent is told so rather than being handed a confident answer from
|
|
15
|
+
* yesterday.
|
|
16
|
+
*
|
|
17
|
+
* The check costs about ten milliseconds. A full read of a seven hundred file
|
|
18
|
+
* project costs a second or two, and a partial one costs almost nothing,
|
|
19
|
+
* because only the files that actually moved are read.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here decides anything. The machine reports what it has; the service
|
|
22
|
+
* says what it needs; the machine reads exactly that and sends structure back.
|
|
23
|
+
*/
|
|
24
|
+
const gateway_1 = require("../lib/gateway");
|
|
25
|
+
const build_1 = require("./build");
|
|
26
|
+
const read_python_1 = require("./read-python");
|
|
27
|
+
const read_typescript_1 = require("./read-typescript");
|
|
28
|
+
/** How many files' facts go in one call.
|
|
29
|
+
*
|
|
30
|
+
* A whole project in one request is several megabytes and fails in the least
|
|
31
|
+
* helpful way available: the request is rejected at the edge and nothing says
|
|
32
|
+
* which part was too big. Batches also mean a project that loses its
|
|
33
|
+
* connection half way has stored half its files rather than none, and the next
|
|
34
|
+
* run reads only what is still missing.
|
|
35
|
+
*/
|
|
36
|
+
const BATCH = 120;
|
|
37
|
+
function reply(r) {
|
|
38
|
+
return (r.graph ?? {});
|
|
39
|
+
}
|
|
40
|
+
/** What the service holds for this project, and whether it matches disk. */
|
|
41
|
+
async function status(root, workspace, opts = {}) {
|
|
42
|
+
const state = (0, build_1.projectState)(root);
|
|
43
|
+
const answer = await (0, gateway_1.ask)({
|
|
44
|
+
intent: "graph", prompt: "", graphOp: "status", workspace,
|
|
45
|
+
graphPayload: {
|
|
46
|
+
fingerprint: state.fingerprint.hash,
|
|
47
|
+
// Ask for what the map holds in the same breath, when we are going to
|
|
48
|
+
// need it anyway. One round trip rather than two.
|
|
49
|
+
with_stamps: Boolean(opts.withStamps),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const graph = reply(answer);
|
|
53
|
+
return {
|
|
54
|
+
stamps: graph.stamps ?? null,
|
|
55
|
+
definedNames: graph.defined_names ?? [],
|
|
56
|
+
enabled: answer.granted !== false && graph.enabled !== false,
|
|
57
|
+
autoRefresh: graph.auto_refresh !== false,
|
|
58
|
+
fresh: Boolean(graph.fresh),
|
|
59
|
+
hasMap: Boolean(graph.has_map),
|
|
60
|
+
held: graph.held ?? null,
|
|
61
|
+
reachable: Boolean(answer.ok),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Bring the map up to date, reading only the files that moved.
|
|
66
|
+
*
|
|
67
|
+
* `force` reads everything again, which is what the manual refresh does: a
|
|
68
|
+
* person asking for a rebuild has usually just had an answer they did not
|
|
69
|
+
* believe, and telling them nothing needed doing is not an answer.
|
|
70
|
+
*/
|
|
71
|
+
async function refresh(root, workspace, opts = {}) {
|
|
72
|
+
const started = Date.now();
|
|
73
|
+
const state = (0, build_1.projectState)(root);
|
|
74
|
+
const blank = {
|
|
75
|
+
current: false, enabled: true, message: "", filesRead: 0,
|
|
76
|
+
filesTotal: state.files.length, seconds: 0, unread: 0,
|
|
77
|
+
};
|
|
78
|
+
const now = await status(root, workspace, { withStamps: !opts.force });
|
|
79
|
+
if (!now.enabled) {
|
|
80
|
+
return { ...blank, enabled: false, current: false, message: "Your company has the code map switched off, so nothing was read or sent." };
|
|
81
|
+
}
|
|
82
|
+
if (!now.reachable) {
|
|
83
|
+
return { ...blank, current: false, message: "The service could not be reached, so the map was left as it is. Work carries on without it." };
|
|
84
|
+
}
|
|
85
|
+
if (now.fresh && !opts.force) {
|
|
86
|
+
return { ...blank, current: true, message: `The map already matches all ${state.files.length} files in this project.` };
|
|
87
|
+
}
|
|
88
|
+
// What the service already holds, so only what moved is read. It came back
|
|
89
|
+
// with the status above when there was a map to describe, which is one round
|
|
90
|
+
// trip saved on every refresh; a forced rebuild asks for none of it because
|
|
91
|
+
// it is going to read everything anyway.
|
|
92
|
+
const known = opts.force ? {} : (now.stamps ?? {});
|
|
93
|
+
// What the service already knows this project defines. Used to drop calls
|
|
94
|
+
// that could never resolve, before they are sent. Absent on a project it has
|
|
95
|
+
// not joined yet, which costs size and never correctness.
|
|
96
|
+
const knownNames = now.definedNames;
|
|
97
|
+
const changed = state.files
|
|
98
|
+
.filter((f) => known[f.rel] !== state.stamps[f.rel])
|
|
99
|
+
.map((f) => f.rel);
|
|
100
|
+
const removed = Object.keys(known).filter((p) => !(p in state.stamps));
|
|
101
|
+
const read = (0, build_1.readFacts)(root, changed, knownNames);
|
|
102
|
+
const unreadTotal = read.unread.python + read.unread.typescript;
|
|
103
|
+
// Sent in batches. Each call is self contained: a connection lost half way
|
|
104
|
+
// leaves the files already sent stored, and the next run reads only the rest.
|
|
105
|
+
const paths = Object.keys(read.facts);
|
|
106
|
+
for (let i = 0; i < paths.length; i += BATCH) {
|
|
107
|
+
const slice = paths.slice(i, i + BATCH);
|
|
108
|
+
const payload = {};
|
|
109
|
+
for (const rel of slice) {
|
|
110
|
+
payload[rel] = { ...read.facts[rel], stamp: state.stamps[rel] };
|
|
111
|
+
}
|
|
112
|
+
const sent = await (0, gateway_1.ask)({
|
|
113
|
+
intent: "graph", prompt: "", graphOp: "update", workspace,
|
|
114
|
+
graphPayload: {
|
|
115
|
+
changed: payload,
|
|
116
|
+
// The removals travel with the first batch, so a project whose files
|
|
117
|
+
// were deleted stops answering questions about them immediately rather
|
|
118
|
+
// than at the end of a long upload.
|
|
119
|
+
removed: i === 0 ? removed : [],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
if (!sent.ok) {
|
|
123
|
+
return { ...blank, current: false, seconds: read.seconds, message: "The map could not be sent in full, so it was left as it is. Work carries on without it." };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const coverage = coverageSentence(read.unread, state.files.length);
|
|
127
|
+
const done = await (0, gateway_1.ask)({
|
|
128
|
+
intent: "graph", prompt: "", graphOp: "finish", workspace,
|
|
129
|
+
graphPayload: {
|
|
130
|
+
commit: state.commit,
|
|
131
|
+
fingerprint: state.fingerprint.hash,
|
|
132
|
+
file_count: state.files.length - unreadTotal,
|
|
133
|
+
unread_files: unreadTotal,
|
|
134
|
+
coverage,
|
|
135
|
+
read_seconds: String(read.seconds),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const seconds = Math.round((Date.now() - started) / 100) / 10;
|
|
139
|
+
if (!done.ok) {
|
|
140
|
+
return { ...blank, current: false, seconds, message: "The files were sent but the map could not be stamped as current, so it will be read again next time." };
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
current: true,
|
|
144
|
+
enabled: true,
|
|
145
|
+
filesRead: paths.length,
|
|
146
|
+
filesTotal: state.files.length,
|
|
147
|
+
unread: unreadTotal,
|
|
148
|
+
seconds,
|
|
149
|
+
message: paths.length === state.files.length
|
|
150
|
+
? `Read all ${paths.length} files in ${seconds}s.`
|
|
151
|
+
: `Read the ${paths.length} files that changed, of ${state.files.length}, in ${seconds}s.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** What was read and what was not, in one sentence an answer can carry.
|
|
155
|
+
*
|
|
156
|
+
* Said out loud rather than hidden. A map that quietly covers less than it
|
|
157
|
+
* claims is the one failure worth engineering against: the agent believes it
|
|
158
|
+
* has seen everything, and writes a confident change that misses a caller. */
|
|
159
|
+
function coverageSentence(unread, total) {
|
|
160
|
+
const gaps = [];
|
|
161
|
+
if (unread.python) {
|
|
162
|
+
gaps.push(`${unread.python} Python files could not be read because this machine has no Python 3`);
|
|
163
|
+
}
|
|
164
|
+
if (unread.typescript) {
|
|
165
|
+
gaps.push(`${unread.typescript} TypeScript files could not be read because this installation has no TypeScript compiler`);
|
|
166
|
+
}
|
|
167
|
+
if (!gaps.length)
|
|
168
|
+
return `It covers all ${total} code files in this project.`;
|
|
169
|
+
return `It covers ${total - unread.python - unread.typescript} of ${total} code files: ${gaps.join("; ")}.`;
|
|
170
|
+
}
|
|
171
|
+
/** What this machine can read at all. Asked before anything else, so a person
|
|
172
|
+
* is told why their map is thin rather than left to guess. */
|
|
173
|
+
function readerReport() {
|
|
174
|
+
const python = Boolean((0, read_python_1.findPython)());
|
|
175
|
+
const typescript = Boolean((0, read_typescript_1.loadCompiler)());
|
|
176
|
+
if (python && typescript)
|
|
177
|
+
return "This machine can read Python, TypeScript and JavaScript.";
|
|
178
|
+
const missing = [];
|
|
179
|
+
if (!python)
|
|
180
|
+
missing.push("Python 3 is not on this machine, so Python files are not mapped");
|
|
181
|
+
if (!typescript)
|
|
182
|
+
missing.push("the TypeScript compiler is not installed beside this tool, so TypeScript and JavaScript files are not mapped");
|
|
183
|
+
return `Partly: ${missing.join(", and ")}.`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Put ONE file back into the map, now, and wait for it.
|
|
187
|
+
*
|
|
188
|
+
* The fast path, and it exists because the slow one loses a race that matters.
|
|
189
|
+
* An agent writes a file and asks the map about it in the same step, because
|
|
190
|
+
* asking for several actions at once is what a well behaved agent does. A
|
|
191
|
+
* refresh started in the background after the write has not finished by the
|
|
192
|
+
* time the question runs, so the question reaches a map that is one file
|
|
193
|
+
* behind, and the agent is told the thing it just created does not exist.
|
|
194
|
+
*
|
|
195
|
+
* So this is awaited before the write's result goes back. One round trip: the
|
|
196
|
+
* file's facts and the project's new fingerprint travel together, and the
|
|
197
|
+
* service stores, stamps and rejoins in that single call.
|
|
198
|
+
*
|
|
199
|
+
* Failure is silent on purpose. A map that could not be updated must never turn
|
|
200
|
+
* a successful write into a failed one; the next check picks the file up.
|
|
201
|
+
*/
|
|
202
|
+
async function pushFile(root, workspace, rels) {
|
|
203
|
+
const wanted = rels.filter(Boolean);
|
|
204
|
+
if (!wanted.length)
|
|
205
|
+
return false;
|
|
206
|
+
try {
|
|
207
|
+
const state = (0, build_1.projectState)(root);
|
|
208
|
+
// Only paths the map covers. Writing a README is not a change to the map,
|
|
209
|
+
// and paying a round trip to say so would tax every write an agent makes.
|
|
210
|
+
const indexed = new Set(state.files.map((f) => f.rel));
|
|
211
|
+
const present = wanted.filter((r) => indexed.has(r));
|
|
212
|
+
const gone = wanted.filter((r) => !indexed.has(r));
|
|
213
|
+
if (!present.length && !gone.length)
|
|
214
|
+
return false;
|
|
215
|
+
const read = present.length ? (0, build_1.readFacts)(root, present) : { facts: {}, unread: { python: 0, typescript: 0 }, seconds: 0, failures: [] };
|
|
216
|
+
const changed = {};
|
|
217
|
+
for (const rel of Object.keys(read.facts)) {
|
|
218
|
+
changed[rel] = { ...read.facts[rel], stamp: state.stamps[rel] };
|
|
219
|
+
}
|
|
220
|
+
const sent = await (0, gateway_1.ask)({
|
|
221
|
+
intent: "graph", prompt: "", graphOp: "update", workspace,
|
|
222
|
+
graphPayload: {
|
|
223
|
+
changed,
|
|
224
|
+
// A file the agent deleted or moved away leaves the map in the same
|
|
225
|
+
// call, or the map keeps answering questions about something gone.
|
|
226
|
+
removed: gone,
|
|
227
|
+
// The stamp, so the service can mark the map current without a second
|
|
228
|
+
// trip. This is what makes the write and the question safe in one step.
|
|
229
|
+
commit: state.commit,
|
|
230
|
+
fingerprint: state.fingerprint.hash,
|
|
231
|
+
file_count: state.files.length,
|
|
232
|
+
unread_files: 0,
|
|
233
|
+
coverage: coverageSentence({ python: 0, typescript: 0 }, state.files.length),
|
|
234
|
+
read_seconds: String(read.seconds),
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
return Boolean(sent.ok);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** One file that was found, and what the server made of it. */
|
|
2
|
+
export interface Attached {
|
|
3
|
+
id: string;
|
|
4
|
+
filename: string;
|
|
5
|
+
/** What the extractor called it: `pdf`, `png`, `text` and so on. */
|
|
6
|
+
format: string;
|
|
7
|
+
/** Characters of text pulled out. Zero for a picture, which is not a failure. */
|
|
8
|
+
chars: number;
|
|
9
|
+
/** The text was longer than one turn can carry and was cut. */
|
|
10
|
+
truncated: boolean;
|
|
11
|
+
isImage: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** A path somebody named, before anything has been read. */
|
|
14
|
+
export interface Mention {
|
|
15
|
+
/** Exactly as it appeared, so it can be taken back out of the sentence. */
|
|
16
|
+
raw: string;
|
|
17
|
+
/** Resolved, unescaped and known to exist. */
|
|
18
|
+
file: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Take the shell's escaping back off a path.
|
|
22
|
+
*
|
|
23
|
+
* A dragged file arrives as `/Users/me/My\ Report.pdf`, and a pasted one is
|
|
24
|
+
* often in quotes. Both are the shell's way of saying "this is one thing", and
|
|
25
|
+
* neither is part of the name.
|
|
26
|
+
*/
|
|
27
|
+
export declare function unescapePath(token: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Every file the typed line points at, in the order they were named.
|
|
30
|
+
*
|
|
31
|
+
* A token that looks like a path but is not on the disk is left alone and stays
|
|
32
|
+
* part of the question. Somebody writing about a path they plan to create is
|
|
33
|
+
* doing something perfectly ordinary, and refusing their line over it would be
|
|
34
|
+
* the tool arguing with them.
|
|
35
|
+
*/
|
|
36
|
+
export declare function mentions(line: string, cwd: string): Mention[];
|
|
37
|
+
/**
|
|
38
|
+
* The question with the long paths taken out and the file names left in.
|
|
39
|
+
*
|
|
40
|
+
* "summarize @/Users/me/Downloads/Q3 Board Pack.pdf" is a worse question than
|
|
41
|
+
* "summarize Q3 Board Pack.pdf", and the second is what the model should be
|
|
42
|
+
* reading: the file is already in front of it under that name, so the path adds
|
|
43
|
+
* nothing but a place for the model to try to open something it cannot reach.
|
|
44
|
+
*/
|
|
45
|
+
export declare function withoutPaths(line: string, found: Mention[]): string;
|
|
46
|
+
/**
|
|
47
|
+
* Hand the files to the server and get back the ids the question will carry.
|
|
48
|
+
*
|
|
49
|
+
* One at a time and in order, because the caps are per file and a refusal names
|
|
50
|
+
* which file it is refusing. A file that cannot be read does not stop the
|
|
51
|
+
* others: the person gets their answer about the four that worked, and is told
|
|
52
|
+
* plainly about the one that did not.
|
|
53
|
+
*/
|
|
54
|
+
export declare function upload(baseUrl: string, credential: string, files: string[]): Promise<{
|
|
55
|
+
attached: Attached[];
|
|
56
|
+
refused: {
|
|
57
|
+
file: string;
|
|
58
|
+
why: string;
|
|
59
|
+
}[];
|
|
60
|
+
}>;
|
|
61
|
+
/** What was attached, in one line each, for the person to see before it runs. */
|
|
62
|
+
export declare function describe(a: Attached): string;
|
|
@@ -0,0 +1,228 @@
|
|
|
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.unescapePath = unescapePath;
|
|
37
|
+
exports.mentions = mentions;
|
|
38
|
+
exports.withoutPaths = withoutPaths;
|
|
39
|
+
exports.upload = upload;
|
|
40
|
+
exports.describe = describe;
|
|
41
|
+
/**
|
|
42
|
+
* Files handed to a question, from a terminal.
|
|
43
|
+
*
|
|
44
|
+
* ── What a person actually does ───────────────────────────────────────────
|
|
45
|
+
*
|
|
46
|
+
* They drag a file onto the terminal window. Every terminal answers that by
|
|
47
|
+
* typing the file's path at the cursor, with spaces backslash-escaped, which is
|
|
48
|
+
* why a path is not simply "the text up to the next space". Or they type `@`
|
|
49
|
+
* and a path, which is what everybody who has used one of these tools expects.
|
|
50
|
+
*
|
|
51
|
+
* Both end up here, and both mean the same thing: read this, it is what my
|
|
52
|
+
* question is about.
|
|
53
|
+
*
|
|
54
|
+
* ── Why the file is uploaded and not just named ───────────────────────────
|
|
55
|
+
*
|
|
56
|
+
* The agent can already open any file inside the folder it is working in, so
|
|
57
|
+
* naming one would have been cheaper. It would also have been wrong three ways:
|
|
58
|
+
* a screenshot is not text and could never be read that way, a PDF or a
|
|
59
|
+
* spreadsheet needs extracting before it means anything, and a file somewhere
|
|
60
|
+
* else on the disk is outside the folder the agent is allowed to touch. A
|
|
61
|
+
* person who attaches a file has said they want it read, so it is read.
|
|
62
|
+
*
|
|
63
|
+
* The reading happens on the server, by the same component the web composer
|
|
64
|
+
* uses. Nothing is stored: the text lives in memory for half an hour and then
|
|
65
|
+
* it is gone.
|
|
66
|
+
*
|
|
67
|
+
* ── What counts as a path, and what deliberately does not ─────────────────
|
|
68
|
+
*
|
|
69
|
+
* Only something said on purpose. An absolute path, a path starting `~/`, `./`
|
|
70
|
+
* or `../`, anything quoted, and anything after an `@`. A bare word like
|
|
71
|
+
* `package.json` mentioned in a sentence is NOT attached, even when a file of
|
|
72
|
+
* that name is sitting right there: the agent can open it itself for nothing,
|
|
73
|
+
* and silently uploading every filename somebody mentions would surprise them
|
|
74
|
+
* and charge them for it.
|
|
75
|
+
*/
|
|
76
|
+
const fs = __importStar(require("node:fs"));
|
|
77
|
+
const os = __importStar(require("node:os"));
|
|
78
|
+
const path = __importStar(require("node:path"));
|
|
79
|
+
/**
|
|
80
|
+
* Take the shell's escaping back off a path.
|
|
81
|
+
*
|
|
82
|
+
* A dragged file arrives as `/Users/me/My\ Report.pdf`, and a pasted one is
|
|
83
|
+
* often in quotes. Both are the shell's way of saying "this is one thing", and
|
|
84
|
+
* neither is part of the name.
|
|
85
|
+
*/
|
|
86
|
+
function unescapePath(token) {
|
|
87
|
+
let out = token.trim();
|
|
88
|
+
if ((out.startsWith('"') && out.endsWith('"'))
|
|
89
|
+
|| (out.startsWith("'") && out.endsWith("'"))) {
|
|
90
|
+
out = out.slice(1, -1);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
out = out.replace(/\\(.)/g, '$1');
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
/** `~/notes.md` against this person's home folder. */
|
|
98
|
+
function expand(file, cwd) {
|
|
99
|
+
const home = os.homedir();
|
|
100
|
+
const full = file === '~' ? home
|
|
101
|
+
: file.startsWith('~/') ? path.join(home, file.slice(2))
|
|
102
|
+
: path.resolve(cwd, file);
|
|
103
|
+
return full;
|
|
104
|
+
}
|
|
105
|
+
function isFile(file) {
|
|
106
|
+
try {
|
|
107
|
+
return fs.statSync(file).isFile();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Every file the typed line points at, in the order they were named.
|
|
115
|
+
*
|
|
116
|
+
* A token that looks like a path but is not on the disk is left alone and stays
|
|
117
|
+
* part of the question. Somebody writing about a path they plan to create is
|
|
118
|
+
* doing something perfectly ordinary, and refusing their line over it would be
|
|
119
|
+
* the tool arguing with them.
|
|
120
|
+
*/
|
|
121
|
+
function mentions(line, cwd) {
|
|
122
|
+
const found = [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const consider = (raw, candidate) => {
|
|
125
|
+
const file = expand(unescapePath(candidate), cwd);
|
|
126
|
+
if (!isFile(file) || seen.has(file))
|
|
127
|
+
return;
|
|
128
|
+
seen.add(file);
|
|
129
|
+
found.push({ raw, file });
|
|
130
|
+
};
|
|
131
|
+
// Quoted first, because a quoted path may contain the spaces that would
|
|
132
|
+
// otherwise end a token, and the quotes are the person saying exactly that.
|
|
133
|
+
for (const m of line.matchAll(/"([^"]+)"|'([^']+)'/g)) {
|
|
134
|
+
consider(m[0], m[1] ?? m[2] ?? '');
|
|
135
|
+
}
|
|
136
|
+
// Then `@`, which is treated generously on purpose.
|
|
137
|
+
//
|
|
138
|
+
// A dragged file arrives escaped and a pasted one is usually quoted, but
|
|
139
|
+
// somebody who TYPES `@` and then a folder with a space in its name has said
|
|
140
|
+
// what they mean as plainly as it can be said. So everything after the `@` is
|
|
141
|
+
// tried, then the last word is dropped and it is tried again, until a real
|
|
142
|
+
// file turns up or the words run out. `@/Users/me/My Report.md in one line`
|
|
143
|
+
// finds the report and leaves the rest of the sentence alone.
|
|
144
|
+
for (const m of line.matchAll(/(?:^|\s)@(\S.*)$/gm)) {
|
|
145
|
+
const rest = m[1] ?? '';
|
|
146
|
+
for (let end = rest.length; end > 0; end = rest.lastIndexOf(' ', end - 1)) {
|
|
147
|
+
const before = found.length;
|
|
148
|
+
consider(`@${rest.slice(0, end)}`, rest.slice(0, end));
|
|
149
|
+
if (found.length > before)
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Then every run of non-space, with `\ ` counting as part of the run rather
|
|
154
|
+
// than as the end of it. This is the dragged-file case and the `@` case.
|
|
155
|
+
for (const m of line.matchAll(/(?:\\.|[^\s\\])+/g)) {
|
|
156
|
+
const raw = m[0];
|
|
157
|
+
if (!raw)
|
|
158
|
+
continue;
|
|
159
|
+
const body = raw.startsWith('@') ? raw.slice(1) : raw;
|
|
160
|
+
const deliberate = raw.startsWith('@')
|
|
161
|
+
|| /^[/~]/.test(body) || body.startsWith('./') || body.startsWith('../');
|
|
162
|
+
if (!deliberate)
|
|
163
|
+
continue;
|
|
164
|
+
consider(raw, body);
|
|
165
|
+
}
|
|
166
|
+
return found;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The question with the long paths taken out and the file names left in.
|
|
170
|
+
*
|
|
171
|
+
* "summarize @/Users/me/Downloads/Q3 Board Pack.pdf" is a worse question than
|
|
172
|
+
* "summarize Q3 Board Pack.pdf", and the second is what the model should be
|
|
173
|
+
* reading: the file is already in front of it under that name, so the path adds
|
|
174
|
+
* nothing but a place for the model to try to open something it cannot reach.
|
|
175
|
+
*/
|
|
176
|
+
function withoutPaths(line, found) {
|
|
177
|
+
let out = line;
|
|
178
|
+
for (const m of found)
|
|
179
|
+
out = out.replace(m.raw, path.basename(m.file));
|
|
180
|
+
return out.replace(/\s{2,}/g, ' ').trim();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Hand the files to the server and get back the ids the question will carry.
|
|
184
|
+
*
|
|
185
|
+
* One at a time and in order, because the caps are per file and a refusal names
|
|
186
|
+
* which file it is refusing. A file that cannot be read does not stop the
|
|
187
|
+
* others: the person gets their answer about the four that worked, and is told
|
|
188
|
+
* plainly about the one that did not.
|
|
189
|
+
*/
|
|
190
|
+
async function upload(baseUrl, credential, files) {
|
|
191
|
+
const root = baseUrl.replace(/\/+$/, '');
|
|
192
|
+
const attached = [];
|
|
193
|
+
const refused = [];
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
try {
|
|
196
|
+
const form = new FormData();
|
|
197
|
+
form.append('file', new Blob([fs.readFileSync(file)]), path.basename(file));
|
|
198
|
+
const res = await fetch(`${root}/api/v1/agent/attachments`, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: { Authorization: `Bearer ${credential}` },
|
|
201
|
+
body: form,
|
|
202
|
+
});
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
const body = await res.json().catch(() => ({}));
|
|
205
|
+
refused.push({ file, why: body.detail || `the server said ${res.status}` });
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const row = await res.json();
|
|
209
|
+
attached.push({
|
|
210
|
+
id: row.id, filename: row.filename, format: row.format,
|
|
211
|
+
chars: row.chars, truncated: row.truncated, isImage: row.is_image,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
refused.push({ file, why: err.message });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { attached, refused };
|
|
219
|
+
}
|
|
220
|
+
/** What was attached, in one line each, for the person to see before it runs. */
|
|
221
|
+
function describe(a) {
|
|
222
|
+
if (a.isImage)
|
|
223
|
+
return `${a.filename} read as a picture`;
|
|
224
|
+
const size = a.chars >= 1000
|
|
225
|
+
? `${Math.round(a.chars / 1000)}k characters`
|
|
226
|
+
: `${a.chars} characters`;
|
|
227
|
+
return `${a.filename} ${size}${a.truncated ? ', shortened to fit' : ''}`;
|
|
228
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ExecutionMode, FooterConfig, Provider } from './types';
|
|
2
|
+
export declare const HOME_DIR_NAME = ".comprism";
|
|
3
|
+
export interface ContentPolicy {
|
|
4
|
+
/**
|
|
5
|
+
* Store request and response text alongside the records. Default false, and
|
|
6
|
+
* every screen and report is built to work without it. Turning it on is a
|
|
7
|
+
* tenant's explicit decision, never a default and never a nudge.
|
|
8
|
+
*/
|
|
9
|
+
storeText: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ProviderConfig {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
/** Shell command printing the key on stdout. Its OUTPUT is never stored. */
|
|
14
|
+
keyCommand?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Config {
|
|
17
|
+
config_version: 1;
|
|
18
|
+
/** Who the records are attributed to locally. Never an email by default. */
|
|
19
|
+
user: string;
|
|
20
|
+
mode: ExecutionMode;
|
|
21
|
+
providers: Record<Provider, ProviderConfig>;
|
|
22
|
+
/** Model ids the policy may consider. Empty means the whole catalog. */
|
|
23
|
+
allowedModels: string[];
|
|
24
|
+
/**
|
|
25
|
+
* The model a session and the SDK dispatch unless the caller names another.
|
|
26
|
+
*
|
|
27
|
+
* This exists because of a real failure. The client used to dispatch the
|
|
28
|
+
* cheapest catalog entry for whatever band the structural classifier
|
|
29
|
+
* guessed - so a short research question was banded `simple`, answered by the
|
|
30
|
+
* cheapest model, and the answer was the plausible-but-useless kind this
|
|
31
|
+
* entire product exists to warn people about.
|
|
32
|
+
*
|
|
33
|
+
* Two things were wrong with that. It contradicted the release's own rule
|
|
34
|
+
* that v1 observes and never alters dispatch. And it made a cost-quality
|
|
35
|
+
* trade-off on the user's behalf using exactly the evidence the estimator
|
|
36
|
+
* refuses to guess from - none. With no evidence, the honest default is a
|
|
37
|
+
* capable model the user chose once, not the cheapest one we inferred.
|
|
38
|
+
*/
|
|
39
|
+
defaultModel: string;
|
|
40
|
+
contentPolicy: ContentPolicy;
|
|
41
|
+
/**
|
|
42
|
+
* Deployment context the tenant declares rather than the product infers.
|
|
43
|
+
*
|
|
44
|
+
* Everything here is optional and inferred from the workspace when absent.
|
|
45
|
+
* `verification` is the one worth setting by hand: whether anything checks the
|
|
46
|
+
* output before it reaches someone outside the company is what decides how
|
|
47
|
+
* expensive a wrong answer is, and a person knows it where a heuristic guesses.
|
|
48
|
+
*/
|
|
49
|
+
context: {
|
|
50
|
+
role?: string | null;
|
|
51
|
+
workspace?: string | null;
|
|
52
|
+
application?: string | null;
|
|
53
|
+
verification?: 'automated_tests' | 'human_review' | 'none' | 'unknown';
|
|
54
|
+
};
|
|
55
|
+
/** Port the local proxy listens on. */
|
|
56
|
+
proxyPort: number;
|
|
57
|
+
/**
|
|
58
|
+
* The stats footer the proxy appends to a completed answer.
|
|
59
|
+
*
|
|
60
|
+
* The proxy is the only surface with no result object and no widget area, so
|
|
61
|
+
* it is the only one that writes into the message. Turning this off restores
|
|
62
|
+
* byte-faithful passthrough in both directions; inbound stripping still runs,
|
|
63
|
+
* because a footer already sitting in someone's history has to come out
|
|
64
|
+
* whether or not new ones are being written.
|
|
65
|
+
*/
|
|
66
|
+
footer: FooterConfig;
|
|
67
|
+
created: string;
|
|
68
|
+
updated: string;
|
|
69
|
+
}
|
|
70
|
+
export type Keys = Partial<Record<Provider, string>>;
|
|
71
|
+
export declare function homeDir(): string;
|
|
72
|
+
/**
|
|
73
|
+
* Create the home directory at mode 700 if it is not there.
|
|
74
|
+
*
|
|
75
|
+
* 700 rather than the default 755: other local accounts have no business
|
|
76
|
+
* reading someone's work records, and a directory created world-readable is
|
|
77
|
+
* never noticed afterwards.
|
|
78
|
+
*/
|
|
79
|
+
export declare function ensureHome(): string;
|
|
80
|
+
export declare function configPath(): string;
|
|
81
|
+
export declare function defaultConfig(): Config;
|
|
82
|
+
export declare function loadConfig(): Config;
|
|
83
|
+
export declare function saveConfig(cfg: Config): void;
|
|
84
|
+
/**
|
|
85
|
+
* Resolve keys for every enabled provider. Environment first, then the
|
|
86
|
+
* configured key command. A provider whose key does not resolve is simply not
|
|
87
|
+
* available; that is not an error until something tries to call it.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveKeys(cfg: Config): Keys;
|
|
90
|
+
export declare function availableProviders(keys: Keys): Provider[];
|
|
91
|
+
/** Where the key for a provider would come from, for `setup` to report. */
|
|
92
|
+
export declare function keySource(cfg: Config, provider: Provider): 'env' | 'command' | 'none';
|
|
93
|
+
export declare function envVarName(provider: Provider): string;
|