thumbgate 1.16.13 → 1.16.19

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 (62) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +3 -1
  5. package/adapters/claude/.mcp.json +2 -2
  6. package/adapters/mcp/server-stdio.js +26 -1
  7. package/adapters/opencode/opencode.json +1 -1
  8. package/bin/cli.js +420 -1
  9. package/config/gate-templates.json +372 -0
  10. package/config/mcp-allowlists.json +25 -0
  11. package/config/model-candidates.json +59 -2
  12. package/config/model-tiers.json +4 -1
  13. package/package.json +79 -22
  14. package/public/compare.html +6 -0
  15. package/public/index.html +144 -11
  16. package/public/numbers.html +8 -8
  17. package/public/pro.html +22 -24
  18. package/scripts/agent-design-governance.js +211 -0
  19. package/scripts/agent-reasoning-traces.js +683 -0
  20. package/scripts/agent-reward-model.js +438 -0
  21. package/scripts/agent-stack-survival-audit.js +231 -0
  22. package/scripts/ai-engineering-stack-guardrails.js +256 -0
  23. package/scripts/billing.js +16 -4
  24. package/scripts/chatgpt-ads-readiness-pack.js +195 -0
  25. package/scripts/cli-schema.js +277 -0
  26. package/scripts/code-graph-guardrails.js +176 -0
  27. package/scripts/deepseek-v4-runtime-guardrails.js +253 -0
  28. package/scripts/gemini-embedding-policy.js +198 -0
  29. package/scripts/inference-cache-policy.js +39 -0
  30. package/scripts/judge-reward-function.js +396 -0
  31. package/scripts/llm-behavior-monitor.js +251 -0
  32. package/scripts/long-running-agent-context-guardrails.js +176 -0
  33. package/scripts/multimodal-retrieval-plan.js +31 -11
  34. package/scripts/oss-pr-opportunity-scout.js +240 -0
  35. package/scripts/proactive-agent-eval-guardrails.js +230 -0
  36. package/scripts/profile-router.js +5 -4
  37. package/scripts/prompting-operating-system.js +273 -0
  38. package/scripts/proxy-pointer-rag-guardrails.js +189 -0
  39. package/scripts/rag-precision-guardrails.js +202 -0
  40. package/scripts/rate-limiter.js +1 -1
  41. package/scripts/reasoning-efficiency-guardrails.js +176 -0
  42. package/scripts/reward-hacking-guardrails.js +251 -0
  43. package/scripts/seo-gsd.js +1201 -11
  44. package/scripts/single-use-credential-gate.js +182 -0
  45. package/scripts/structured-prompt-driven.js +226 -0
  46. package/scripts/telemetry-analytics.js +31 -6
  47. package/scripts/tool-registry.js +92 -0
  48. package/scripts/upstream-contribution-engine.js +379 -0
  49. package/scripts/vector-store.js +119 -4
  50. package/src/api/server.js +333 -100
  51. package/scripts/agents-sdk-sandbox-plan.js +0 -57
  52. package/scripts/ai-org-governance.js +0 -98
  53. package/scripts/artifact-agent-plan.js +0 -81
  54. package/scripts/enterprise-agent-rollout.js +0 -34
  55. package/scripts/experience-replay-governance.js +0 -69
  56. package/scripts/inference-economics.js +0 -53
  57. package/scripts/knowledge-layer-plan.js +0 -108
  58. package/scripts/memory-store-governance.js +0 -60
  59. package/scripts/post-training-governance.js +0 -34
  60. package/scripts/production-agent-readiness.js +0 -40
  61. package/scripts/scaling-law-claims.js +0 -60
  62. package/scripts/student-consistent-training.js +0 -73
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ const ROOT = path.join(__dirname, '..');
9
+ const DEFAULT_OUTPUT_DIR = path.join(ROOT, 'docs', 'marketing');
10
+
11
+ const DEFAULT_REPO_OVERRIDES = Object.freeze({
12
+ '@anthropic-ai/sdk': 'anthropics/anthropic-sdk-typescript',
13
+ '@google/genai': 'googleapis/js-genai',
14
+ '@huggingface/transformers': 'huggingface/transformers.js',
15
+ '@lancedb/lancedb': 'lancedb/lancedb',
16
+ 'apache-arrow': 'apache/arrow',
17
+ 'better-sqlite3': 'WiseLibs/better-sqlite3',
18
+ dotenv: 'motdotla/dotenv',
19
+ 'playwright-core': 'microsoft/playwright',
20
+ protobufjs: 'protobufjs/protobuf.js',
21
+ stripe: 'stripe/stripe-node',
22
+ '@changesets/cli': 'changesets/changesets',
23
+ '@changesets/changelog-github': 'changesets/changesets',
24
+ c8: 'bcoe/c8',
25
+ undici: 'nodejs/undici',
26
+ });
27
+
28
+ function normalizeRepo(value) {
29
+ if (!value) return '';
30
+ let source = String(value).trim();
31
+ source = source.replace(/^git\+/, '').replace(/\.git$/, '');
32
+ source = source.replace(/^https:\/\/github\.com\//i, '');
33
+ source = source.replace(/^git@github\.com:/i, '');
34
+ const match = source.match(/^([^/\s]+)\/([^/\s#?]+)/);
35
+ return match ? `${match[1]}/${match[2]}` : '';
36
+ }
37
+
38
+ function readJson(filePath, fallback = null) {
39
+ try {
40
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
41
+ } catch (_) {
42
+ return fallback;
43
+ }
44
+ }
45
+
46
+ function listDirectPackages(root = ROOT) {
47
+ const pkg = readJson(path.join(root, 'package.json'), {});
48
+ const runtime = Object.keys(pkg.dependencies || {}).map((name) => ({ name, dependencyType: 'runtime' }));
49
+ const dev = Object.keys(pkg.devDependencies || {}).map((name) => ({ name, dependencyType: 'dev' }));
50
+ const seen = new Set();
51
+ return [...runtime, ...dev].filter((entry) => {
52
+ if (seen.has(entry.name)) return false;
53
+ seen.add(entry.name);
54
+ return true;
55
+ });
56
+ }
57
+
58
+ function resolvePackageRepo(name, options = {}) {
59
+ const overrides = { ...DEFAULT_REPO_OVERRIDES, ...(options.repoOverrides || {}) };
60
+ if (overrides[name]) return overrides[name];
61
+
62
+ const packageLock = options.packageLock || readJson(path.join(options.root || ROOT, 'package-lock.json'), {});
63
+ const lockEntry = packageLock.packages && packageLock.packages[`node_modules/${name}`];
64
+ const fromLock = normalizeRepo(lockEntry && lockEntry.repository && (lockEntry.repository.url || lockEntry.repository));
65
+ if (fromLock) return fromLock;
66
+
67
+ const metadata = options.packageMetadata && options.packageMetadata[name];
68
+ const fromMetadata = normalizeRepo(metadata && metadata.repository && (metadata.repository.url || metadata.repository));
69
+ if (fromMetadata) return fromMetadata;
70
+
71
+ return '';
72
+ }
73
+
74
+ function packageIssueQueries(pkg) {
75
+ const terms = [
76
+ 'is:issue is:open label:bug',
77
+ 'is:issue is:open label:"good first issue"',
78
+ 'is:issue is:open label:"help wanted"',
79
+ 'is:issue is:open bounty',
80
+ 'is:issue is:open "bug bounty"',
81
+ 'is:issue is:open security',
82
+ 'is:issue is:open regression',
83
+ 'is:issue is:open docs OR documentation',
84
+ 'is:issue is:open typescript OR types',
85
+ 'is:issue is:open test OR ci OR flake',
86
+ ];
87
+ return terms.map((term) => `repo:${pkg.repo} ${term}`);
88
+ }
89
+
90
+ function packageIssueSearchTerms() {
91
+ return [
92
+ 'label:bug',
93
+ 'label:"good first issue"',
94
+ 'label:"help wanted"',
95
+ 'bounty',
96
+ '"bug bounty"',
97
+ 'security',
98
+ 'regression',
99
+ 'docs OR documentation',
100
+ 'typescript OR types',
101
+ 'test OR ci OR flake',
102
+ ];
103
+ }
104
+
105
+ function parseGhIssueList(stdout, repo) {
106
+ const payload = readJsonFromString(stdout, []);
107
+ return payload.map((issue) => ({
108
+ repo,
109
+ number: issue.number,
110
+ title: issue.title || '',
111
+ url: issue.url || `https://github.com/${repo}/issues/${issue.number}`,
112
+ labels: (issue.labels || []).map((label) => typeof label === 'string' ? label : label.name).filter(Boolean),
113
+ updatedAt: issue.updatedAt || issue.updated_at || '',
114
+ commentSignals: detectCommentSignals((issue.comments || []).map((comment) => comment.body || '')),
115
+ }));
116
+ }
117
+
118
+ function readJsonFromString(source, fallback) {
119
+ try {
120
+ return JSON.parse(source);
121
+ } catch (_) {
122
+ return fallback;
123
+ }
124
+ }
125
+
126
+ function detectCommentSignals(commentBodies = []) {
127
+ const combined = commentBodies.join('\n').toLowerCase();
128
+ const claimed = /\b(take|taking this|take this up|would like to work|like to work on this|i'?d like to work|i'?d like to take|i'?ll work|working on this)\b/.test(combined);
129
+ const existingPr = /\b(opened|made|created|proposal)\s+(a\s+)?pr\b|\/pull\/\d+|pull\/new|resolved in #\d+|will be resolved in #\d+/.test(combined);
130
+ return {
131
+ claimed,
132
+ existingPr,
133
+ blocked: claimed || existingPr,
134
+ };
135
+ }
136
+
137
+ function fetchRepoIssues(repo, options = {}) {
138
+ if (options.offline) return [];
139
+ const gh = options.ghBinary || 'gh';
140
+ const limit = Math.max(1, Number(options.limit || 30));
141
+ const perQueryLimit = Math.max(5, Math.ceil(limit / 2));
142
+ const seen = new Set();
143
+ const issues = [];
144
+
145
+ for (const search of packageIssueSearchTerms()) {
146
+ if (issues.length >= limit) break;
147
+ const result = spawnSync(gh, [
148
+ 'issue',
149
+ 'list',
150
+ '--repo',
151
+ repo,
152
+ '--state',
153
+ 'open',
154
+ '--search',
155
+ search,
156
+ '--limit',
157
+ String(perQueryLimit),
158
+ '--json',
159
+ 'number,title,url,labels,updatedAt,comments',
160
+ ], {
161
+ encoding: 'utf8',
162
+ timeout: options.timeoutMs || 10000,
163
+ });
164
+ if (result.status !== 0) continue;
165
+ for (const issue of parseGhIssueList(result.stdout, repo)) {
166
+ const key = `${repo}#${issue.number}`;
167
+ if (seen.has(key)) continue;
168
+ seen.add(key);
169
+ issues.push(issue);
170
+ if (issues.length >= limit) break;
171
+ }
172
+ }
173
+
174
+ return issues;
175
+ }
176
+
177
+ function issueScore(issue, pkg) {
178
+ const labels = (issue.labels || []).map((label) => String(label).toLowerCase());
179
+ const title = String(issue.title || '').toLowerCase();
180
+ let score = pkg.dependencyType === 'runtime' ? 20 : 12;
181
+ if (labels.some((label) => /bug|regression|defect/.test(label)) || /\bbug|regression|crash|fail/.test(title)) score += 25;
182
+ if (labels.some((label) => /good first issue|help wanted|up for grabs/.test(label))) score += 18;
183
+ if (labels.some((label) => /security|vulnerability/.test(label)) || /\bsecurity|vulnerability|cve\b/.test(title)) score += 16;
184
+ if (labels.some((label) => /bounty|reward|paid/.test(label)) || /\bbounty|reward|paid\b/.test(title)) score += 22;
185
+ if (/\b(?:docs?|test|typescript|types|ci|flake)\b/.test(title)) score += 10;
186
+ return score;
187
+ }
188
+
189
+ function buildPatchReadiness(issue, pkg) {
190
+ const labels = (issue.labels || []).map((label) => String(label).toLowerCase());
191
+ const title = String(issue.title || '').toLowerCase();
192
+ const commentSignals = issue.commentSignals || {};
193
+ const explicitSmallLabel = labels.some((label) => /good first issue|documentation|docs|test|typescript|types|ci/.test(label));
194
+ const smallPatchTitle = /\b(?:docs?|documentation|readme|example|test|typescript|types|ci|flake|lint|typo)\b/.test(title);
195
+ const helpWantedOnly = labels.some((label) => /help wanted|up for grabs/.test(label))
196
+ && !explicitSmallLabel
197
+ && !smallPatchTitle;
198
+ const highRiskTitle = /\bcrash|segfault|sigill|security|vulnerability|cve|attestation|supply chain|silent data|corruption\b/.test(title);
199
+ const smallPatch = (explicitSmallLabel || smallPatchTitle) && (!highRiskTitle || smallPatchTitle);
200
+ const canAutofix = smallPatch && !helpWantedOnly && !commentSignals.blocked;
201
+ const evidenceGate = commentSignals.blocked
202
+ ? 'claimed-or-existing-pr'
203
+ : canAutofix ? 'autonomous-patch-ready' : 'triage-before-pr';
204
+ return {
205
+ canAutofix,
206
+ prAllowed: canAutofix,
207
+ effort: canAutofix ? 'small' : 'needs-triage',
208
+ evidenceGate,
209
+ blockers: [
210
+ commentSignals.claimed ? 'issue appears claimed in comments' : '',
211
+ commentSignals.existingPr ? 'issue appears to have an existing PR or proposal' : '',
212
+ ].filter(Boolean),
213
+ requiredProof: [
214
+ 'Fork or branch only; never push to upstream default branch.',
215
+ 'Reproduce or cite the issue before changing code.',
216
+ 'Run the upstream repo test command for the touched package path.',
217
+ 'Do not open a public PR if reproduction, test proof, or maintainer relevance is missing.',
218
+ 'Open a PR only with a minimal patch, issue link, test proof, and no ThumbGate sales copy.',
219
+ ],
220
+ promotionRule: 'Earn trust by fixing the dependency; mention ThumbGate only in profile/context, not in the PR body unless directly relevant.',
221
+ suggestedBranch: `codex/upstream-${pkg.name.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase()}-${issue.number || 'issue'}`,
222
+ };
223
+ }
224
+
225
+ function buildUpstreamContributionPlan(options = {}) {
226
+ const root = options.root || ROOT;
227
+ const issuesByRepo = options.issuesByRepo || {};
228
+ const packages = listDirectPackages(root)
229
+ .map((pkg) => ({ ...pkg, repo: resolvePackageRepo(pkg.name, { ...options, root }) }))
230
+ .filter((pkg) => pkg.repo)
231
+ .sort((left, right) => {
232
+ const leftHasEvidence = Object.prototype.hasOwnProperty.call(issuesByRepo, left.repo) ? 1 : 0;
233
+ const rightHasEvidence = Object.prototype.hasOwnProperty.call(issuesByRepo, right.repo) ? 1 : 0;
234
+ if (leftHasEvidence !== rightHasEvidence) return rightHasEvidence - leftHasEvidence;
235
+ return left.name.localeCompare(right.name);
236
+ });
237
+ const maxRepos = Math.max(1, Number(options.maxRepos || options['max-repos'] || 12));
238
+ const maxIssues = Math.max(1, Number(options.maxIssues || options['max-issues'] || 5));
239
+
240
+ const repoRows = packages
241
+ .slice(0, maxRepos)
242
+ .map((pkg) => {
243
+ const issues = issuesByRepo[pkg.repo] || fetchRepoIssues(pkg.repo, {
244
+ offline: options.offline !== false,
245
+ limit: options.issueFetchLimit || 30,
246
+ ghBinary: options.ghBinary,
247
+ timeoutMs: options.timeoutMs,
248
+ });
249
+ const rankedIssues = issues
250
+ .map((issue) => ({
251
+ ...issue,
252
+ score: issueScore(issue, pkg),
253
+ readiness: buildPatchReadiness(issue, pkg),
254
+ }))
255
+ .sort((left, right) => right.score - left.score)
256
+ .slice(0, maxIssues);
257
+ return {
258
+ package: pkg.name,
259
+ dependencyType: pkg.dependencyType,
260
+ repo: pkg.repo,
261
+ searchQueries: packageIssueQueries(pkg),
262
+ issues: rankedIssues,
263
+ nextAction: rankedIssues.some((issue) => issue.readiness.canAutofix)
264
+ ? 'Clone/fork the top autofix-ready issue, produce a minimal patch, run upstream tests, then open PR with proof.'
265
+ : 'Monitor issue search queries; wait for a small bug, docs, CI, type, or test issue before patching.',
266
+ };
267
+ });
268
+
269
+ const opportunities = repoRows
270
+ .flatMap((row) => row.issues.map((issue) => ({
271
+ package: row.package,
272
+ repo: row.repo,
273
+ issueNumber: issue.number,
274
+ issueUrl: issue.url,
275
+ title: issue.title,
276
+ score: issue.score,
277
+ canAutofix: issue.readiness.canAutofix,
278
+ evidenceGate: issue.readiness.evidenceGate,
279
+ blockers: issue.readiness.blockers,
280
+ suggestedBranch: issue.readiness.suggestedBranch,
281
+ })))
282
+ .sort((left, right) => right.score - left.score);
283
+
284
+ return {
285
+ name: 'thumbgate-upstream-contribution-engine',
286
+ generatedAt: new Date().toISOString(),
287
+ status: opportunities.length > 0 ? 'actionable' : 'discovery-ready',
288
+ summary: {
289
+ packageCount: packages.length,
290
+ repoCount: repoRows.length,
291
+ issueCount: repoRows.reduce((sum, row) => sum + row.issues.length, 0),
292
+ autofixReadyCount: opportunities.filter((entry) => entry.canAutofix).length,
293
+ },
294
+ guardrails: [
295
+ 'Only target repos ThumbGate actually depends on or uses in shipped workflows.',
296
+ 'Do not create promotional PRs; fix real upstream issues with tests.',
297
+ 'Prefer small bugs, tests, docs, types, CI flakes, and security hardening over large feature work.',
298
+ 'Open external PRs only after reproduction evidence, a minimal patch, and upstream tests pass.',
299
+ 'Never paste secrets, customer data, or private ThumbGate context into upstream issues or PRs.',
300
+ ],
301
+ autonomousWorkflow: [
302
+ 'Run live discovery on schedule and rank only dependency-backed upstream repos.',
303
+ 'Clone/fork the highest autonomous-patch-ready issue into the suggested branch.',
304
+ 'Capture reproduction, apply the smallest patch, and run upstream tests.',
305
+ 'Open a public PR only when the evidence gate is autonomous-patch-ready and proof artifacts exist.',
306
+ 'Stop at a local worktree and operator report when the issue is high-risk, security-sensitive, or unreproduced.',
307
+ ],
308
+ repos: repoRows,
309
+ opportunities,
310
+ };
311
+ }
312
+
313
+ function renderUpstreamContributionPlan(plan) {
314
+ const lines = [
315
+ '# Upstream Contribution Engine',
316
+ '',
317
+ 'Use this to earn developer trust by fixing repos ThumbGate actually depends on. This is not a spam lane.',
318
+ '',
319
+ `Status: ${plan.status}`,
320
+ `Repos scanned: ${plan.summary.repoCount}`,
321
+ `Issues ranked: ${plan.summary.issueCount}`,
322
+ `Autofix-ready: ${plan.summary.autofixReadyCount}`,
323
+ '',
324
+ '## Guardrails',
325
+ '',
326
+ ...plan.guardrails.map((item) => `- ${item}`),
327
+ '',
328
+ '## Autonomous Workflow',
329
+ '',
330
+ ...plan.autonomousWorkflow.map((item) => `- ${item}`),
331
+ '',
332
+ '## Top Opportunities',
333
+ '',
334
+ ];
335
+
336
+ if (plan.opportunities.length === 0) {
337
+ lines.push('No live issues were provided or discovered. Run with GitHub access enabled or review the search queries below.');
338
+ } else {
339
+ for (const item of plan.opportunities.slice(0, 20)) {
340
+ lines.push(`- ${item.repo}#${item.issueNumber || 'n/a'} (${item.score}, ${item.evidenceGate}) ${item.title}`);
341
+ lines.push(` ${item.issueUrl || `https://github.com/${item.repo}/issues`}`);
342
+ lines.push(` Branch: ${item.suggestedBranch}`);
343
+ if (item.blockers.length > 0) lines.push(` Blockers: ${item.blockers.join('; ')}`);
344
+ }
345
+ }
346
+
347
+ lines.push('', '## Repo Search Queries', '');
348
+ for (const repo of plan.repos) {
349
+ lines.push(`### ${repo.package} -> ${repo.repo}`);
350
+ for (const query of repo.searchQueries) lines.push(`- ${query}`);
351
+ lines.push(`- Next: ${repo.nextAction}`, '');
352
+ }
353
+
354
+ return `${lines.join('\n').trimEnd()}\n`;
355
+ }
356
+
357
+ function writeUpstreamContributionPlan(plan, outputDir = DEFAULT_OUTPUT_DIR) {
358
+ fs.mkdirSync(outputDir, { recursive: true });
359
+ const jsonPath = path.join(outputDir, 'upstream-contribution-engine.json');
360
+ const mdPath = path.join(outputDir, 'upstream-contribution-engine.md');
361
+ fs.writeFileSync(jsonPath, `${JSON.stringify(plan, null, 2)}\n`);
362
+ fs.writeFileSync(mdPath, renderUpstreamContributionPlan(plan));
363
+ return { jsonPath, mdPath };
364
+ }
365
+
366
+ module.exports = {
367
+ DEFAULT_REPO_OVERRIDES,
368
+ buildPatchReadiness,
369
+ buildUpstreamContributionPlan,
370
+ detectCommentSignals,
371
+ fetchRepoIssues,
372
+ issueScore,
373
+ listDirectPackages,
374
+ normalizeRepo,
375
+ packageIssueQueries,
376
+ renderUpstreamContributionPlan,
377
+ resolvePackageRepo,
378
+ writeUpstreamContributionPlan,
379
+ };
@@ -8,6 +8,12 @@ const {
8
8
  writeModelFitReport,
9
9
  resolveFeedbackDir,
10
10
  } = require('./local-model-profile');
11
+ const {
12
+ prepareEmbeddingText,
13
+ resolveGeminiEmbeddingConfig,
14
+ resolveGeminiModelResource,
15
+ resolveGeminiTaskType,
16
+ } = require('./gemini-embedding-policy');
11
17
  const { runStep } = require('./durability/step');
12
18
 
13
19
  const DEFAULT_FEEDBACK_DIR = resolveFeedbackDir();
@@ -20,6 +26,7 @@ let _lancedbLoader = null;
20
26
  const _pipelineCache = new Map();
21
27
  let _lastEmbeddingProfile = null;
22
28
  let _pipelineLoader = null;
29
+ let _geminiEmbedderForTests = null;
23
30
  const TABLE_NAME = 'thumbgate_memories';
24
31
 
25
32
  async function getLanceDB() {
@@ -97,13 +104,105 @@ async function getEmbeddingPipeline() {
97
104
  // Set THUMBGATE_VECTOR_STUB_EMBED=true to get a deterministic 384-dim unit vector.
98
105
  // The real embed() is used in production and integration tests
99
106
  // (gated by absence of this env var).
100
- async function embed(text) {
107
+ async function embedWithGemini(text, options = {}) {
108
+ const config = resolveGeminiEmbeddingConfig();
109
+ if (!config.apiKey && !_geminiEmbedderForTests) {
110
+ throw new Error('Gemini embeddings requested but no GEMINI_API_KEY, GOOGLE_API_KEY, or GOOGLE_GENERATIVE_AI_API_KEY is configured');
111
+ }
112
+
113
+ const preparedText = prepareEmbeddingText({
114
+ content: text,
115
+ kind: options.kind,
116
+ task: options.task || config.defaultTask,
117
+ title: options.title,
118
+ });
119
+
120
+ if (_geminiEmbedderForTests) {
121
+ return _geminiEmbedderForTests(preparedText, config, options);
122
+ }
123
+
124
+ if (typeof fetch !== 'function') {
125
+ throw new Error('Gemini embeddings require global fetch. Use Node 18.18+ or the local embedding provider.');
126
+ }
127
+
128
+ const modelResource = resolveGeminiModelResource(config.model);
129
+ const requestBody = {
130
+ model: modelResource,
131
+ content: {
132
+ parts: [{ text: preparedText }],
133
+ },
134
+ outputDimensionality: config.outputDimensionality,
135
+ };
136
+ const taskType = resolveGeminiTaskType({
137
+ kind: options.kind,
138
+ task: options.task || config.defaultTask,
139
+ });
140
+ if (taskType) {
141
+ requestBody.taskType = taskType;
142
+ }
143
+ if (taskType === 'RETRIEVAL_DOCUMENT' && options.title) {
144
+ requestBody.title = String(options.title);
145
+ }
146
+
147
+ const endpoint = `${config.apiBaseUrl}/${modelResource}:embedContent`;
148
+ const response = await fetch(endpoint, {
149
+ method: 'POST',
150
+ headers: {
151
+ 'Content-Type': 'application/json',
152
+ 'x-goog-api-key': config.apiKey,
153
+ },
154
+ body: JSON.stringify(requestBody),
155
+ });
156
+
157
+ if (!response.ok) {
158
+ const body = await response.text().catch(() => '');
159
+ throw new Error(`Gemini embedding request failed: ${response.status} ${response.statusText}${body ? ` — ${body.slice(0, 240)}` : ''}`);
160
+ }
161
+
162
+ const payload = await response.json();
163
+ const values = payload && (
164
+ (payload.embedding && payload.embedding.values)
165
+ || (Array.isArray(payload.embeddings) && payload.embeddings[0] && payload.embeddings[0].values)
166
+ );
167
+
168
+ if (!Array.isArray(values) || values.length === 0) {
169
+ throw new Error('Gemini embedding response did not include vector values');
170
+ }
171
+
172
+ return values.map(Number);
173
+ }
174
+
175
+ async function embed(text, options = {}) {
101
176
  if (process.env.THUMBGATE_VECTOR_STUB_EMBED === 'true') {
102
177
  // Deterministic 384-dim unit vector: first element = 1.0, rest = 0.0
103
178
  const stub = Array(384).fill(0);
104
179
  stub[0] = 1.0;
105
180
  return stub;
106
181
  }
182
+ const geminiConfig = resolveGeminiEmbeddingConfig();
183
+ if (geminiConfig.enabled) {
184
+ try {
185
+ const vector = await embedWithGemini(text, options);
186
+ _lastEmbeddingProfile = {
187
+ generatedAt: new Date().toISOString(),
188
+ source: 'managed',
189
+ activeProfile: {
190
+ id: 'gemini',
191
+ model: geminiConfig.model,
192
+ outputDimensionality: geminiConfig.outputDimensionality,
193
+ task: options.task || geminiConfig.defaultTask,
194
+ rationale: 'Managed Gemini Embedding 2 path with task-specific query/document prefixes.',
195
+ },
196
+ fallbackUsed: false,
197
+ };
198
+ return vector;
199
+ } catch (geminiError) {
200
+ if (!geminiConfig.fallbackToLocal) {
201
+ throw geminiError;
202
+ }
203
+ console.warn(`Gemini embedding fallback: ${geminiError.message}`);
204
+ }
205
+ }
107
206
  const { pipe, profile } = await getEmbeddingPipeline();
108
207
  const output = await pipe(truncateForEmbedding(text, profile.activeProfile.maxChars), {
109
208
  pooling: 'mean',
@@ -129,7 +228,11 @@ async function upsertFeedback(feedbackEvent) {
129
228
  // Embed is pure CPU/model work (transformers.js or stub) — deterministic
130
229
  // for a given input, so no retry is needed here. Retry wraps the table
131
230
  // write below, which is the actual I/O failure surface.
132
- const vector = await embed(textForEmbedding);
231
+ const vector = await embed(textForEmbedding, {
232
+ kind: 'document',
233
+ task: 'code retrieval',
234
+ title: feedbackEvent.id || 'thumbgate feedback',
235
+ });
133
236
 
134
237
  const record = {
135
238
  id: feedbackEvent.id,
@@ -170,14 +273,20 @@ async function searchSimilar(queryText, limit = 5) {
170
273
  const tableNames = await db.tableNames();
171
274
  if (!tableNames.includes(TABLE_NAME)) return [];
172
275
 
173
- const vector = await embed(queryText);
276
+ const vector = await embed(queryText, {
277
+ kind: 'query',
278
+ task: 'code retrieval',
279
+ });
174
280
  const table = await db.openTable(TABLE_NAME);
175
281
  const results = await table.search(vector).limit(limit).toArray();
176
282
  return results;
177
283
  }
178
284
 
179
285
  function getEmbeddingConfig() {
180
- return resolveEmbeddingProfile();
286
+ return {
287
+ ...resolveEmbeddingProfile(),
288
+ managed: resolveGeminiEmbeddingConfig(),
289
+ };
181
290
  }
182
291
 
183
292
  function getLastEmbeddingProfile() {
@@ -195,6 +304,11 @@ function setLanceLoaderForTests(loader) {
195
304
  _lancedb = null;
196
305
  }
197
306
 
307
+ function setGeminiEmbedderForTests(loader) {
308
+ _geminiEmbedderForTests = loader;
309
+ _lastEmbeddingProfile = null;
310
+ }
311
+
198
312
  module.exports = {
199
313
  upsertFeedback,
200
314
  searchSimilar,
@@ -203,5 +317,6 @@ module.exports = {
203
317
  getLastEmbeddingProfile,
204
318
  setPipelineLoaderForTests,
205
319
  setLanceLoaderForTests,
320
+ setGeminiEmbedderForTests,
206
321
  truncateForEmbedding,
207
322
  };