thumbgate 1.26.8 → 1.27.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 (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.well-known/agentic-verify.txt +1 -0
  3. package/.well-known/llms.txt +2 -0
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +44 -31
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/gcp/dfcx-webhook-gate.js +295 -0
  8. package/adapters/mcp/server-stdio.js +41 -1
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/thumbgate-bench.json +2 -2
  11. package/bin/cli.js +184 -8
  12. package/bin/dashboard-cli.js +7 -0
  13. package/config/gate-classifier-routing.json +98 -0
  14. package/config/gate-templates.json +60 -0
  15. package/config/mcp-allowlists.json +8 -7
  16. package/config/model-candidates.json +71 -6
  17. package/package.json +28 -12
  18. package/public/about.html +162 -0
  19. package/public/chatgpt-app.html +330 -0
  20. package/public/codex-plugin.html +66 -14
  21. package/public/compare.html +2 -2
  22. package/public/dashboard.html +224 -36
  23. package/public/guide.html +2 -2
  24. package/public/index.html +122 -40
  25. package/public/learn.html +70 -0
  26. package/public/lessons.html +129 -6
  27. package/public/numbers.html +2 -2
  28. package/public/pricing.html +28 -23
  29. package/public/pro.html +3 -3
  30. package/scripts/agent-operations-planner.js +621 -0
  31. package/scripts/agent-reward-model.js +53 -1
  32. package/scripts/ai-component-inventory.js +367 -0
  33. package/scripts/classifier-routing.js +130 -0
  34. package/scripts/cli-schema.js +26 -0
  35. package/scripts/commercial-offer.js +10 -2
  36. package/scripts/dashboard-chat.js +199 -51
  37. package/scripts/feedback-sanitizer.js +105 -0
  38. package/scripts/gates-engine.js +301 -67
  39. package/scripts/hybrid-feedback-context.js +141 -7
  40. package/scripts/memory-scope-readiness.js +159 -0
  41. package/scripts/oss-pr-opportunity-scout.js +35 -5
  42. package/scripts/parallel-workflow-orchestrator.js +293 -0
  43. package/scripts/plausible-domain-config.js +86 -0
  44. package/scripts/plausible-server-events.js +4 -2
  45. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  46. package/scripts/qa-scenario-planner.js +136 -0
  47. package/scripts/rate-limiter.js +2 -2
  48. package/scripts/repeat-metric.js +28 -12
  49. package/scripts/secret-fixture-tokens.js +61 -0
  50. package/scripts/secret-scanner.js +44 -5
  51. package/scripts/security-scanner.js +80 -0
  52. package/scripts/seo-gsd.js +113 -0
  53. package/scripts/thumbgate-bench.js +16 -1
  54. package/scripts/tool-registry.js +37 -0
  55. package/scripts/workflow-sentinel.js +282 -54
  56. package/src/api/server.js +466 -60
  57. package/.claude-plugin/marketplace.json +0 -85
@@ -18,6 +18,11 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { resolveFeedbackDir } = require('./feedback-paths');
20
20
  const { readJsonl } = require('./fs-utils');
21
+ const {
22
+ TRANSPORT_WORDS,
23
+ sanitizeFeedbackText,
24
+ transportWordsOnly,
25
+ } = require('./feedback-sanitizer');
21
26
 
22
27
  // ---------------------------------------------------------------------------
23
28
  // Paths
@@ -51,6 +56,7 @@ const STOPWORDS = new Set([
51
56
  'has', 'had', 'not', 'but', 'they', 'you', 'can', 'will', 'all', 'any',
52
57
  'one', 'its', 'our', 'also', 'more', 'very', 'just', 'into', 'been',
53
58
  'bash', 'edit', 'write', 'tool', 'hook', 'clear',
59
+ ...TRANSPORT_WORDS,
54
60
  ]);
55
61
 
56
62
  const NEG = new Set([
@@ -74,7 +80,7 @@ const HYBRID_JSONL_READ_LIMIT = 400;
74
80
  */
75
81
  function normalize(text) {
76
82
  if (!text || typeof text !== 'string') return '';
77
- return text
83
+ return sanitizeFeedbackText(text)
78
84
  .replace(/\/Users\/[^\s/]+/g, '/Users/redacted')
79
85
  .replace(/:\d{4,5}\b/g, ':PORT')
80
86
  .toLowerCase()
@@ -97,7 +103,9 @@ function stripFeedbackPrefix(text) {
97
103
  * Compose normalize + stripFeedbackPrefix.
98
104
  */
99
105
  function normalizePatternText(text) {
100
- return normalize(stripFeedbackPrefix(text));
106
+ const normalized = normalize(stripFeedbackPrefix(text));
107
+ if (transportWordsOnly(normalized)) return '';
108
+ return normalized;
101
109
  }
102
110
 
103
111
  /**
@@ -125,6 +133,104 @@ function classify(entry) {
125
133
  return 'neutral';
126
134
  }
127
135
 
136
+ function isHookPromptEnvelope(context) {
137
+ if (!context || typeof context !== 'string') return false;
138
+ try {
139
+ const parsed = JSON.parse(context);
140
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
141
+ return Boolean(
142
+ parsed.prompt &&
143
+ (
144
+ parsed.hookEventName ||
145
+ parsed.hook_event_name ||
146
+ parsed.workspaceRoot ||
147
+ parsed.workspace_root ||
148
+ parsed.session_id ||
149
+ parsed.sessionId ||
150
+ parsed.transcript_path ||
151
+ parsed.transcriptPath
152
+ )
153
+ );
154
+ } catch (_) {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ function patternContext(entry) {
160
+ const context = entry && entry.context ? String(entry.context) : '';
161
+ if (!context) return '';
162
+ const hasExplicitFeedback = Boolean(
163
+ entry.whatWentWrong ||
164
+ entry.what_went_wrong ||
165
+ entry.whatToChange ||
166
+ entry.what_to_change ||
167
+ entry.failureType ||
168
+ (Array.isArray(entry.tags) && entry.tags.length > 0) ||
169
+ entry.structuredRule
170
+ );
171
+ if (isHookPromptEnvelope(context) && !hasExplicitFeedback) return '';
172
+ if (isHookPromptEnvelope(context) && hasExplicitFeedback) {
173
+ return '';
174
+ }
175
+ return context;
176
+ }
177
+
178
+ /**
179
+ * Check if the feedback entry is an automated enforcement log (e.g. from gates engine)
180
+ * rather than real developer/user feedback.
181
+ */
182
+ function isAutomatedFeedback(entry) {
183
+ const tags = entry.tags || [];
184
+ if (tags.includes('auto-capture') || tags.includes('gates-engine') || tags.includes('audit-trail')) {
185
+ return true;
186
+ }
187
+ const context = String(entry.context || entry.whatWentWrong || '').toLowerCase();
188
+ return context.includes('gate "') || context.includes('blocked tool') || context.includes('warned tool');
189
+ }
190
+
191
+
192
+ function isHookPromptEnvelope(context) {
193
+ if (!context || typeof context !== 'string') return false;
194
+ try {
195
+ const parsed = JSON.parse(context);
196
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
197
+ return Boolean(
198
+ parsed.prompt &&
199
+ (
200
+ parsed.hookEventName ||
201
+ parsed.hook_event_name ||
202
+ parsed.workspaceRoot ||
203
+ parsed.workspace_root ||
204
+ parsed.session_id ||
205
+ parsed.sessionId ||
206
+ parsed.transcript_path ||
207
+ parsed.transcriptPath
208
+ )
209
+ );
210
+ } catch (_) {
211
+ return false;
212
+ }
213
+ }
214
+
215
+ function patternContext(entry) {
216
+ const context = entry && entry.context ? String(entry.context) : '';
217
+ if (!context) return '';
218
+ const hasExplicitFeedback = Boolean(
219
+ entry.whatWentWrong ||
220
+ entry.what_went_wrong ||
221
+ entry.whatToChange ||
222
+ entry.what_to_change ||
223
+ entry.failureType ||
224
+ (Array.isArray(entry.tags) && entry.tags.length > 0) ||
225
+ entry.structuredRule
226
+ );
227
+ if (isHookPromptEnvelope(context) && !hasExplicitFeedback) return '';
228
+ if (isHookPromptEnvelope(context) && hasExplicitFeedback) {
229
+ return '';
230
+ }
231
+ return context;
232
+ }
233
+
128
234
  /**
129
235
  * Extract ms from a timestamp value. Returns 0 on failure.
130
236
  */
@@ -212,13 +318,15 @@ function buildHybridState(opts) {
212
318
  if (cls === 'positive') positive++;
213
319
  if (cls === 'negative') {
214
320
  negative++;
215
- // Track tool-level negative counts
216
- const toolName = inferToolName(entry.toolName || entry.tool_name || 'unknown', entry.context || '');
217
- toolNegatives[toolName] = (toolNegatives[toolName] || 0) + 1;
321
+ // Track tool-level negative counts (exclude automated gate logs)
322
+ if (!isAutomatedFeedback(entry)) {
323
+ const toolName = inferToolName(entry.toolName || entry.tool_name || 'unknown', entry.context || '');
324
+ toolNegatives[toolName] = (toolNegatives[toolName] || 0) + 1;
325
+ }
218
326
 
219
327
  // Build pattern from context / whatWentWrong / what_went_wrong
220
328
  const rawText = [
221
- entry.context || '',
329
+ patternContext(entry),
222
330
  entry.whatWentWrong || entry.what_went_wrong || '',
223
331
  entry.whatToChange || entry.what_to_change || '',
224
332
  entry.failureType || '',
@@ -254,11 +362,13 @@ function buildHybridState(opts) {
254
362
 
255
363
  // Process attributed feedback separately to track attributed tool counts
256
364
  for (const entry of attributedEntries) {
365
+ if (classify(entry) !== 'negative') continue; // skip pruned/positive
366
+ if (isAutomatedFeedback(entry)) continue; // skip automated gate blocks
257
367
  const toolName = inferToolName(entry.toolName || entry.tool_name || entry.attributed_tool || 'unknown', entry.context || '');
258
368
  toolNegativesAttributed[toolName] = (toolNegativesAttributed[toolName] || 0) + 1;
259
369
 
260
370
  const rawText = [
261
- entry.context || '',
371
+ patternContext(entry),
262
372
  entry.whatWentWrong || entry.what_went_wrong || '',
263
373
  ...(Array.isArray(entry.tags) ? entry.tags : []),
264
374
  ...(entry.richContext && Array.isArray(entry.richContext.filePaths) ? entry.richContext.filePaths : []),
@@ -626,6 +736,29 @@ function evaluatePretool(toolName, toolInput, opts) {
626
736
  return evaluatePretoolFromState(state, toolName, toolInput);
627
737
  }
628
738
 
739
+ // Claw-style agent support (high-ROI for EnterpriseClaw / OpenShell agents from Automation Anywhere / Nvidia)
740
+ // Extends hybrid context for claw_action_type (file, screen, dynamic-tool, orchestration), agent_identity, hybrid_route.
741
+ // Use in evaluatePretool calls from claw-aware MCP/hooks: pass {clawContext: {actionType: 'dynamic-tool-creation', agentId: '...', route: 'local/cloud'}} in opts.
742
+ function evaluateClawPretool(toolName, toolInput, clawContext, opts) {
743
+ const o = opts || {};
744
+ const claw = clawContext || {};
745
+ // Merge claw metadata into toolInput for gate evaluation (so templates like block-dynamic-tool-creation can match)
746
+ const enrichedInput = {
747
+ ...(typeof toolInput === 'object' ? toolInput : { raw: toolInput }),
748
+ _claw: {
749
+ actionType: claw.actionType || 'unknown',
750
+ agentId: claw.agentId || 'unknown',
751
+ hybridRoute: claw.hybridRoute || 'unknown',
752
+ screenInteraction: !!claw.screenInteraction,
753
+ fileAccess: !!claw.fileAccess,
754
+ }
755
+ };
756
+ const result = evaluatePretool(toolName, JSON.stringify(enrichedInput), o);
757
+ // Tag result with claw metadata for logging/feedback
758
+ result.clawContext = claw;
759
+ return result;
760
+ }
761
+
629
762
  // ---------------------------------------------------------------------------
630
763
  // CLI main()
631
764
  // ---------------------------------------------------------------------------
@@ -674,6 +807,7 @@ function main() {
674
807
  module.exports = {
675
808
  buildHybridState,
676
809
  evaluatePretool,
810
+ evaluateClawPretool,
677
811
  compileGuardArtifact,
678
812
  writeGuardArtifact,
679
813
  readGuardArtifact,
@@ -2,6 +2,38 @@
2
2
  'use strict';
3
3
 
4
4
  const REQUIRED_SCOPE_FIELDS = ['entityId', 'projectId', 'processId', 'sessionId'];
5
+ const MEMORY_OS_LAYERS = Object.freeze([
6
+ {
7
+ id: 'file_layer',
8
+ name: 'File Layer',
9
+ purpose: 'Raw feedback, tool receipts, sessions, and memory rows are durably stored before interpretation.',
10
+ },
11
+ {
12
+ id: 'vector_db_layer',
13
+ name: 'Vector DB Layer',
14
+ purpose: 'Semantic retrieval can find related lessons without stuffing every raw memory into context.',
15
+ },
16
+ {
17
+ id: 'structured_facts_layer',
18
+ name: 'Structured Facts Layer',
19
+ purpose: 'Confirmed account, project, policy, and budget facts are typed separately from fuzzy memories.',
20
+ },
21
+ {
22
+ id: 'auto_curation_layer',
23
+ name: 'Auto Curation Layer',
24
+ purpose: 'Duplicate, stale, contradictory, and unscoped memories are consolidated before retrieval quality decays.',
25
+ },
26
+ {
27
+ id: 'context_layer',
28
+ name: 'Context Layer',
29
+ purpose: 'Only relevant scoped memories enter a given tool call, PR, deployment, or support session.',
30
+ },
31
+ {
32
+ id: 'interface_layer',
33
+ name: 'Interface Layer',
34
+ purpose: 'The memory contract is exposed through CLI, MCP, hooks, dashboards, and agent adapters without model lock-in.',
35
+ },
36
+ ]);
5
37
 
6
38
  const FIELD_ALIASES = {
7
39
  entityId: [
@@ -228,6 +260,128 @@ function buildRecommendations({ unscopedRecords, crossScopeDuplicates }) {
228
260
  return recommendations;
229
261
  }
230
262
 
263
+ function hasEmbeddingEvidence(record = {}) {
264
+ return Boolean(
265
+ record.embedding
266
+ || record.vector
267
+ || record.embeddingId
268
+ || record.metadata?.embedding
269
+ || record.metadata?.embeddingId
270
+ || record.metadata?.vectorId
271
+ || record.semanticKey
272
+ || record.metadata?.semanticKey
273
+ );
274
+ }
275
+
276
+ function hasStructuredFactEvidence(record = {}) {
277
+ const type = String(record.type || record.kind || record.memoryType || record.metadata?.type || '').toLowerCase();
278
+ return type === 'fact'
279
+ || type === 'structured_fact'
280
+ || Boolean(record.factKey || record.fact || record.metadata?.factKey || record.metadata?.fact);
281
+ }
282
+
283
+ function hasContextEvidence(record = {}) {
284
+ return Boolean(
285
+ record.contextPackId
286
+ || record.contextPack
287
+ || record.metadata?.contextPackId
288
+ || record.metadata?.contextPack
289
+ || record.retrievalQuery
290
+ || record.metadata?.retrievalQuery
291
+ );
292
+ }
293
+
294
+ function boolCapability(capabilities = {}, ...keys) {
295
+ return keys.some((key) => capabilities[key] === true);
296
+ }
297
+
298
+ function buildMemoryOsLayerReport(records = [], capabilities = {}) {
299
+ const scopeReport = buildMemoryScopeReadinessReport(records);
300
+ const semanticRecords = records.filter(hasEmbeddingEvidence);
301
+ const structuredFactRecords = records.filter(hasStructuredFactEvidence);
302
+ const contextRecords = records.filter(hasContextEvidence);
303
+ const curationReady = scopeReport.unscopedRecords === 0 && scopeReport.crossScopeDuplicates.length === 0;
304
+
305
+ const checks = [
306
+ {
307
+ id: 'file_layer',
308
+ ok: records.length > 0 || boolCapability(capabilities, 'rawStorage', 'fileLayer'),
309
+ evidence: {
310
+ records: records.length,
311
+ durableStore: Boolean(records.length > 0 || capabilities.rawStorage || capabilities.fileLayer),
312
+ },
313
+ recommendation: 'Capture raw feedback, action receipts, and tool outcomes before promoting memories.',
314
+ },
315
+ {
316
+ id: 'vector_db_layer',
317
+ ok: semanticRecords.length > 0 || boolCapability(capabilities, 'semanticSearch', 'vectorDbLayer'),
318
+ evidence: {
319
+ semanticRecords: semanticRecords.length,
320
+ semanticSearch: Boolean(capabilities.semanticSearch || capabilities.vectorDbLayer),
321
+ },
322
+ recommendation: 'Index lessons with semantic keys or embeddings so related failures are retrieved before action.',
323
+ },
324
+ {
325
+ id: 'structured_facts_layer',
326
+ ok: structuredFactRecords.length > 0 || boolCapability(capabilities, 'structuredFacts', 'structuredFactsLayer'),
327
+ evidence: {
328
+ structuredFactRecords: structuredFactRecords.length,
329
+ structuredFacts: Boolean(capabilities.structuredFacts || capabilities.structuredFactsLayer),
330
+ },
331
+ recommendation: 'Store confirmed customer, project, policy, and budget facts as typed records, not just prose.',
332
+ },
333
+ {
334
+ id: 'auto_curation_layer',
335
+ ok: curationReady && boolCapability(capabilities, 'autoCuration', 'dedupe', 'autoCurationLayer'),
336
+ evidence: {
337
+ unscopedRecords: scopeReport.unscopedRecords,
338
+ crossScopeDuplicates: scopeReport.crossScopeDuplicates.length,
339
+ autoCuration: Boolean(capabilities.autoCuration || capabilities.dedupe || capabilities.autoCurationLayer),
340
+ },
341
+ recommendation: 'Run dedupe, contradiction, stale-memory, and scope-isolation checks before memories can become gates.',
342
+ },
343
+ {
344
+ id: 'context_layer',
345
+ ok: contextRecords.length > 0 || boolCapability(capabilities, 'contextPacks', 'contextLayer', 'scopedRetrieval'),
346
+ evidence: {
347
+ contextRecords: contextRecords.length,
348
+ scopedRetrieval: Boolean(capabilities.contextPacks || capabilities.contextLayer || capabilities.scopedRetrieval),
349
+ },
350
+ recommendation: 'Inject scoped context packs per task instead of loading every memory into the model window.',
351
+ },
352
+ {
353
+ id: 'interface_layer',
354
+ ok: boolCapability(capabilities, 'mcp', 'cli', 'hooks', 'dashboard', 'interfaceLayer'),
355
+ evidence: {
356
+ cli: Boolean(capabilities.cli),
357
+ mcp: Boolean(capabilities.mcp),
358
+ hooks: Boolean(capabilities.hooks),
359
+ dashboard: Boolean(capabilities.dashboard),
360
+ },
361
+ recommendation: 'Expose the same memory contract through CLI, MCP, hooks, dashboard, and agent adapters.',
362
+ },
363
+ ].map((check) => {
364
+ const layer = MEMORY_OS_LAYERS.find((candidate) => candidate.id === check.id);
365
+ return {
366
+ ...layer,
367
+ ...check,
368
+ };
369
+ });
370
+
371
+ const missingLayers = checks.filter((check) => !check.ok).map((check) => check.id);
372
+
373
+ return {
374
+ ready: missingLayers.length === 0,
375
+ riskLevel: missingLayers.length === 0 ? 'low' : missingLayers.length <= 2 ? 'medium' : 'high',
376
+ layers: checks,
377
+ missingLayers,
378
+ scopeReport,
379
+ recommendations: checks
380
+ .filter((check) => !check.ok)
381
+ .map((check) => check.recommendation),
382
+ };
383
+ }
384
+
231
385
  function selectRecordsForScope(records = [], requestedScope = {}, options = {}) {
232
386
  const requested = normalizeScope(requestedScope);
233
387
  const requestedKey = memoryScopeKey(requested);
@@ -265,6 +419,7 @@ function buildMemoriStyleBenchmarkRecords() {
265
419
  projectId: 'thumbgate',
266
420
  processId: 'agent-a',
267
421
  sessionId: 'session-1',
422
+ metadata: { semanticKey: 'checkout-readiness', contextPackId: 'checkout-pro' },
268
423
  content: 'Use the paid sprint checklist before changing checkout code.',
269
424
  },
270
425
  {
@@ -298,14 +453,18 @@ function buildMemoriStyleBenchmarkRecords() {
298
453
  processId: 'agent-a',
299
454
  sessionId: 'session-1',
300
455
  visibility: 'shared',
456
+ type: 'fact',
457
+ factKey: 'checkout.mutation_policy',
301
458
  content: 'Shared rule: checkout mutations require audit evidence.',
302
459
  },
303
460
  ];
304
461
  }
305
462
 
306
463
  module.exports = {
464
+ MEMORY_OS_LAYERS,
307
465
  REQUIRED_SCOPE_FIELDS,
308
466
  buildMemoriStyleBenchmarkRecords,
467
+ buildMemoryOsLayerReport,
309
468
  buildMemoryScopeReadinessReport,
310
469
  isSharedMemory,
311
470
  memoryScopeKey,
@@ -8,6 +8,7 @@ const DEFAULT_PACKAGE_PATH = path.join(__dirname, '..', 'package.json');
8
8
  const DEFAULT_OUTPUT_DIR = path.join(__dirname, '..', 'docs', 'marketing');
9
9
  const KNOWN_REPOS = Object.freeze({
10
10
  '@anthropic-ai/sdk': 'anthropics/anthropic-sdk-typescript',
11
+ '@modelcontextprotocol/sdk': 'modelcontextprotocol/typescript-sdk',
11
12
  '@google/genai': 'googleapis/js-genai',
12
13
  '@huggingface/transformers': 'huggingface/transformers.js',
13
14
  '@lancedb/lancedb': 'lancedb/lancedb',
@@ -23,6 +24,24 @@ const KNOWN_REPOS = Object.freeze({
23
24
  undici: 'nodejs/undici',
24
25
  });
25
26
 
27
+ // Communities where ThumbGate's buyers live even though they are not npm
28
+ // dependencies. ThumbGate ships an MCP server, so the Model Context Protocol
29
+ // repos are the single highest-ROI ecosystem to contribute to — but the
30
+ // dependency-scan above would never surface them. These are always scouted on
31
+ // the default (no explicit --dependencies) path, de-duped against package.json.
32
+ const STRATEGIC_DEPENDENCIES = Object.freeze([
33
+ '@modelcontextprotocol/sdk', // MCP TypeScript SDK — the protocol ThumbGate implements
34
+ 'modelcontextprotocol/servers', // MCP servers ecosystem — where MCP authors (our buyers) collaborate
35
+ ]);
36
+
37
+ // Honest, repo-accurate framing for the outreach draft. ThumbGate does not
38
+ // `import` these — it implements the protocol — so the generic "while using X"
39
+ // line would be a false claim. Keep drafts truthful (CEO honesty rule).
40
+ const RELATIONSHIP_OVERRIDES = Object.freeze({
41
+ '@modelcontextprotocol/sdk': 'building ThumbGate as an MCP server against the Model Context Protocol spec',
42
+ 'modelcontextprotocol/servers': 'building and testing ThumbGate as an MCP server alongside the reference MCP servers',
43
+ });
44
+
26
45
  const BOUNTY_KEYWORDS = [
27
46
  'bug bounty',
28
47
  'bounty',
@@ -70,7 +89,10 @@ function dependencyNames(pkg = {}) {
70
89
  }
71
90
 
72
91
  function repoFromDependency(name) {
73
- return KNOWN_REPOS[name] || '';
92
+ if (KNOWN_REPOS[name]) return KNOWN_REPOS[name];
93
+ // A strategic identifier may itself be an "owner/repo" slug (not an npm name).
94
+ if (!name.startsWith('@') && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(name)) return name;
95
+ return '';
74
96
  }
75
97
 
76
98
  function buildIssueSearchQueries(repo) {
@@ -89,14 +111,18 @@ function scoreOpportunity(depName, repo, options = {}) {
89
111
  score += 20;
90
112
  reasons.push('known upstream repository');
91
113
  }
92
- if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici/i.test(depName)) {
114
+ if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici|mcp|modelcontext/i.test(depName)) {
93
115
  score += 20;
94
116
  reasons.push('high product adjacency for agent tooling');
95
117
  }
96
- if (/anthropic|google|huggingface|stripe|microsoft|nodejs/i.test(repo)) {
118
+ if (/anthropic|google|huggingface|stripe|microsoft|nodejs|modelcontextprotocol/i.test(repo)) {
97
119
  score += 15;
98
120
  reasons.push('large ecosystem visibility');
99
121
  }
122
+ if (/modelcontext|mcp/i.test(depName) || /modelcontextprotocol/i.test(repo)) {
123
+ score += 12;
124
+ reasons.push("ThumbGate's own protocol surface — buyers are MCP authors");
125
+ }
100
126
  if (options.includeBounties) {
101
127
  score += 10;
102
128
  reasons.push('bounty search enabled');
@@ -138,7 +164,7 @@ function buildOpportunity(depName, options = {}) {
138
164
  'no bounty, security, or maintainer-policy claim without source link',
139
165
  ],
140
166
  outreachDraft: repo
141
- ? `I found this while using ${depName} in ThumbGate. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
167
+ ? `I found this while ${RELATIONSHIP_OVERRIDES[depName] || `using ${depName} in ThumbGate`}. I reproduced the issue, added a minimal fix with tests, and kept the PR scoped to the maintainer's issue.`
142
168
  : '',
143
169
  };
144
170
  }
@@ -149,7 +175,9 @@ function buildOssPrOpportunityScoutPlan(rawOptions = {}) {
149
175
  const explicitDeps = splitList(rawOptions.dependencies || rawOptions.deps);
150
176
  const includeBounties = rawOptions.includeBounties !== false && rawOptions['include-bounties'] !== false;
151
177
  const maxRepos = Math.max(1, Number.parseInt(String(rawOptions.maxRepos || rawOptions['max-repos'] || 12), 10) || 12);
152
- const deps = explicitDeps.length ? explicitDeps : dependencyNames(pkg);
178
+ const deps = explicitDeps.length
179
+ ? explicitDeps
180
+ : [...new Set([...dependencyNames(pkg), ...STRATEGIC_DEPENDENCIES])];
153
181
  const opportunities = deps
154
182
  .map((dep) => buildOpportunity(dep, { includeBounties }))
155
183
  .filter((opportunity) => opportunity.repo)
@@ -223,6 +251,8 @@ function writeOssPrOpportunityScoutPack(outputDir = DEFAULT_OUTPUT_DIR, options
223
251
 
224
252
  module.exports = {
225
253
  KNOWN_REPOS,
254
+ STRATEGIC_DEPENDENCIES,
255
+ RELATIONSHIP_OVERRIDES,
226
256
  buildIssueSearchQueries,
227
257
  buildOpportunity,
228
258
  buildOssPrOpportunityScoutPlan,