syncstaff-mcp 0.2.3

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 (52) hide show
  1. package/README.md +86 -0
  2. package/dist/lib/agent-state.js +119 -0
  3. package/dist/lib/blast.js +462 -0
  4. package/dist/lib/client-config.js +81 -0
  5. package/dist/lib/env-compat.js +66 -0
  6. package/dist/lib/globs.js +0 -0
  7. package/dist/lib/ids.js +24 -0
  8. package/dist/lib/index/aliases.js +244 -0
  9. package/dist/lib/index/call-sites.js +178 -0
  10. package/dist/lib/index/checker-resolver.js +257 -0
  11. package/dist/lib/index/context-card.js +140 -0
  12. package/dist/lib/index/coverage.js +218 -0
  13. package/dist/lib/index/delivery.js +66 -0
  14. package/dist/lib/index/discovery.js +90 -0
  15. package/dist/lib/index/embedding.js +110 -0
  16. package/dist/lib/index/file-index.js +222 -0
  17. package/dist/lib/index/fingerprint.js +0 -0
  18. package/dist/lib/index/git-history.js +136 -0
  19. package/dist/lib/index/graph.js +234 -0
  20. package/dist/lib/index/impact.js +174 -0
  21. package/dist/lib/index/incremental.js +332 -0
  22. package/dist/lib/index/lexical.js +462 -0
  23. package/dist/lib/index/order.js +43 -0
  24. package/dist/lib/index/pages.js +357 -0
  25. package/dist/lib/index/persistence.js +233 -0
  26. package/dist/lib/index/pipeline.js +527 -0
  27. package/dist/lib/index/registry.js +106 -0
  28. package/dist/lib/index/resolve.js +280 -0
  29. package/dist/lib/index/semantic.js +381 -0
  30. package/dist/lib/index/surfaces.js +27 -0
  31. package/dist/lib/index/symbols.js +426 -0
  32. package/dist/lib/index/transformers-embedder.js +73 -0
  33. package/dist/lib/index/typescript-parser.js +532 -0
  34. package/dist/lib/index/vector-cache.js +176 -0
  35. package/dist/lib/index/verification.js +58 -0
  36. package/dist/lib/mcp-compaction.js +241 -0
  37. package/dist/lib/model-roles.js +206 -0
  38. package/dist/lib/path-warnings.js +90 -0
  39. package/dist/lib/protocol.js +95 -0
  40. package/dist/lib/types.js +69 -0
  41. package/dist/lib/version.js +21 -0
  42. package/dist/lib/worktree.js +211 -0
  43. package/dist/mcp/approval.js +0 -0
  44. package/dist/mcp/cloud-connector.js +99 -0
  45. package/dist/mcp/daemon-client.js +156 -0
  46. package/dist/mcp/daemon-protocol.js +100 -0
  47. package/dist/mcp/escalation-waiter.js +183 -0
  48. package/dist/mcp/graph-ops.js +169 -0
  49. package/dist/mcp/index.js +1151 -0
  50. package/dist/mcp/login.js +169 -0
  51. package/dist/mcp/setup.js +90 -0
  52. package/package.json +42 -0
@@ -0,0 +1,27 @@
1
+ /** Discover useful non-TypeScript relationships without executing application code. */
2
+ export function discoverSurfaceEdges(sources) {
3
+ const edges = [];
4
+ const css = new Set(sources.filter((source) => /\.(?:css|scss|less)$/.test(source.path)).map((source) => source.path));
5
+ const tokens = new Set();
6
+ for (const source of sources) {
7
+ for (const match of source.content.matchAll(/--([A-Za-z0-9_-]+)\s*:/g))
8
+ tokens.add(`--${match[1]}`);
9
+ }
10
+ for (const source of sources) {
11
+ for (const match of source.content.matchAll(/(?:import|require)\s*\(?\s*["']([^"']+\.(?:css|scss|less))["']/g)) {
12
+ const targetPath = join(dirname(source.path), match[1]).replaceAll("\\", "/");
13
+ const target = sources.find((candidate) => candidate.path === targetPath)?.path ?? targetPath;
14
+ edges.push({ from: source.path, to: target, kind: "stylesheet", evidence: match[0] });
15
+ }
16
+ for (const match of source.content.matchAll(/var\(\s*(--[A-Za-z0-9_-]+)/g)) {
17
+ if (tokens.has(match[1]))
18
+ edges.push({ from: source.path, to: match[1], kind: "token", evidence: match[0] });
19
+ }
20
+ for (const match of source.content.matchAll(/(?:app|router)\.(?:get|post|put|patch|delete)\(\s*["']([^"']+)["']\s*,\s*([A-Za-z_$][\w$]*)/g)) {
21
+ edges.push({ from: source.path, to: `${match[1]} -> ${match[2]}`, kind: "route-handler", evidence: match[0] });
22
+ }
23
+ }
24
+ return edges.sort((a, b) => byCodeUnit(`${a.from}\0${a.kind}\0${a.to}`, `${b.from}\0${b.kind}\0${b.to}`));
25
+ }
26
+ import { dirname, join } from "node:path";
27
+ import { byCodeUnit } from "./order.js";
@@ -0,0 +1,426 @@
1
+ import { resolveSpecifier } from "./resolve.js";
2
+ import { byCodeUnit } from "./order.js";
3
+ import { tokenize } from "./embedding.js";
4
+ /** Index the bindings visible in each file. Pure. */
5
+ export function buildSymbolTable(index) {
6
+ const known = new Set(index.files.map((file) => file.path));
7
+ const table = new Map();
8
+ /** path -> files it re-exports wholesale via `export *`. */
9
+ const starReExports = new Map();
10
+ /** path -> specifiers that were star re-exports, from the parser's diagnostic. */
11
+ const starTargets = new Map();
12
+ for (const file of index.files) {
13
+ const stars = file.result.diagnostics.filter((d) => d.code === "star-reexport");
14
+ if (stars.length === 0)
15
+ continue;
16
+ // The parser records a star re-export as a diagnostic on the same line as
17
+ // the import it also emitted; match them by line so the right specifier is
18
+ // expanded rather than every specifier in the file.
19
+ const lines = new Set(stars.map((d) => d.line));
20
+ starTargets.set(file.path, new Set(file.result.imports.filter((i) => lines.has(i.line)).map((i) => i.specifier)));
21
+ }
22
+ for (const file of index.files) {
23
+ const defined = new Map();
24
+ for (const definition of file.result.definitions ?? []) {
25
+ // First definition wins. A duplicate name in one file is either an
26
+ // overload or shadowing, and picking arbitrarily between them would make
27
+ // the table depend on parse order.
28
+ if (!defined.has(definition.name))
29
+ defined.set(definition.name, definition);
30
+ }
31
+ const imported = new Map();
32
+ for (const importRef of file.result.imports) {
33
+ if (importRef.dynamic)
34
+ continue; // recorded as unresolved by the graph
35
+ const resolution = resolveSpecifier(importRef.specifier, file.path, known, index.aliases ?? []);
36
+ const resolvedPath = resolution.kind === "internal" ? resolution.path : null;
37
+ for (const name of importRef.names) {
38
+ imported.set(name, {
39
+ specifier: importRef.specifier,
40
+ resolvedPath,
41
+ exportedAs: importRef.aliases?.[name] ?? name,
42
+ confidence: importRef.confidence,
43
+ });
44
+ }
45
+ }
46
+ const exported = new Map();
47
+ for (const exportRef of file.result.exports) {
48
+ if (!exported.has(exportRef.name))
49
+ exported.set(exportRef.name, exportRef);
50
+ }
51
+ // `export * from "./x.js"` names nothing the parser can see — the names
52
+ // live in x. Recorded here so the second pass below can expand them once
53
+ // every file's own exports are known.
54
+ starReExports.set(file.path, file.result.imports
55
+ .filter((i) => !i.dynamic && starTargets.get(file.path)?.has(i.specifier))
56
+ .map((i) => resolveSpecifier(i.specifier, file.path, known, index.aliases ?? []))
57
+ .flatMap((r) => (r.kind === "internal" ? [r.path] : [])));
58
+ table.set(file.path, {
59
+ path: file.path,
60
+ defined,
61
+ imported,
62
+ exported,
63
+ extractsDefinitions: file.result.definitions !== undefined,
64
+ extractsCalls: file.result.calls !== undefined,
65
+ });
66
+ }
67
+ // Expand `export *` now that every file's own exports are known. Bounded
68
+ // depth: barrels legitimately chain, and a cycle would otherwise spin.
69
+ for (let pass = 0; pass < 4; pass += 1) {
70
+ let grew = false;
71
+ for (const [path, targets] of starReExports) {
72
+ const entry = table.get(path);
73
+ if (!entry)
74
+ continue;
75
+ for (const target of targets) {
76
+ const source = table.get(target);
77
+ if (!source)
78
+ continue;
79
+ for (const [name, ref] of source.exported) {
80
+ if (name === "default")
81
+ continue; // `export *` never re-exports default
82
+ if (entry.exported.has(name))
83
+ continue;
84
+ entry.exported.set(name, {
85
+ ...ref,
86
+ from: target,
87
+ // The name is certainly exported by the barrel; which file
88
+ // ultimately owns it took a hop this pass did not verify.
89
+ confidence: "probable",
90
+ });
91
+ grew = true;
92
+ }
93
+ }
94
+ }
95
+ if (!grew)
96
+ break;
97
+ }
98
+ return table;
99
+ }
100
+ /**
101
+ * Resolve every recorded call site against the symbol table.
102
+ *
103
+ * Deterministic: edges and unresolved entries are sorted, so two machines with
104
+ * the same index produce the same bytes.
105
+ */
106
+ export function resolveCallEdges(index, table) {
107
+ const edges = [];
108
+ const unresolved = [];
109
+ const unanalyzed = [];
110
+ for (const file of index.files) {
111
+ const entry = table.get(file.path);
112
+ if (!entry)
113
+ continue;
114
+ if (!entry.extractsCalls) {
115
+ // Only worth reporting for files a backend actually read. A markdown
116
+ // file having no call extraction is not a gap in the call graph.
117
+ if (file.parser)
118
+ unanalyzed.push(file.path);
119
+ continue;
120
+ }
121
+ for (const call of file.result.calls ?? []) {
122
+ const resolvedEdge = resolveOne(call.callee, call.receiver, entry, table, call.ownerClass);
123
+ if (resolvedEdge.kind === "resolved") {
124
+ edges.push({
125
+ from: file.path,
126
+ to: resolvedEdge.to,
127
+ symbol: resolvedEdge.symbol,
128
+ scope: resolvedEdge.scope,
129
+ ...(call.enclosing ? { enclosing: call.enclosing } : {}),
130
+ // An edge inherits the weaker of the call's confidence and the
131
+ // binding's. A speculative import cannot yield an exact call.
132
+ confidence: weaker(call.confidence, resolvedEdge.confidence),
133
+ line: call.line,
134
+ });
135
+ }
136
+ else {
137
+ unresolved.push({
138
+ from: file.path,
139
+ callee: call.callee,
140
+ ...(call.receiver ? { receiver: call.receiver } : {}),
141
+ reason: resolvedEdge.reason,
142
+ line: call.line,
143
+ });
144
+ }
145
+ }
146
+ }
147
+ const byCall = (a, b) => byCodeUnit(a.from, b.from) || byCodeUnit(a.to, b.to) || byCodeUnit(a.symbol, b.symbol) ||
148
+ (a.line ?? 0) - (b.line ?? 0);
149
+ const byUnresolved = (a, b) => byCodeUnit(a.from, b.from) || byCodeUnit(a.callee, b.callee) || (a.line ?? 0) - (b.line ?? 0);
150
+ return {
151
+ edges: edges.sort(byCall),
152
+ unresolved: unresolved.sort(byUnresolved),
153
+ unanalyzed: unanalyzed.sort(),
154
+ };
155
+ }
156
+ /**
157
+ * Resolve every recorded reference site — reads, `new`, types, JSX, imports and
158
+ * calls — against the symbol table.
159
+ *
160
+ * This is the answer to "what would break if I changed this symbol", and it is
161
+ * the single path both blast-radius surfaces read. Before it existed, the graph
162
+ * could name every caller of a function and not one user of a constant: the
163
+ * extractor only fired inside `isCallExpression`, so `KEEL_RELEASE_VERSION`,
164
+ * read in four files, resolved to silence.
165
+ *
166
+ * Deterministic, for the same reason as resolveCallEdges: two machines with the
167
+ * same index must produce the same bytes.
168
+ */
169
+ export function resolveReferenceEdges(index, table) {
170
+ const edges = [];
171
+ const unresolved = [];
172
+ const unanalyzed = [];
173
+ // Names defined anywhere in the index. See ReferenceResolution.unresolved.
174
+ const definedAnywhere = new Set();
175
+ for (const file of index.files) {
176
+ for (const definition of file.result.definitions ?? [])
177
+ definedAnywhere.add(definition.name);
178
+ }
179
+ for (const file of index.files) {
180
+ const entry = table.get(file.path);
181
+ if (!entry)
182
+ continue;
183
+ if (file.result.references === undefined) {
184
+ if (file.parser)
185
+ unanalyzed.push(file.path);
186
+ continue;
187
+ }
188
+ for (const reference of file.result.references) {
189
+ const resolved = resolveOne(reference.name, reference.receiver, entry, table, reference.ownerClass);
190
+ if (resolved.kind === "resolved") {
191
+ edges.push({
192
+ from: file.path,
193
+ to: resolved.to,
194
+ symbol: resolved.symbol,
195
+ scope: resolved.scope,
196
+ kind: reference.kind,
197
+ ...(reference.enclosing ? { enclosing: reference.enclosing } : {}),
198
+ confidence: weaker(reference.confidence, resolved.confidence),
199
+ line: reference.line,
200
+ });
201
+ }
202
+ else if (definedAnywhere.has(reference.name)) {
203
+ unresolved.push({
204
+ from: file.path,
205
+ callee: reference.name,
206
+ ...(reference.receiver ? { receiver: reference.receiver } : {}),
207
+ reason: resolved.reason,
208
+ line: reference.line,
209
+ });
210
+ }
211
+ }
212
+ }
213
+ const byReference = (a, b) => byCodeUnit(a.from, b.from) || byCodeUnit(a.to, b.to) || byCodeUnit(a.symbol, b.symbol) ||
214
+ (a.line ?? 0) - (b.line ?? 0) || byCodeUnit(a.kind, b.kind);
215
+ const byUnresolved = (a, b) => byCodeUnit(a.from, b.from) || byCodeUnit(a.callee, b.callee) || (a.line ?? 0) - (b.line ?? 0);
216
+ return {
217
+ edges: edges.sort(byReference),
218
+ unresolved: unresolved.sort(byUnresolved),
219
+ unanalyzed: unanalyzed.sort(),
220
+ };
221
+ }
222
+ function resolveOne(callee, receiver, entry, table, ownerClass) {
223
+ const dot = callee.indexOf(".");
224
+ if (dot > 0 || receiver) {
225
+ const base = receiver ?? callee.slice(0, dot);
226
+ const member = callee.slice(callee.indexOf(".") + 1);
227
+ // `this.flush()` inside class Store needs no type inference: the class is
228
+ // in the AST at the call site. Resolve against a method that class defines
229
+ // in this same file. Inherited methods are not followed — that needs the
230
+ // heritage chain, and guessing across it would invent edges.
231
+ if (base === "this" && ownerClass) {
232
+ const method = entry.defined.get(member);
233
+ if (method && method.owner === ownerClass) {
234
+ return { kind: "resolved", to: entry.path, symbol: member, scope: "local", confidence: "exact" };
235
+ }
236
+ return {
237
+ kind: "unresolved",
238
+ reason: `"this.${member}" is not defined on ${ownerClass} in this file (inherited, or assigned at runtime)`,
239
+ };
240
+ }
241
+ // A namespace import is the one member call that is statically knowable:
242
+ // `import * as db from "./db.js"; db.query()`. Everything else needs the
243
+ // receiver's type, which this layer deliberately does not infer.
244
+ const binding = entry.imported.get(base);
245
+ if (binding?.resolvedPath) {
246
+ const target = table.get(binding.resolvedPath);
247
+ if (target?.exported.has(member)) {
248
+ return {
249
+ kind: "resolved",
250
+ to: binding.resolvedPath,
251
+ symbol: member,
252
+ scope: "imported",
253
+ // Probable, not exact: the base could have been reassigned between
254
+ // the import and the call, and nothing here proves it was not.
255
+ confidence: "probable",
256
+ };
257
+ }
258
+ }
259
+ return {
260
+ kind: "unresolved",
261
+ reason: `member call on "${base}" whose type is not tracked`,
262
+ };
263
+ }
264
+ const local = entry.defined.get(callee);
265
+ if (local) {
266
+ return { kind: "resolved", to: entry.path, symbol: callee, scope: "local", confidence: "exact" };
267
+ }
268
+ const binding = entry.imported.get(callee);
269
+ if (binding) {
270
+ if (!binding.resolvedPath) {
271
+ return {
272
+ kind: "unresolved",
273
+ reason: `imported from "${binding.specifier}", which is outside the index`,
274
+ };
275
+ }
276
+ const target = table.get(binding.resolvedPath);
277
+ if (target?.exported.has(binding.exportedAs)) {
278
+ return {
279
+ kind: "resolved",
280
+ to: binding.resolvedPath,
281
+ // The TARGET's name for it. `import { query as run }` then `run()`
282
+ // reaches `query`, and reporting `run` would name a symbol the target
283
+ // does not have.
284
+ symbol: binding.exportedAs,
285
+ scope: "imported",
286
+ confidence: binding.confidence,
287
+ };
288
+ }
289
+ // Imported by this name, but the target does not export it. An alias or a
290
+ // re-export through a barrel. Both are real edges this layer cannot follow,
291
+ // and guessing which file behind the barrel owns the symbol is exactly the
292
+ // guess that produces wrong leases.
293
+ return {
294
+ kind: "unresolved",
295
+ reason: `"${callee}" is imported from ${binding.resolvedPath} as "${binding.exportedAs}", which that file does not export (re-export through a barrel)`,
296
+ };
297
+ }
298
+ // Not bound in this file. A global, a builtin, or a name this backend saw
299
+ // without seeing its declaration. Emphatically not "the function of the same
300
+ // name over in some other directory".
301
+ return { kind: "unresolved", reason: `"${callee}" is not defined in this file and not imported into it` };
302
+ }
303
+ function weaker(a, b) {
304
+ const rank = { exact: 2, probable: 1, speculative: 0 };
305
+ return rank[a] <= rank[b] ? a : b;
306
+ }
307
+ export function extractContracts(index) {
308
+ const contracts = [];
309
+ for (const file of index.files) {
310
+ for (const exported of file.result.exports) {
311
+ contracts.push({
312
+ path: file.path,
313
+ name: exported.name,
314
+ kind: exported.kind,
315
+ confidence: exported.confidence,
316
+ });
317
+ }
318
+ }
319
+ return contracts.sort((a, b) => byCodeUnit(a.path, b.path) || byCodeUnit(a.name, b.name) || byCodeUnit(a.kind, b.kind));
320
+ }
321
+ /** Files that export a given symbol name. Used to explain a blast radius. */
322
+ export function definersOf(index, symbol) {
323
+ return index.files
324
+ .filter((file) => file.result.exports.some((e) => e.name === symbol))
325
+ .map((file) => file.path)
326
+ .sort();
327
+ }
328
+ const symbolCorpusCache = new WeakMap();
329
+ function corpusFor(index) {
330
+ const cached = symbolCorpusCache.get(index);
331
+ if (cached)
332
+ return cached;
333
+ const byKey = new Map();
334
+ for (const file of index.files) {
335
+ const names = [
336
+ ...(file.result.definitions ?? []).map((definition) => definition.name),
337
+ ...file.result.exports.map((exported) => exported.name),
338
+ ];
339
+ for (const symbol of new Set(names)) {
340
+ const terms = new Set(tokenize(symbol));
341
+ if (terms.size === 0)
342
+ continue;
343
+ const key = `${file.path}\0${symbol}`;
344
+ byKey.set(key, {
345
+ path: file.path,
346
+ symbol,
347
+ terms,
348
+ leaf: [...terms].at(-1) ?? symbol.toLowerCase(),
349
+ });
350
+ }
351
+ }
352
+ const candidates = [...byKey.values()].sort((a, b) => byCodeUnit(a.path, b.path) || byCodeUnit(a.symbol, b.symbol));
353
+ const postings = new Map();
354
+ for (const candidate of candidates) {
355
+ for (const term of candidate.terms) {
356
+ const matches = postings.get(term) ?? [];
357
+ matches.push(candidate);
358
+ postings.set(term, matches);
359
+ }
360
+ }
361
+ const corpus = { candidates, postings };
362
+ symbolCorpusCache.set(index, corpus);
363
+ return corpus;
364
+ }
365
+ /**
366
+ * Search the local symbol table from prose without an embedding model.
367
+ *
368
+ * This deliberately uses per-term candidate windows. A common word such as
369
+ * "update" should contribute a quarter-weight signal when it matches many
370
+ * symbols, while a pair of less-saturated terms can corroborate a candidate.
371
+ * A leaf-name match is sufficient on its own because it is the strongest
372
+ * evidence available without type inference.
373
+ */
374
+ export function searchSymbols(index, query, options = {}) {
375
+ const limit = options.limit ?? 20;
376
+ const maxFiles = options.maxFiles ?? 8;
377
+ const candidateWindow = options.candidateWindow ?? 20;
378
+ if (limit < 1 || maxFiles < 1 || candidateWindow < 1)
379
+ return [];
380
+ const { candidates, postings } = corpusFor(index);
381
+ const queryTerms = [...new Set(tokenize(query))];
382
+ const scored = [];
383
+ for (const candidate of candidates) {
384
+ let score = 0;
385
+ let unsaturatedMatches = 0;
386
+ let exactLeaf = false;
387
+ const matched = new Set();
388
+ for (const term of queryTerms) {
389
+ if (!candidate.terms.has(term))
390
+ continue;
391
+ const matches = postings.get(term) ?? [];
392
+ const saturated = matches.length >= candidateWindow;
393
+ const weight = saturated ? 0.25 : 1;
394
+ const leaf = candidate.leaf === term;
395
+ score += weight * (leaf ? 2 : 1);
396
+ if (!saturated)
397
+ unsaturatedMatches += 1;
398
+ if (leaf)
399
+ exactLeaf = true;
400
+ matched.add(term);
401
+ }
402
+ // Corroboration prevents one broad prose term from producing a noisy
403
+ // symbol leg; a direct leaf-name mention is the intentional exception.
404
+ if (!exactLeaf && unsaturatedMatches < 2)
405
+ continue;
406
+ scored.push({
407
+ path: candidate.path,
408
+ symbol: candidate.symbol,
409
+ score,
410
+ matched_terms: [...matched].sort(byCodeUnit),
411
+ });
412
+ }
413
+ scored.sort((a, b) => b.score - a.score || b.matched_terms.length - a.matched_terms.length ||
414
+ byCodeUnit(a.path, b.path) || byCodeUnit(a.symbol, b.symbol));
415
+ const paths = new Set();
416
+ const results = [];
417
+ for (const hit of scored) {
418
+ if (!paths.has(hit.path) && paths.size >= maxFiles)
419
+ continue;
420
+ paths.add(hit.path);
421
+ results.push(hit);
422
+ if (results.length >= limit)
423
+ break;
424
+ }
425
+ return results;
426
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A local sentence embedding model running on CPU using @xenova/transformers.
3
+ * Fulfills R4.1.
4
+ */
5
+ export class TransformersEmbedder {
6
+ modelId;
7
+ queryPrefix;
8
+ documentPrefix;
9
+ _dimensions = 0;
10
+ pipelinePromise = null;
11
+ constructor(modelId = "Xenova/bge-small-en-v1.5", queryPrefix = "", documentPrefix = "") {
12
+ this.modelId = modelId;
13
+ this.queryPrefix = queryPrefix;
14
+ this.documentPrefix = documentPrefix;
15
+ }
16
+ get dimensions() {
17
+ return this._dimensions;
18
+ }
19
+ get id() {
20
+ return `transformers:${this.modelId}:${this.queryPrefix}:${this.documentPrefix}`;
21
+ }
22
+ async getPipeline() {
23
+ if (!this.pipelinePromise) {
24
+ this.pipelinePromise = (async () => {
25
+ try {
26
+ // Dynamic import allows Keel to run if the dependency is missing (R4.2)
27
+ const { pipeline } = await import("@xenova/transformers");
28
+ return await pipeline("feature-extraction", this.modelId, {
29
+ quantized: true // Use INT8 quantization for smaller size and faster CPU inference
30
+ });
31
+ }
32
+ catch (error) {
33
+ throw new Error("Failed to load @xenova/transformers. Vector leg will be disabled.", { cause: error });
34
+ }
35
+ })();
36
+ }
37
+ return this.pipelinePromise;
38
+ }
39
+ async embedText(text) {
40
+ const extractor = await this.getPipeline();
41
+ const output = await extractor(text, { pooling: "mean", normalize: true });
42
+ const vector = Array.from(output.data);
43
+ this._dimensions = vector.length;
44
+ return vector;
45
+ }
46
+ async embed(text) {
47
+ return this.embedText(text);
48
+ }
49
+ async embedQuery(text) {
50
+ return this.embedText(`${this.queryPrefix}${text}`);
51
+ }
52
+ async embedDocument(text) {
53
+ return this.embedText(`${this.documentPrefix}${text}`);
54
+ }
55
+ }
56
+ export async function createTransformersEmbedder(modelId = "Xenova/bge-small-en-v1.5") {
57
+ const asymmetric = modelId.includes("bge-small-en-v1.5")
58
+ ? { query: "Represent this sentence for searching relevant passages: ", document: "" }
59
+ : modelId.includes("nomic-embed-text")
60
+ ? { query: "search_query: ", document: "search_document: " }
61
+ : modelId.includes("embeddinggemma")
62
+ ? { query: "task: search result | query: ", document: "title: none | text: " }
63
+ : { query: "", document: "" };
64
+ const embedder = new TransformersEmbedder(modelId, asymmetric.query, asymmetric.document);
65
+ try {
66
+ // Eagerly try to load the pipeline. If it fails, degrade cleanly to null.
67
+ await embedder.embed("warmup");
68
+ return embedder;
69
+ }
70
+ catch (err) {
71
+ return null;
72
+ }
73
+ }