tryharness 0.7.23 → 0.7.26

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 (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5268 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +384 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
package/lib/atlas.mjs CHANGED
@@ -15,15 +15,27 @@ export const ATLAS_VERSION = "embeddinggemma-2:q8:768:v1";
15
15
  const MAX_FILE = 300_000;
16
16
  const MIN_PIECE = 300;
17
17
  const MAX_PIECE = 2_400;
18
- const TEXT_EXT = /\.(?:[cm]?[jt]sx?|json|md|mdx|txt|ya?ml|toml|sql|sol|rs|go|py|rb|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sh|zsh|css|scss|html|vue|svelte|astro|graphql|proto|ini|cfg|conf|env\.example|lua|ex|exs|dart|scala|zig)$/i;
19
- const SKIP_PATH = /(?:^|\/)(?:node_modules|dist|build|out|\.next|coverage|vendor|target|\.git)\/|(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum)$|\.min\.[jc]ss?$|\.map$/;
20
- const SECRET_PATH = /(?:^|\/)(?:\.env(?:\.[\w-]+)?|id_[rd]sa\w*|[\w.-]+\.(?:pem|key|p12|pfx|keystore|jks)|\.npmrc|\.pypirc|credentials(?:\.json)?)$/i;
18
+ const TEXT_EXT =
19
+ /\.(?:[cm]?[jt]sx?|json|md|mdx|txt|ya?ml|toml|sql|sol|rs|go|py|rb|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sh|zsh|css|scss|html|vue|svelte|astro|graphql|proto|ini|cfg|conf|env\.example|lua|ex|exs|dart|scala|zig)$/i;
20
+ const SKIP_PATH =
21
+ /(?:^|\/)(?:node_modules|dist|build|out|\.next|coverage|vendor|target|\.git)\/|(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum)$|\.min\.[jc]ss?$|\.map$/;
22
+ const SECRET_PATH =
23
+ /(?:^|\/)(?:\.env(?:\.[\w-]+)?|id_[rd]sa\w*|[\w.-]+\.(?:pem|key|p12|pfx|keystore|jks)|\.npmrc|\.pypirc|credentials(?:\.json)?)$/i;
21
24
  const SECRET_TEXT = /-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----/;
22
- const DECLARATION = /^(?:export\s|import\s|async\s|function\s|class\s|const\s|let\s|var\s|type\s|interface\s|enum\s|def\s|fn\s|pub\s|impl\s|struct\s|mod\s|func\s|package\s|contract\s|library\s|module\s|describe\(|it\(|test\(|#{1,6}\s|\[[\w.-]+\]$|create\s|alter\s)/i;
23
- const STOP = new Set("the a an and or of to in on for with is are be it this that from by at as was were can should would could will do does did not no yes we you i me my our your they them their its into out up so if then else than but about how what why when where which who whom also just like make made use used using need needs want".split(" "));
25
+ const DECLARATION =
26
+ /^(?:export\s|import\s|async\s|function\s|class\s|const\s|let\s|var\s|type\s|interface\s|enum\s|def\s|fn\s|pub\s|impl\s|struct\s|mod\s|func\s|package\s|contract\s|library\s|module\s|describe\(|it\(|test\(|#{1,6}\s|\[[\w.-]+\]$|create\s|alter\s)/i;
27
+ const STOP = new Set(
28
+ "the a an and or of to in on for with is are be it this that from by at as was were can should would could will do does did not no yes we you i me my our your they them their its into out up so if then else than but about how what why when where which who whom also just like make made use used using need needs want".split(
29
+ " "
30
+ )
31
+ );
24
32
 
25
33
  /** Whether a repository path may be indexed: a text file, not generated, not a secret. */
26
- export const indexable = (rel, size = 0) => TEXT_EXT.test(rel) && !SKIP_PATH.test(rel) && !SECRET_PATH.test(rel) && size <= MAX_FILE;
34
+ export const indexable = (rel, size = 0) =>
35
+ TEXT_EXT.test(rel) &&
36
+ !SKIP_PATH.test(rel) &&
37
+ !SECRET_PATH.test(rel) &&
38
+ size <= MAX_FILE;
27
39
 
28
40
  /** A file's pieces: { text, line } cut at top-level declarations and headings, small ones merged, big ones split at blank lines. */
29
41
  export function pieces(text) {
@@ -32,27 +44,52 @@ export function pieces(text) {
32
44
  let start = 0;
33
45
  for (let i = 1; i <= lines.length; i += 1) {
34
46
  const line = lines[i];
35
- const boundary = i === lines.length || (line && !/^\s/.test(line) && (lines[i - 1].trim() === "" || DECLARATION.test(line)) && !/^[)}\]]/.test(line));
36
- if (boundary) { segments.push({ line: start + 1, text: lines.slice(start, i).join("\n") }); start = i; }
47
+ const boundary =
48
+ i === lines.length ||
49
+ (line &&
50
+ !/^\s/.test(line) &&
51
+ (lines[i - 1].trim() === "" || DECLARATION.test(line)) &&
52
+ !/^[)}\]]/.test(line));
53
+ if (boundary) {
54
+ segments.push({
55
+ line: start + 1,
56
+ text: lines.slice(start, i).join("\n"),
57
+ });
58
+ start = i;
59
+ }
37
60
  }
38
61
  const merged = [];
39
62
  for (const segment of segments) {
40
63
  const last = merged.at(-1);
41
- if (last && last.text.length < MIN_PIECE) { last.text += `\n${segment.text}`; continue; }
64
+ if (last && last.text.length < MIN_PIECE) {
65
+ last.text += `\n${segment.text}`;
66
+ continue;
67
+ }
42
68
  merged.push({ ...segment });
43
69
  }
44
70
  const out = [];
45
71
  for (const segment of merged) {
46
72
  if (!segment.text.trim()) continue;
47
- if (segment.text.length <= MAX_PIECE) { out.push(segment); continue; }
73
+ if (segment.text.length <= MAX_PIECE) {
74
+ out.push(segment);
75
+ continue;
76
+ }
48
77
  let buffer = "";
49
78
  let line = segment.line;
50
79
  let at = segment.line;
51
80
  for (const block of segment.text.split(/\n(?=\s*\n)/)) {
52
- if (buffer && buffer.length + block.length > MAX_PIECE) { out.push({ line, text: buffer }); buffer = ""; line = at; }
81
+ if (buffer && buffer.length + block.length > MAX_PIECE) {
82
+ out.push({ line, text: buffer });
83
+ buffer = "";
84
+ line = at;
85
+ }
53
86
  for (let i = 0; i < block.length; i += MAX_PIECE) {
54
87
  const part = block.slice(i, i + MAX_PIECE);
55
- if (buffer && buffer.length + part.length > MAX_PIECE) { out.push({ line, text: buffer }); buffer = ""; line = at; }
88
+ if (buffer && buffer.length + part.length > MAX_PIECE) {
89
+ out.push({ line, text: buffer });
90
+ buffer = "";
91
+ line = at;
92
+ }
56
93
  buffer += (buffer ? "\n" : "") + part;
57
94
  }
58
95
  at += block.split("\n").length;
@@ -62,7 +99,8 @@ export function pieces(text) {
62
99
  return out;
63
100
  }
64
101
 
65
- export const pieceHash = (rel, text) => createHash("sha1").update(`${rel}\0${text}`).digest("hex");
102
+ export const pieceHash = (rel, text) =>
103
+ createHash("sha1").update(`${rel}\0${text}`).digest("hex");
66
104
 
67
105
  /** Words and identifiers worth searching for in a request, most specific first (identifiers, paths, then words). */
68
106
  export function queryTerms(text, max = 24) {
@@ -71,16 +109,22 @@ export function queryTerms(text, max = 24) {
71
109
  for (const token of raw) {
72
110
  const clean = token.replace(/^[./-]+|[./-]+$/g, "");
73
111
  if (clean.length < 3 || STOP.has(clean.toLowerCase())) continue;
74
- for (const part of clean.split(/[./-]/)) if (part.length >= 3 && !STOP.has(part.toLowerCase())) terms.push(part);
112
+ for (const part of clean.split(/[./-]/))
113
+ if (part.length >= 3 && !STOP.has(part.toLowerCase())) terms.push(part);
75
114
  }
76
- const specific = (term) => (/[A-Z_$]/.test(term.slice(1)) || /\d/.test(term) ? 0 : 1);
77
- return [...new Set(terms)].sort((a, b) => specific(a) - specific(b)).slice(0, max);
115
+ const specific = (term) =>
116
+ /[A-Z_$]/.test(term.slice(1)) || /\d/.test(term) ? 0 : 1;
117
+ return [...new Set(terms)]
118
+ .sort((a, b) => specific(a) - specific(b))
119
+ .slice(0, max);
78
120
  }
79
121
 
80
122
  export function openAtlas(file) {
81
123
  const db = new Database(file);
82
124
  // The atlas is a cache the relay can always rebuild from the repositories and session files, so writes skip the disk sync.
83
- db.exec("pragma journal_mode = MEMORY; pragma synchronous = OFF; pragma busy_timeout = 5000;");
125
+ db.exec(
126
+ "pragma journal_mode = MEMORY; pragma synchronous = OFF; pragma busy_timeout = 5000;"
127
+ );
84
128
  db.exec(`
85
129
  create table if not exists pieces (hash text primary key, path text not null, line integer not null, text text not null);
86
130
  create table if not exists piece_vectors (hash text not null, version text not null, vector blob not null, primary key(hash, version));
@@ -97,9 +141,12 @@ export function openAtlas(file) {
97
141
  `);
98
142
  // A blob's pieces never change, so lookups are kept (a few MB for a large repository and its recent versions).
99
143
  const blobPieces = new Map();
100
- const run = (sql, ...values) => db.run(sql, values.length ? values : undefined);
101
- const all = (sql, ...values) => db.all(sql, values.length ? values : undefined);
102
- const get = (sql, ...values) => db.get(sql, values.length ? values : undefined);
144
+ const run = (sql, ...values) =>
145
+ db.run(sql, values.length ? values : undefined);
146
+ const all = (sql, ...values) =>
147
+ db.all(sql, values.length ? values : undefined);
148
+ const get = (sql, ...values) =>
149
+ db.get(sql, values.length ? values : undefined);
103
150
  return {
104
151
  close: () => db.close(),
105
152
  blob(blob) {
@@ -113,7 +160,14 @@ export function openAtlas(file) {
113
160
  /** Runs fn inside one transaction. */
114
161
  batch(fn) {
115
162
  db.exec("begin");
116
- try { const out = fn(); db.exec("commit"); return out; } catch (error) { db.exec("rollback"); throw error; }
163
+ try {
164
+ const out = fn();
165
+ db.exec("commit");
166
+ return out;
167
+ } catch (error) {
168
+ db.exec("rollback");
169
+ throw error;
170
+ }
117
171
  },
118
172
  putBlob(blob, rel, parts) {
119
173
  const hashes = [];
@@ -121,11 +175,27 @@ export function openAtlas(file) {
121
175
  const hash = pieceHash(rel, part.text);
122
176
  hashes.push(hash);
123
177
  if (!get("select 1 as one from pieces where hash = ?", hash)) {
124
- run("insert into pieces(hash, path, line, text) values(?, ?, ?, ?)", hash, rel, part.line, part.text);
125
- run("insert into pieces_fts(hash, path, text) values(?, ?, ?)", hash, rel.replace(/[/._-]/g, " "), part.text);
178
+ run(
179
+ "insert into pieces(hash, path, line, text) values(?, ?, ?, ?)",
180
+ hash,
181
+ rel,
182
+ part.line,
183
+ part.text
184
+ );
185
+ run(
186
+ "insert into pieces_fts(hash, path, text) values(?, ?, ?)",
187
+ hash,
188
+ rel.replace(/[/._-]/g, " "),
189
+ part.text
190
+ );
126
191
  }
127
192
  }
128
- run("insert or replace into blobs(blob, path, pieces) values(?, ?, ?)", blob, rel, JSON.stringify(hashes));
193
+ run(
194
+ "insert or replace into blobs(blob, path, pieces) values(?, ?, ?)",
195
+ blob,
196
+ rel,
197
+ JSON.stringify(hashes)
198
+ );
129
199
  return hashes;
130
200
  },
131
201
  setFiles(root, rows) {
@@ -133,69 +203,198 @@ export function openAtlas(file) {
133
203
  try {
134
204
  run("delete from files where root = ?", root);
135
205
  const now = new Date().toISOString();
136
- for (const row of rows) run("insert into files(root, path, blob, indexed_at) values(?, ?, ?, ?)", root, row.path, row.blob, now);
206
+ for (const row of rows)
207
+ run(
208
+ "insert into files(root, path, blob, indexed_at) values(?, ?, ?, ?)",
209
+ root,
210
+ row.path,
211
+ row.blob,
212
+ now
213
+ );
137
214
  db.exec("commit");
138
- } catch (error) { db.exec("rollback"); throw error; }
215
+ } catch (error) {
216
+ db.exec("rollback");
217
+ throw error;
218
+ }
139
219
  },
140
220
  files: (root) => all("select path, blob from files where root = ?", root),
141
- roots: () => all("select root, count(*) as files, max(indexed_at) as indexed_at from files group by root").map((row) => ({ ...row, files: Number(row.files) })),
142
- toEmbed: (version, limit = 64) => all("select p.hash, p.path, p.text from pieces p left join piece_vectors v on v.hash = p.hash and v.version = ? where v.hash is null limit ?", version, limit),
143
- missing: (version) => Number(get("select count(*) as n from pieces p left join piece_vectors v on v.hash = p.hash and v.version = ? where v.hash is null", version).n),
144
- putVector: (hash, version, vector) => run("insert or replace into piece_vectors(hash, version, vector) values(?, ?, ?)", hash, version, toBlob(vector)),
221
+ roots: () =>
222
+ all(
223
+ "select root, count(*) as files, max(indexed_at) as indexed_at from files group by root"
224
+ ).map((row) => ({ ...row, files: Number(row.files) })),
225
+ toEmbed: (version, limit = 64) =>
226
+ all(
227
+ "select p.hash, p.path, p.text from pieces p left join piece_vectors v on v.hash = p.hash and v.version = ? where v.hash is null limit ?",
228
+ version,
229
+ limit
230
+ ),
231
+ missing: (version) =>
232
+ Number(
233
+ get(
234
+ "select count(*) as n from pieces p left join piece_vectors v on v.hash = p.hash and v.version = ? where v.hash is null",
235
+ version
236
+ ).n
237
+ ),
238
+ putVector: (hash, version, vector) =>
239
+ run(
240
+ "insert or replace into piece_vectors(hash, version, vector) values(?, ?, ?)",
241
+ hash,
242
+ version,
243
+ toBlob(vector)
244
+ ),
145
245
  vectors(hashes, version) {
146
246
  const out = new Map();
147
247
  const list = [...hashes];
148
248
  for (let i = 0; i < list.length; i += 500) {
149
249
  const slice = list.slice(i, i + 500);
150
- for (const row of all(`select hash, vector from piece_vectors where version = ? and hash in (${slice.map(() => "?").join(",")})`, version, ...slice)) out.set(row.hash, fromBlob(row.vector));
250
+ for (const row of all(
251
+ `select hash, vector from piece_vectors where version = ? and hash in (${slice.map(() => "?").join(",")})`,
252
+ version,
253
+ ...slice
254
+ ))
255
+ out.set(row.hash, fromBlob(row.vector));
151
256
  }
152
257
  return out;
153
258
  },
154
- piece: (hash) => get("select hash, path, line, text from pieces where hash = ?", hash),
259
+ piece: (hash) =>
260
+ get("select hash, path, line, text from pieces where hash = ?", hash),
155
261
  meta(key, value) {
156
- if (value === undefined) return get("select value from meta where key = ?", key)?.value ?? null;
157
- run("insert or replace into meta(key, value) values(?, ?)", key, String(value));
262
+ if (value === undefined)
263
+ return get("select value from meta where key = ?", key)?.value ?? null;
264
+ run(
265
+ "insert or replace into meta(key, value) values(?, ?)",
266
+ key,
267
+ String(value)
268
+ );
158
269
  return value;
159
270
  },
160
- runSeen: (agent, sessionId) => get("select source_mtime from runs where agent = ? and session_id = ?", agent, sessionId)?.source_mtime ?? null,
271
+ runSeen: (agent, sessionId) =>
272
+ get(
273
+ "select source_mtime from runs where agent = ? and session_id = ?",
274
+ agent,
275
+ sessionId
276
+ )?.source_mtime ?? null,
161
277
  putRun(row) {
162
- run(`insert or replace into runs(agent, session_id, root, at, brief, edited, read, before, source_mtime) values(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
163
- row.agent, row.sessionId, row.root, row.at, row.brief.slice(0, 4_000), JSON.stringify(row.edited), JSON.stringify(row.read), JSON.stringify(row.before), row.sourceMtime);
278
+ run(
279
+ `insert or replace into runs(agent, session_id, root, at, brief, edited, read, before, source_mtime) values(?, ?, ?, ?, ?, ?, ?, ?, ?)`,
280
+ row.agent,
281
+ row.sessionId,
282
+ row.root,
283
+ row.at,
284
+ row.brief.slice(0, 4_000),
285
+ JSON.stringify(row.edited),
286
+ JSON.stringify(row.read),
287
+ JSON.stringify(row.before),
288
+ row.sourceMtime
289
+ );
164
290
  },
165
- runs: (root, since = 0) => all("select agent, session_id, root, at, brief, edited, read, before from runs where root = ? and at >= ? order by at", root, since)
166
- .map((row) => ({ ...row, at: Number(row.at), edited: JSON.parse(row.edited), read: JSON.parse(row.read), before: JSON.parse(row.before) })),
167
- runsToEmbed: (version, limit = 32) => all("select r.agent, r.session_id, r.brief from runs r left join run_vectors v on v.agent = r.agent and v.session_id = r.session_id and v.version = ? where v.agent is null limit ?", version, limit),
168
- putRunVector: (agent, sessionId, version, vector) => run("insert or replace into run_vectors(agent, session_id, version, vector) values(?, ?, ?, ?)", agent, sessionId, version, toBlob(vector)),
169
- runVectors: (root, version) => all("select r.agent, r.session_id, r.at, r.brief, r.edited, v.vector from runs r join run_vectors v on v.agent = r.agent and v.session_id = r.session_id and v.version = ? where r.root = ?", version, root)
170
- .map((row) => ({ agent: row.agent, sessionId: row.session_id, at: Number(row.at), brief: row.brief, edited: JSON.parse(row.edited), vector: fromBlob(row.vector) })),
291
+ runs: (root, since = 0) =>
292
+ all(
293
+ "select agent, session_id, root, at, brief, edited, read, before from runs where root = ? and at >= ? order by at",
294
+ root,
295
+ since
296
+ ).map((row) => ({
297
+ ...row,
298
+ at: Number(row.at),
299
+ edited: JSON.parse(row.edited),
300
+ read: JSON.parse(row.read),
301
+ before: JSON.parse(row.before),
302
+ })),
303
+ runsToEmbed: (version, limit = 32) =>
304
+ all(
305
+ "select r.agent, r.session_id, r.brief from runs r left join run_vectors v on v.agent = r.agent and v.session_id = r.session_id and v.version = ? where v.agent is null limit ?",
306
+ version,
307
+ limit
308
+ ),
309
+ putRunVector: (agent, sessionId, version, vector) =>
310
+ run(
311
+ "insert or replace into run_vectors(agent, session_id, version, vector) values(?, ?, ?, ?)",
312
+ agent,
313
+ sessionId,
314
+ version,
315
+ toBlob(vector)
316
+ ),
317
+ runVectors: (root, version) =>
318
+ all(
319
+ "select r.agent, r.session_id, r.at, r.brief, r.edited, v.vector from runs r join run_vectors v on v.agent = r.agent and v.session_id = r.session_id and v.version = ? where r.root = ?",
320
+ version,
321
+ root
322
+ ).map((row) => ({
323
+ agent: row.agent,
324
+ sessionId: row.session_id,
325
+ at: Number(row.at),
326
+ brief: row.brief,
327
+ edited: JSON.parse(row.edited),
328
+ vector: fromBlob(row.vector),
329
+ })),
171
330
  /** Recordings' windows and images (media.mjs): one row each, its vector beside the pieces'. */
172
- mediaSeen: (file) => get("select max(mtime) as mtime from media where path = ?", file)?.mtime ?? null,
173
- mediaPaths: () => all("select distinct path from media").map((row) => row.path),
331
+ mediaSeen: (file) =>
332
+ get("select max(mtime) as mtime from media where path = ?", file)
333
+ ?.mtime ?? null,
334
+ mediaPaths: () =>
335
+ all("select distinct path from media").map((row) => row.path),
174
336
  dropMedia: (file) => run("delete from media where path = ?", file),
175
- putMedia: (row) => run("insert or replace into media(hash, path, kind, start_ms, end_ms, mtime) values(?, ?, ?, ?, ?, ?)", row.hash, row.path, row.kind, row.startMs, row.endMs, row.mtime),
176
- mediaVectors: (version) => all("select m.hash, m.path, m.kind, m.start_ms, m.end_ms, v.vector from media m join piece_vectors v on v.hash = m.hash and v.version = ?", version)
177
- .map((row) => ({ hash: row.hash, path: row.path, kind: row.kind, startMs: Number(row.start_ms), endMs: Number(row.end_ms), vector: fromBlob(row.vector) })),
337
+ putMedia: (row) =>
338
+ run(
339
+ "insert or replace into media(hash, path, kind, start_ms, end_ms, mtime) values(?, ?, ?, ?, ?, ?)",
340
+ row.hash,
341
+ row.path,
342
+ row.kind,
343
+ row.startMs,
344
+ row.endMs,
345
+ row.mtime
346
+ ),
347
+ mediaVectors: (version) =>
348
+ all(
349
+ "select m.hash, m.path, m.kind, m.start_ms, m.end_ms, v.vector from media m join piece_vectors v on v.hash = m.hash and v.version = ?",
350
+ version
351
+ ).map((row) => ({
352
+ hash: row.hash,
353
+ path: row.path,
354
+ kind: row.kind,
355
+ startMs: Number(row.start_ms),
356
+ endMs: Number(row.end_ms),
357
+ vector: fromBlob(row.vector),
358
+ })),
178
359
  /** Full-text matches: { hash, rank } best first (bm25, lower is better), over every indexed piece. */
179
360
  words(terms, limit = 400) {
180
361
  if (!terms.length) return [];
181
- const match = terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" OR ");
182
- return all("select hash, bm25(pieces_fts, 0, 2.0, 1.0) as rank from pieces_fts where pieces_fts match ? order by rank limit ?", match, limit);
362
+ const match = terms
363
+ .map((term) => `"${term.replaceAll('"', '""')}"`)
364
+ .join(" OR ");
365
+ return all(
366
+ "select hash, bm25(pieces_fts, 0, 2.0, 1.0) as rank from pieces_fts where pieces_fts match ? order by rank limit ?",
367
+ match,
368
+ limit
369
+ );
183
370
  },
184
371
  };
185
372
  }
186
373
 
187
- const git = (cwd, args, input) => spawnSync("git", args, { cwd, input, encoding: "utf8", maxBuffer: 256 * 2 ** 20 });
374
+ const git = (cwd, args, input) =>
375
+ spawnSync("git", args, {
376
+ cwd,
377
+ input,
378
+ encoding: "utf8",
379
+ maxBuffer: 256 * 2 ** 20,
380
+ });
188
381
 
189
382
  /** The repository's files at a commit (or the working tree when commit is null): [{ path, blob, size }]. */
190
383
  export function treeFiles(root, commit = null) {
191
384
  if (commit) {
192
385
  const listed = git(root, ["ls-tree", "-r", "-l", commit]);
193
386
  if (listed.status !== 0) return [];
194
- return listed.stdout.split("\n").filter(Boolean).map((line) => {
195
- const [meta, rel] = line.split("\t");
196
- const [, type, blob, size] = meta.split(/\s+/);
197
- return type === "blob" ? { path: rel, blob, size: Number(size) || 0 } : null;
198
- }).filter((row) => row && indexable(row.path, row.size));
387
+ return listed.stdout
388
+ .split("\n")
389
+ .filter(Boolean)
390
+ .map((line) => {
391
+ const [meta, rel] = line.split("\t");
392
+ const [, type, blob, size] = meta.split(/\s+/);
393
+ return type === "blob"
394
+ ? { path: rel, blob, size: Number(size) || 0 }
395
+ : null;
396
+ })
397
+ .filter((row) => row && indexable(row.path, row.size));
199
398
  }
200
399
  // The working tree: git's index already holds the blob of every unchanged tracked file; only files changed on disk
201
400
  // and new files git does not ignore are hashed, so a pass over a large repository costs about what `git status` does.
@@ -208,23 +407,42 @@ export function treeFiles(root, commit = null) {
208
407
  const [, blob, stage] = entry.slice(0, tab).split(" ");
209
408
  if (stage === "0") blobs.set(entry.slice(tab + 1), blob);
210
409
  }
211
- const changed = git(root, ["diff", "--name-only", "-z"]).stdout.split("\0").filter(Boolean);
212
- const untracked = git(root, ["ls-files", "--others", "--exclude-standard", "-z"]).stdout.split("\0").filter(Boolean);
410
+ const changed = git(root, ["diff", "--name-only", "-z"])
411
+ .stdout.split("\0")
412
+ .filter(Boolean);
413
+ const untracked = git(root, [
414
+ "ls-files",
415
+ "--others",
416
+ "--exclude-standard",
417
+ "-z",
418
+ ])
419
+ .stdout.split("\0")
420
+ .filter(Boolean);
213
421
  const rows = [];
214
422
  const rehash = [];
215
423
  for (const rel of new Set([...blobs.keys(), ...untracked])) {
216
424
  if (!indexable(rel, 0)) continue;
217
425
  let stat;
218
- try { stat = fs.statSync(path.join(root, rel)); } catch { continue; }
426
+ try {
427
+ stat = fs.statSync(path.join(root, rel));
428
+ } catch {
429
+ continue;
430
+ }
219
431
  if (!stat.isFile() || stat.size > MAX_FILE) continue;
220
432
  const row = { path: rel, size: stat.size, blob: blobs.get(rel) };
221
433
  rows.push(row);
222
434
  if (!row.blob || changed.includes(rel)) rehash.push(row);
223
435
  }
224
436
  if (rehash.length) {
225
- const hashed = git(root, ["hash-object", "--stdin-paths"], rehash.map((row) => path.join(root, row.path)).join("\n"));
437
+ const hashed = git(
438
+ root,
439
+ ["hash-object", "--stdin-paths"],
440
+ rehash.map((row) => path.join(root, row.path)).join("\n")
441
+ );
226
442
  const out = hashed.status === 0 ? hashed.stdout.split("\n") : [];
227
- rehash.forEach((row, i) => { row.blob = out[i]; });
443
+ rehash.forEach((row, i) => {
444
+ row.blob = out[i];
445
+ });
228
446
  }
229
447
  return rows.filter((row) => /^[0-9a-f]{40}$/.test(row.blob || ""));
230
448
  }
@@ -233,18 +451,29 @@ export function treeFiles(root, commit = null) {
233
451
  export function readBlobs(root, blobs) {
234
452
  const out = new Map();
235
453
  if (!blobs.length) return out;
236
- const result = spawnSync("git", ["cat-file", "--batch"], { cwd: root, input: blobs.join("\n") + "\n", maxBuffer: 2 * 2 ** 30 });
454
+ const result = spawnSync("git", ["cat-file", "--batch"], {
455
+ cwd: root,
456
+ input: blobs.join("\n") + "\n",
457
+ maxBuffer: 2 * 2 ** 30,
458
+ });
237
459
  const buffer = result.stdout;
238
460
  let at = 0;
239
461
  while (at < buffer.length) {
240
462
  const end = buffer.indexOf(10, at);
241
463
  if (end < 0) break;
242
- const [blob, type, size] = buffer.subarray(at, end).toString("utf8").split(" ");
243
- if (type === "missing" || !size) { at = end + 1; continue; }
464
+ const [blob, type, size] = buffer
465
+ .subarray(at, end)
466
+ .toString("utf8")
467
+ .split(" ");
468
+ if (type === "missing" || !size) {
469
+ at = end + 1;
470
+ continue;
471
+ }
244
472
  const length = Number(size);
245
473
  const body = buffer.subarray(end + 1, end + 1 + length);
246
474
  at = end + 1 + length + 1;
247
- if (!body.includes(0) && !SECRET_TEXT.test(body.toString("latin1"))) out.set(blob, body.toString("utf8"));
475
+ if (!body.includes(0) && !SECRET_TEXT.test(body.toString("latin1")))
476
+ out.set(blob, body.toString("utf8"));
248
477
  }
249
478
  return out;
250
479
  }
@@ -253,23 +482,41 @@ export function readBlobs(root, blobs) {
253
482
  export function addFiles(atlas, root, rows) {
254
483
  const fresh = rows.filter((row) => !atlas.blob(row.blob));
255
484
  const texts = readBlobs(root, [...new Set(fresh.map((row) => row.blob))]);
256
- for (let i = 0; i < fresh.length; i += 500) atlas.batch(() => {
257
- for (const row of fresh.slice(i, i + 500)) {
258
- const text = texts.get(row.blob);
259
- atlas.putBlob(row.blob, row.path, text === undefined ? [] : pieces(text));
260
- }
261
- });
485
+ for (let i = 0; i < fresh.length; i += 500)
486
+ atlas.batch(() => {
487
+ for (const row of fresh.slice(i, i + 500)) {
488
+ const text = texts.get(row.blob);
489
+ atlas.putBlob(
490
+ row.blob,
491
+ row.path,
492
+ text === undefined ? [] : pieces(text)
493
+ );
494
+ }
495
+ });
262
496
  return fresh.length;
263
497
  }
264
498
 
265
499
  /** Embeds every piece that has no vector yet, in batches, on the given lane. */
266
- export async function embedMissing(atlas, inference, { lane = "background", version = ATLAS_VERSION, batch = 32, onProgress } = {}) {
500
+ export async function embedMissing(
501
+ atlas,
502
+ inference,
503
+ { lane = "background", version = ATLAS_VERSION, batch = 32, onProgress } = {}
504
+ ) {
267
505
  let done = 0;
268
506
  for (;;) {
269
507
  const rows = atlas.toEmbed(version, batch);
270
508
  if (!rows.length) return done;
271
- const vectors = await inference.embed(rows.map((row) => ({ kind: "document", title: row.path, text: row.text })), { lane });
272
- atlas.batch(() => rows.forEach((row, i) => atlas.putVector(row.hash, version, vectors[i])));
509
+ const vectors = await inference.embed(
510
+ rows.map((row) => ({
511
+ kind: "document",
512
+ title: row.path,
513
+ text: row.text,
514
+ })),
515
+ { lane }
516
+ );
517
+ atlas.batch(() =>
518
+ rows.forEach((row, i) => atlas.putVector(row.hash, version, vectors[i]))
519
+ );
273
520
  done += rows.length;
274
521
  onProgress?.(done);
275
522
  }
@@ -279,26 +526,79 @@ export async function embedMissing(atlas, inference, { lane = "background", vers
279
526
  * Files in scope ranked for a request, with the signals each one scored on. scope: [{ path, blob }]. Returns
280
527
  * [{ path, blob, dense, lexical, named, best: { hash, line } }] sorted by the given scorer (default: dense+lexical+named).
281
528
  */
282
- export async function searchFiles(atlas, inference, query, scope, { version = ATLAS_VERSION, lane = "interactive", queryVector = null, vectors = null, wordLimit = 400 } = {}) {
529
+ export async function searchFiles(
530
+ atlas,
531
+ inference,
532
+ query,
533
+ scope,
534
+ {
535
+ version = ATLAS_VERSION,
536
+ lane = "interactive",
537
+ queryVector = null,
538
+ vectors = null,
539
+ wordLimit = 400,
540
+ } = {}
541
+ ) {
283
542
  const byHash = new Map();
284
- const files = scope.map((row) => ({ path: row.path, blob: row.blob, hashes: atlas.blob(row.blob) ?? [] }));
285
- for (const file of files) for (const hash of file.hashes) { if (!byHash.has(hash)) byHash.set(hash, []); byHash.get(hash).push(file); }
286
- const qv = queryVector ?? (await inference.embed([{ kind: "code-query", text: String(query).slice(0, 6_000) }], { lane }))[0];
543
+ const files = scope.map((row) => ({
544
+ path: row.path,
545
+ blob: row.blob,
546
+ hashes: atlas.blob(row.blob) ?? [],
547
+ }));
548
+ for (const file of files)
549
+ for (const hash of file.hashes) {
550
+ if (!byHash.has(hash)) byHash.set(hash, []);
551
+ byHash.get(hash).push(file);
552
+ }
553
+ const qv =
554
+ queryVector ??
555
+ (
556
+ await inference.embed(
557
+ [{ kind: "code-query", text: String(query).slice(0, 6_000) }],
558
+ { lane }
559
+ )
560
+ )[0];
287
561
  const pieceVectors = vectors ?? atlas.vectors(byHash.keys(), version);
288
562
  const terms = queryTerms(query);
289
563
  const lexical = new Map();
290
- for (const row of atlas.words(terms, wordLimit)) if (byHash.has(row.hash)) lexical.set(row.hash, -row.rank);
564
+ for (const row of atlas.words(terms, wordLimit))
565
+ if (byHash.has(row.hash)) lexical.set(row.hash, -row.rank);
291
566
  const lowered = String(query).toLowerCase();
292
567
  return files.map((file) => {
293
- let dense = -1, best = null, words = 0;
568
+ let dense = -1,
569
+ best = null,
570
+ words = 0;
294
571
  for (const hash of file.hashes) {
295
572
  const vector = pieceVectors.get(hash);
296
- if (vector) { let dot = 0; for (let i = 0; i < qv.length; i += 1) dot += qv[i] * vector[i]; if (dot > dense) { dense = dot; best = hash; } }
573
+ if (vector) {
574
+ let dot = 0;
575
+ for (let i = 0; i < qv.length; i += 1) dot += qv[i] * vector[i];
576
+ if (dot > dense) {
577
+ dense = dot;
578
+ best = hash;
579
+ }
580
+ }
297
581
  if (lexical.has(hash)) words = Math.max(words, lexical.get(hash));
298
582
  }
299
583
  const base = path.basename(file.path).toLowerCase();
300
584
  const stem = base.replace(/\.[^.]+$/, "");
301
- const named = lowered.includes(file.path.toLowerCase()) ? 2 : lowered.includes(base) ? 1.5 : stem.length > 3 && new RegExp(`\\b${stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(lowered) ? 1 : 0;
302
- return { path: file.path, blob: file.blob, dense, lexical: words, named, best };
585
+ const named = lowered.includes(file.path.toLowerCase())
586
+ ? 2
587
+ : lowered.includes(base)
588
+ ? 1.5
589
+ : stem.length > 3 &&
590
+ new RegExp(
591
+ `\\b${stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`
592
+ ).test(lowered)
593
+ ? 1
594
+ : 0;
595
+ return {
596
+ path: file.path,
597
+ blob: file.blob,
598
+ dense,
599
+ lexical: words,
600
+ named,
601
+ best,
602
+ };
303
603
  });
304
604
  }