tryharness 0.7.23 → 0.7.25
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/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5248 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +382 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- 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 =
|
|
19
|
-
|
|
20
|
-
const
|
|
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 =
|
|
23
|
-
const
|
|
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) =>
|
|
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 =
|
|
36
|
-
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) =>
|
|
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(/[./-]/))
|
|
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) =>
|
|
77
|
-
|
|
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(
|
|
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) =>
|
|
101
|
-
|
|
102
|
-
const
|
|
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 {
|
|
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(
|
|
125
|
-
|
|
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(
|
|
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)
|
|
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) {
|
|
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: () =>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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(
|
|
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) =>
|
|
259
|
+
piece: (hash) =>
|
|
260
|
+
get("select hash, path, line, text from pieces where hash = ?", hash),
|
|
155
261
|
meta(key, value) {
|
|
156
|
-
if (value === undefined)
|
|
157
|
-
|
|
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) =>
|
|
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(
|
|
163
|
-
|
|
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) =>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
.map((row) => ({
|
|
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) =>
|
|
173
|
-
|
|
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) =>
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
182
|
-
|
|
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) =>
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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"])
|
|
212
|
-
|
|
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 {
|
|
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(
|
|
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) => {
|
|
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"], {
|
|
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
|
|
243
|
-
|
|
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")))
|
|
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)
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
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(
|
|
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(
|
|
272
|
-
|
|
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(
|
|
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) => ({
|
|
285
|
-
|
|
286
|
-
|
|
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))
|
|
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,
|
|
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) {
|
|
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())
|
|
302
|
-
|
|
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
|
}
|