thumbgate 1.4.1 → 1.4.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 (60) hide show
  1. package/.claude-plugin/README.md +45 -34
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +3 -3
  4. package/.well-known/llms.txt +1 -1
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +26 -2
  7. package/adapters/README.md +4 -1
  8. package/adapters/chatgpt/INSTALL.md +39 -19
  9. package/adapters/claude/.mcp.json +2 -2
  10. package/adapters/codex/config.toml +2 -2
  11. package/adapters/mcp/server-stdio.js +10 -4
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/adapters/perplexity/.mcp.json +36 -0
  14. package/adapters/perplexity/config.toml +16 -0
  15. package/adapters/perplexity/opencode.json +29 -0
  16. package/bin/cli.js +246 -90
  17. package/config/mcp-allowlists.json +11 -3
  18. package/package.json +28 -13
  19. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  20. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  21. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  22. package/plugins/codex-profile/.mcp.json +1 -1
  23. package/plugins/codex-profile/INSTALL.md +1 -1
  24. package/plugins/codex-profile/README.md +1 -1
  25. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  26. package/plugins/opencode-profile/INSTALL.md +1 -1
  27. package/public/index.html +121 -24
  28. package/public/llm-context.md +17 -1
  29. package/scripts/ai-search-visibility.js +10 -36
  30. package/scripts/audit-trail.js +25 -15
  31. package/scripts/auto-wire-hooks.js +127 -0
  32. package/scripts/cli-demo.js +102 -0
  33. package/scripts/cli-schema.js +285 -0
  34. package/scripts/cli-status.js +166 -0
  35. package/scripts/cross-encoder-reranker.js +235 -0
  36. package/scripts/explore-subcommands.js +277 -0
  37. package/scripts/explore.js +569 -0
  38. package/scripts/feedback-loop.js +20 -6
  39. package/scripts/lesson-inference.js +27 -2
  40. package/scripts/lesson-reranker.js +263 -0
  41. package/scripts/lesson-retrieval.js +34 -17
  42. package/scripts/lesson-search.js +69 -0
  43. package/scripts/perplexity-client.js +210 -0
  44. package/scripts/perplexity-command-center.js +644 -0
  45. package/scripts/perplexity-marketing.js +17 -29
  46. package/scripts/prove-packaged-runtime.js +5 -4
  47. package/scripts/ralph-mode-ci.js +122 -19
  48. package/scripts/reflector-agent.js +2 -2
  49. package/scripts/session-analyzer.js +533 -0
  50. package/scripts/social-analytics/db/marketing-db.js +179 -0
  51. package/scripts/social-analytics/db/schema.sql +23 -0
  52. package/scripts/social-analytics/generate-instagram-card.js +31 -5
  53. package/scripts/social-analytics/generate-slides.js +268 -0
  54. package/scripts/social-analytics/post-video.js +316 -0
  55. package/scripts/social-analytics/publishers/zernio.js +52 -23
  56. package/scripts/statusline-local-stats.js +3 -1
  57. package/scripts/statusline.sh +15 -10
  58. package/scripts/thumbgate-bench.js +494 -0
  59. package/src/api/server.js +65 -1
  60. package/scripts/social-analytics/db/analytics.sqlite +0 -0
@@ -21,10 +21,14 @@
21
21
 
22
22
  const fs = require('fs');
23
23
  const path = require('path');
24
+ const {
25
+ PerplexityClient,
26
+ extractChatText,
27
+ extractCitations,
28
+ } = require('./perplexity-client');
24
29
 
25
- const API_KEY = process.env.PERPLEXITY_API_KEY;
26
30
  const OUTPUT_DIR = path.join(__dirname, '..', '.amp', 'in', 'artifacts', 'marketing');
27
- const SONAR_URL = 'https://api.perplexity.ai/chat/completions';
31
+ const perplexity = new PerplexityClient();
28
32
 
29
33
  const PRODUCT = {
30
34
  name: 'ThumbGate',
@@ -39,31 +43,15 @@ const PRODUCT = {
39
43
  };
40
44
 
41
45
  async function sonarRequest(model, messages, options = {}) {
42
- if (!API_KEY) {
46
+ if (!perplexity.hasApiKey()) {
43
47
  throw new Error('PERPLEXITY_API_KEY not set. Get yours at https://www.perplexity.ai/settings/api');
44
48
  }
45
49
 
46
- const body = {
50
+ return perplexity.chatCompletion({
47
51
  model,
48
52
  messages,
49
- ...options,
50
- };
51
-
52
- const resp = await fetch(SONAR_URL, {
53
- method: 'POST',
54
- headers: {
55
- 'Authorization': `Bearer ${API_KEY}`,
56
- 'Content-Type': 'application/json',
57
- },
58
- body: JSON.stringify(body),
53
+ options,
59
54
  });
60
-
61
- if (!resp.ok) {
62
- const text = await resp.text();
63
- throw new Error(`Perplexity API ${resp.status}: ${text}`);
64
- }
65
-
66
- return resp.json();
67
55
  }
68
56
 
69
57
  function ensureOutputDir() {
@@ -107,8 +95,8 @@ Be specific with URLs, community names, and actionable steps.`,
107
95
  },
108
96
  ]);
109
97
 
110
- const content = result.choices[0].message.content;
111
- const citations = result.citations || [];
98
+ const content = extractChatText(result);
99
+ const citations = extractCitations(result);
112
100
 
113
101
  let output = `# Market Research Report — ThumbGate\n\nGenerated: ${new Date().toISOString()}\n\n`;
114
102
  output += content;
@@ -149,8 +137,8 @@ async function findThreads() {
149
137
  web_search_options: { search_context_size: 'high' },
150
138
  });
151
139
 
152
- const content = result.choices[0].message.content;
153
- const citations = result.citations || [];
140
+ const content = extractChatText(result);
141
+ const citations = extractCitations(result);
154
142
  results.push({ query: q, content, citations });
155
143
  } catch (err) {
156
144
  console.log(` ⚠ Query failed: ${q} — ${err.message}`);
@@ -314,7 +302,7 @@ Requirements:
314
302
  web_search_options: { search_context_size: 'low' },
315
303
  });
316
304
 
317
- const content = result.choices[0].message.content;
305
+ const content = extractChatText(result);
318
306
  saveOutput(platform.file, `# ${platform.name.toUpperCase()} — Launch Post\n\nGenerated: ${new Date().toISOString()}\n\n${content}`);
319
307
  posts.push({ platform: platform.name, content });
320
308
  } catch (err) {
@@ -358,7 +346,7 @@ Make all content factually accurate and technically specific.`,
358
346
  web_search_options: { search_context_size: 'medium' },
359
347
  });
360
348
 
361
- const content = result.choices[0].message.content;
349
+ const content = extractChatText(result);
362
350
  saveOutput('04-seo-geo-content.md', `# SEO/GEO Optimization Content\n\nGenerated: ${new Date().toISOString()}\n\n${content}`);
363
351
  console.log(' ✓ SEO content generated\n');
364
352
  return content;
@@ -389,7 +377,7 @@ Include [PERSONALIZATION] placeholders.`,
389
377
  },
390
378
  ]);
391
379
 
392
- const content = result.choices[0].message.content;
380
+ const content = extractChatText(result);
393
381
  saveOutput('05-outreach-templates.md', `# Outreach Templates\n\nGenerated: ${new Date().toISOString()}\n\n${content}`);
394
382
  console.log(' ✓ Outreach templates generated\n');
395
383
  return content;
@@ -405,7 +393,7 @@ async function main() {
405
393
  console.log('║ ThumbGate — First Dollar Campaign ║');
406
394
  console.log('╚══════════════════════════════════════════════════════╝');
407
395
 
408
- if (!API_KEY) {
396
+ if (!perplexity.hasApiKey()) {
409
397
  console.error('\n❌ PERPLEXITY_API_KEY not set.');
410
398
  console.error(' Add it to .env: PERPLEXITY_API_KEY=pplx-...');
411
399
  console.error(' Get your key: https://www.perplexity.ai/settings/api\n');
@@ -71,14 +71,14 @@ function isTransientRegistryMiss(error) {
71
71
  ]
72
72
  .filter(Boolean)
73
73
  .join('\n');
74
- return /ETARGET|No matching version found|npm error code E404|404 Not Found/i.test(text);
74
+ return /ETARGET|No matching version found|npm error code E404|404 Not Found|ETIMEDOUT|ENETUNREACH|ECONNRESET|EAI_AGAIN|ECONNREFUSED|network timeout/i.test(text);
75
75
  }
76
76
 
77
77
  async function installPackageWithRetry(prefixDir, packageSpec, options = {}) {
78
78
  const installImpl = options.installImpl || installPackage;
79
79
  const sleepImpl = options.sleepImpl || sleep;
80
80
  const remotePackage = options.remotePackage !== undefined ? options.remotePackage : isRemotePackageSpec(packageSpec);
81
- const attempts = remotePackage ? Number(options.attempts || DEFAULT_PUBLISH_INSTALL_RETRIES) : 1;
81
+ const attempts = Number(options.attempts || (remotePackage ? DEFAULT_PUBLISH_INSTALL_RETRIES : 3));
82
82
  let delayMs = Number(options.delayMs || DEFAULT_PUBLISH_INSTALL_DELAY_MS);
83
83
  let lastError = null;
84
84
 
@@ -90,12 +90,13 @@ async function installPackageWithRetry(prefixDir, packageSpec, options = {}) {
90
90
  return installImpl(prefixDir, packageSpec);
91
91
  } catch (error) {
92
92
  lastError = error;
93
- const retryable = remotePackage && isTransientRegistryMiss(error) && attempt < attempts;
93
+ const transient = isTransientRegistryMiss(error);
94
+ const retryable = transient && attempt < attempts;
94
95
  if (!retryable) {
95
96
  throw error;
96
97
  }
97
98
  process.stderr.write(
98
- `Retrying published package install for ${packageSpec} after transient registry miss (${attempt}/${attempts - 1})\n`
99
+ `Retrying package install for ${packageSpec} after transient npm failure (${attempt}/${attempts - 1})\n`
99
100
  );
100
101
  await sleepImpl(delayMs);
101
102
  delayMs = Math.min(Math.round(delayMs * 1.5), MAX_PUBLISH_INSTALL_DELAY_MS);
@@ -10,12 +10,12 @@
10
10
  const crypto = require('crypto');
11
11
  const https = require('https');
12
12
 
13
- // ── Env ─────────────────────────────────────────────────────────────────
14
- const X_API_KEY = process.env.X_API_KEY;
15
- const X_API_SECRET = process.env.X_API_SECRET;
16
- const X_ACCESS_TOKEN = process.env.X_ACCESS_TOKEN;
17
- const X_ACCESS_TOKEN_SECRET = process.env.X_ACCESS_TOKEN_SECRET;
18
- const X_BEARER_TOKEN = process.env.X_BEARER_TOKEN;
13
+ // ── Env (trim to handle GitHub Actions trailing whitespace) ─────────────
14
+ const X_API_KEY = (process.env.X_API_KEY || '').trim();
15
+ const X_API_SECRET = (process.env.X_API_SECRET || '').trim();
16
+ const X_ACCESS_TOKEN = (process.env.X_ACCESS_TOKEN || '').trim();
17
+ const X_ACCESS_TOKEN_SECRET = (process.env.X_ACCESS_TOKEN_SECRET || '').trim();
18
+ const X_BEARER_TOKEN = (process.env.X_BEARER_TOKEN || '').trim();
19
19
  const LINKEDIN_ACCESS_TOKEN = process.env.LINKEDIN_ACCESS_TOKEN;
20
20
  const LINKEDIN_PERSON_URN = process.env.LINKEDIN_PERSON_URN;
21
21
  const DEVTO_API_KEY = process.env.DEVTO_API_KEY;
@@ -44,13 +44,21 @@ function xAuthHeader(method, url) {
44
44
  // ── Helpers ─────────────────────────────────────────────────────────────
45
45
  async function postTweet(text) {
46
46
  const url = 'https://api.twitter.com/2/tweets';
47
+ console.log(' X auth debug: credentials ' + (X_API_KEY && X_ACCESS_TOKEN ? 'present' : 'missing'));
47
48
  const r = await fetch(url, {
48
49
  method: 'POST',
49
50
  headers: { Authorization: xAuthHeader('POST', url), 'Content-Type': 'application/json' },
50
51
  body: JSON.stringify({ text }),
51
52
  });
52
- const j = await r.json();
53
- return { id: j.data?.id, error: j.detail };
53
+ const j = await parseJsonResponse(r);
54
+ if (!j.data?.id) console.log(' X error detail:', JSON.stringify(j).slice(0, 200));
55
+ const id = j.data?.id || '';
56
+ return {
57
+ id,
58
+ ok: r.ok && Boolean(id),
59
+ status: r.status,
60
+ error: id ? '' : extractApiError(j, r.status),
61
+ };
54
62
  }
55
63
 
56
64
  async function replyTweet(text, replyTo) {
@@ -60,8 +68,50 @@ async function replyTweet(text, replyTo) {
60
68
  headers: { Authorization: xAuthHeader('POST', url), 'Content-Type': 'application/json' },
61
69
  body: JSON.stringify({ text, reply: { in_reply_to_tweet_id: replyTo } }),
62
70
  });
63
- const j = await r.json();
64
- return { id: j.data?.id, error: j.detail };
71
+ const j = await parseJsonResponse(r);
72
+ const id = j.data?.id || '';
73
+ return {
74
+ id,
75
+ ok: r.ok && Boolean(id),
76
+ status: r.status,
77
+ error: id ? '' : extractApiError(j, r.status),
78
+ };
79
+ }
80
+
81
+ async function parseJsonResponse(response) {
82
+ try {
83
+ return await response.json();
84
+ } catch {
85
+ return {};
86
+ }
87
+ }
88
+
89
+ function extractApiError(payload = {}, status = 0) {
90
+ const firstError = Array.isArray(payload.errors) && payload.errors[0]
91
+ ? payload.errors[0].message || payload.errors[0].detail || payload.errors[0].title
92
+ : '';
93
+ return payload.detail || payload.title || firstError || `HTTP ${status || 'unknown'}`;
94
+ }
95
+
96
+ function recordTweetPost(report, result, log = console.log) {
97
+ if (result && result.ok && result.id) {
98
+ log('Tweet posted: ' + result.id);
99
+ report.tweets++;
100
+ return true;
101
+ }
102
+ log('Tweet skipped: ' + (result?.error || `HTTP ${result?.status || 'unknown'}`));
103
+ return false;
104
+ }
105
+
106
+ function recordTweetReply(report, username, result, log = console.log) {
107
+ const handle = username ? '@' + username : 'unknown user';
108
+ if (result && result.ok && result.id) {
109
+ log(' Replied to ' + handle + ': ' + result.id);
110
+ report.replies++;
111
+ return true;
112
+ }
113
+ log(' Reply skipped for ' + handle + ': ' + (result?.error || `HTTP ${result?.status || 'unknown'}`));
114
+ return false;
65
115
  }
66
116
 
67
117
  async function postLinkedIn(text) {
@@ -128,7 +178,7 @@ const TWEET_ANGLES = [
128
178
  'Thompson Sampling for AI agent gates:\n\nEach gate: Beta(alpha, beta)\nCorrect block → alpha++ → tighter\nFalse positive → beta++ → relaxes\n\nNo thresholds. Gates converge on their own.\n\nhttps://github.com/IgorGanapolsky/ThumbGate',
129
179
  'Google DeepMind: hidden prompt injections commandeer AI agents 86% of the time.\n\nThumbGate gates the action, not the prompt. PreToolUse hooks are the last defense.\n\nhttps://github.com/IgorGanapolsky/ThumbGate',
130
180
  'Every AI agent framework ships memory. None ship enforcement.\n\nMemory: "Don\'t force-push to main"\nEnforcement: *physically blocked*\n\nThumbGate is the enforcement layer.\n\nhttps://github.com/IgorGanapolsky/ThumbGate',
131
- 'Founding Member: $49 once. ThumbGate Pro forever.\n\n50 spots. No subscription.\n\nSelf-distillation, SQL MCP gates, Thompson Sampling, context-stuffing, 68 tools on Smithery.\n\nhttps://buy.stripe.com/aFa4gz1M84r419v7mb3sI05',
181
+ 'ThumbGate Pro is $19/mo or $149/yr for solo AI agent operators.\n\nLocal dashboard, DPO export, self-distillation, SQL MCP gates, Thompson Sampling, and pre-action enforcement.\n\nTeam rollout starts with intake: $99/seat/mo.\n\nhttps://buy.stripe.com/5kQ4gzbmI9Lo6tPayn3sI06',
132
182
  'Context-stuffing: skip RAG entirely.\n\nDump ALL prevention rules into agent context at session start. 20-200 rules = 1K-10K tokens.\n\nInspired by Karpathy. Simpler. Faster.\n\nhttps://github.com/IgorGanapolsky/ThumbGate',
133
183
  'The AI agent safety stack:\n\nGovernance: Paperclip\nOrchestration: iloom\nContext: RepoWise\nEnforcement: ThumbGate\n\nAll open source. All necessary.\n\nhttps://github.com/IgorGanapolsky/ThumbGate',
134
184
  ];
@@ -170,8 +220,7 @@ async function main() {
170
220
  const u = mu[t.author_id] || {};
171
221
  const replyText = '@' + u.username + ' ThumbGate: PreToolUse enforcement for AI agents. Thompson Sampling adapts confidence. 68 tools on Smithery.\n\nhttps://github.com/IgorGanapolsky/ThumbGate';
172
222
  const r = await replyTweet(replyText, t.id);
173
- console.log(' Replied to @' + u.username + ': ' + (r.id || r.error));
174
- report.replies++;
223
+ recordTweetReply(report, u.username, r);
175
224
  }
176
225
 
177
226
  state.lastMentionCheck = new Date().toISOString();
@@ -183,8 +232,7 @@ async function main() {
183
232
  try {
184
233
  const angleIndex = Math.floor(Date.now() / 7200000) % TWEET_ANGLES.length;
185
234
  const r = await postTweet(TWEET_ANGLES[angleIndex]);
186
- console.log('Tweet posted: ' + (r.id || r.error));
187
- report.tweets++;
235
+ recordTweetPost(report, r);
188
236
  } catch (e) {
189
237
  console.log('Tweet error: ' + e.message);
190
238
  }
@@ -311,6 +359,9 @@ async function main() {
311
359
  console.log('GitHub: skipped (no token)');
312
360
  }
313
361
 
362
+ // ── 8. Perplexity visibility ──
363
+ await checkPerplexityVisibility();
364
+
314
365
  // ── Save state ──
315
366
  state.lastRun = new Date().toISOString();
316
367
  saveState(state);
@@ -325,7 +376,59 @@ async function main() {
325
376
  console.log('=== DONE ===');
326
377
  }
327
378
 
328
- main().catch(e => {
329
- console.error('Ralph Mode CI fatal error:', e.message);
330
- process.exit(1);
331
- });
379
+ function isDirectInvocation(mainModule = require.main) {
380
+ return Boolean(mainModule && mainModule.filename === __filename);
381
+ }
382
+
383
+ if (isDirectInvocation()) {
384
+ main().catch(e => {
385
+ console.error('Ralph Mode CI fatal error:', e.message);
386
+ process.exit(1);
387
+ });
388
+ }
389
+
390
+ // ── Perplexity AI Search Visibility (appended) ──────────────────────────
391
+ async function checkPerplexityVisibility() {
392
+ const PPLX_KEY = (process.env.PERPLEXITY_API_KEY || '').trim();
393
+ if (!PPLX_KEY) { console.log('Perplexity: skipped (no key)'); return; }
394
+
395
+ const prompts = [
396
+ 'best pre-action gate tools for AI coding agents',
397
+ 'how to prevent AI coding agent from making mistakes',
398
+ 'Claude Code safety tools',
399
+ 'alternatives to thumbgate',
400
+ ];
401
+
402
+ console.log('Perplexity visibility check (' + prompts.length + ' queries):');
403
+ let found = 0;
404
+ for (const prompt of prompts) {
405
+ try {
406
+ const r = await fetch('https://api.perplexity.ai/chat/completions', {
407
+ method: 'POST',
408
+ headers: { Authorization: 'Bearer ' + PPLX_KEY, 'Content-Type': 'application/json' },
409
+ body: JSON.stringify({ model: 'sonar', messages: [{ role: 'user', content: prompt }] }),
410
+ });
411
+ const j = await r.json();
412
+ const text = j.choices?.[0]?.message?.content || '';
413
+ const mentioned = /thumbgate/i.test(text);
414
+ console.log(' "' + prompt.slice(0, 50) + '": ' + (mentioned ? 'FOUND' : 'MISSING'));
415
+ if (mentioned) found++;
416
+ } catch (e) {
417
+ console.log(' "' + prompt.slice(0, 50) + '": ERROR ' + e.message.slice(0, 30));
418
+ }
419
+ }
420
+ console.log('Perplexity: ThumbGate mentioned in ' + found + '/' + prompts.length + ' queries');
421
+ }
422
+
423
+ module.exports = {
424
+ TWEET_ANGLES,
425
+ extractApiError,
426
+ main,
427
+ parseJsonResponse,
428
+ postTweet,
429
+ isDirectInvocation,
430
+ recordTweetPost,
431
+ recordTweetReply,
432
+ replyTweet,
433
+ xAuthHeader,
434
+ };
@@ -12,7 +12,7 @@
12
12
 
13
13
  'use strict';
14
14
 
15
- const { retrieveRelevantLessons } = require('./lesson-retrieval');
15
+ const { retrieveWithRerankingSync } = require('./cross-encoder-reranker');
16
16
  const {
17
17
  extractFilePaths,
18
18
  extractToolCalls,
@@ -99,7 +99,7 @@ function checkRecurrence(analysis, feedbackEvent) {
99
99
  try {
100
100
  const context = `${analysis.userIntent} ${analysis.assistantAction} ${analysis.corrections.join(' ')}`;
101
101
  const toolName = analysis.toolsUsed[0] || 'unknown';
102
- previousLessons = retrieveRelevantLessons(toolName, context, { maxResults: 5 });
102
+ previousLessons = retrieveWithRerankingSync(toolName, context, { candidateCount: 20, maxResults: 5 });
103
103
  // Filter to only negative lessons
104
104
  previousLessons = previousLessons.filter(l => l.signal === 'negative');
105
105
  } catch (_err) {