@illuminis/comprism 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +61 -2
  2. package/out/agent/command.d.ts +75 -4
  3. package/out/agent/command.js +221 -26
  4. package/out/agent/render.d.ts +56 -8
  5. package/out/agent/render.js +441 -39
  6. package/out/agent/session.d.ts +156 -2
  7. package/out/agent/session.js +795 -154
  8. package/out/commands/agents.d.ts +2 -0
  9. package/out/commands/agents.js +79 -0
  10. package/out/commands/ask.d.ts +1 -1
  11. package/out/commands/ask.js +88 -12
  12. package/out/commands/commands-thin.js +79 -17
  13. package/out/commands/config.d.ts +1 -0
  14. package/out/commands/config.js +138 -0
  15. package/out/commands/cost.d.ts +1 -0
  16. package/out/commands/cost.js +167 -0
  17. package/out/commands/hooks.d.ts +1 -0
  18. package/out/commands/hooks.js +83 -0
  19. package/out/commands/install.d.ts +44 -1
  20. package/out/commands/install.js +198 -4
  21. package/out/commands/instructions.d.ts +1 -0
  22. package/out/commands/instructions.js +113 -0
  23. package/out/commands/integrations.d.ts +3 -0
  24. package/out/commands/integrations.js +215 -0
  25. package/out/commands/jobs.d.ts +5 -0
  26. package/out/commands/jobs.js +157 -0
  27. package/out/commands/login.js +249 -43
  28. package/out/commands/memory.d.ts +3 -0
  29. package/out/commands/memory.js +113 -0
  30. package/out/commands/permissions.d.ts +1 -0
  31. package/out/commands/permissions.js +94 -0
  32. package/out/commands/plugins.d.ts +4 -0
  33. package/out/commands/plugins.js +192 -0
  34. package/out/commands/privacy.d.ts +1 -0
  35. package/out/commands/privacy.js +57 -0
  36. package/out/commands/providerKey.d.ts +32 -0
  37. package/out/commands/providerKey.js +108 -0
  38. package/out/commands/repl.d.ts +12 -1
  39. package/out/commands/repl.js +1649 -122
  40. package/out/commands/report.d.ts +39 -0
  41. package/out/commands/report.js +115 -0
  42. package/out/commands/review.d.ts +5 -0
  43. package/out/commands/review.js +223 -0
  44. package/out/commands/sessions.d.ts +23 -0
  45. package/out/commands/sessions.js +115 -0
  46. package/out/commands/settings.d.ts +3 -1
  47. package/out/commands/settings.js +18 -16
  48. package/out/commands/skills.d.ts +21 -0
  49. package/out/commands/skills.js +207 -0
  50. package/out/commands/unattended.d.ts +7 -0
  51. package/out/commands/unattended.js +351 -0
  52. package/out/commands/update.d.ts +1 -0
  53. package/out/commands/update.js +123 -0
  54. package/out/commands/welcome.d.ts +25 -1
  55. package/out/commands/welcome.js +71 -35
  56. package/out/commands/worktrees.d.ts +5 -0
  57. package/out/commands/worktrees.js +186 -0
  58. package/out/executor/browser.d.ts +14 -0
  59. package/out/executor/browser.js +270 -0
  60. package/out/executor/diagnostics.d.ts +2 -0
  61. package/out/executor/diagnostics.js +181 -0
  62. package/out/executor/files.js +270 -40
  63. package/out/executor/git.js +42 -29
  64. package/out/executor/hooks.d.ts +42 -58
  65. package/out/executor/hooks.js +89 -182
  66. package/out/executor/index.d.ts +21 -5
  67. package/out/executor/index.js +160 -14
  68. package/out/executor/paths.d.ts +6 -1
  69. package/out/executor/paths.js +34 -6
  70. package/out/executor/sandbox.d.ts +40 -0
  71. package/out/executor/sandbox.js +299 -0
  72. package/out/executor/shell.d.ts +49 -4
  73. package/out/executor/shell.js +302 -56
  74. package/out/executor/toolservers.d.ts +20 -0
  75. package/out/executor/toolservers.js +189 -0
  76. package/out/executor/worktree.d.ts +9 -0
  77. package/out/executor/worktree.js +119 -0
  78. package/out/graph/read-python.js +2 -1
  79. package/out/lib/attach.d.ts +56 -12
  80. package/out/lib/attach.js +230 -63
  81. package/out/lib/clipboard.d.ts +23 -0
  82. package/out/lib/clipboard.js +182 -0
  83. package/out/lib/commandlist.d.ts +20 -0
  84. package/out/lib/commandlist.js +58 -0
  85. package/out/lib/decision.d.ts +22 -0
  86. package/out/lib/decision.js +50 -0
  87. package/out/lib/fingerprint.d.ts +25 -0
  88. package/out/lib/fingerprint.js +58 -0
  89. package/out/lib/gateway.d.ts +190 -2
  90. package/out/lib/gateway.js +59 -4
  91. package/out/lib/history.d.ts +24 -0
  92. package/out/lib/history.js +137 -0
  93. package/out/lib/ide.d.ts +19 -0
  94. package/out/lib/ide.js +131 -0
  95. package/out/lib/keyboard.d.ts +98 -0
  96. package/out/lib/keyboard.js +390 -0
  97. package/out/lib/lastlogin.d.ts +14 -0
  98. package/out/lib/lastlogin.js +105 -0
  99. package/out/lib/machine.d.ts +21 -0
  100. package/out/lib/machine.js +91 -0
  101. package/out/lib/notify.d.ts +4 -0
  102. package/out/lib/notify.js +52 -0
  103. package/out/lib/output.d.ts +48 -0
  104. package/out/lib/output.js +108 -0
  105. package/out/lib/picker.d.ts +1 -0
  106. package/out/lib/picker.js +158 -0
  107. package/out/lib/project-ops.d.ts +19 -0
  108. package/out/lib/project-ops.js +146 -0
  109. package/out/lib/project.d.ts +28 -0
  110. package/out/lib/project.js +114 -0
  111. package/out/lib/prompt.d.ts +3 -0
  112. package/out/lib/prompt.js +36 -2
  113. package/out/lib/queue.d.ts +13 -0
  114. package/out/lib/queue.js +116 -0
  115. package/out/lib/readiness.d.ts +19 -0
  116. package/out/lib/readiness.js +170 -1
  117. package/out/lib/self.d.ts +23 -0
  118. package/out/lib/self.js +124 -0
  119. package/out/lib/sessions.d.ts +19 -0
  120. package/out/lib/sessions.js +221 -0
  121. package/out/lib/startcache.d.ts +13 -0
  122. package/out/lib/startcache.js +78 -0
  123. package/out/lib/stdin.d.ts +32 -0
  124. package/out/lib/stdin.js +117 -0
  125. package/out/lib/store.d.ts +40 -0
  126. package/out/lib/store.js +138 -0
  127. package/out/lib/sync.d.ts +18 -0
  128. package/out/lib/sync.js +81 -0
  129. package/out/lib/ui.d.ts +10 -4
  130. package/out/lib/ui.js +65 -25
  131. package/out/lib/voice.d.ts +36 -0
  132. package/out/lib/voice.js +160 -1
  133. package/out/native/dictate +0 -0
  134. package/out/postinstall.js +42 -17
  135. package/out/providers/anthropic.d.ts +22 -0
  136. package/out/providers/anthropic.js +80 -0
  137. package/out/providers/index.d.ts +8 -0
  138. package/out/providers/index.js +83 -0
  139. package/out/providers/openai.d.ts +11 -0
  140. package/out/providers/openai.js +57 -0
  141. package/out/thin.js +595 -33
  142. package/package.json +11 -50
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
- /** `~/notes.md` against this person's home folder. */
98
- function expand(file, cwd) {
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
- const full = file === '~' ? home
101
- : file.startsWith('~/') ? path.join(home, file.slice(2))
102
- : path.resolve(cwd, file);
103
- return full;
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
- function isFile(file) {
164
+ /** Something is on the disk at this path, even a link to nothing. */
165
+ function present(file) {
106
166
  try {
107
- return fs.statSync(file).isFile();
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
- * 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.
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, cwd) {
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 file = expand(unescapePath(candidate), cwd);
126
- if (!isFile(file) || seen.has(file))
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
- for (const m of line.matchAll(/"([^"]+)"|'([^']+)'/g)) {
134
- consider(m[0], m[1] ?? m[2] ?? '');
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 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)
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. This is the dragged-file case and the `@` case.
155
- for (const m of line.matchAll(/(?:\\.|[^\s\\])+/g)) {
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 body = raw.startsWith('@') ? raw.slice(1) : raw;
160
- const deliberate = raw.startsWith('@')
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, body);
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. 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.
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 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,
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
- 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
- });
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
- /** 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' : ''}`;
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>;