@illuminis/comprism 0.1.2 → 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.
- package/README.md +98 -36
- package/out/agent/command.d.ts +75 -4
- package/out/agent/command.js +220 -25
- package/out/agent/render.d.ts +17 -2
- package/out/agent/render.js +157 -14
- package/out/agent/session.d.ts +141 -2
- package/out/agent/session.js +735 -149
- package/out/commands/agents.d.ts +2 -0
- package/out/commands/agents.js +79 -0
- package/out/commands/ask.d.ts +1 -1
- package/out/commands/ask.js +78 -11
- package/out/commands/commands-thin.js +79 -17
- package/out/commands/config.d.ts +1 -0
- package/out/commands/config.js +138 -0
- package/out/commands/cost.d.ts +1 -0
- package/out/commands/cost.js +167 -0
- package/out/commands/hooks.d.ts +1 -0
- package/out/commands/hooks.js +83 -0
- package/out/commands/install.d.ts +44 -1
- package/out/commands/install.js +198 -4
- package/out/commands/instructions.d.ts +1 -0
- package/out/commands/instructions.js +113 -0
- package/out/commands/integrations.d.ts +3 -0
- package/out/commands/integrations.js +215 -0
- package/out/commands/jobs.d.ts +5 -0
- package/out/commands/jobs.js +157 -0
- package/out/commands/login.js +188 -36
- package/out/commands/memory.d.ts +3 -0
- package/out/commands/memory.js +113 -0
- package/out/commands/permissions.d.ts +1 -0
- package/out/commands/permissions.js +94 -0
- package/out/commands/plugins.d.ts +4 -0
- package/out/commands/plugins.js +192 -0
- package/out/commands/privacy.d.ts +1 -0
- package/out/commands/privacy.js +57 -0
- package/out/commands/providerKey.d.ts +32 -0
- package/out/commands/providerKey.js +108 -0
- package/out/commands/repl.d.ts +8 -1
- package/out/commands/repl.js +1207 -118
- package/out/commands/report.d.ts +39 -0
- package/out/commands/report.js +115 -0
- package/out/commands/review.d.ts +5 -0
- package/out/commands/review.js +223 -0
- package/out/commands/sessions.d.ts +23 -0
- package/out/commands/sessions.js +115 -0
- package/out/commands/settings.d.ts +3 -1
- package/out/commands/settings.js +18 -16
- package/out/commands/skills.d.ts +21 -0
- package/out/commands/skills.js +207 -0
- package/out/commands/unattended.d.ts +7 -0
- package/out/commands/unattended.js +351 -0
- package/out/commands/update.d.ts +1 -0
- package/out/commands/update.js +123 -0
- package/out/commands/worktrees.d.ts +5 -0
- package/out/commands/worktrees.js +186 -0
- package/out/executor/browser.d.ts +14 -0
- package/out/executor/browser.js +270 -0
- package/out/executor/diagnostics.d.ts +2 -0
- package/out/executor/diagnostics.js +181 -0
- package/out/executor/files.js +270 -40
- package/out/executor/git.js +42 -29
- package/out/executor/hooks.d.ts +42 -58
- package/out/executor/hooks.js +89 -182
- package/out/executor/index.d.ts +21 -5
- package/out/executor/index.js +160 -14
- package/out/executor/paths.d.ts +6 -1
- package/out/executor/paths.js +34 -6
- package/out/executor/sandbox.d.ts +40 -0
- package/out/executor/sandbox.js +299 -0
- package/out/executor/shell.d.ts +49 -4
- package/out/executor/shell.js +302 -56
- package/out/executor/toolservers.d.ts +20 -0
- package/out/executor/toolservers.js +189 -0
- package/out/executor/worktree.d.ts +9 -0
- package/out/executor/worktree.js +119 -0
- package/out/graph/read-python.js +2 -1
- package/out/lib/attach.d.ts +56 -12
- package/out/lib/attach.js +230 -63
- package/out/lib/clipboard.d.ts +23 -0
- package/out/lib/clipboard.js +182 -0
- package/out/lib/commandlist.d.ts +20 -0
- package/out/lib/commandlist.js +58 -0
- package/out/lib/decision.d.ts +22 -0
- package/out/lib/decision.js +50 -0
- package/out/lib/fingerprint.d.ts +25 -0
- package/out/lib/fingerprint.js +58 -0
- package/out/lib/gateway.d.ts +186 -2
- package/out/lib/gateway.js +59 -4
- package/out/lib/history.d.ts +24 -0
- package/out/lib/history.js +137 -0
- package/out/lib/ide.d.ts +19 -0
- package/out/lib/ide.js +131 -0
- package/out/lib/keyboard.d.ts +95 -0
- package/out/lib/keyboard.js +383 -0
- package/out/lib/machine.d.ts +21 -0
- package/out/lib/machine.js +91 -0
- package/out/lib/notify.d.ts +4 -0
- package/out/lib/notify.js +52 -0
- package/out/lib/output.d.ts +48 -0
- package/out/lib/output.js +108 -0
- package/out/lib/project-ops.d.ts +19 -0
- package/out/lib/project-ops.js +146 -0
- package/out/lib/project.d.ts +28 -0
- package/out/lib/project.js +114 -0
- package/out/lib/prompt.js +15 -2
- package/out/lib/queue.d.ts +13 -0
- package/out/lib/queue.js +116 -0
- package/out/lib/readiness.d.ts +19 -0
- package/out/lib/readiness.js +170 -1
- package/out/lib/self.d.ts +23 -0
- package/out/lib/self.js +124 -0
- package/out/lib/sessions.d.ts +19 -0
- package/out/lib/sessions.js +221 -0
- package/out/lib/stdin.d.ts +32 -0
- package/out/lib/stdin.js +117 -0
- package/out/lib/store.d.ts +40 -0
- package/out/lib/store.js +138 -0
- package/out/lib/sync.d.ts +18 -0
- package/out/lib/sync.js +81 -0
- package/out/lib/ui.d.ts +2 -4
- package/out/lib/ui.js +31 -25
- package/out/lib/voice.js +24 -0
- package/out/postinstall.js +42 -14
- package/out/providers/anthropic.d.ts +22 -0
- package/out/providers/anthropic.js +80 -0
- package/out/providers/index.d.ts +8 -0
- package/out/providers/index.js +83 -0
- package/out/providers/openai.d.ts +11 -0
- package/out/providers/openai.js +57 -0
- package/out/thin.js +609 -21
- package/package.json +9 -49
package/out/lib/attach.js
CHANGED
|
@@ -34,10 +34,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.unescapePath = unescapePath;
|
|
37
|
+
exports.localProblem = localProblem;
|
|
37
38
|
exports.mentions = mentions;
|
|
39
|
+
exports.startsWithPath = startsWithPath;
|
|
38
40
|
exports.withoutPaths = withoutPaths;
|
|
41
|
+
exports.withoutMentions = withoutMentions;
|
|
39
42
|
exports.upload = upload;
|
|
40
43
|
exports.describe = describe;
|
|
44
|
+
exports.listing = listing;
|
|
45
|
+
exports.projectFiles = projectFiles;
|
|
46
|
+
exports.completeAt = completeAt;
|
|
41
47
|
/**
|
|
42
48
|
* Files handed to a question, from a terminal.
|
|
43
49
|
*
|
|
@@ -76,35 +82,90 @@ exports.describe = describe;
|
|
|
76
82
|
const fs = __importStar(require("node:fs"));
|
|
77
83
|
const os = __importStar(require("node:os"));
|
|
78
84
|
const path = __importStar(require("node:path"));
|
|
85
|
+
const gateway_1 = require("./gateway");
|
|
79
86
|
/**
|
|
80
87
|
* Take the shell's escaping back off a path.
|
|
81
88
|
*
|
|
82
89
|
* A dragged file arrives as `/Users/me/My\ Report.pdf`, and a pasted one is
|
|
83
90
|
* often in quotes. Both are the shell's way of saying "this is one thing", and
|
|
84
91
|
* neither is part of the name.
|
|
92
|
+
*
|
|
93
|
+
* On Windows a backslash is the folder separator and never an escape: its
|
|
94
|
+
* terminals quote a dragged path instead. Stripping backslashes there turned
|
|
95
|
+
* `C:\Users\me\notes.txt` into `C:Usersmenotes.txt`, which was the Windows
|
|
96
|
+
* failure found in prompt 01.
|
|
85
97
|
*/
|
|
86
|
-
function unescapePath(token) {
|
|
98
|
+
function unescapePath(token, platform = process.platform) {
|
|
87
99
|
let out = token.trim();
|
|
88
|
-
if ((out.startsWith('"') && out.endsWith('"'))
|
|
89
|
-
|| (out.startsWith("'") && out.endsWith("'"))) {
|
|
100
|
+
if ((out.startsWith('"') && out.endsWith('"') && out.length > 1)
|
|
101
|
+
|| (out.startsWith("'") && out.endsWith("'") && out.length > 1)) {
|
|
90
102
|
out = out.slice(1, -1);
|
|
91
103
|
}
|
|
92
|
-
else {
|
|
104
|
+
else if (platform !== 'win32') {
|
|
93
105
|
out = out.replace(/\\(.)/g, '$1');
|
|
94
106
|
}
|
|
95
107
|
return out;
|
|
96
108
|
}
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
const WINDOWS_ABSOLUTE = /^(?:[A-Za-z]:[\\/]|\\\\)/;
|
|
110
|
+
/**
|
|
111
|
+
* Where a named path points.
|
|
112
|
+
*
|
|
113
|
+
* `~/` is this person's home folder. `./` and `../` start from the folder they
|
|
114
|
+
* are in. Anything else relative starts from the project root (manual 3.1).
|
|
115
|
+
*/
|
|
116
|
+
function expand(file, root, here) {
|
|
99
117
|
const home = os.homedir();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
if (file === '~')
|
|
119
|
+
return home;
|
|
120
|
+
if (file.startsWith('~/') || file.startsWith('~\\'))
|
|
121
|
+
return path.join(home, file.slice(2));
|
|
122
|
+
if (path.isAbsolute(file) || WINDOWS_ABSOLUTE.test(file))
|
|
123
|
+
return path.resolve(file);
|
|
124
|
+
if (/^\.\.?[\\/]/.test(file))
|
|
125
|
+
return path.resolve(here, file);
|
|
126
|
+
return path.resolve(root, file);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* What stops a file being attached, checked on this machine before anything is
|
|
130
|
+
* sent (manual 3.10). Only the four facts the service cannot see: whether the
|
|
131
|
+
* file is there, is a folder, can be read, or is a link to nothing.
|
|
132
|
+
*/
|
|
133
|
+
function localProblem(file, shown) {
|
|
134
|
+
let link;
|
|
135
|
+
try {
|
|
136
|
+
link = fs.lstatSync(file);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return { code: 'not_found', message: `not found: ${shown}` };
|
|
140
|
+
}
|
|
141
|
+
let stat;
|
|
142
|
+
try {
|
|
143
|
+
stat = fs.statSync(file);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return link.isSymbolicLink()
|
|
147
|
+
? { code: 'broken_link', message: `${shown} points to a file that does not exist` }
|
|
148
|
+
: { code: 'unreadable', message: `cannot read ${shown}: permission denied` };
|
|
149
|
+
}
|
|
150
|
+
if (stat.isDirectory()) {
|
|
151
|
+
return {
|
|
152
|
+
code: 'folder',
|
|
153
|
+
message: `${shown} is a folder. Attach the files you need, or ask the agent to look in it.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
fs.closeSync(fs.openSync(file, 'r'));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return { code: 'unreadable', message: `cannot read ${shown}: permission denied` };
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
104
163
|
}
|
|
105
|
-
|
|
164
|
+
/** Something is on the disk at this path, even a link to nothing. */
|
|
165
|
+
function present(file) {
|
|
106
166
|
try {
|
|
107
|
-
|
|
167
|
+
fs.lstatSync(file);
|
|
168
|
+
return true;
|
|
108
169
|
}
|
|
109
170
|
catch {
|
|
110
171
|
return false;
|
|
@@ -113,58 +174,95 @@ function isFile(file) {
|
|
|
113
174
|
/**
|
|
114
175
|
* Every file the typed line points at, in the order they were named.
|
|
115
176
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
177
|
+
* Anything after an `@` is a request to attach, so a problem with it is
|
|
178
|
+
* reported. Any other path (absolute, `~/`, `./`, `../`, quoted) that is not a
|
|
179
|
+
* readable file is left alone and stays part of the question: somebody writing
|
|
180
|
+
* about a path they plan to create, or a folder they want looked at, is doing
|
|
181
|
+
* something perfectly ordinary.
|
|
120
182
|
*/
|
|
121
|
-
function mentions(line,
|
|
183
|
+
function mentions(line, root, here = root, platform = process.platform) {
|
|
122
184
|
const found = [];
|
|
123
185
|
const seen = new Set();
|
|
124
|
-
const consider = (raw, candidate) => {
|
|
125
|
-
const
|
|
126
|
-
if (!
|
|
127
|
-
return;
|
|
186
|
+
const consider = (raw, candidate, asked) => {
|
|
187
|
+
const shown = unescapePath(candidate, platform);
|
|
188
|
+
if (!shown)
|
|
189
|
+
return false;
|
|
190
|
+
const file = expand(shown, root, here);
|
|
191
|
+
if (seen.has(file))
|
|
192
|
+
return true;
|
|
193
|
+
const problem = localProblem(file, shown);
|
|
194
|
+
// A path in a sentence is only attached when it is a readable file, or a
|
|
195
|
+
// link to nothing (which is plainly a file the person meant).
|
|
196
|
+
if (!asked && problem && problem.code !== 'broken_link')
|
|
197
|
+
return false;
|
|
128
198
|
seen.add(file);
|
|
129
|
-
found.push({ raw, file });
|
|
199
|
+
found.push({ raw, file, shown, ...(problem ? { problem } : {}) });
|
|
200
|
+
return true;
|
|
130
201
|
};
|
|
131
202
|
// Quoted first, because a quoted path may contain the spaces that would
|
|
132
203
|
// otherwise end a token, and the quotes are the person saying exactly that.
|
|
133
|
-
|
|
134
|
-
|
|
204
|
+
// An `@` straight before the quotes belongs to the same mention.
|
|
205
|
+
for (const m of line.matchAll(/(@?)(?:"([^"]+)"|'([^']+)')/g)) {
|
|
206
|
+
consider(m[0], `"${m[2] ?? m[3] ?? ''}"`, m[1] === '@');
|
|
135
207
|
}
|
|
136
208
|
// Then `@`, which is treated generously on purpose.
|
|
137
209
|
//
|
|
138
210
|
// A dragged file arrives escaped and a pasted one is usually quoted, but
|
|
139
211
|
// somebody who TYPES `@` and then a folder with a space in its name has said
|
|
140
212
|
// 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
|
|
142
|
-
//
|
|
143
|
-
// finds the report and leaves the rest of the sentence alone.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
213
|
+
// tried, then the last word is dropped and it is tried again, until something
|
|
214
|
+
// on the disk turns up or the words run out. `@/Users/me/My Report.md in one
|
|
215
|
+
// line` finds the report and leaves the rest of the sentence alone. When
|
|
216
|
+
// nothing turns up, the first word is what was named, and it is not found.
|
|
217
|
+
// Every `@`, each with the rest of the line after it. A single match of
|
|
218
|
+
// "@ to end of line" swallowed every later `@` on the same line, so
|
|
219
|
+
// `@a.txt @b.txt` only ever looked at the first.
|
|
220
|
+
for (const m of line.matchAll(/(?:^|\s)@(?=[^\s"'])/g)) {
|
|
221
|
+
const rest = (line.slice((m.index ?? 0) + m[0].length).split(/\r?\n/)[0]) ?? '';
|
|
222
|
+
let hit = false;
|
|
223
|
+
// Punctuation that closes a sentence is tried off as well as on, because
|
|
224
|
+
// `@layout.png,` means layout.png. Tried second, so a name that really
|
|
225
|
+
// ends in one of these is still found first.
|
|
226
|
+
const trimmed = (text) => text.replace(/[,.:;!?)\]]+$/, '');
|
|
227
|
+
for (let end = rest.length; end > 0 && !hit; end = rest.lastIndexOf(' ', end - 1)) {
|
|
228
|
+
for (const candidate of new Set([rest.slice(0, end), trimmed(rest.slice(0, end))])) {
|
|
229
|
+
if (!candidate || !present(expand(unescapePath(candidate, platform), root, here)))
|
|
230
|
+
continue;
|
|
231
|
+
hit = consider(`@${candidate}`, candidate, true);
|
|
150
232
|
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!hit) {
|
|
236
|
+
const word = (platform === 'win32' ? /^\S+/ : /^(?:\\.|[^\s\\])+/).exec(rest)?.[0];
|
|
237
|
+
const named = word ? trimmed(word) || word : '';
|
|
238
|
+
if (named)
|
|
239
|
+
consider(`@${named}`, named, true);
|
|
151
240
|
}
|
|
152
241
|
}
|
|
153
242
|
// Then every run of non-space, with `\ ` counting as part of the run rather
|
|
154
|
-
// than as the end of it
|
|
155
|
-
|
|
243
|
+
// than as the end of it (except on Windows, where a backslash is a folder).
|
|
244
|
+
// This is the dragged-file case.
|
|
245
|
+
const runs = platform === 'win32' ? /\S+/g : /(?:\\.|[^\s\\])+/g;
|
|
246
|
+
for (const m of line.matchAll(runs)) {
|
|
156
247
|
const raw = m[0];
|
|
157
|
-
if (!raw)
|
|
248
|
+
if (!raw || raw.startsWith('@') || raw.startsWith('"') || raw.startsWith("'"))
|
|
158
249
|
continue;
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|| /^[/~]/.test(body) || body.startsWith('./') || body.startsWith('../');
|
|
250
|
+
const deliberate = /^[/~]/.test(raw) || /^\.\.?[\\/]/.test(raw)
|
|
251
|
+
|| WINDOWS_ABSOLUTE.test(raw);
|
|
162
252
|
if (!deliberate)
|
|
163
253
|
continue;
|
|
164
|
-
consider(raw,
|
|
254
|
+
consider(raw, raw, false);
|
|
165
255
|
}
|
|
166
256
|
return found;
|
|
167
257
|
}
|
|
258
|
+
/** The line begins with an absolute path to something on the disk, as a
|
|
259
|
+
* dragged file does, rather than with a slashed command word. */
|
|
260
|
+
function startsWithPath(line, platform = process.platform) {
|
|
261
|
+
const first = (platform === 'win32' ? /^\S+/ : /^(?:\\.|[^\s\\])+/).exec(line.trim())?.[0];
|
|
262
|
+
if (!first || !first.startsWith('/') || first.indexOf('/', 1) < 0)
|
|
263
|
+
return false;
|
|
264
|
+
return present(unescapePath(first, platform));
|
|
265
|
+
}
|
|
168
266
|
/**
|
|
169
267
|
* The question with the long paths taken out and the file names left in.
|
|
170
268
|
*
|
|
@@ -179,13 +277,31 @@ function withoutPaths(line, found) {
|
|
|
179
277
|
out = out.replace(m.raw, path.basename(m.file));
|
|
180
278
|
return out.replace(/\s{2,}/g, ' ').trim();
|
|
181
279
|
}
|
|
280
|
+
/** The line with every mention taken out: empty when it named files and said
|
|
281
|
+
* nothing else, which is a drag and drop waiting for its question. */
|
|
282
|
+
function withoutMentions(line, found) {
|
|
283
|
+
let out = line;
|
|
284
|
+
for (const m of found)
|
|
285
|
+
out = out.replace(m.raw, ' ');
|
|
286
|
+
return out.replace(/\s+/g, ' ').trim();
|
|
287
|
+
}
|
|
288
|
+
function fromServer(row) {
|
|
289
|
+
return {
|
|
290
|
+
id: row.id, filename: row.filename, format: row.format,
|
|
291
|
+
chars: row.chars, truncated: row.truncated, isImage: row.is_image,
|
|
292
|
+
sizeBytes: row.size_bytes ?? 0,
|
|
293
|
+
// An older service sent no summary. The bare facts, never a local verdict.
|
|
294
|
+
summary: row.summary
|
|
295
|
+
?? (row.is_image ? row.format : `${row.format} ${row.chars} chars`),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
182
298
|
/**
|
|
183
299
|
* Hand the files to the server and get back the ids the question will carry.
|
|
184
300
|
*
|
|
185
301
|
* One at a time and in order, because the caps are per file and a refusal names
|
|
186
|
-
* which file it is refusing.
|
|
187
|
-
*
|
|
188
|
-
*
|
|
302
|
+
* which file it is refusing. The service is asked first, by name and size, so a
|
|
303
|
+
* program file or a file over the limit is refused before a byte of it moves.
|
|
304
|
+
* A file that cannot be read does not stop the others.
|
|
189
305
|
*/
|
|
190
306
|
async function upload(baseUrl, credential, files) {
|
|
191
307
|
const root = baseUrl.replace(/\/+$/, '');
|
|
@@ -193,23 +309,27 @@ async function upload(baseUrl, credential, files) {
|
|
|
193
309
|
const refused = [];
|
|
194
310
|
for (const file of files) {
|
|
195
311
|
try {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
headers: { Authorization: `Bearer ${credential}` },
|
|
201
|
-
body: form,
|
|
312
|
+
const name = path.basename(file);
|
|
313
|
+
const size = fs.statSync(file).size;
|
|
314
|
+
const granted = await (0, gateway_1.ask)({
|
|
315
|
+
intent: 'attach', prompt: '', attachment: { filename: name, size_bytes: size },
|
|
202
316
|
});
|
|
317
|
+
if (granted.ok && granted.served !== false && granted.granted === false) {
|
|
318
|
+
refused.push({ file, why: granted.message || `${name} was not accepted`,
|
|
319
|
+
code: granted.refusal ?? undefined });
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const pass = granted.ok && granted.served !== false ? granted.upload : null;
|
|
323
|
+
const where = pass?.upload_path || '/api/v1/agent/attachments';
|
|
324
|
+
const form = new FormData();
|
|
325
|
+
form.append('file', new Blob([fs.readFileSync(file)]), name);
|
|
326
|
+
const res = await fetch(`${root}${where}${pass?.ticket ? `?ticket=${encodeURIComponent(pass.ticket)}` : ''}`, { method: 'POST', headers: { Authorization: `Bearer ${credential}` }, body: form });
|
|
203
327
|
if (!res.ok) {
|
|
204
328
|
const body = await res.json().catch(() => ({}));
|
|
205
|
-
refused.push({ file, why: body.detail || `the server said ${res.status}
|
|
329
|
+
refused.push({ file, why: body.detail || `the server said ${res.status}`, code: body.code });
|
|
206
330
|
continue;
|
|
207
331
|
}
|
|
208
|
-
|
|
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
|
-
});
|
|
332
|
+
attached.push(fromServer(await res.json()));
|
|
213
333
|
}
|
|
214
334
|
catch (err) {
|
|
215
335
|
refused.push({ file, why: err.message });
|
|
@@ -217,12 +337,59 @@ async function upload(baseUrl, credential, files) {
|
|
|
217
337
|
}
|
|
218
338
|
return { attached, refused };
|
|
219
339
|
}
|
|
220
|
-
/**
|
|
221
|
-
function describe(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
340
|
+
/** One `attached` line per file, names in a column, as manual 3.1 shows. */
|
|
341
|
+
function describe(items) {
|
|
342
|
+
const width = Math.max(0, ...items.map((a) => a.filename.length));
|
|
343
|
+
return items.map((a) => `attached ${a.filename.padEnd(width)} ${a.summary}`);
|
|
344
|
+
}
|
|
345
|
+
/** The waiting list, numbered for `detach N` (manual 3.6). */
|
|
346
|
+
function listing(items) {
|
|
347
|
+
const width = Math.max(0, ...items.map((a) => a.filename.length));
|
|
348
|
+
return items.map((a, i) => `${String(i + 1).padStart(2)} ${a.filename.padEnd(width)} ${a.summary}`);
|
|
349
|
+
}
|
|
350
|
+
/** Folders never offered by Tab: nobody attaches from them. */
|
|
351
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', 'build', 'out', '__pycache__', '.venv', 'venv']);
|
|
352
|
+
/**
|
|
353
|
+
* File names from the project for Tab after `@` (manual 3.1).
|
|
354
|
+
*
|
|
355
|
+
* A bounded walk, because a completion that takes a second is one nobody uses.
|
|
356
|
+
*/
|
|
357
|
+
function projectFiles(root, limit = 5000) {
|
|
358
|
+
const out = [];
|
|
359
|
+
const walk = (dir) => {
|
|
360
|
+
if (out.length >= limit)
|
|
361
|
+
return;
|
|
362
|
+
let entries;
|
|
363
|
+
try {
|
|
364
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
if (out.length >= limit)
|
|
371
|
+
return;
|
|
372
|
+
if (entry.isDirectory()) {
|
|
373
|
+
if (entry.name.startsWith('.') || SKIP.has(entry.name))
|
|
374
|
+
continue;
|
|
375
|
+
walk(path.join(dir, entry.name));
|
|
376
|
+
}
|
|
377
|
+
else if (entry.isFile()) {
|
|
378
|
+
out.push(path.relative(root, path.join(dir, entry.name)).split(path.sep).join('/'));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
walk(root);
|
|
383
|
+
return out.sort();
|
|
384
|
+
}
|
|
385
|
+
/** What Tab completes the line to, in readline's shape: the choices, and the
|
|
386
|
+
* part of the line they replace. */
|
|
387
|
+
function completeAt(line, files) {
|
|
388
|
+
const m = /(?:^|\s)(@[^\s]*)$/.exec(line);
|
|
389
|
+
if (!m)
|
|
390
|
+
return [[], line];
|
|
391
|
+
const typed = m[1];
|
|
392
|
+
const prefix = typed.slice(1);
|
|
393
|
+
const hits = files().filter((f) => f.startsWith(prefix)).map((f) => `@${f}`);
|
|
394
|
+
return [hits.slice(0, 200), typed];
|
|
228
395
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** What reading the clipboard came to. */
|
|
2
|
+
export type Clip = {
|
|
3
|
+
ok: true;
|
|
4
|
+
file: string;
|
|
5
|
+
folder: string;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
why: string;
|
|
9
|
+
};
|
|
10
|
+
/** Runs one program and reports its exit, output and whether it was found.
|
|
11
|
+
* With `out`, the program's output is written to that file as bytes. */
|
|
12
|
+
export type Runner = (file: string, args: string[], out?: string) => Promise<{
|
|
13
|
+
code: number;
|
|
14
|
+
stdout: string;
|
|
15
|
+
missing: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Save the clipboard's picture as `clipboard-N.png` in a new temporary folder.
|
|
19
|
+
* `n` numbers the pastes in one session, so three screenshots are three names.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readClipboardImage(n: number, exec?: Runner, platform?: string): Promise<Clip>;
|
|
22
|
+
/** Remove a paste's temporary folder. Always called, whatever happened. */
|
|
23
|
+
export declare function discard(folder: string): void;
|
|
@@ -0,0 +1,182 @@
|
|
|
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.readClipboardImage = readClipboardImage;
|
|
37
|
+
exports.discard = discard;
|
|
38
|
+
/**
|
|
39
|
+
* The picture on the clipboard, saved to a temporary file (manual 3.4).
|
|
40
|
+
*
|
|
41
|
+
* A terminal cannot hand a program a picture: Ctrl+V and Cmd+V paste text.
|
|
42
|
+
* So `paste` asks the operating system for the clipboard's picture with the
|
|
43
|
+
* tool each one already has, writes it to a file in a fresh temporary folder,
|
|
44
|
+
* and the session attaches that file like any other. The caller removes the
|
|
45
|
+
* folder straight after the upload; nothing is kept.
|
|
46
|
+
*
|
|
47
|
+
* macOS osascript, which every Mac has (pngpaste is used when present)
|
|
48
|
+
* Linux wl-paste on Wayland, xclip on X11
|
|
49
|
+
* Windows PowerShell and the .NET clipboard
|
|
50
|
+
*
|
|
51
|
+
* Every program is run with an argument list, never a shell string, so
|
|
52
|
+
* nothing in a path is ever read as a command.
|
|
53
|
+
*/
|
|
54
|
+
const node_child_process_1 = require("node:child_process");
|
|
55
|
+
const fs = __importStar(require("node:fs"));
|
|
56
|
+
const os = __importStar(require("node:os"));
|
|
57
|
+
const path = __importStar(require("node:path"));
|
|
58
|
+
const run = (file, args, out) => new Promise((resolve) => {
|
|
59
|
+
(0, node_child_process_1.execFile)(file, args, { encoding: out ? 'buffer' : 'utf8', timeout: 15_000,
|
|
60
|
+
maxBuffer: 64 * 1024 * 1024 }, (err, stdout) => {
|
|
61
|
+
const failure = err;
|
|
62
|
+
if (out && !err && Buffer.isBuffer(stdout))
|
|
63
|
+
fs.writeFileSync(out, stdout);
|
|
64
|
+
resolve({
|
|
65
|
+
code: failure ? (typeof failure.code === 'number' ? failure.code : 1) : 0,
|
|
66
|
+
stdout: Buffer.isBuffer(stdout) ? '' : String(stdout ?? ''),
|
|
67
|
+
missing: failure?.code === 'ENOENT',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
const SAVE_INSTEAD = 'save the picture and attach its path instead.';
|
|
72
|
+
const NO_IMAGE = 'The clipboard holds no image. Nothing was attached.';
|
|
73
|
+
const TEXT_ONLY = 'The clipboard holds no image, only text. Nothing was attached.';
|
|
74
|
+
function landed(file) {
|
|
75
|
+
try {
|
|
76
|
+
return fs.statSync(file).size > 0;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function mac(file, exec) {
|
|
83
|
+
const types = await exec('osascript', ['-e', 'clipboard info']);
|
|
84
|
+
const info = types.stdout;
|
|
85
|
+
if (!/PNGf|TIFF|JPEG|GIFf|picture/i.test(info)) {
|
|
86
|
+
return /utf8|string|text/i.test(info) ? TEXT_ONLY : NO_IMAGE;
|
|
87
|
+
}
|
|
88
|
+
const quick = await exec('pngpaste', [file]);
|
|
89
|
+
if (!quick.missing && quick.code === 0 && landed(file))
|
|
90
|
+
return null;
|
|
91
|
+
// AppleScript writes the clipboard's PNG straight into the file. The path is
|
|
92
|
+
// an argument to the script, never part of its text.
|
|
93
|
+
const script = [
|
|
94
|
+
'on run argv',
|
|
95
|
+
'set f to open for access (POSIX file (item 1 of argv)) with write permission',
|
|
96
|
+
'try',
|
|
97
|
+
'write (the clipboard as «class PNGf») to f',
|
|
98
|
+
'end try',
|
|
99
|
+
'close access f',
|
|
100
|
+
'end run',
|
|
101
|
+
];
|
|
102
|
+
await exec('osascript', [...script.flatMap((l) => ['-e', l]), file]);
|
|
103
|
+
return landed(file) ? null : NO_IMAGE;
|
|
104
|
+
}
|
|
105
|
+
async function linux(file, exec) {
|
|
106
|
+
const wayland = Boolean(process.env.WAYLAND_DISPLAY);
|
|
107
|
+
const x11 = Boolean(process.env.DISPLAY);
|
|
108
|
+
if (!wayland && !x11) {
|
|
109
|
+
return `There is no desktop session here to paste from, so ${SAVE_INSTEAD}`;
|
|
110
|
+
}
|
|
111
|
+
if (wayland) {
|
|
112
|
+
const types = await exec('wl-paste', ['--list-types']);
|
|
113
|
+
if (!types.missing) {
|
|
114
|
+
if (!/image\/png/.test(types.stdout)) {
|
|
115
|
+
return /text\//.test(types.stdout) ? TEXT_ONLY : NO_IMAGE;
|
|
116
|
+
}
|
|
117
|
+
const got = await exec('wl-paste', ['--type', 'image/png'], file);
|
|
118
|
+
return got.code === 0 && landed(file) ? null : NO_IMAGE;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (x11) {
|
|
122
|
+
const targets = await exec('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o']);
|
|
123
|
+
if (!targets.missing) {
|
|
124
|
+
if (!/image\/png/.test(targets.stdout)) {
|
|
125
|
+
return /UTF8_STRING|STRING|text\//.test(targets.stdout) ? TEXT_ONLY : NO_IMAGE;
|
|
126
|
+
}
|
|
127
|
+
const got = await exec('xclip', ['-selection', 'clipboard', '-t', 'image/png', '-o'], file);
|
|
128
|
+
return got.code === 0 && landed(file) ? null : NO_IMAGE;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return `Pasting a picture needs ${wayland ? 'wl-paste' : 'xclip'}, which is not installed. `
|
|
132
|
+
+ `Install it, or ${SAVE_INSTEAD}`;
|
|
133
|
+
}
|
|
134
|
+
async function windows(file, exec) {
|
|
135
|
+
// The file name reaches PowerShell as an argument, never inside the script.
|
|
136
|
+
const script = [
|
|
137
|
+
'param($out)',
|
|
138
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
139
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
140
|
+
'$img = [System.Windows.Forms.Clipboard]::GetImage()',
|
|
141
|
+
'if ($img) { $img.Save($out, [System.Drawing.Imaging.ImageFormat]::Png); "image" }',
|
|
142
|
+
'elseif ([System.Windows.Forms.Clipboard]::ContainsText()) { "text" }',
|
|
143
|
+
'else { "empty" }',
|
|
144
|
+
].join('; ');
|
|
145
|
+
const got = await exec('powershell.exe', ['-NoProfile', '-STA', '-Command', `& { ${script} }`, file]);
|
|
146
|
+
if (got.missing)
|
|
147
|
+
return `Pasting a picture needs PowerShell, which was not found, so ${SAVE_INSTEAD}`;
|
|
148
|
+
if (/image/.test(got.stdout) && landed(file))
|
|
149
|
+
return null;
|
|
150
|
+
return /text/.test(got.stdout) ? TEXT_ONLY : NO_IMAGE;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Save the clipboard's picture as `clipboard-N.png` in a new temporary folder.
|
|
154
|
+
* `n` numbers the pastes in one session, so three screenshots are three names.
|
|
155
|
+
*/
|
|
156
|
+
async function readClipboardImage(n, exec = run, platform = process.platform) {
|
|
157
|
+
const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'comprism-paste-'));
|
|
158
|
+
const file = path.join(folder, `clipboard-${n}.png`);
|
|
159
|
+
let why;
|
|
160
|
+
try {
|
|
161
|
+
why = platform === 'darwin' ? await mac(file, exec)
|
|
162
|
+
: platform === 'win32' ? await windows(file, exec)
|
|
163
|
+
: await linux(file, exec);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
why = `The clipboard could not be read (${err.message}), so ${SAVE_INSTEAD}`;
|
|
167
|
+
}
|
|
168
|
+
if (why) {
|
|
169
|
+
discard(folder);
|
|
170
|
+
return { ok: false, why };
|
|
171
|
+
}
|
|
172
|
+
return { ok: true, file, folder };
|
|
173
|
+
}
|
|
174
|
+
/** Remove a paste's temporary folder. Always called, whatever happened. */
|
|
175
|
+
function discard(folder) {
|
|
176
|
+
try {
|
|
177
|
+
fs.rmSync(folder, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
/* already gone */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Listed {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
/** `session`, `project skill`, `company skill`, `project command`, ... */
|
|
5
|
+
source: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CommandList {
|
|
8
|
+
/** False when the service could not be asked. Nothing below is then known. */
|
|
9
|
+
ok: boolean;
|
|
10
|
+
items: Listed[];
|
|
11
|
+
/** Words the service knows that this version of the tool does not run. */
|
|
12
|
+
needsUpdate: string[];
|
|
13
|
+
/** Whether typed requests may be kept on this machine between sessions. */
|
|
14
|
+
keepHistory: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function readOne(root: string, rel: string): string | null;
|
|
17
|
+
export declare function listOne(root: string, rel: string): string[] | null;
|
|
18
|
+
/** Ask the service for this project's list. `words` are the session words
|
|
19
|
+
* this tool runs. */
|
|
20
|
+
export declare function fetchCommandList(root: string, words: string[]): Promise<CommandList>;
|