thumbgate 1.27.16 → 1.27.18

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 (165) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/.well-known/llms.txt +12 -33
  4. package/.well-known/mcp/server-card.json +8 -8
  5. package/README.md +30 -215
  6. package/adapters/chatgpt/openapi.yaml +1695 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/gemini/function-declarations.json +0 -1
  9. package/adapters/mcp/server-stdio.js +11 -263
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bench/thumbgate-bench.json +2 -2
  12. package/bin/cli.js +122 -1421
  13. package/bin/postinstall.js +27 -11
  14. package/config/gate-templates.json +0 -216
  15. package/config/gates/claim-verification.json +0 -12
  16. package/config/gates/default.json +2 -31
  17. package/config/github-about.json +2 -2
  18. package/config/mcp-allowlists.json +13 -23
  19. package/config/merge-quality-checks.json +1 -0
  20. package/config/model-candidates.json +6 -121
  21. package/openapi/openapi.yaml +0 -12
  22. package/package.json +98 -228
  23. package/public/blog.html +4 -4
  24. package/public/codex-plugin.html +20 -72
  25. package/public/compare.html +8 -31
  26. package/public/dashboard.html +166 -930
  27. package/public/federal.html +2 -2
  28. package/public/guide.html +9 -33
  29. package/public/index.html +116 -495
  30. package/public/learn.html +18 -191
  31. package/public/lessons.html +10 -168
  32. package/public/numbers.html +7 -7
  33. package/public/pro.html +11 -34
  34. package/scripts/agent-memory-lifecycle.js +0 -211
  35. package/scripts/agent-readiness.js +3 -167
  36. package/scripts/agent-reward-model.js +1 -53
  37. package/scripts/auto-promote-gates.js +10 -82
  38. package/scripts/auto-wire-hooks.js +0 -14
  39. package/scripts/billing.js +1 -93
  40. package/scripts/build-metadata.js +10 -50
  41. package/scripts/cli-feedback.js +6 -18
  42. package/scripts/cli-schema.js +0 -90
  43. package/scripts/cli-telemetry.js +1 -6
  44. package/scripts/commercial-offer.js +2 -82
  45. package/scripts/context-manager.js +6 -74
  46. package/scripts/dashboard.js +2 -68
  47. package/scripts/durability/step.js +3 -3
  48. package/scripts/export-databricks-bundle.js +1 -5
  49. package/scripts/export-dpo-pairs.js +2 -7
  50. package/scripts/feedback-loop.js +1 -123
  51. package/scripts/feedback-quality.js +0 -87
  52. package/scripts/filesystem-search.js +10 -35
  53. package/scripts/gate-stats.js +0 -89
  54. package/scripts/gates-engine.js +85 -1105
  55. package/scripts/gemini-embedding-policy.js +1 -2
  56. package/scripts/hook-runtime.js +14 -20
  57. package/scripts/hook-thumbgate-cache-updater.js +2 -18
  58. package/scripts/hybrid-feedback-context.js +7 -142
  59. package/scripts/lesson-inference.js +3 -8
  60. package/scripts/lesson-search.js +1 -17
  61. package/scripts/license.js +1 -0
  62. package/scripts/llm-client.js +9 -390
  63. package/scripts/local-model-profile.js +8 -15
  64. package/scripts/memory-scope-readiness.js +0 -159
  65. package/scripts/meta-agent-loop.js +0 -36
  66. package/scripts/operational-integrity.js +5 -39
  67. package/scripts/oss-pr-opportunity-scout.js +5 -35
  68. package/scripts/plausible-server-events.js +6 -9
  69. package/scripts/pro-local-dashboard.js +4 -4
  70. package/scripts/proxy-pointer-rag-guardrails.js +1 -42
  71. package/scripts/rate-limiter.js +13 -64
  72. package/scripts/secret-scanner.js +5 -44
  73. package/scripts/security-scanner.js +10 -260
  74. package/scripts/self-distill-agent.js +1 -3
  75. package/scripts/seo-gsd.js +2 -916
  76. package/scripts/statusline-cache-path.js +2 -17
  77. package/scripts/statusline-local-stats.js +1 -9
  78. package/scripts/statusline-meta.js +2 -28
  79. package/scripts/statusline.sh +4 -20
  80. package/scripts/telemetry-analytics.js +0 -357
  81. package/scripts/thompson-sampling.js +10 -31
  82. package/scripts/thumbgate-bench.js +1 -16
  83. package/scripts/thumbgate-search.js +19 -85
  84. package/scripts/tool-registry.js +1 -169
  85. package/scripts/vector-store.js +0 -45
  86. package/scripts/workflow-sentinel.js +53 -286
  87. package/scripts/workspace-evolver.js +2 -62
  88. package/src/api/server.js +290 -2578
  89. package/.claude/commands/dashboard.md +0 -15
  90. package/.claude/commands/thumbgate-blocked.md +0 -27
  91. package/.claude/commands/thumbgate-dashboard.md +0 -15
  92. package/.claude/commands/thumbgate-doctor.md +0 -30
  93. package/.claude/commands/thumbgate-guard.md +0 -36
  94. package/.claude/commands/thumbgate-protect.md +0 -30
  95. package/.claude/commands/thumbgate-rules.md +0 -30
  96. package/.well-known/agentic-verify.txt +0 -1
  97. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  98. package/adapters/letta/README.md +0 -41
  99. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  100. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  101. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  102. package/bench/observability-eval-suite.json +0 -26
  103. package/bin/dashboard-cli.js +0 -7
  104. package/commands/dashboard.md +0 -15
  105. package/commands/thumbgate-dashboard.md +0 -15
  106. package/config/builtin-lessons.json +0 -23
  107. package/config/gate-classifier-routing.json +0 -98
  108. package/config/post-deploy-marketing-pages.json +0 -61
  109. package/public/about.html +0 -162
  110. package/public/agent-manager.html +0 -179
  111. package/public/agents-cost-savings.html +0 -153
  112. package/public/ai-malpractice-prevention.html +0 -818
  113. package/public/assets/brand/github-social-preview.png +0 -0
  114. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  115. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  116. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  117. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  118. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  119. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  120. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  121. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  122. package/public/assets/brand/thumbgate-mark.svg +0 -21
  123. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  124. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  125. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  126. package/public/assets/legal-intake-control-flow.svg +0 -66
  127. package/public/brand/thumbgate-mark.svg +0 -19
  128. package/public/brand/thumbgate-og.svg +0 -16
  129. package/public/chatgpt-app.html +0 -330
  130. package/public/codex-enterprise.html +0 -123
  131. package/public/js/buyer-intent.js +0 -672
  132. package/public/pricing.html +0 -399
  133. package/scripts/action-receipts.js +0 -324
  134. package/scripts/activation-quickstart.js +0 -187
  135. package/scripts/agent-operations-planner.js +0 -621
  136. package/scripts/ai-component-inventory.js +0 -367
  137. package/scripts/async-eval-observability.js +0 -236
  138. package/scripts/audit.js +0 -65
  139. package/scripts/aws-blocks-guardrails.js +0 -228
  140. package/scripts/classifier-routing.js +0 -130
  141. package/scripts/dashboard-chat.js +0 -332
  142. package/scripts/feedback-aggregate.js +0 -281
  143. package/scripts/feedback-sanitizer.js +0 -105
  144. package/scripts/hob-pack.js +0 -591
  145. package/scripts/hook-stop-anti-claim.js +0 -301
  146. package/scripts/install-shim.js +0 -87
  147. package/scripts/mcp-oauth.js +0 -293
  148. package/scripts/noop-detect.js +0 -285
  149. package/scripts/omlx-smoke.js +0 -192
  150. package/scripts/parallel-workflow-orchestrator.js +0 -293
  151. package/scripts/plan-gate.js +0 -243
  152. package/scripts/plausible-domain-config.js +0 -99
  153. package/scripts/qa-scenario-planner.js +0 -136
  154. package/scripts/repeat-metric.js +0 -137
  155. package/scripts/secret-fixture-tokens.js +0 -61
  156. package/scripts/secret-redaction.js +0 -166
  157. package/scripts/self-harness-optimizer.js +0 -141
  158. package/scripts/self-healing-check.js +0 -193
  159. package/scripts/silent-failure-cluster.js +0 -531
  160. package/scripts/statusline-cache-read.js +0 -57
  161. package/scripts/sync-telemetry-from-prod.js +0 -374
  162. package/scripts/tool-contract-validator.js +0 -76
  163. package/scripts/trajectory-scorer.js +0 -63
  164. package/scripts/verify-marketing-pages-deployed.js +0 -195
  165. package/scripts/visitor-journey.js +0 -172
@@ -282,8 +282,6 @@ function buildPromotedGate(candidate, metrics, runId) {
282
282
  occurrences: metrics.hits,
283
283
  promotedAt: new Date().toISOString(),
284
284
  source: 'meta-agent',
285
- // origin distinguishes silent-failure-clustered candidates from feedback-derived ones
286
- origin: candidate.origin || 'user-feedback',
287
285
  runId,
288
286
  score: parseFloat(metrics.score.toFixed(3)),
289
287
  hitRate: parseFloat(metrics.hitRate.toFixed(3)),
@@ -373,38 +371,6 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
373
371
  candidates = generateCandidatesHeuristic(failures, blockPatterns);
374
372
  }
375
373
 
376
- // Tag existing-pipeline candidates with their origin so downstream precision
377
- // measurement (silentFailureDerivedGates vs user-feedback-derived) is possible.
378
- candidates = candidates.map((c) => (c.origin ? c : { ...c, origin: 'user-feedback' }));
379
-
380
- // Step 3b: Silent-failure clustering — DEFAULT-ON as of 2026-05-21
381
- // (flipped from opt-in by PR #2289). Opt out via
382
- // THUMBGATE_SILENT_FAILURE_CLUSTERING=0 (or NODE_ENV=test). Candidates flow
383
- // through the SAME scoring / fp-rate eval below; we do not bypass any
384
- // guardrail. The point of this clustering is to cover the case where users
385
- // are lazy and never give thumbs-down — keeping it opt-in meant the users
386
- // who need it most never got the benefit.
387
- let silentFailureStats = null;
388
- const { isSilentFailureClusteringEnabled, generateSilentFailureCandidates } = require('./silent-failure-cluster');
389
- if (isSilentFailureClusteringEnabled()) {
390
- try {
391
- const sfResult = generateSilentFailureCandidates({ feedbackLogPath });
392
- silentFailureStats = sfResult.stats;
393
- if (sfResult.candidates && sfResult.candidates.length > 0) {
394
- candidates = candidates.concat(sfResult.candidates);
395
- }
396
- if (verbose) {
397
- process.stdout.write(
398
- `[meta-agent] silent-failure-cluster: candidates=${sfResult.candidates.length} `
399
- + `failed=${sfResult.stats.failedCalls} clusters=${sfResult.stats.clusters} `
400
- + `skipped=${sfResult.stats.skippedReason || 'none'}\n`
401
- );
402
- }
403
- } catch (err) {
404
- if (verbose) process.stdout.write(`[meta-agent] silent-failure-cluster failed (non-fatal): ${err.message}\n`);
405
- }
406
- }
407
-
408
374
  if (verbose) {
409
375
  process.stdout.write(`[meta-agent] candidates generated: ${candidates.length} (mode=${analysisMode})\n`);
410
376
  }
@@ -541,8 +507,6 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
541
507
  skipped: evolutionResult.skipped || false,
542
508
  }
543
509
  : null,
544
- silentFailureCluster: silentFailureStats,
545
- silentFailureDerivedGates: promotedGates.filter((g) => g.origin === 'silent-failure-cluster').length,
546
510
  };
547
511
 
548
512
  if (!dryRun) {
@@ -580,51 +580,17 @@ function resolveGitHubRepository(env = process.env) {
580
580
  function findOpenPrForBranch({ branchName, runner = runGh, env = process.env } = {}) {
581
581
  const normalizedBranch = String(branchName || '').trim();
582
582
  if (!normalizedBranch) return null;
583
- const token = env.GH_TOKEN || env.GITHUB_TOKEN;
584
- if (!token) {
583
+ if (!env.GH_TOKEN && !env.GITHUB_TOKEN) {
585
584
  return null;
586
585
  }
586
+ const args = ['pr', 'list'];
587
587
  const repository = resolveGitHubRepository(env);
588
- if (!repository) return null;
589
-
590
- // Try REST API first as it is much more robust and doesn't rely on gh CLI's GraphQL auth
591
- try {
592
- const owner = repository.split('/')[0];
593
- const url = `https://api.github.com/repos/${repository}/pulls?head=${encodeURIComponent(owner + ':' + normalizedBranch)}&state=open`;
594
- const curlConfig = `header = "Authorization: token ${token}"\nheader = "User-Agent: ThumbGate-CI"`;
595
- const curlResult = spawnSync('curl', [
596
- '-s',
597
- '-K', '-',
598
- url
599
- ], {
600
- encoding: 'utf8',
601
- input: curlConfig
602
- });
603
-
604
- if (curlResult && curlResult.status === 0) {
605
- const prs = JSON.parse(curlResult.stdout || '[]');
606
- if (Array.isArray(prs) && prs.length > 0) {
607
- return {
608
- number: prs[0].number,
609
- state: prs[0].state,
610
- isDraft: prs[0].draft || false,
611
- url: prs[0].html_url
612
- };
613
- }
614
- }
615
- } catch (err) {
616
- console.warn(`[findOpenPrForBranch] REST fallback failed: ${err.message}`);
588
+ if (repository) {
589
+ args.push('--repo', repository);
617
590
  }
618
-
619
- // Fall back to gh CLI
620
- const args = ['pr', 'list', '--repo', repository, '--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url'];
591
+ args.push('--head', normalizedBranch, '--state', 'open', '--json', 'number,state,isDraft,url');
621
592
  const result = runner(args);
622
593
  if (!result || result.status !== 0) {
623
- if (result) {
624
- console.warn(`[findOpenPrForBranch] gh command failed with status ${result.status}. Stderr: ${result.stderr || ''}`);
625
- } else {
626
- console.warn(`[findOpenPrForBranch] gh command failed to spawn.`);
627
- }
628
594
  return null;
629
595
  }
630
596
  try {
@@ -8,7 +8,6 @@ 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',
12
11
  '@google/genai': 'googleapis/js-genai',
13
12
  '@huggingface/transformers': 'huggingface/transformers.js',
14
13
  '@lancedb/lancedb': 'lancedb/lancedb',
@@ -24,24 +23,6 @@ const KNOWN_REPOS = Object.freeze({
24
23
  undici: 'nodejs/undici',
25
24
  });
26
25
 
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
-
45
26
  const BOUNTY_KEYWORDS = [
46
27
  'bug bounty',
47
28
  'bounty',
@@ -89,10 +70,7 @@ function dependencyNames(pkg = {}) {
89
70
  }
90
71
 
91
72
  function repoFromDependency(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 '';
73
+ return KNOWN_REPOS[name] || '';
96
74
  }
97
75
 
98
76
  function buildIssueSearchQueries(repo) {
@@ -111,18 +89,14 @@ function scoreOpportunity(depName, repo, options = {}) {
111
89
  score += 20;
112
90
  reasons.push('known upstream repository');
113
91
  }
114
- if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici|mcp|modelcontext/i.test(depName)) {
92
+ if (/sdk|genai|stripe|playwright|lancedb|transformers|sqlite|undici/i.test(depName)) {
115
93
  score += 20;
116
94
  reasons.push('high product adjacency for agent tooling');
117
95
  }
118
- if (/anthropic|google|huggingface|stripe|microsoft|nodejs|modelcontextprotocol/i.test(repo)) {
96
+ if (/anthropic|google|huggingface|stripe|microsoft|nodejs/i.test(repo)) {
119
97
  score += 15;
120
98
  reasons.push('large ecosystem visibility');
121
99
  }
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
- }
126
100
  if (options.includeBounties) {
127
101
  score += 10;
128
102
  reasons.push('bounty search enabled');
@@ -164,7 +138,7 @@ function buildOpportunity(depName, options = {}) {
164
138
  'no bounty, security, or maintainer-policy claim without source link',
165
139
  ],
166
140
  outreachDraft: repo
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.`
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.`
168
142
  : '',
169
143
  };
170
144
  }
@@ -175,9 +149,7 @@ function buildOssPrOpportunityScoutPlan(rawOptions = {}) {
175
149
  const explicitDeps = splitList(rawOptions.dependencies || rawOptions.deps);
176
150
  const includeBounties = rawOptions.includeBounties !== false && rawOptions['include-bounties'] !== false;
177
151
  const maxRepos = Math.max(1, Number.parseInt(String(rawOptions.maxRepos || rawOptions['max-repos'] || 12), 10) || 12);
178
- const deps = explicitDeps.length
179
- ? explicitDeps
180
- : [...new Set([...dependencyNames(pkg), ...STRATEGIC_DEPENDENCIES])];
152
+ const deps = explicitDeps.length ? explicitDeps : dependencyNames(pkg);
181
153
  const opportunities = deps
182
154
  .map((dep) => buildOpportunity(dep, { includeBounties }))
183
155
  .filter((opportunity) => opportunity.repo)
@@ -251,8 +223,6 @@ function writeOssPrOpportunityScoutPack(outputDir = DEFAULT_OUTPUT_DIR, options
251
223
 
252
224
  module.exports = {
253
225
  KNOWN_REPOS,
254
- STRATEGIC_DEPENDENCIES,
255
- RELATIONSHIP_OVERRIDES,
256
226
  buildIssueSearchQueries,
257
227
  buildOpportunity,
258
228
  buildOssPrOpportunityScoutPlan,
@@ -24,25 +24,23 @@
24
24
  */
25
25
 
26
26
  const https = require('node:https');
27
- const {
28
- resolvePlausibleDataDomain,
29
- } = require('./plausible-domain-config');
30
27
 
28
+ const DEFAULT_PLAUSIBLE_DOMAIN = 'thumbgate-production.up.railway.app';
31
29
  const PLAUSIBLE_ENDPOINT = 'https://plausible.io/api/event';
32
30
  const REQUEST_TIMEOUT_MS = 2_000;
33
31
 
34
32
  function isPlausibleDisabled() {
35
33
  if (process.env.THUMBGATE_PLAUSIBLE_DISABLE === '1') return true;
36
34
  if (process.env.DO_NOT_TRACK === '1') return true;
37
- // Automatically disable Plausible events in local development unless explicitly overridden in tests
38
- if (process.env.THUMBGATE_PLAUSIBLE_DISABLE !== '0' && process.env.NODE_ENV !== 'production') {
39
- return true;
40
- }
35
+ // NODE_ENV-based detection was tried and dropped: `node --test` does not
36
+ // set NODE_ENV automatically, so the check produced surprising behavior
37
+ // in test vs. production. Tests must opt out explicitly via the dedicated
38
+ // THUMBGATE_PLAUSIBLE_DISABLE flag.
41
39
  return false;
42
40
  }
43
41
 
44
42
  function getPlausibleDomain() {
45
- return resolvePlausibleDataDomain();
43
+ return process.env.THUMBGATE_PLAUSIBLE_DOMAIN || DEFAULT_PLAUSIBLE_DOMAIN;
46
44
  }
47
45
 
48
46
  /**
@@ -144,7 +142,6 @@ const CHECKOUT_EVENT_NAMES = Object.freeze({
144
142
  view: 'Checkout Pro Viewed',
145
143
  emailSubmitted: 'Checkout Pro Email Submitted',
146
144
  stripeRedirect: 'Checkout Pro Stripe Redirect Started',
147
- purchase: 'Checkout Pro Purchase Completed',
148
145
  });
149
146
 
150
147
  function recordCheckoutFunnelEvent(stage, options = {}) {
@@ -16,7 +16,7 @@ const CREATOR_SYNTHETIC_KEY = process.env.THUMBGATE_DEV_KEY || '';
16
16
  * 2. Env var: THUMBGATE_DEV_BYPASS=[set via THUMBGATE_DEV_SECRET env var]
17
17
  * Requires a specific non-obvious value (not boolean) to prevent accidental activation.
18
18
  */
19
- function isCreatorDev({ env = process.env, homeDir = env.HOME || env.USERPROFILE || os.homedir() } = {}) {
19
+ function isCreatorDev({ env = process.env, homeDir = os.homedir() } = {}) {
20
20
  // Layer 1: env var with specific value
21
21
  if (CREATOR_BYPASS_VALUE && String(env[CREATOR_BYPASS_ENV] || '') === CREATOR_BYPASS_VALUE) {
22
22
  return true;
@@ -37,7 +37,7 @@ function isCreatorDev({ env = process.env, homeDir = env.HOME || env.USERPROFILE
37
37
  * with any non-empty bypass value. No env var needed — just the config file.
38
38
  * Used by the server to skip auth on localhost during local development.
39
39
  */
40
- function hasDevOverride(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
40
+ function hasDevOverride(homeDir = os.homedir()) {
41
41
  // Disabled during test runs to avoid interfering with auth assertions
42
42
  if (process.env.NODE_TEST_CONTEXT || process.env.THUMBGATE_TESTING) return false;
43
43
  try {
@@ -47,11 +47,11 @@ function hasDevOverride(homeDir = process.env.HOME || process.env.USERPROFILE ||
47
47
  } catch { return false; }
48
48
  }
49
49
 
50
- function getLicenseDir(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
50
+ function getLicenseDir(homeDir = os.homedir()) {
51
51
  return path.join(homeDir, '.thumbgate');
52
52
  }
53
53
 
54
- function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir()) {
54
+ function getLicensePath(homeDir = os.homedir()) {
55
55
  return path.join(getLicenseDir(homeDir), 'license.json');
56
56
  }
57
57
 
@@ -39,23 +39,9 @@ function normalizeOptions(options = {}) {
39
39
  ...splitCsv(options.documents),
40
40
  ...splitCsv(options['document-ids']),
41
41
  ]);
42
- const sourcePointers = unique([
43
- ...splitCsv(options['source-pointers']),
44
- ...splitCsv(options.pointers),
45
- ...splitCsv(options.sources),
46
- ]);
47
42
  const candidateImages = Number.isFinite(Number(options['candidate-images']))
48
43
  ? Number(options['candidate-images'])
49
44
  : null;
50
- const extractedEntities = Number.isFinite(Number(options['extracted-entities']))
51
- ? Number(options['extracted-entities'])
52
- : 0;
53
- const extractedRelations = Number.isFinite(Number(options['extracted-relations']))
54
- ? Number(options['extracted-relations'])
55
- : 0;
56
- const promotionThreshold = Number.isFinite(Number(options['promotion-threshold']))
57
- ? Number(options['promotion-threshold'])
58
- : 3;
59
45
 
60
46
  return {
61
47
  ragTool: String(options['rag-tool'] || options.tool || 'proxy-pointer-rag').trim() || 'proxy-pointer-rag',
@@ -63,15 +49,10 @@ function normalizeOptions(options = {}) {
63
49
  sectionIds,
64
50
  imagePointers,
65
51
  documentIds,
66
- sourcePointers,
67
52
  candidateImages,
68
- extractedEntities,
69
- extractedRelations,
70
- promotionThreshold,
71
53
  crossDocumentPolicy: String(options['cross-doc-policy'] || options['cross-document-policy'] || '').trim().toLowerCase(),
72
54
  visionFilter: normalizeBoolean(options['vision-filter']),
73
55
  visualClaims: normalizeBoolean(options['visual-claims']),
74
- pointerFirst: normalizeBoolean(options['pointer-first']) || normalizeBoolean(options['proxy-pointer']),
75
56
  };
76
57
  }
77
58
 
@@ -91,14 +72,6 @@ function gateApplicability(template, options) {
91
72
  return false;
92
73
  }
93
74
 
94
- function hasExtractionSprawl(options) {
95
- const extractedFacts = options.extractedEntities + options.extractedRelations;
96
- if (extractedFacts === 0) return false;
97
- if (options.pointerFirst) return true;
98
- if (options.sourcePointers.length === 0) return true;
99
- return extractedFacts > options.sourcePointers.length * Math.max(2, options.promotionThreshold);
100
- }
101
-
102
75
  function buildSignalSummary(options) {
103
76
  const signals = [];
104
77
  if (options.treePath || options.sectionIds.length > 0) {
@@ -137,19 +110,6 @@ function buildSignalSummary(options) {
137
110
  risk: 'answers that describe image content may need a vision-model sanity check',
138
111
  });
139
112
  }
140
- if (hasExtractionSprawl(options)) {
141
- signals.push({
142
- id: 'entity_relation_sprawl',
143
- label: 'Entity/relation extraction sprawl',
144
- values: unique([
145
- `${options.extractedEntities} extracted entities`,
146
- `${options.extractedRelations} extracted relations`,
147
- `${options.sourcePointers.length} source pointers`,
148
- `promotion threshold ${options.promotionThreshold}`,
149
- ]),
150
- risk: 'eager graph extraction can create stale aliases, weak edges, and unauditable memory; keep source pointers first and promote relations only after repeated retrieval value',
151
- });
152
- }
153
113
  return signals;
154
114
  }
155
115
 
@@ -179,12 +139,11 @@ function buildProxyPointerRagGuardrailsPlan(rawOptions = {}, templatesPath) {
179
139
  templates: recommendedTemplates,
180
140
  nextActions: [
181
141
  'Preserve document hierarchy, section IDs, and image file paths during ingestion.',
182
- 'Store source pointers before extracting entities or relations; promote a relation only after repeated retrieval value and source verification.',
183
142
  'Pass section-tree and image-pointer metadata into the agent before it answers with visuals.',
184
143
  'Enable the recommended Document RAG Safety templates as pre-action gates.',
185
144
  'Use a vision filter only for high-impact answers that make claims about visual content.',
186
145
  ],
187
- exampleCommand: 'npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --source-pointers=lesson/fb_123,tool/run_456 --extracted-entities=120 --extracted-relations=80 --pointer-first --json',
146
+ exampleCommand: 'npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --image-pointers=paper-1/figures/fig2.png --documents=paper-1 --visual-claims --json',
188
147
  };
189
148
  }
190
149
 
@@ -6,78 +6,42 @@ const path = require('path');
6
6
  const {
7
7
  PRO_MONTHLY_PAYMENT_LINK,
8
8
  PRO_PRICE_LABEL,
9
- ENTERPRISE_PRICE_LABEL,
9
+ TEAM_PRICE_LABEL,
10
10
  } = require('./commercial-offer');
11
11
 
12
12
  const USAGE_FILE = path.join(process.env.HOME || '/tmp', '.thumbgate', 'usage-limits.json');
13
13
 
14
14
  // ──────────────────────────────────────────────────────────
15
- // Free tier: tight enough to create upgrade pressure after
16
- // real usage. Captures and rules are capped so heavy users
17
- // hit the wall within the first week, not the first quarter.
15
+ // Free tier: generous on captures (habit formation) and rules
16
+ // (5 active gates), gated on Pro-only features (recall, search,
17
+ // exports). Dashboard, exports, and unlimited rules drive Pro.
18
18
  // ──────────────────────────────────────────────────────────
19
19
  const FREE_TIER_LIMITS = {
20
- capture_feedback: { daily: 2, lifetime: 10, label: 'feedback captures (2/day, 10 total on free)' },
21
- prevention_rules: { daily: 2, lifetime: 3, label: 'prevention rules generated (2/day on free)' },
20
+ capture_feedback: { daily: Infinity, lifetime: Infinity, label: 'feedback captures' },
21
+ prevention_rules: { daily: Infinity, lifetime: Infinity, label: 'prevention rules generated' },
22
22
  recall: { daily: 0, lifetime: 0, label: 'recall queries (Pro only)' },
23
23
  search_lessons: { daily: 0, lifetime: 0, label: 'lesson searches (Pro only)' },
24
24
  search_thumbgate: { daily: 0, lifetime: 0, label: 'ThumbGate searches (Pro only)' },
25
25
  commerce_recall: { daily: 0, lifetime: 0, label: 'commerce recalls (Pro only)' },
26
26
  export_dpo: { daily: 0, lifetime: 0, label: 'DPO exports (Pro only)' },
27
27
  export_databricks: { daily: 0, lifetime: 0, label: 'Databricks exports (Pro only)' },
28
- construct_context_pack: { daily: 3, lifetime: Infinity, label: 'context packs (3/day on free)' },
28
+ construct_context_pack: { daily: Infinity, lifetime: Infinity, label: 'context packs' },
29
29
  };
30
30
 
31
- const FREE_TIER_MAX_GATES = 3; // 3 active prevention rules on free; Pro is unlimited
32
- const FREE_TIER_DAILY_BLOCKS = 2; // 3 gate blocks/day on free; after limit, deny → warn + upgrade CTA
31
+ const FREE_TIER_MAX_GATES = 5; // 5 active prevention rules on free; Pro is unlimited
33
32
 
34
- const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
33
+ const UPGRADE_MESSAGE = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${PRO_MONTHLY_PAYMENT_LINK}\n Team: ${TEAM_PRICE_LABEL} after workflow qualification.`;
35
34
 
36
35
  const PAYWALL_MESSAGES = {
37
- capture_feedback: 'Free tier: 2 captures/day (10 total). Your feedback is stored locally upgrade to capture unlimited.',
38
- prevention_rules: 'Free tier includes 3 active prevention rules and 2 rule generations/day. Upgrade to Pro for unlimited rules.',
36
+ prevention_rules: 'Free tier includes 5 active prevention rules. Promote more or unlock unlimited rules with Pro.',
39
37
  recall: 'Recall is a Pro feature. Your past feedback is stored locally — upgrade to search and reuse it.',
40
38
  search_lessons: 'Lesson search is a Pro feature. Upgrade to find patterns in your agent\'s mistakes.',
41
- construct_context_pack: 'Free tier: 3 context packs/day. Upgrade to Pro for unlimited.',
42
39
  default: 'This feature requires Pro. Start Pro — card required; billed today.',
43
40
  };
44
41
 
45
- const TRIAL_DAYS = 7;
46
-
47
- function getInstallAgeDays() {
48
- try {
49
- const { INSTALL_ID_PATH } = require('./cli-telemetry');
50
- if (!fs.existsSync(INSTALL_ID_PATH)) return null;
51
- // Use mtimeMs — birthtimeMs is unreliable on Linux (ext4 doesn't backdate creation time).
52
- // The install-id file is written once at install, so mtime == creation time in practice.
53
- const stat = fs.statSync(INSTALL_ID_PATH);
54
- const created = stat.mtimeMs || stat.birthtimeMs;
55
- if (!Number.isFinite(created) || created <= 0) return null;
56
- return (Date.now() - created) / (1000 * 60 * 60 * 24);
57
- } catch (_) {
58
- return null;
59
- }
60
- }
61
-
62
- function isInTrialPeriod() {
63
- if (process.env.CI || process.env.GITHUB_ACTIONS) return false;
64
- if (process.env.THUMBGATE_NO_TRIAL === '1') return false;
65
- const age = getInstallAgeDays();
66
- if (age === null) return false;
67
- if (age < 0.0007) return false; // <1 minute old — just-created install, not a real trial
68
- return age < TRIAL_DAYS;
69
- }
70
-
71
- function trialDaysRemaining() {
72
- const age = getInstallAgeDays();
73
- if (age === null) return 0;
74
- return Math.max(0, Math.ceil(TRIAL_DAYS - age));
75
- }
76
-
77
42
  function isProTier(authContext) {
78
43
  if (authContext && authContext.tier === 'pro') return true;
79
- if (process.env.THUMBGATE_API_KEY) return true;
80
- if (process.env.THUMBGATE_NO_RATE_LIMIT === '1') return true;
44
+ if (process.env.THUMBGATE_API_KEY || process.env.THUMBGATE_PRO_MODE === '1' || process.env.THUMBGATE_NO_RATE_LIMIT === '1') return true;
81
45
  // Creator/dogfooding bypass: when the owner has the dev secret + bypass
82
46
  // configured (env or ~/.config/thumbgate/dev.json), treat the install as Pro
83
47
  // so marketing nudges and rate limits stop firing on the maintainer's own
@@ -93,9 +57,6 @@ function isProTier(authContext) {
93
57
  const { isProLicensed } = require('./license');
94
58
  if (isProLicensed()) return true;
95
59
  } catch (_) {}
96
- // 7-day reverse trial: new installs get full Pro access, then hit a clear
97
- // hosted-sync/unlimited-rules pay moment while the product is still fresh.
98
- if (isInTrialPeriod()) return true;
99
60
  return false;
100
61
  }
101
62
 
@@ -153,18 +114,12 @@ function checkLimit(action, authContext) {
153
114
  const dailyCurrent = usage.counts[action] || 0;
154
115
  const lifetimeCurrent = usage.lifetime[action] || 0;
155
116
 
156
- const isCapture = action === 'capture_feedback';
157
- const monthlyLink = isCapture
158
- ? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
159
- : PRO_MONTHLY_PAYMENT_LINK;
160
- const upgradeMessage = `Pro: ${PRO_PRICE_LABEL} — unlimited rules, recall, lesson search, dashboard, and exports: ${monthlyLink}\n Enterprise: ${ENTERPRISE_PRICE_LABEL} after workflow qualification.`;
161
-
162
117
  // Check lifetime limit first (the hard wall)
163
118
  if (lifetimeLimit !== Infinity && lifetimeCurrent >= lifetimeLimit) {
164
119
  const paywallMsg = PAYWALL_MESSAGES[action] || PAYWALL_MESSAGES.default;
165
120
  return {
166
121
  allowed: false,
167
- message: `${paywallMsg}\n\n${upgradeMessage}`,
122
+ message: `${paywallMsg}\n\n${UPGRADE_MESSAGE}`,
168
123
  used: lifetimeCurrent,
169
124
  limit: lifetimeLimit,
170
125
  limitType: 'lifetime',
@@ -173,10 +128,9 @@ function checkLimit(action, authContext) {
173
128
 
174
129
  // Check daily limit
175
130
  if (dailyLimit !== Infinity && dailyCurrent >= dailyLimit) {
176
- const paywallMsg = PAYWALL_MESSAGES[action] || PAYWALL_MESSAGES.default;
177
131
  return {
178
132
  allowed: false,
179
- message: `Daily limit reached. ${paywallMsg}\n\n${upgradeMessage}`,
133
+ message: `Daily limit reached. ${UPGRADE_MESSAGE}`,
180
134
  used: dailyCurrent,
181
135
  limit: dailyLimit,
182
136
  limitType: 'daily',
@@ -225,17 +179,12 @@ function getUsage(action, authContext) {
225
179
  module.exports = {
226
180
  checkLimit,
227
181
  getUsage,
228
- getInstallAgeDays,
229
182
  isProTier,
230
- isInTrialPeriod,
231
- trialDaysRemaining,
232
183
  loadUsage,
233
184
  saveUsage,
234
185
  todayKey,
235
186
  FREE_TIER_LIMITS,
236
187
  FREE_TIER_MAX_GATES,
237
- FREE_TIER_DAILY_BLOCKS,
238
- TRIAL_DAYS,
239
188
  UPGRADE_MESSAGE,
240
189
  PAYWALL_MESSAGES,
241
190
  USAGE_FILE,
@@ -55,11 +55,6 @@ const BASH_SECRET_READ_PREFIXES = [
55
55
  ];
56
56
 
57
57
  const EDIT_LIKE_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
58
- const SAFE_SECRET_STORAGE_DIRS = [
59
- '.resume_secrets',
60
- '.thumbgate/secrets',
61
- '.config/thumbgate',
62
- ];
63
58
 
64
59
  function redactText(text) {
65
60
  if (!text) return '';
@@ -177,13 +172,6 @@ function heuristicScanText(text, source = 'text') {
177
172
  pattern.regex.lastIndex = 0;
178
173
  let match = pattern.regex.exec(input);
179
174
  while (match) {
180
- // Safe test key bypass
181
- const matchedString = match[0].toLowerCase();
182
- if (pattern.id === 'generic_assignment' && (matchedString.includes('sk_test_') || matchedString.includes('test_token'))) {
183
- match = pattern.regex.exec(input);
184
- continue;
185
- }
186
-
187
175
  findings.push({
188
176
  id: pattern.id,
189
177
  label: pattern.label,
@@ -307,26 +295,6 @@ function resolvePathToken(token, cwd) {
307
295
  return path.join(cwd || process.cwd(), normalized);
308
296
  }
309
297
 
310
- function normalizePathForPolicy(filePath) {
311
- return path.resolve(String(filePath || '').replace(/^~(?=\/|$)/, os.homedir()));
312
- }
313
-
314
- function isSafeSecretStoragePath(filePath) {
315
- if (!filePath) return false;
316
- const normalized = normalizePathForPolicy(filePath);
317
- const home = normalizePathForPolicy(os.homedir());
318
- return SAFE_SECRET_STORAGE_DIRS.some((dir) => {
319
- const allowedRoot = path.join(home, dir);
320
- return normalized === allowedRoot || normalized.startsWith(`${allowedRoot}${path.sep}`);
321
- });
322
- }
323
-
324
- function isSafeSecretStorageWrite(toolName, toolInput = {}, cwd = process.cwd()) {
325
- if (!EDIT_LIKE_TOOLS.has(toolName)) return false;
326
- const paths = getToolInputPaths(toolInput, cwd);
327
- return paths.length > 0 && paths.every((filePath) => isSafeSecretStoragePath(filePath));
328
- }
329
-
330
298
  function scanBashCommand(command, options = {}) {
331
299
  const cwd = options.cwd || process.cwd();
332
300
  const findings = [];
@@ -379,7 +347,6 @@ function scanHookInput(input = {}, options = {}) {
379
347
  let provider = resolveProvider(options.provider);
380
348
  let commandHash = null;
381
349
  let fileHashes = [];
382
- const safeSecretStorageWrite = isSafeSecretStorageWrite(toolName, toolInput, cwd);
383
350
 
384
351
  const contentFields = [
385
352
  toolInput.content,
@@ -409,13 +376,11 @@ function scanHookInput(input = {}, options = {}) {
409
376
  }
410
377
  }
411
378
 
412
- if (!safeSecretStorageWrite) {
413
- for (const content of contentFields) {
414
- const result = scanText(content, { provider, source: 'tool_input' });
415
- if (result.detected) {
416
- provider = result.provider;
417
- findings.push(...result.findings);
418
- }
379
+ for (const content of contentFields) {
380
+ const result = scanText(content, { provider, source: 'tool_input' });
381
+ if (result.detected) {
382
+ provider = result.provider;
383
+ findings.push(...result.findings);
419
384
  }
420
385
  }
421
386
 
@@ -437,8 +402,6 @@ function buildSafeSummary(findings, prefix) {
437
402
  module.exports = {
438
403
  SECRET_PATTERNS,
439
404
  SECRET_FILE_PATTERNS,
440
- SAFE_SECRET_STORAGE_DIRS,
441
- EDIT_LIKE_TOOLS,
442
405
  redactText,
443
406
  resolveProvider,
444
407
  scanText,
@@ -446,8 +409,6 @@ module.exports = {
446
409
  scanBashCommand,
447
410
  scanHookInput,
448
411
  classifySecretPath,
449
- isSafeSecretStoragePath,
450
- isSafeSecretStorageWrite,
451
412
  buildSafeSummary,
452
413
  tokenizeCommand,
453
414
  };