thumbgate 1.27.7 → 1.27.9

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 (106) hide show
  1. package/.well-known/llms.txt +1 -2
  2. package/README.md +0 -2
  3. package/bin/cli.js +259 -78
  4. package/package.json +12 -18
  5. package/public/blog.html +30 -0
  6. package/public/compare/adopt-ai.html +219 -0
  7. package/public/compare/agentix-labs.html +197 -0
  8. package/public/compare/ai-experience-orchestration.html +216 -0
  9. package/public/compare/anthropic-claude-for-legal.html +260 -0
  10. package/public/compare/anthropic-containment.html +280 -0
  11. package/public/compare/arcade.html +175 -0
  12. package/public/compare/arcjet.html +239 -0
  13. package/public/compare/bumblebee.html +307 -0
  14. package/public/compare/claude-code-hooks.html +294 -0
  15. package/public/compare/databricks-unity-ai-gateway.html +215 -0
  16. package/public/compare/fallow.html +351 -0
  17. package/public/compare/heidi.html +233 -0
  18. package/public/compare/mem0.html +342 -0
  19. package/public/compare/oak-and-sparrow-gatekeeper.html +289 -0
  20. package/public/compare/rein.html +236 -0
  21. package/public/compare/sigmashake.html +256 -0
  22. package/public/compare/speclock.html +342 -0
  23. package/public/compare.html +2 -0
  24. package/public/guides/agent-harness-optimization.html +342 -0
  25. package/public/guides/agentic-web-governance.html +406 -0
  26. package/public/guides/ai-agent-governance-sprint.html +415 -0
  27. package/public/guides/ai-agent-pre-action-approval-gates.html +401 -0
  28. package/public/guides/ai-agent-workflow-migration-checklist.html +392 -0
  29. package/public/guides/ai-deployment-readiness.html +415 -0
  30. package/public/guides/ai-mode-ads-agent-governance.html +401 -0
  31. package/public/guides/ai-search-topical-presence.html +342 -0
  32. package/public/guides/autoresearch-agent-safety.html +342 -0
  33. package/public/guides/background-agent-governance.html +358 -0
  34. package/public/guides/best-tools-stop-ai-agents-breaking-production.html +363 -0
  35. package/public/guides/browser-automation-safety.html +342 -0
  36. package/public/guides/chatgpt-ads-trust.html +353 -0
  37. package/public/guides/claude-code-feedback.html +339 -0
  38. package/public/guides/claude-code-prevent-repeated-mistakes.html +161 -0
  39. package/public/guides/claude-code-skills-guardrails.html +343 -0
  40. package/public/guides/claude-desktop.html +356 -0
  41. package/public/guides/code-knowledge-graph-guardrails.html +365 -0
  42. package/public/guides/codex-cli-guardrails.html +339 -0
  43. package/public/guides/cursor-agent-guardrails.html +339 -0
  44. package/public/guides/cursor-prevent-repeated-mistakes.html +161 -0
  45. package/public/guides/database-agent-safety.html +406 -0
  46. package/public/guides/deepseek-v4-runtime-guardrails.html +346 -0
  47. package/public/guides/developer-machine-supply-chain-guardrails.html +358 -0
  48. package/public/guides/gcp-mcp-guardrails.html +147 -0
  49. package/public/guides/gemini-cli-feedback-memory.html +339 -0
  50. package/public/guides/gpt-5-5-model-evaluation.html +358 -0
  51. package/public/guides/internal-ai-engineering-stack-guardrails.html +348 -0
  52. package/public/guides/long-running-agent-context-management.html +346 -0
  53. package/public/guides/mcp-tool-governance.html +401 -0
  54. package/public/guides/multica-thumbgate-setup.html +134 -0
  55. package/public/guides/native-messaging-host-security.html +342 -0
  56. package/public/guides/policy-engine-pre-action-gates.html +346 -0
  57. package/public/guides/pre-action-checks.html +342 -0
  58. package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +342 -0
  59. package/public/guides/prompt-tricks-to-workflow-rules.html +365 -0
  60. package/public/guides/proxy-pointer-rag-guardrails.html +352 -0
  61. package/public/guides/rag-precision-tuning-guardrails.html +352 -0
  62. package/public/guides/reasoning-compression-guardrails.html +346 -0
  63. package/public/guides/relational-knowledge-ai-recommendations.html +342 -0
  64. package/public/guides/roo-code-alternative-cline.html +339 -0
  65. package/public/guides/semantic-programmatic-seo-guardrails.html +352 -0
  66. package/public/guides/seo-agent-skills-guardrails.html +344 -0
  67. package/public/guides/stop-repeated-ai-agent-mistakes.html +342 -0
  68. package/public/index.html +10 -48
  69. package/public/learn/ac-dc-runtime-enforcement.html +277 -0
  70. package/public/learn/agent-harness-pattern.html +181 -0
  71. package/public/learn/agent-swarms-shared-gates.html +173 -0
  72. package/public/learn/agentic-enterprise-context-brain.html +117 -0
  73. package/public/learn/agentic-os-team-governance.html +146 -0
  74. package/public/learn/ai-agent-governance.html +158 -0
  75. package/public/learn/ai-agent-persistent-memory.html +211 -0
  76. package/public/learn/background-agent-control-layer.html +184 -0
  77. package/public/learn/claude-code-goal-with-rubrics.html +205 -0
  78. package/public/learn/codex-role-plugins-need-governance.html +125 -0
  79. package/public/learn/cost-aware-agent-gate-routing.html +173 -0
  80. package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +157 -0
  81. package/public/learn/deterministic-agent-workflows.html +185 -0
  82. package/public/learn/feedback-loop-vs-decision-layer.html +283 -0
  83. package/public/learn/from-prototype-to-production.html +223 -0
  84. package/public/learn/learn.css +51 -0
  85. package/public/learn/mcp-pre-action-checks-explained.html +172 -0
  86. package/public/learn/pretix-stripe-connect-marketplaces.html +161 -0
  87. package/public/learn/regulated-agent-execution-boundary.html +196 -0
  88. package/public/learn/spec-driven-development.html +168 -0
  89. package/public/learn/stop-ai-agent-force-push.html +134 -0
  90. package/public/learn/vibe-coding-safety-net.html +142 -0
  91. package/public/learn.html +6 -50
  92. package/public/pro.html +6 -6
  93. package/scripts/cli-schema.js +10 -22
  94. package/scripts/dashboard-chat.js +1 -2
  95. package/scripts/document-intake.js +49 -1
  96. package/scripts/gemini-embedding-policy.js +1 -2
  97. package/scripts/hosted-config.js +12 -0
  98. package/scripts/plausible-domain-config.js +1 -3
  99. package/scripts/reddit-browser-notification-watch.js +230 -0
  100. package/scripts/seo-gsd.js +0 -239
  101. package/scripts/vector-store.js +0 -44
  102. package/scripts/workspace-evolver.js +2 -62
  103. package/src/api/server.js +124 -335
  104. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  105. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  106. package/scripts/hook-stop-anti-claim.js +0 -227
package/public/learn.html CHANGED
@@ -145,54 +145,42 @@
145
145
  {
146
146
  "@type": "ListItem",
147
147
  "position": 10,
148
- "url": "https://thumbgate.ai/guides/hermes-agent-guardrails",
149
- "name": "Hermes Agent Guardrails for Self-Improving Agents"
150
- },
151
- {
152
- "@type": "ListItem",
153
- "position": 11,
154
- "url": "https://thumbgate.ai/guides/agent-context-governance",
155
- "name": "Agent Context Governance for Long-Running Agents"
156
- },
157
- {
158
- "@type": "ListItem",
159
- "position": 12,
160
148
  "url": "https://thumbgate.ai/guides/roo-code-alternative-cline",
161
149
  "name": "Roo Code Alternative: Migrating to Cline with Portable Lesson Memory"
162
150
  },
163
151
  {
164
152
  "@type": "ListItem",
165
- "position": 13,
153
+ "position": 11,
166
154
  "url": "https://thumbgate.ai/guides/browser-automation-safety",
167
155
  "name": "Browser Automation Safety for AI Agents"
168
156
  },
169
157
  {
170
158
  "@type": "ListItem",
171
- "position": 14,
159
+ "position": 12,
172
160
  "url": "https://thumbgate.ai/guides/native-messaging-host-security",
173
161
  "name": "Native Messaging Host Security"
174
162
  },
175
163
  {
176
164
  "@type": "ListItem",
177
- "position": 15,
165
+ "position": 13,
178
166
  "url": "https://thumbgate.ai/guides/ai-search-topical-presence",
179
167
  "name": "AI Search Topical Presence"
180
168
  },
181
169
  {
182
170
  "@type": "ListItem",
183
- "position": 16,
171
+ "position": 14,
184
172
  "url": "https://thumbgate.ai/guides/relational-knowledge-ai-recommendations",
185
173
  "name": "Relational Knowledge in AI Recommendations"
186
174
  },
187
175
  {
188
176
  "@type": "ListItem",
189
- "position": 17,
177
+ "position": 15,
190
178
  "url": "https://thumbgate.ai/guides/ai-deployment-readiness",
191
179
  "name": "AI Deployment Readiness Before Production Rollout"
192
180
  },
193
181
  {
194
182
  "@type": "ListItem",
195
- "position": 18,
183
+ "position": 16,
196
184
  "url": "https://thumbgate.ai/guides/database-agent-safety",
197
185
  "name": "Database Safety for AI Agents"
198
186
  }
@@ -473,14 +461,6 @@
473
461
  <span class="article-tag">Governance</span>
474
462
  </a>
475
463
 
476
- <a href="/guides/vllm-serving-guardrails" class="article-card">
477
- <h3>vLLM Serving Guardrails</h3>
478
- <p>Gate self-hosted inference changes before agents route production work through PagedAttention, batching, prefix-cache, or model-swap optimizations.</p>
479
- <span class="article-tag">vLLM</span>
480
- <span class="article-tag">LLM Serving</span>
481
- <span class="article-tag">Runtime Safety</span>
482
- </a>
483
-
484
464
  <a href="/guides/relational-knowledge-ai-recommendations" class="article-card">
485
465
  <h3>Relational Knowledge in AI Recommendations</h3>
486
466
  <p>How stored brand-to-problem associations shape AI answers, and why ThumbGate should own the pre-action-checks category in those retrieval paths.</p>
@@ -521,30 +501,6 @@
521
501
  <span class="article-tag">Enforcement</span>
522
502
  </a>
523
503
 
524
- <a href="/guides/hermes-agent-guardrails" class="article-card">
525
- <h3>Hermes Agent Guardrails for Self-Improving Agents</h3>
526
- <p>Hermes-style agents bring persistent memory, generated skills, gateways, automations, and sandboxes. ThumbGate adds the pre-action firewall before those agents touch real systems.</p>
527
- <span class="article-tag">Hermes Agent</span>
528
- <span class="article-tag">Persistent Agents</span>
529
- <span class="article-tag">Execution Firewall</span>
530
- </a>
531
-
532
- <a href="/guides/safe-self-evolution" class="article-card">
533
- <h3>Safe Self-Evolution: Prompt Optimization without Regression</h3>
534
- <p>Hermes-style autonomous agents learn by observing failures and rewriting skills. ThumbGate introduces Safe Self-Evolution: weakness mining, automated prompt optimization, verification suites, and atomic git rollbacks.</p>
535
- <span class="article-tag">Self-Evolution</span>
536
- <span class="article-tag">Harness Optimizer</span>
537
- <span class="article-tag">Verification Gate</span>
538
- </a>
539
-
540
- <a href="/guides/agent-context-governance" class="article-card">
541
- <h3>Agent Context Governance for Long-Running Agents</h3>
542
- <p>AdaCoM, tokenmaxxing backlash, Managed Agents, lockdown modes, and model-provenance scares all point to one need: clean context and tool gates before agents act.</p>
543
- <span class="article-tag">Context Governance</span>
544
- <span class="article-tag">Tool Lockdown</span>
545
- <span class="article-tag">Model Provenance</span>
546
- </a>
547
-
548
504
  <a href="/guides/roo-code-alternative-cline" class="article-card">
549
505
  <h3>Roo Code Alternative: Migrate to Cline Without Losing Agent Memory</h3>
550
506
  <p>Use the Roo shutdown window to pitch portable lesson memory and local-first enforcement instead of making operators re-teach the same failures after they switch.</p>
package/public/pro.html CHANGED
@@ -37,7 +37,7 @@ __GA_BOOTSTRAP__
37
37
  <script type="application/ld+json">
38
38
  {
39
39
  "@context": "https://schema.org",
40
- "@type": "FAQPage", "mainEntity": [{ "@type": "Question", "name": "How is Pro different from the free install?", "acceptedAnswer": { "@type": "Answer", "text": "Free keeps local recall, checks, and MCP. Pro adds the personal dashboard, DPO export, auto-connect, and founder support." } }, { "@type": "Question", "name": "Does Pro require a cloud account?", "acceptedAnswer": { "@type": "Answer", "text": "No. Pro stays local-first; Enterprise is the hosted rollout lane for shared lessons, org visibility, and reviews." } }, { "@type": "Question", "name": "What happens after checkout?", "acceptedAnswer": { "@type": "Answer", "text": "You activate Pro, connect the local dashboard, and inspect blocked actions, lessons, and exports." } }, { "@type": "Question", "name": "When should I choose Enterprise instead of Pro?", "acceptedAnswer": { "@type": "Answer", "text": "Choose Enterprise when one correction needs to protect multiple developers or agents across shared repositories." } }]
40
+ "@type": "FAQPage", "mainEntity": [{ "@type": "Question", "name": "How is Pro different from the free install?", "acceptedAnswer": { "@type": "Answer", "text": "Free keeps local recall, checks, and MCP. Pro adds the personal dashboard, DPO export, auto-connect, and founder support." } }, { "@type": "Question", "name": "Does Pro require a cloud account?", "acceptedAnswer": { "@type": "Answer", "text": "No. Pro stays local-first; Team is the hosted rollout lane for shared lessons, org visibility, and reviews." } }, { "@type": "Question", "name": "What happens after checkout?", "acceptedAnswer": { "@type": "Answer", "text": "You activate Pro, connect the local dashboard, and inspect blocked actions, lessons, and exports." } }, { "@type": "Question", "name": "When should I choose Team instead of Pro?", "acceptedAnswer": { "@type": "Answer", "text": "Choose Team when one correction needs to protect multiple developers or agents across shared repositories." } }]
41
41
  }
42
42
  </script>
43
43
 
@@ -731,7 +731,7 @@ __GA_BOOTSTRAP__
731
731
  <h1>Buy the operator loop that proves your AI agent stopped repeating the mistake.</h1>
732
732
  <p style="font-size:13px;opacity:0.8;margin-bottom:0.5rem;">Updated: <time datetime="2026-04-20">2026-04-20</time> · by <a href="https://github.com/IgorGanapolsky" style="color:inherit;">Igor Ganapolsky</a></p>
733
733
  <p>ThumbGate Pro is for one operator who already hit a repeated AI-agent failure and now needs proof: what was blocked, why it was blocked, and what changed before the next risky run.</p>
734
- <p>Start Pro when you want the local dashboard, DPO export, and a single proof lane for the repeated mistake you need to stop. Enterprise diagnostics and custom services are handled through intake, not this buyer path.</p>
734
+ <p>Start Pro when you want the local dashboard, DPO export, and a single proof lane for the repeated mistake you need to stop. Team diagnostics and custom services are handled through intake, not this buyer path.</p>
735
735
  <div class="hero-proof">
736
736
  <div class="proof-pill">Personal local dashboard</div>
737
737
  <div class="proof-pill">DPO export from real corrections</div>
@@ -951,15 +951,15 @@ __GA_BOOTSTRAP__
951
951
  </div>
952
952
  <div class="faq-item">
953
953
  <button class="faq-q" type="button" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)" aria-expanded="false">Does Pro require a cloud account?</button>
954
- <div class="faq-a">No. Pro is still local-first for the individual operator lane. Enterprise is the hosted rollout lane for shared lessons, org visibility, and hosted review views.</div>
954
+ <div class="faq-a">No. Pro is still local-first for the individual operator lane. Team is the hosted rollout lane for shared lessons, org visibility, and hosted review views.</div>
955
955
  </div>
956
956
  <div class="faq-item">
957
957
  <button class="faq-q" type="button" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)" aria-expanded="false">What happens after checkout?</button>
958
958
  <div class="faq-a">You activate Pro, connect the personal local dashboard, and your running agents can appear automatically so you can inspect blocked actions, active lessons, and exportable DPO pairs without adding a separate cloud dashboard dependency.</div>
959
959
  </div>
960
960
  <div class="faq-item">
961
- <button class="faq-q" type="button" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)" aria-expanded="false">When should I choose Enterprise instead of Pro?</button>
962
- <div class="faq-a">Choose Enterprise when one thumbs-down should protect multiple people or agents across shared repositories, or when you need shared hosted lessons, org dashboard visibility, and a workflow hardening pilot with rollout review views.</div>
961
+ <button class="faq-q" type="button" onclick="toggleFaq(this)" onkeydown="handleFaqKeydown(event)" aria-expanded="false">When should I choose Team instead of Pro?</button>
962
+ <div class="faq-a">Choose Team when one thumbs-down should protect multiple people or agents across shared repositories, or when you need shared hosted lessons, org dashboard visibility, and a workflow hardening pilot with rollout review views.</div>
963
963
  </div>
964
964
  </div>
965
965
  </div>
@@ -987,7 +987,7 @@ __GA_BOOTSTRAP__
987
987
  <a href="__VERIFICATION_URL__" target="_blank" rel="noopener">Verification Evidence</a>
988
988
  <a href="https://github.com/IgorGanapolsky/ThumbGate" target="_blank" rel="noopener">GitHub</a>
989
989
  </div>
990
- <div class="footer-copy">ThumbGate Pro for individual operators. Enterprise stays intake-first.</div>
990
+ <div class="footer-copy">ThumbGate Pro for individual operators. Team stays intake-first.</div>
991
991
  </div>
992
992
  </footer>
993
993
 
@@ -123,6 +123,16 @@ const CLI_COMMANDS = [
123
123
  { name: 'remote', type: 'boolean', description: 'Fetch from hosted Railway instance' },
124
124
  ],
125
125
  },
126
+ {
127
+ name: 'community',
128
+ aliases: ['registry'],
129
+ description: 'Query or share verified prevention rules with the community knowledge registry',
130
+ group: 'discovery',
131
+ flags: [
132
+ { name: 'json', type: 'boolean', description: 'Output as JSON' },
133
+ { name: 'remote', type: 'boolean', description: 'Fetch from community remote API' },
134
+ ],
135
+ },
126
136
  {
127
137
  name: 'gate-stats',
128
138
  description: 'Check engine statistics — active checks, blocks, warns, time saved',
@@ -505,12 +515,6 @@ const CLI_COMMANDS = [
505
515
  group: 'gates',
506
516
  flags: [],
507
517
  },
508
- {
509
- name: 'hermes-gate',
510
- description: 'Hermes Agent pre_tool_call hook: gate runtime tool calls (incl. skill_manage) before they run',
511
- group: 'gates',
512
- flags: [],
513
- },
514
518
  {
515
519
  name: 'force-gate',
516
520
  description: 'Immediately create a blocking gate from a pattern string',
@@ -656,22 +660,6 @@ const CLI_COMMANDS = [
656
660
  { name: 'json', type: 'boolean', description: 'Output results as JSON' },
657
661
  ],
658
662
  },
659
- {
660
- name: 'check-update',
661
- aliases: ['upgrade-check'],
662
- description: 'Check for newer versions of ThumbGate from npm or GitHub',
663
- group: 'ops',
664
- flags: [
665
- { name: 'json', type: 'boolean', description: 'Output results as JSON' },
666
- ],
667
- },
668
- {
669
- name: 'self-update',
670
- aliases: ['upgrade-cli'],
671
- description: 'Automatically install the latest version of ThumbGate globally',
672
- group: 'ops',
673
- flags: [],
674
- },
675
663
  ];
676
664
 
677
665
  /**
@@ -317,8 +317,7 @@ async function answerDataQuestion(question, opts = {}) {
317
317
  if (isPerplexity) return await callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources });
318
318
  return await callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources });
319
319
  } catch (err) {
320
- const safeMessage = (err && err.message) ? String(err.message).split('\n')[0].slice(0, 100) : 'An unexpected error occurred.';
321
- return { ok: false, error: 'network', message: safeMessage, sources };
320
+ return { ok: false, error: 'network', message: err?.message || String(err), sources };
322
321
  }
323
322
  }
324
323
 
@@ -708,6 +708,7 @@ function buildDocumentSummary(document) {
708
708
  sourcePath: document.sourcePath || null,
709
709
  sourceName: document.sourceName || null,
710
710
  sourceFormat: document.sourceFormat,
711
+ sourceUrl: document.sourceUrl || null,
711
712
  importedAt: document.importedAt,
712
713
  tags: normalizeTags(document.tags),
713
714
  excerpt: document.excerpt,
@@ -768,7 +769,11 @@ function persistDocument(document, options = {}) {
768
769
  const summaries = listImportedDocuments({
769
770
  ...options,
770
771
  limit: MAX_SEARCH_SCAN,
771
- }).documents.filter((entry) => entry.documentId !== document.documentId);
772
+ }).documents.filter((entry) => {
773
+ if (entry.documentId === document.documentId) return false;
774
+ if (document.sourceUrl && entry.sourceUrl === document.sourceUrl) return false;
775
+ return true;
776
+ });
772
777
  const nextSummaries = [
773
778
  buildDocumentSummary(document),
774
779
  ...summaries,
@@ -882,6 +887,48 @@ function importDocument(options = {}) {
882
887
  sourceFormat,
883
888
  });
884
889
  const fingerprint = sha256(`${title}\n${normalizedContent}`);
890
+
891
+ // -- deduplication and RAG drift tracking ----------------------------------
892
+ const paths = getDocumentStorePaths(options);
893
+ let duplicate = null;
894
+ if (fs.existsSync(paths.catalogPath)) {
895
+ try {
896
+ const catalog = readJsonl(paths.catalogPath);
897
+ const urlMatch = options.sourceUrl ? String(options.sourceUrl).trim() : null;
898
+ const matchedSummary = catalog.find((summary) =>
899
+ (urlMatch && summary.sourceUrl === urlMatch) ||
900
+ (summary.fingerprint === fingerprint)
901
+ );
902
+ if (matchedSummary) {
903
+ const fullDoc = readImportedDocument(matchedSummary.documentId, options);
904
+ if (fullDoc) {
905
+ if (fullDoc.fingerprint === fingerprint) {
906
+ // Case A: Content is identical
907
+ const dedupReason = urlMatch && fullDoc.sourceUrl === urlMatch
908
+ ? 'url-and-content-unchanged'
909
+ : 'content-identical';
910
+ return {
911
+ ...fullDoc,
912
+ duplicate: true,
913
+ updated: false,
914
+ dedupReason,
915
+ };
916
+ } else {
917
+ // Case B: URL matches but content changed (RAG Drift!)
918
+ duplicate = {
919
+ previousDocumentId: fullDoc.documentId,
920
+ previousFingerprint: fullDoc.fingerprint,
921
+ updated: true,
922
+ dedupReason: 'url-content-updated',
923
+ };
924
+ }
925
+ }
926
+ }
927
+ } catch (err) {
928
+ // best-effort
929
+ }
930
+ }
931
+
885
932
  const importedAt = nowIso();
886
933
  const sourceName = sourcePath ? path.basename(sourcePath) : null;
887
934
  const documentId = `doc_${slugify(title || sourceName || 'document').slice(0, 24) || 'document'}_${fingerprint.slice(0, 12)}`;
@@ -901,6 +948,7 @@ function importDocument(options = {}) {
901
948
  contentBytes: Buffer.byteLength(normalizedContent, 'utf8'),
902
949
  lineCount: normalizedContent.split('\n').filter(Boolean).length,
903
950
  headings: extractHeadings(normalizedContent),
951
+ ...(duplicate || {}),
904
952
  };
905
953
  document.proposals = options.proposeGates === false
906
954
  ? []
@@ -122,7 +122,7 @@ function resolveGeminiEmbeddingConfig(env = process.env) {
122
122
 
123
123
  return {
124
124
  enabled,
125
- provider: provider === 'coreai' ? 'coreai' : (enabled ? 'gemini' : 'local'),
125
+ provider: enabled ? 'gemini' : 'local',
126
126
  model: String(env.THUMBGATE_GEMINI_EMBED_MODEL || GEMINI_EMBEDDING_2_MODEL).trim() || GEMINI_EMBEDDING_2_MODEL,
127
127
  apiKey,
128
128
  apiBaseUrl: trimTrailingSlashes(env.THUMBGATE_GEMINI_API_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta'),
@@ -171,7 +171,6 @@ function buildGeminiEmbeddingRolloutPlan(args = {}) {
171
171
  },
172
172
  rolloutSteps: [
173
173
  'Keep local embeddings as the default offline path.',
174
- 'For Apple Silicon developers, route local queries through Core AI (AOT compiled models) to bypass CPU overhead.',
175
174
  'Enable Gemini Embedding 2 only when a Gemini API key is present.',
176
175
  'Use task-specific query/document prefixes at index and retrieval time.',
177
176
  'Start at 768 dimensions, then benchmark 1536 only if recall misses show up.',
@@ -13,6 +13,7 @@ const DEFAULT_PRO_PRICE_DOLLARS = PRO_MONTHLY_PRICE_DOLLARS;
13
13
  const DEFAULT_PRO_PRICE_LABEL = PRO_PRICE_LABEL;
14
14
  const DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS = 499;
15
15
  const DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS = 1500;
16
+ const DEFAULT_SNAPSHOT_PRICE_DOLLARS = 97;
16
17
  const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]+$/i;
17
18
 
18
19
  function normalizeOrigin(value) {
@@ -129,6 +130,10 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
129
130
  const googleSiteVerification = normalizeTrackingId(env.THUMBGATE_GOOGLE_SITE_VERIFICATION);
130
131
  const sprintDiagnosticCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_SPRINT_DIAGNOSTIC_CHECKOUT_URL);
131
132
  const workflowSprintCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_WORKFLOW_SPRINT_CHECKOUT_URL);
133
+ const paypalDiagnosticCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_PAYPAL_DIAGNOSTIC_CHECKOUT_URL);
134
+ const paypalWorkflowSprintCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_PAYPAL_WORKFLOW_SPRINT_CHECKOUT_URL);
135
+ const morSnapshotCheckoutUrl = normalizeAbsoluteUrl(env.THUMBGATE_MOR_SNAPSHOT_CHECKOUT_URL);
136
+ const morProvider = String(env.THUMBGATE_MOR_PROVIDER || '').trim();
132
137
 
133
138
  return {
134
139
  appOrigin,
@@ -142,10 +147,16 @@ function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
142
147
  proPriceLabel,
143
148
  sprintDiagnosticCheckoutUrl,
144
149
  workflowSprintCheckoutUrl,
150
+ paypalDiagnosticCheckoutUrl,
151
+ paypalWorkflowSprintCheckoutUrl,
152
+ morSnapshotCheckoutUrl,
153
+ morProvider,
145
154
  sprintDiagnosticPriceDollars: normalizePriceDollars(env.THUMBGATE_SPRINT_DIAGNOSTIC_PRICE_DOLLARS)
146
155
  || DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS,
147
156
  workflowSprintPriceDollars: normalizePriceDollars(env.THUMBGATE_WORKFLOW_SPRINT_PRICE_DOLLARS)
148
157
  || DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS,
158
+ snapshotPriceDollars: normalizePriceDollars(env.THUMBGATE_SNAPSHOT_PRICE_DOLLARS)
159
+ || DEFAULT_SNAPSHOT_PRICE_DOLLARS,
149
160
  gaMeasurementId,
150
161
  googleSiteVerification,
151
162
  posthogApiKey,
@@ -159,6 +170,7 @@ module.exports = {
159
170
  DEFAULT_PRO_PRICE_LABEL,
160
171
  DEFAULT_SPRINT_DIAGNOSTIC_PRICE_DOLLARS,
161
172
  DEFAULT_WORKFLOW_SPRINT_PRICE_DOLLARS,
173
+ DEFAULT_SNAPSHOT_PRICE_DOLLARS,
162
174
  GA_MEASUREMENT_ID_PATTERN,
163
175
  normalizeAbsoluteUrl,
164
176
  normalizeOrigin,
@@ -16,9 +16,7 @@ function normalizeDomain(value) {
16
16
  try {
17
17
  return new URL(input.includes('://') ? input : `https://${input}`).hostname.toLowerCase();
18
18
  } catch {
19
- const withoutProtocol = input.replace(/^https?:\/\//i, '');
20
- const hostnameAndPort = withoutProtocol.split('/')[0];
21
- return hostnameAndPort.toLowerCase().split(':')[0];
19
+ return input.replace(/^https?:\/\//i, '').replace(/\/.*$/, '').toLowerCase().split(':')[0];
22
20
  }
23
21
  }
24
22
 
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { chromium } = require('playwright-core');
7
+
8
+ const DEFAULT_CDP_ENDPOINT = 'http://127.0.0.1:9222';
9
+ const DEFAULT_STATE_FILE = path.resolve(__dirname, '..', '.thumbgate', 'reddit-browser-notification-state.json');
10
+ const DEFAULT_EVENTS_FILE = path.resolve(__dirname, '..', '.thumbgate', 'reddit-browser-notifications.jsonl');
11
+ const REDDIT_NOTIFICATIONS_URL = 'https://www.reddit.com/notifications';
12
+
13
+ function resolveRuntimeFile(envName, defaultPath) {
14
+ const configured = process.env[envName];
15
+ return configured ? path.resolve(configured) : defaultPath;
16
+ }
17
+
18
+ function loadJson(filePath, fallback) {
19
+ try {
20
+ if (fs.existsSync(filePath)) return JSON.parse(fs.readFileSync(filePath, 'utf8'));
21
+ } catch {
22
+ // Ignore corrupt transient state; a later write will repair it.
23
+ }
24
+ return fallback;
25
+ }
26
+
27
+ function writeJson(filePath, value) {
28
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
29
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
30
+ }
31
+
32
+ function appendJsonl(filePath, rows) {
33
+ if (rows.length === 0) return;
34
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
35
+ fs.appendFileSync(filePath, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`);
36
+ }
37
+
38
+ function fingerprintNotification(notification) {
39
+ return [
40
+ notification.author || '',
41
+ notification.kind || '',
42
+ notification.subreddit || '',
43
+ notification.preview || '',
44
+ notification.age || '',
45
+ ].join('|').toLowerCase();
46
+ }
47
+
48
+ function scoreNotification(notification) {
49
+ const text = `${notification.author || ''} ${notification.kind || ''} ${notification.preview || ''}`.toLowerCase();
50
+ let score = 0;
51
+ const reasons = [];
52
+
53
+ if (/accepted your chat invite|chat invite/i.test(text)) {
54
+ score += 5;
55
+ reasons.push('chat_accepted');
56
+ }
57
+ if (/\b(interested|try|paid|diagnostic|workflow|failure|gate|thumbgate|thubgate)\b/i.test(text)) {
58
+ score += 4;
59
+ reasons.push('buyer_signal');
60
+ }
61
+ if (/\b(replied|mentioned)\b/i.test(text)) {
62
+ score += 2;
63
+ reasons.push('reply_or_mention');
64
+ }
65
+ if (/\b(spam|slop|bot|report|ignore all previous instructions)\b/i.test(text)) {
66
+ score -= 5;
67
+ reasons.push('hostile_or_meta');
68
+ }
69
+ if (/automoderator|mod-bot|minimum karma|removed|reviewed shortly/i.test(text)) {
70
+ score -= 1;
71
+ reasons.push('platform_moderation');
72
+ }
73
+
74
+ return { score, reasons };
75
+ }
76
+
77
+ function ageMinutes(age) {
78
+ const text = String(age || '').trim().toLowerCase();
79
+ if (!text || text === 'just now') return 0;
80
+ const match = /^(\d+)\s*([mhdw])\s+ago$/.exec(text);
81
+ if (!match) return Number.POSITIVE_INFINITY;
82
+ const value = Number(match[1]);
83
+ const unit = match[2];
84
+ if (unit === 'm') return value;
85
+ if (unit === 'h') return value * 60;
86
+ if (unit === 'd') return value * 24 * 60;
87
+ return value * 7 * 24 * 60;
88
+ }
89
+
90
+ function isAgeLine(line) {
91
+ return /^(?:just now|\d+\s*[mhdw]\s+ago)$/i.test(String(line || '').trim());
92
+ }
93
+
94
+ function isRecentNotification(notification, maxAgeMinutes = 48 * 60) {
95
+ return ageMinutes(notification.age) <= maxAgeMinutes;
96
+ }
97
+
98
+ function parseNotificationBlocks(bodyText) {
99
+ const lines = String(bodyText || '')
100
+ .split('\n')
101
+ .map((line) => line.trim())
102
+ .filter(Boolean);
103
+ const notifications = [];
104
+
105
+ for (let index = 0; index < lines.length; index += 1) {
106
+ let author = lines[index];
107
+ let kind = lines[index + 1] || '';
108
+ let kindIndex = index + 1;
109
+ if (isAgeLine(author)) continue;
110
+
111
+ if (/\b(replied to|mentioned you|new mentions)\b/i.test(author)) {
112
+ kind = author;
113
+ kindIndex = index;
114
+ const authorMatch = /^u\/([^\s]+)/i.exec(kind);
115
+ author = authorMatch ? authorMatch[1] : author;
116
+ }
117
+
118
+ if (!kind || !/\b(accepted your chat invite|replied to|mentioned you|new mentions)\b/i.test(kind)) continue;
119
+
120
+ const hasPreview = !/accepted your chat invite|new mentions/i.test(kind);
121
+ const preview = hasPreview ? (lines[kindIndex + 1] || '') : '';
122
+ const age = hasPreview ? (lines[kindIndex + 2] || '') : (lines[kindIndex + 1] || '');
123
+ const subredditMatch = /\bin\s+r\/([A-Za-z0-9_]+)/.exec(kind);
124
+ const notification = {
125
+ author,
126
+ kind,
127
+ subreddit: subredditMatch ? subredditMatch[1] : null,
128
+ preview,
129
+ age,
130
+ };
131
+ const scored = scoreNotification(notification);
132
+ notifications.push({
133
+ ...notification,
134
+ ...scored,
135
+ ageMinutes: ageMinutes(notification.age),
136
+ fingerprint: fingerprintNotification(notification),
137
+ });
138
+ }
139
+
140
+ return notifications;
141
+ }
142
+
143
+ async function readRedditNotifications({
144
+ cdpEndpoint = process.env.THUMBGATE_CHROME_CDP_ENDPOINT || DEFAULT_CDP_ENDPOINT,
145
+ timeoutMs = Number(process.env.THUMBGATE_REDDIT_BROWSER_TIMEOUT_MS || 15000),
146
+ } = {}) {
147
+ const browser = await chromium.connectOverCDP(cdpEndpoint);
148
+ const context = browser.contexts()[0] || await browser.newContext();
149
+ const page = await context.newPage();
150
+ try {
151
+ await page.goto(REDDIT_NOTIFICATIONS_URL, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
152
+ await page.waitForTimeout(3000);
153
+ const bodyText = await page.locator('body').innerText({ timeout: timeoutMs });
154
+ return parseNotificationBlocks(bodyText);
155
+ } finally {
156
+ await page.close().catch(() => {});
157
+ await browser.close().catch(() => {});
158
+ }
159
+ }
160
+
161
+ async function run({ dryRun = false, now = new Date().toISOString() } = {}) {
162
+ const stateFile = resolveRuntimeFile('THUMBGATE_REDDIT_BROWSER_STATE_FILE', DEFAULT_STATE_FILE);
163
+ const eventsFile = resolveRuntimeFile('THUMBGATE_REDDIT_BROWSER_EVENTS_FILE', DEFAULT_EVENTS_FILE);
164
+ const state = loadJson(stateFile, { seen: {} });
165
+ const notifications = await readRedditNotifications();
166
+ const fresh = notifications.filter((notification) => !state.seen[notification.fingerprint]);
167
+ const actionable = fresh.filter((notification) => notification.score > 0 && isRecentNotification(notification));
168
+ const rows = actionable.map((notification) => ({
169
+ checkedAt: now,
170
+ platform: 'reddit',
171
+ source: 'browser_notifications',
172
+ status: 'pending_review',
173
+ ...notification,
174
+ }));
175
+
176
+ for (const notification of fresh) {
177
+ state.seen[notification.fingerprint] = { seenAt: now, score: notification.score };
178
+ }
179
+ state.lastCheck = now;
180
+
181
+ if (!dryRun) {
182
+ writeJson(stateFile, state);
183
+ appendJsonl(eventsFile, rows);
184
+ }
185
+
186
+ return {
187
+ notifications: notifications.length,
188
+ fresh: fresh.length,
189
+ actionable: actionable.length,
190
+ eventsFile,
191
+ actionableItems: actionable,
192
+ dryRun,
193
+ };
194
+ }
195
+
196
+ function parseArgs(argv = process.argv.slice(2)) {
197
+ return {
198
+ dryRun: argv.includes('--dry-run'),
199
+ json: argv.includes('--json'),
200
+ };
201
+ }
202
+
203
+ if (require.main === module) {
204
+ const args = parseArgs();
205
+ run({ dryRun: args.dryRun })
206
+ .then((result) => {
207
+ if (args.json) {
208
+ console.log(JSON.stringify(result, null, 2));
209
+ } else {
210
+ console.log(`[reddit-browser-watch] notifications=${result.notifications} fresh=${result.fresh} actionable=${result.actionable} dryRun=${result.dryRun}`);
211
+ for (const item of result.actionableItems) {
212
+ console.log(`- score=${item.score} author=${item.author} kind=${item.kind} preview=${item.preview.slice(0, 120)}`);
213
+ }
214
+ }
215
+ })
216
+ .catch((err) => {
217
+ console.error(`[reddit-browser-watch] ${err.message}`);
218
+ process.exitCode = 1;
219
+ });
220
+ }
221
+
222
+ module.exports = {
223
+ fingerprintNotification,
224
+ ageMinutes,
225
+ isRecentNotification,
226
+ parseNotificationBlocks,
227
+ readRedditNotifications,
228
+ run,
229
+ scoreNotification,
230
+ };