promptgraph-mcp 2.4.7 → 2.5.0

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 (99) hide show
  1. package/README.md +3 -6
  2. package/ann.js +23 -51
  3. package/api.js +200 -0
  4. package/config.js +14 -0
  5. package/db.js +48 -0
  6. package/embedder.js +10 -0
  7. package/github-import.js +167 -27
  8. package/indexer.js +49 -12
  9. package/marketplace.js +130 -7
  10. package/package.json +9 -3
  11. package/search.js +86 -20
  12. package/src/filter/hard-filter.js +2 -0
  13. package/src/filter/train.js +3 -1
  14. package/src/reranker/reranker.js +27 -0
  15. package/src/store/flat-store.js +61 -0
  16. package/src/store/hnsw-store.js +187 -0
  17. package/src/store/index.js +19 -0
  18. package/src/store/vector-store.js +9 -0
  19. package/src/utils/rate-limiter.js +33 -0
  20. package/validator.js +31 -1
  21. package/registry/training/bad/accepts-HISTORY.md +0 -250
  22. package/registry/training/bad/argparse-CHANGELOG.md +0 -185
  23. package/registry/training/bad/balanced-match-LICENSE.md +0 -23
  24. package/registry/training/bad/better-sqlite3-README.md +0 -99
  25. package/registry/training/bad/bindings-LICENSE.md +0 -22
  26. package/registry/training/bad/bl-LICENSE.md +0 -13
  27. package/registry/training/bad/body-parser-README.md +0 -494
  28. package/registry/training/bad/bytes-HISTORY.md +0 -97
  29. package/registry/training/bad/camelcase-README.md +0 -135
  30. package/registry/training/bad/chai-README.md +0 -162
  31. package/registry/training/bad/cli-progress-LICENSE.md +0 -24
  32. package/registry/training/bad/content-type-HISTORY.md +0 -29
  33. package/registry/training/bad/cookie-SECURITY.md +0 -25
  34. package/registry/training/bad/cookie-signature-HISTORY.md +0 -70
  35. package/registry/training/bad/cors-README.md +0 -277
  36. package/registry/training/bad/cross-spawn-README.md +0 -89
  37. package/registry/training/bad/deep-extend-CHANGELOG.md +0 -46
  38. package/registry/training/bad/depd-HISTORY.md +0 -103
  39. package/registry/training/bad/esprima-README.md +0 -46
  40. package/registry/training/bad/etag-HISTORY.md +0 -83
  41. package/registry/training/bad/expect-type-SECURITY.md +0 -14
  42. package/registry/training/bad/finalhandler-HISTORY.md +0 -239
  43. package/registry/training/bad/fresh-HISTORY.md +0 -80
  44. package/registry/training/bad/glob-LICENSE.md +0 -63
  45. package/registry/training/bad/hono-README.md +0 -85
  46. package/registry/training/bad/http-errors-HISTORY.md +0 -186
  47. package/registry/training/bad/jose-LICENSE.md +0 -21
  48. package/registry/training/bad/jose-README.md +0 -153
  49. package/registry/training/bad/js-yaml-README.md +0 -299
  50. package/registry/training/bad/json-schema-typed-LICENSE.md +0 -57
  51. package/registry/training/bad/json-stringify-safe-CHANGELOG.md +0 -14
  52. package/registry/training/bad/lru-cache-LICENSE.md +0 -55
  53. package/registry/training/bad/media-typer-HISTORY.md +0 -50
  54. package/registry/training/bad/minimatch-LICENSE.md +0 -55
  55. package/registry/training/bad/minimist-CHANGELOG.md +0 -298
  56. package/registry/training/bad/minimist-README.md +0 -121
  57. package/registry/training/bad/ms-LICENSE.md +0 -21
  58. package/registry/training/bad/negotiator-HISTORY.md +0 -114
  59. package/registry/training/bad/on-finished-HISTORY.md +0 -98
  60. package/registry/training/bad/pkce-challenge-CHANGELOG.md +0 -114
  61. package/registry/training/bad/postcss-README.md +0 -28
  62. package/registry/training/bad/prebuild-install-CHANGELOG.md +0 -131
  63. package/registry/training/bad/proxy-addr-HISTORY.md +0 -161
  64. package/registry/training/bad/qs-CHANGELOG.md +0 -822
  65. package/registry/training/bad/rc-README.md +0 -227
  66. package/registry/training/bad/readable-stream-CONTRIBUTING.md +0 -38
  67. package/registry/training/bad/router-HISTORY.md +0 -228
  68. package/registry/training/bad/semver-README.md +0 -680
  69. package/registry/training/bad/send-README.md +0 -317
  70. package/registry/training/bad/serve-static-README.md +0 -253
  71. package/registry/training/bad/statuses-HISTORY.md +0 -87
  72. package/registry/training/bad/type-is-HISTORY.md +0 -292
  73. package/registry/training/bad/vary-HISTORY.md +0 -39
  74. package/registry/training/bad/vite-LICENSE.md +0 -2230
  75. package/registry/training/bad/which-CHANGELOG.md +0 -166
  76. package/registry/training/bad/zod-README.md +0 -191
  77. package/registry/training/good/skills-store-autopilot.md +0 -85
  78. package/registry/training/good/skills-store-bot-builder.md +0 -70
  79. package/registry/training/good/skills-store-chain.md +0 -136
  80. package/registry/training/good/skills-store-evolve.md +0 -100
  81. package/registry/training/good/skills-store-game.md +0 -27
  82. package/registry/training/good/skills-store-hunt.md +0 -102
  83. package/registry/training/good/skills-store-intel.md +0 -56
  84. package/registry/training/good/skills-store-memory-gc.md +0 -58
  85. package/registry/training/good/skills-store-pcsort.md +0 -207
  86. package/registry/training/good/skills-store-pickup.md +0 -60
  87. package/registry/training/good/skills-store-recon.md +0 -141
  88. package/registry/training/good/skills-store-remember.md +0 -64
  89. package/registry/training/good/skills-store-report.md +0 -117
  90. package/registry/training/good/skills-store-router.md +0 -225
  91. package/registry/training/good/skills-store-search.md +0 -168
  92. package/registry/training/good/skills-store-surface.md +0 -53
  93. package/registry/training/good/skills-store-token-scan.md +0 -141
  94. package/registry/training/good/skills-store-triage.md +0 -97
  95. package/registry/training/good/skills-store-unity.md +0 -733
  96. package/registry/training/good/skills-store-validate.md +0 -135
  97. package/registry/training/good/skills-store-web3-audit.md +0 -217
  98. package/src/filter/cluster.js +0 -52
  99. package/src/filter/dedup.js +0 -36
package/README.md CHANGED
@@ -154,12 +154,9 @@ Claude uses these automatically when the MCP server is running:
154
154
 
155
155
  ## Search modes
156
156
 
157
- | Mode | How | Quality |
158
- |---|---|---|
159
- | After `pg reindex` | Cosine similarity on 384-dim BGE vectors | Semantic — finds by meaning |
160
- | After `pg reindex --fast` | SQLite FTS5 BM25 | Keyword — exact word match |
161
-
162
- Search falls back to FTS5 automatically if no embeddings exist.
157
+ Search falls back to FTS5 automatically if no embeddings exist. When embeddings are
158
+ available, results are a hybrid of semantic similarity (embedding cosine) and keyword
159
+ relevance (BM25) giving you the best of both approaches.
163
160
 
164
161
  ---
165
162
 
package/ann.js CHANGED
@@ -1,61 +1,33 @@
1
1
  import { getDb, blobToVec } from './db.js';
2
+ import { getStore, resetStore } from './src/store/index.js';
2
3
 
3
- // In-memory flat index — no external dependency.
4
- // For typical skill counts (<5000) this is faster than vectra's disk-based HNSW
5
- // because all data fits in RAM and no I/O is needed per query.
6
- let _cache = null;
7
- let _cacheChunkCount = -1;
8
-
9
- function loadCache(db) {
10
- const count = db.prepare('SELECT COUNT(*) as n FROM chunks').get().n;
11
- if (_cache && count === _cacheChunkCount) return _cache;
12
- const rows = db.prepare('SELECT skill_id, embedding FROM chunks').all();
13
- _cache = rows.map(r => ({
14
- skill_id: r.skill_id,
15
- vec: new Float32Array(blobToVec(r.embedding)),
16
- }));
17
- _cacheChunkCount = count;
18
- return _cache;
19
- }
20
-
21
- function cosineSim(a, b) {
22
- let dot = 0, na = 0, nb = 0;
23
- for (let i = 0; i < a.length; i++) {
24
- dot += a[i] * b[i];
25
- na += a[i] * a[i];
26
- nb += b[i] * b[i];
27
- }
28
- return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-8);
29
- }
30
-
31
- // Called after reindex — invalidate cache so next search reloads
32
4
  export async function buildAnnIndex() {
33
- _cache = null;
34
- _cacheChunkCount = -1;
35
- const db = getDb();
36
- const count = db.prepare('SELECT COUNT(*) as n FROM chunks').get().n;
37
- console.error(`[PromptGraph] In-memory index ready: ${count} chunks`);
5
+ try {
6
+ const db = getDb();
7
+ const rows = db.prepare('SELECT skill_id, embedding FROM chunks').all();
8
+ const entries = rows.map(r => ({
9
+ skill_id: r.skill_id,
10
+ vector: blobToVec(r.embedding),
11
+ }));
12
+ const store = getStore();
13
+ await store.build(entries);
14
+ console.error(`[PromptGraph] ANN index ready: ${store.size} vectors`);
15
+ } catch (e) {
16
+ console.error(`[PromptGraph] ANN build failed: ${e.message}`);
17
+ }
38
18
  }
39
19
 
40
20
  export async function annSearch(queryVec, topK = 20) {
41
21
  try {
42
- const db = getDb();
43
- const cache = loadCache(db);
44
- if (!cache.length) return null;
45
-
46
- const qArr = new Float32Array(queryVec);
47
- const bestBySkill = new Map();
48
- for (const entry of cache) {
49
- const score = cosineSim(qArr, entry.vec);
50
- const prev = bestBySkill.get(entry.skill_id);
51
- if (!prev || score > prev) bestBySkill.set(entry.skill_id, score);
52
- }
53
-
54
- return [...bestBySkill.entries()]
55
- .sort((a, b) => b[1] - a[1])
56
- .slice(0, topK)
57
- .map(([skill_id, score]) => ({ skill_id, score }));
58
- } catch {
22
+ const store = getStore();
23
+ if (store.size === 0) return null;
24
+ return await store.search(queryVec, topK);
25
+ } catch (e) {
26
+ console.error(`[PromptGraph] ANN search error: ${e.message}`);
59
27
  return null;
60
28
  }
61
29
  }
30
+
31
+ export function resetAnnIndex() {
32
+ resetStore();
33
+ }
package/api.js ADDED
@@ -0,0 +1,200 @@
1
+ import { search as internalSearch, getContext, listAll as internalList } from './search.js';
2
+ import { indexAll, indexSource } from './indexer.js';
3
+ import { getDb } from './db.js';
4
+ import { loadConfig } from './config.js';
5
+
6
+ /**
7
+ * Search for skills by query text.
8
+ * @param {string} query - Natural language search query
9
+ * @param {{ topK?: number, embedWeight?: number, bm25Weight?: number, source?: string, category?: string }} [options]
10
+ * @returns {Promise<Array<{id: string, name: string, description: string, source: string, score: number, snippet: string}>>}
11
+ */
12
+ export async function search(query, options = {}) {
13
+ try {
14
+ const { topK = 5, source } = options;
15
+ const results = await internalSearch(query, source ? topK * 3 : topK);
16
+ if (source) {
17
+ return results.filter(r => r.source === source).slice(0, topK);
18
+ }
19
+ return results;
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Index all .md files in a directory under a source name.
27
+ * @param {string} sourceDir - Path to directory containing .md skill files
28
+ * @param {string} sourceName - Source label stored with each skill
29
+ * @returns {Promise<{indexed: number, skipped: number, errors: number}>}
30
+ */
31
+ export async function index(sourceDir, sourceName) {
32
+ try {
33
+ if (sourceDir.includes('..')) {
34
+ return { indexed: 0, skipped: 0, errors: 1, error: 'Path traversal detected in sourceDir' };
35
+ }
36
+ const db = getDb();
37
+ const { globSync } = await import('glob');
38
+ const fs = await import('fs');
39
+ const path = await import('path');
40
+ const { createHash } = await import('crypto');
41
+ const { parseSkillFile, isSkillFile } = await import('./parser.js');
42
+ const { embedBatch, BATCH_SIZE } = await import('./embedder.js');
43
+ const { skillId, vecToBlob } = await import('./db.js');
44
+ const { chunkText } = await import('./chunker.js');
45
+
46
+ const files = globSync(`${sourceDir}/**/*.md`);
47
+ let indexed = 0, skipped = 0, errors = 0;
48
+ const batch = [];
49
+
50
+ for (const file of files) {
51
+ try {
52
+ const stat = fs.statSync(file);
53
+ if (stat.size > 5 * 1024 * 1024) { skipped++; continue; }
54
+ const raw = fs.readFileSync(file, 'utf8');
55
+ const hash = createHash('md5').update(raw).digest('hex');
56
+ if (!isSkillFile(file, raw)) { skipped++; continue; }
57
+ const parsed = parseSkillFile(file, sourceName, { raw });
58
+ batch.push({ ...parsed, hash });
59
+ if (batch.length >= BATCH_SIZE) {
60
+ await indexBatch(db, batch);
61
+ indexed += batch.length;
62
+ batch.length = 0;
63
+ }
64
+ } catch { errors++; }
65
+ }
66
+ if (batch.length > 0) {
67
+ await indexBatch(db, batch);
68
+ indexed += batch.length;
69
+ }
70
+ return { indexed, skipped, errors };
71
+ } catch (e) {
72
+ return { indexed: 0, skipped: 0, errors: 1, error: e.message };
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Remove a skill by its ID.
78
+ * @param {string} skillId - Skill ID (format: "source::name")
79
+ * @returns {{ok: boolean, error?: string}}
80
+ */
81
+ export function remove(skillId) {
82
+ try {
83
+ const db = getDb();
84
+ db.prepare('DELETE FROM skills WHERE id = ?').run(skillId);
85
+ db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(skillId);
86
+ db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(skillId, skillId);
87
+ db.prepare('DELETE FROM ratings WHERE skill_id = ?').run(skillId);
88
+ return { ok: true };
89
+ } catch (e) {
90
+ return { ok: false, error: e.message };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Reindex changed files only (hash-based). Scans all configured sources,
96
+ * updates files whose content hash changed, and removes stale skills.
97
+ * @returns {Promise<{updated: number, removed: number, errors: number}>}
98
+ */
99
+ export async function update() {
100
+ try {
101
+ const db = getDb();
102
+ const config = loadConfig();
103
+ const { globSync } = await import('glob');
104
+ const fs = await import('fs');
105
+ const path = await import('path');
106
+ const { createHash } = await import('crypto');
107
+ const { parseSkillFile, isSkillFile } = await import('./parser.js');
108
+ const { embedBatch, BATCH_SIZE } = await import('./embedder.js');
109
+ const { skillId, vecToBlob } = await import('./db.js');
110
+ const { chunkText } = await import('./chunker.js');
111
+ const { indexBatch } = await import('./indexer.js');
112
+
113
+ const normalizedSources = config.sources.map(s => ({ ...s, normDir: path.resolve(s.dir) }))
114
+ .sort((a, b) => b.normDir.length - a.normDir.length);
115
+
116
+ const seenFiles = new Set();
117
+ const allFiles = [];
118
+ for (const { dir, source } of normalizedSources) {
119
+ const files = globSync(`${dir}/**/*.md`);
120
+ for (const f of files) {
121
+ const norm = path.resolve(f);
122
+ if (!seenFiles.has(norm)) { seenFiles.add(norm); allFiles.push({ file: norm, source }); }
123
+ }
124
+ }
125
+
126
+ const dbByPath = new Map();
127
+ for (const row of db.prepare('SELECT id, path, hash FROM skills').all()) {
128
+ dbByPath.set(row.path, row);
129
+ }
130
+
131
+ const existingPaths = new Set(allFiles.map(f => f.file));
132
+ let removed = 0;
133
+ for (const [filePath, row] of dbByPath) {
134
+ if (!existingPaths.has(filePath)) {
135
+ db.prepare('DELETE FROM skills WHERE id = ?').run(row.id);
136
+ db.prepare('DELETE FROM chunks WHERE skill_id = ?').run(row.id);
137
+ db.prepare('DELETE FROM edges WHERE from_skill = ? OR to_skill = ?').run(row.id, row.id);
138
+ db.prepare('DELETE FROM ratings WHERE skill_id = ?').run(row.id);
139
+ removed++;
140
+ }
141
+ }
142
+
143
+ let updated = 0, errors = 0, batch = [];
144
+ for (const { file, source } of allFiles) {
145
+ try {
146
+ const raw = fs.readFileSync(file, 'utf8');
147
+ const hash = createHash('md5').update(raw).digest('hex');
148
+ const dbRow = dbByPath.get(file);
149
+ if (dbRow?.hash === hash) continue;
150
+ if (!isSkillFile(file, raw)) continue;
151
+ const parsed = parseSkillFile(file, source, { raw });
152
+ batch.push({ ...parsed, hash });
153
+ if (batch.length >= BATCH_SIZE) {
154
+ await indexBatch(db, batch);
155
+ updated += batch.length;
156
+ batch.length = 0;
157
+ }
158
+ } catch { errors++; }
159
+ }
160
+ if (batch.length > 0) {
161
+ await indexBatch(db, batch);
162
+ updated += batch.length;
163
+ }
164
+
165
+ return { updated, removed, errors };
166
+ } catch (e) {
167
+ return { updated: 0, removed: 0, errors: 1, error: e.message };
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get full skill details by ID, including edges.
173
+ * @param {string} skillId
174
+ * @returns {object|null}
175
+ */
176
+ export function get(skillId) {
177
+ try {
178
+ return getContext(skillId);
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * List all skills with optional filtering.
186
+ * @param {{ source?: string, category?: string, limit?: number, offset?: number }} [options]
187
+ * @returns {Array<object>}
188
+ */
189
+ export function list(options = {}) {
190
+ try {
191
+ const { source, limit, offset } = options;
192
+ let results = internalList();
193
+ if (source) results = results.filter(r => r.source === source);
194
+ if (offset) results = results.slice(offset);
195
+ if (limit) results = results.slice(0, limit);
196
+ return results;
197
+ } catch {
198
+ return [];
199
+ }
200
+ }
package/config.js CHANGED
@@ -8,6 +8,13 @@ export const PROMPTGRAPH_DIR = path.join(CLAUDE_DIR, '.promptgraph');
8
8
  export const SKILLS_STORE_DIR = path.join(CLAUDE_DIR, 'skills-store');
9
9
  const CONFIG_PATH = path.join(PROMPTGRAPH_DIR, 'config.json');
10
10
 
11
+ export const MAX_DOWNLOAD_SIZE = 50 * 1024 * 1024 // 50 MB per file
12
+ export const MAX_FILE_COUNT = 50000 // max files per repo
13
+ export const MAX_REPO_SIZE = 500 * 1024 * 1024 // 500 MB per repo
14
+ export const RATE_LIMIT_REQUESTS = 30 // requests per window
15
+ export const RATE_LIMIT_WINDOW_MS = 60000 // 1 minute window
16
+ export const BATCH_SIZE = 100 // batch indexing size
17
+
11
18
  const DEFAULTS = {
12
19
  sources: [
13
20
  { dir: path.join(CLAUDE_DIR, 'skills-store'), source: 'skills-store' },
@@ -56,3 +63,10 @@ export async function promptConfig() {
56
63
  console.log(`\nConfig saved to ${CONFIG_PATH}`);
57
64
  return config;
58
65
  }
66
+
67
+ export function sanitizePath(inputPath) {
68
+ if (inputPath.includes('..')) {
69
+ throw new Error(`Path traversal blocked: "${inputPath}"`);
70
+ }
71
+ return path.resolve(inputPath);
72
+ }
package/db.js CHANGED
@@ -63,6 +63,54 @@ export function getDb() {
63
63
  db.exec('ALTER TABLE skills ADD COLUMN hash TEXT');
64
64
  }
65
65
 
66
+ // migrate: add registry metadata columns
67
+ if (!cols.includes('version')) {
68
+ db.exec('ALTER TABLE skills ADD COLUMN version TEXT');
69
+ }
70
+ if (!cols.includes('author')) {
71
+ db.exec('ALTER TABLE skills ADD COLUMN author TEXT');
72
+ }
73
+ if (!cols.includes('license')) {
74
+ db.exec('ALTER TABLE skills ADD COLUMN license TEXT');
75
+ }
76
+ if (!cols.includes('updated_at')) {
77
+ db.exec('ALTER TABLE skills ADD COLUMN updated_at TEXT');
78
+ }
79
+ if (!cols.includes('downloads')) {
80
+ db.exec('ALTER TABLE skills ADD COLUMN downloads INTEGER DEFAULT 0');
81
+ }
82
+ if (!cols.includes('verified')) {
83
+ db.exec('ALTER TABLE skills ADD COLUMN verified INTEGER DEFAULT 0');
84
+ }
85
+ if (!cols.includes('trust_level')) {
86
+ db.exec('ALTER TABLE skills ADD COLUMN trust_level TEXT DEFAULT \'unknown\'');
87
+ }
88
+ if (!cols.includes('rating')) {
89
+ db.exec('ALTER TABLE skills ADD COLUMN rating REAL DEFAULT 0');
90
+ }
91
+ if (!cols.includes('rating_count')) {
92
+ db.exec('ALTER TABLE skills ADD COLUMN rating_count INTEGER DEFAULT 0');
93
+ }
94
+ if (!cols.includes('popularity')) {
95
+ db.exec('ALTER TABLE skills ADD COLUMN popularity REAL DEFAULT 0');
96
+ }
97
+ if (!cols.includes('last_update')) {
98
+ db.exec('ALTER TABLE skills ADD COLUMN last_update TEXT');
99
+ }
100
+
101
+ // migrate: registry entries metadata table
102
+ db.exec(`
103
+ CREATE TABLE IF NOT EXISTS registry_entries (
104
+ id TEXT PRIMARY KEY,
105
+ trust_level TEXT DEFAULT 'unknown',
106
+ downloads INTEGER DEFAULT 0,
107
+ rating REAL DEFAULT 0,
108
+ rating_count INTEGER DEFAULT 0,
109
+ popularity REAL DEFAULT 0,
110
+ last_update TEXT
111
+ );
112
+ `);
113
+
66
114
  // migrate: convert JSON text embeddings to Float32 BLOB (one-time, ~10x smaller)
67
115
  const textEmbeddings = db.prepare("SELECT COUNT(*) as n FROM chunks WHERE typeof(embedding) = 'text'").get();
68
116
  if (textEmbeddings?.n > 0) {
package/embedder.js CHANGED
@@ -4,6 +4,11 @@ import os from 'os';
4
4
 
5
5
  const CACHE_DIR = path.join(os.homedir(), '.claude', '.promptgraph', 'model-cache');
6
6
  const BATCH_SIZE = 256;
7
+ const MAX_EMBEDDING_CALLS = 10000;
8
+ let embedCallCount = 0;
9
+
10
+ export function getEmbedCallCount() { return embedCallCount; }
11
+ export function resetEmbedCallCount() { embedCallCount = 0; }
7
12
 
8
13
  let model = null;
9
14
 
@@ -18,6 +23,7 @@ async function getModel() {
18
23
  }
19
24
 
20
25
  export async function embed(text) {
26
+ embedCallCount++;
21
27
  const m = await getModel();
22
28
  const results = [];
23
29
  for await (const batch of m.embed([text])) {
@@ -27,6 +33,10 @@ export async function embed(text) {
27
33
  }
28
34
 
29
35
  export async function embedBatch(texts) {
36
+ if (embedCallCount + texts.length > MAX_EMBEDDING_CALLS) {
37
+ throw new Error(`Embedding queue limit exceeded (max ${MAX_EMBEDDING_CALLS} chunks per session). Use --fast or reindex incrementally.`);
38
+ }
39
+ embedCallCount += texts.length;
30
40
  const m = await getModel();
31
41
  const all = [];
32
42
  for await (const batch of m.embed(texts)) {