thumbgate 0.9.13 → 1.0.0

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 (70) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +6 -3
  5. package/adapters/README.md +1 -1
  6. package/adapters/chatgpt/openapi.yaml +105 -0
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +2 -2
  9. package/adapters/forge/forge.yaml +28 -0
  10. package/adapters/mcp/server-stdio.js +32 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +53 -3
  13. package/config/mcp-allowlists.json +10 -0
  14. package/openapi/openapi.yaml +105 -0
  15. package/package.json +4 -4
  16. package/plugins/amp-skill/INSTALL.md +3 -4
  17. package/plugins/amp-skill/SKILL.md +0 -1
  18. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  19. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  20. package/plugins/claude-skill/INSTALL.md +1 -2
  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/blog.html +1 -0
  28. package/public/dashboard.html +1 -1
  29. package/public/guide.html +1 -1
  30. package/public/index.html +29 -5
  31. package/public/learn/agent-harness-pattern.html +1 -1
  32. package/public/learn/ai-agent-persistent-memory.html +1 -1
  33. package/public/learn/mcp-pre-action-gates-explained.html +1 -1
  34. package/public/learn/stop-ai-agent-force-push.html +1 -1
  35. package/public/learn/vibe-coding-safety-net.html +1 -1
  36. package/public/learn.html +62 -1
  37. package/public/lessons.html +1 -1
  38. package/public/pro.html +1 -1
  39. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/async-job-runner.js +84 -24
  42. package/scripts/auto-wire-hooks.js +59 -1
  43. package/scripts/context-manager.js +330 -0
  44. package/scripts/dashboard.js +1 -1
  45. package/scripts/distribution-surfaces.js +12 -0
  46. package/scripts/ensure-repo-bootstrap.js +15 -14
  47. package/scripts/feedback-history-distiller.js +7 -1
  48. package/scripts/feedback-loop.js +10 -4
  49. package/scripts/feedback-paths.js +142 -10
  50. package/scripts/feedback-root-consolidator.js +18 -4
  51. package/scripts/gates-engine.js +96 -10
  52. package/scripts/hook-auto-capture.sh +1 -1
  53. package/scripts/hosted-job-launcher.js +260 -0
  54. package/scripts/managed-dpo-export.js +91 -0
  55. package/scripts/obsidian-export.js +0 -1
  56. package/scripts/operational-integrity.js +50 -7
  57. package/scripts/post-everywhere.js +10 -0
  58. package/scripts/prove-lancedb.js +62 -4
  59. package/scripts/publish-decision.js +16 -0
  60. package/scripts/self-healing-check.js +6 -1
  61. package/scripts/seo-gsd.js +217 -4
  62. package/scripts/social-analytics/load-env.js +33 -2
  63. package/scripts/social-analytics/store.js +200 -2
  64. package/scripts/statusline-cache-path.js +9 -6
  65. package/scripts/sync-version.js +18 -11
  66. package/scripts/tool-registry.js +37 -0
  67. package/scripts/train_from_feedback.py +0 -4
  68. package/scripts/workflow-sentinel.js +793 -0
  69. package/src/api/server.js +297 -38
  70. /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
@@ -67,12 +67,30 @@ const HIGH_ROI_QUERY_SEEDS = [
67
67
  source: 'seed',
68
68
  notes: 'Problem-led copy that maps to landing-page positioning.',
69
69
  },
70
+ {
71
+ query: 'cursor prevent repeated mistakes',
72
+ businessValue: 87,
73
+ source: 'seed',
74
+ notes: 'High-intent Cursor workflow page for developers already feeling repeat-failure pain.',
75
+ },
70
76
  {
71
77
  query: 'claude code prevent repeated mistakes',
72
78
  businessValue: 86,
73
79
  source: 'seed',
74
80
  notes: 'High-intent pain query for Claude Code buyers.',
75
81
  },
82
+ {
83
+ query: 'codex cli guardrails',
84
+ businessValue: 84,
85
+ source: 'seed',
86
+ notes: 'Guardrail-focused page for Codex CLI buyers who want prevention, not just memory.',
87
+ },
88
+ {
89
+ query: 'gemini cli feedback memory',
90
+ businessValue: 82,
91
+ source: 'seed',
92
+ notes: 'Integration page for Gemini CLI users who need memory plus enforcement.',
93
+ },
76
94
  ];
77
95
 
78
96
  const PAGE_BLUEPRINTS = [
@@ -225,6 +243,55 @@ const PAGE_BLUEPRINTS = [
225
243
  ],
226
244
  relatedPaths: ['/compare/speclock', '/guides/claude-code-feedback'],
227
245
  },
246
+ {
247
+ query: 'stop ai coding agents from repeating mistakes',
248
+ path: '/guides/stop-repeated-ai-agent-mistakes',
249
+ pageType: 'guide',
250
+ pillar: 'pre-action-gates',
251
+ title: 'How to Stop AI Coding Agents From Repeating Mistakes | ThumbGate',
252
+ heroTitle: 'How to Stop AI Coding Agents From Repeating Mistakes',
253
+ heroSummary: 'If your agent keeps repeating the same bad move, the fix is not more memory alone. The fix is a feedback loop that turns repeated failures into pre-action gates before the next tool call executes.',
254
+ takeaways: [
255
+ 'Repeated mistakes are a workflow problem, not just a context-window problem.',
256
+ 'ThumbGate turns thumbs-down feedback into prevention rules and runtime gates.',
257
+ 'This page is meant to move problem-aware buyers into the Pro path or a concrete install.',
258
+ ],
259
+ sections: [
260
+ {
261
+ heading: 'Why repeated mistakes keep happening',
262
+ paragraphs: [
263
+ 'AI coding agents are fast, but they forget operational pain surprisingly easily. One bad deployment, force-push, or skipped verification step often turns into another because the system remembered the transcript but never enforced the lesson.',
264
+ 'That is why teams feel stuck in a correction loop. They keep teaching the same rule, but the next session still allows the same risky action.',
265
+ ],
266
+ },
267
+ {
268
+ heading: 'What changes when feedback becomes enforcement',
269
+ bullets: [
270
+ 'Thumbs down captures the exact failure you do not want repeated.',
271
+ 'Repeated failures promote into linked prevention rules.',
272
+ 'Pre-action gates intercept the risky tool call before execution.',
273
+ 'Thumbs up reinforces the safe path so the agent learns what good looks like too.',
274
+ ],
275
+ },
276
+ {
277
+ heading: 'What a buyer should do next',
278
+ paragraphs: [
279
+ 'If the pain is already real, do not start with a long architecture project. Start by wiring ThumbGate into the workflow where the agent has already burned time or trust, then watch the next repeat attempt get blocked before damage lands.',
280
+ ],
281
+ },
282
+ ],
283
+ faq: [
284
+ {
285
+ question: 'Is memory alone enough to stop repeated mistakes?',
286
+ answer: 'Usually no. Memory helps retrieval, but ThumbGate adds pre-action gates so the same risky move can be blocked before the next command executes.',
287
+ },
288
+ {
289
+ question: 'Does ThumbGate only punish bad behavior?',
290
+ answer: 'No. Thumbs up reinforces good behavior, so the loop captures safe patterns as well as failures.',
291
+ },
292
+ ],
293
+ relatedPaths: ['/guides/pre-action-gates', '/guides/claude-code-feedback'],
294
+ },
228
295
  {
229
296
  query: 'claude code feedback memory',
230
297
  path: '/guides/claude-code-feedback',
@@ -273,6 +340,150 @@ const PAGE_BLUEPRINTS = [
273
340
  ],
274
341
  relatedPaths: ['/guides/pre-action-gates', '/compare/mem0'],
275
342
  },
343
+ {
344
+ query: 'cursor prevent repeated mistakes',
345
+ path: '/guides/cursor-agent-guardrails',
346
+ pageType: 'integration',
347
+ pillar: 'agent-workflows',
348
+ title: 'Cursor Agent Guardrails | Stop Repeated Mistakes with ThumbGate',
349
+ heroTitle: 'Cursor Guardrails That Block Repeated Mistakes',
350
+ heroSummary: 'Cursor moves fast, which makes repeated mistakes expensive. ThumbGate gives Cursor users a feedback loop that turns thumbs-down corrections into pre-action gates before the next risky step fires.',
351
+ takeaways: [
352
+ 'Cursor users want speed without trusting the agent blindly.',
353
+ 'ThumbGate adds enforcement without forcing a platform switch.',
354
+ 'The page should answer the buyer question in one line: how do I stop Cursor from doing the same bad thing again?',
355
+ ],
356
+ sections: [
357
+ {
358
+ heading: 'The Cursor workflow problem',
359
+ paragraphs: [
360
+ 'Cursor can move from idea to edits quickly, but the failure mode is familiar: the same wrong refactor, risky shell command, or skipped check comes back in the next session because nothing hardened the workflow.',
361
+ ],
362
+ },
363
+ {
364
+ heading: 'How ThumbGate fits into Cursor',
365
+ bullets: [
366
+ 'Capture thumbs-up/down feedback on agent behavior.',
367
+ 'Promote repeated failures into prevention rules.',
368
+ 'Block known-bad commands with pre-action gates before execution.',
369
+ 'Keep the memory and gates local-first so the operator retains control.',
370
+ ],
371
+ },
372
+ {
373
+ heading: 'What makes this different from a rule file',
374
+ paragraphs: [
375
+ 'Static rules help on day one. ThumbGate helps on day two and day twenty because it keeps learning from live corrections instead of relying on a fixed checklist that drifts out of date.',
376
+ ],
377
+ },
378
+ ],
379
+ faq: [
380
+ {
381
+ question: 'Do I need to leave Cursor to use ThumbGate?',
382
+ answer: 'No. ThumbGate is designed to sit alongside existing coding-agent workflows so you can add enforcement without switching tools.',
383
+ },
384
+ {
385
+ question: 'What kind of mistakes can Cursor guardrails stop?',
386
+ answer: 'Repeated failures like risky git actions, destructive scripts, skipped verification, or any other known-bad pattern you have already corrected once.',
387
+ },
388
+ ],
389
+ relatedPaths: ['/guides/stop-repeated-ai-agent-mistakes', '/guides/pre-action-gates'],
390
+ },
391
+ {
392
+ query: 'codex cli guardrails',
393
+ path: '/guides/codex-cli-guardrails',
394
+ pageType: 'integration',
395
+ pillar: 'agent-workflows',
396
+ title: 'Codex CLI Guardrails | Prevent Repeated Mistakes with ThumbGate',
397
+ heroTitle: 'Codex CLI Guardrails That Actually Enforce',
398
+ heroSummary: 'Codex CLI can move quickly through repo tasks, but buyers need more than good intentions. ThumbGate adds a reliability gateway so repeated mistakes become searchable lessons, linked rules, and pre-action enforcement.',
399
+ takeaways: [
400
+ 'Codex CLI buyers are usually looking for safe autonomy, not just more prompts.',
401
+ 'ThumbGate sits in the critical gap between feedback and execution.',
402
+ 'This page should rank for people who want guardrails without giving up CLI speed.',
403
+ ],
404
+ sections: [
405
+ {
406
+ heading: 'What Codex CLI users usually need',
407
+ paragraphs: [
408
+ 'The problem is rarely a single bad command. It is the cost of the same failure pattern showing up across branches, sessions, or rushed workflows. Once that pattern is obvious, the buyer wants a durable control point.',
409
+ ],
410
+ },
411
+ {
412
+ heading: 'What ThumbGate adds',
413
+ bullets: [
414
+ 'Feedback capture with explicit thumbs-up/down signals.',
415
+ 'Searchable lessons and linked prevention rules.',
416
+ 'Pre-action gates that block repeated bad commands before they run.',
417
+ 'Verification evidence that gives teams something concrete to audit.',
418
+ ],
419
+ },
420
+ {
421
+ heading: 'Why this matters for revenue',
422
+ paragraphs: [
423
+ 'Guardrails are easier to buy when the outcome is obvious: less rework, fewer repeated failures, and a visible chain from operator feedback to enforced behavior.',
424
+ ],
425
+ },
426
+ ],
427
+ faq: [
428
+ {
429
+ question: 'Is ThumbGate only for Codex CLI?',
430
+ answer: 'No. Codex CLI is one supported workflow, but the same feedback and enforcement loop also works across Claude Code, Cursor, Gemini, Amp, and OpenCode.',
431
+ },
432
+ {
433
+ question: 'How are Codex CLI guardrails different from prompt instructions?',
434
+ answer: 'Prompt instructions are advisory. ThumbGate pre-action gates intercept the tool call itself and block the known-bad pattern before execution.',
435
+ },
436
+ ],
437
+ relatedPaths: ['/guides/pre-action-gates', '/compare/mem0'],
438
+ },
439
+ {
440
+ query: 'gemini cli feedback memory',
441
+ path: '/guides/gemini-cli-feedback-memory',
442
+ pageType: 'integration',
443
+ pillar: 'agent-workflows',
444
+ title: 'Gemini CLI Feedback Memory | Memory Plus Enforcement with ThumbGate',
445
+ heroTitle: 'Gemini CLI Feedback Memory That Leads to Enforcement',
446
+ heroSummary: 'Gemini CLI users often start by asking for better memory. ThumbGate answers the bigger need: memory that can become prevention rules and pre-action gates when the same mistake shows up twice.',
447
+ takeaways: [
448
+ 'Gemini CLI searchers often begin with memory but buy because of enforcement.',
449
+ 'ThumbGate keeps the local-first memory story while adding runtime blocking.',
450
+ 'The ideal conversion path here is memory query to product proof to Pro page.',
451
+ ],
452
+ sections: [
453
+ {
454
+ heading: 'Why memory is only step one',
455
+ paragraphs: [
456
+ 'Persistent memory helps Gemini CLI recall past context, but it still leaves a blind spot. Remembering that a workflow went badly is different from preventing the next risky action when the same pattern appears again.',
457
+ ],
458
+ },
459
+ {
460
+ heading: 'What ThumbGate adds on top',
461
+ bullets: [
462
+ 'Local-first lessons you can search across sessions.',
463
+ 'Structured thumbs-up/down feedback for reinforcement and correction.',
464
+ 'Prevention rules linked to past failures.',
465
+ 'Pre-action gates that stop repeated mistakes before execution.',
466
+ ],
467
+ },
468
+ {
469
+ heading: 'Who this is really for',
470
+ paragraphs: [
471
+ 'This page is for operators who already know memory matters, but now need a reliability layer that protects live workflows instead of just preserving notes about them.',
472
+ ],
473
+ },
474
+ ],
475
+ faq: [
476
+ {
477
+ question: 'Does ThumbGate replace Gemini CLI memory?',
478
+ answer: 'No. ThumbGate extends the memory story with searchable lessons, rules, and gates so memory becomes operationally useful instead of purely historical.',
479
+ },
480
+ {
481
+ question: 'Can this stay local-first?',
482
+ answer: 'Yes. ThumbGate is built for local-first workflows, which lowers risk for developers who do not want sensitive history pushed into a hosted memory layer.',
483
+ },
484
+ ],
485
+ relatedPaths: ['/compare/mem0', '/guides/stop-repeated-ai-agent-mistakes'],
486
+ },
276
487
  {
277
488
  query: 'claude desktop extension plugin thumbgate',
278
489
  path: '/guides/claude-desktop',
@@ -444,7 +655,7 @@ function classifyIntent(query) {
444
655
  return 'commercial';
445
656
  }
446
657
  if (/\b(what is|how to|guide|best practices|why)\b/.test(normalized)) return 'informational';
447
- if (/\b(guardrails|pre-action gates|feedback|prevent repeated mistakes|memory)\b/.test(normalized)) {
658
+ if (/\b(guardrails|pre-action gates|feedback|prevent repeated mistakes|repeating mistakes|memory)\b/.test(normalized)) {
448
659
  return 'commercial';
449
660
  }
450
661
  return 'informational';
@@ -454,7 +665,7 @@ function inferPillar(query) {
454
665
  const normalized = normalizeText(query).toLowerCase();
455
666
  if (/\b(speclock|mem0|alternative|vs|compare|comparison)\b/.test(normalized)) return 'comparison';
456
667
  if (/\b(thumbs up|thumbs down|feedback|reinforce|mistake)\b/.test(normalized)) return 'feedback-loop';
457
- if (/\b(pre-action gates|guardrails|block|prevent repeated mistakes)\b/.test(normalized)) return 'pre-action-gates';
668
+ if (/\b(pre-action gates|guardrails|block|prevent repeated mistakes|repeating mistakes)\b/.test(normalized)) return 'pre-action-gates';
458
669
  if (/\b(claude code|cursor|codex|gemini|amp|opencode|integration|plugin)\b/.test(normalized)) return 'agent-workflows';
459
670
  return 'ai-agent-reliability';
460
671
  }
@@ -463,6 +674,8 @@ function inferPersona(query) {
463
674
  const normalized = normalizeText(query).toLowerCase();
464
675
  if (normalized.includes('claude code')) return 'claude-code-builder';
465
676
  if (normalized.includes('cursor')) return 'cursor-builder';
677
+ if (normalized.includes('codex')) return 'codex-builder';
678
+ if (normalized.includes('gemini')) return 'gemini-builder';
466
679
  if (/\b(vs|alternative|compare)\b/.test(normalized)) return 'tool-evaluator';
467
680
  if (/\b(guardrails|pre-action gates)\b/.test(normalized)) return 'engineering-lead';
468
681
  return 'ai-engineer';
@@ -628,8 +841,8 @@ function createPageSpec(blueprint, row) {
628
841
  faq: blueprint.faq,
629
842
  relatedPages,
630
843
  cta: {
631
- label: 'Review verification evidence',
632
- href: PRODUCT.verificationUrl,
844
+ label: 'See ThumbGate Pro',
845
+ href: `/pro?utm_source=website&utm_medium=seo_page&utm_campaign=${blueprint.path.split('/').filter(Boolean).join('_')}`,
633
846
  },
634
847
  proofLinks: [
635
848
  { label: 'Verification evidence', href: PRODUCT.verificationUrl },
@@ -2,7 +2,12 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
- const dotenv = require('dotenv');
5
+ let dotenv = null;
6
+ try {
7
+ dotenv = require('dotenv');
8
+ } catch (_) {
9
+ dotenv = null;
10
+ }
6
11
 
7
12
  const DEFAULT_ENV_PATH = path.resolve(__dirname, '..', '..', '.env');
8
13
 
@@ -20,7 +25,8 @@ function loadLocalEnv(options = {}) {
20
25
  };
21
26
  }
22
27
 
23
- const parsed = dotenv.parse(fs.readFileSync(resolvedPath, 'utf8'));
28
+ const source = fs.readFileSync(resolvedPath, 'utf8');
29
+ const parsed = dotenv ? dotenv.parse(source) : parseEnvFallback(source);
24
30
  const loadedKeys = [];
25
31
  const override = options.override === true;
26
32
 
@@ -39,8 +45,33 @@ function loadLocalEnv(options = {}) {
39
45
  };
40
46
  }
41
47
 
48
+ function parseEnvFallback(source) {
49
+ const parsed = {};
50
+ for (const rawLine of String(source).split(/\r?\n/)) {
51
+ const line = rawLine.trim();
52
+ if (!line || line.startsWith('#')) {
53
+ continue;
54
+ }
55
+ const separatorIndex = line.indexOf('=');
56
+ if (separatorIndex <= 0) {
57
+ continue;
58
+ }
59
+ const key = line.slice(0, separatorIndex).trim();
60
+ let value = line.slice(separatorIndex + 1).trim();
61
+ if (
62
+ (value.startsWith('"') && value.endsWith('"')) ||
63
+ (value.startsWith("'") && value.endsWith("'"))
64
+ ) {
65
+ value = value.slice(1, -1);
66
+ }
67
+ parsed[key] = value;
68
+ }
69
+ return parsed;
70
+ }
71
+
42
72
  module.exports = {
43
73
  DEFAULT_ENV_PATH,
44
74
  loadLocalEnv,
75
+ parseEnvFallback,
45
76
  resolveEnvPath,
46
77
  };
@@ -2,11 +2,209 @@
2
2
 
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
- const Database = require('better-sqlite3');
5
+ let Database = null;
6
+ try {
7
+ Database = require('better-sqlite3');
8
+ } catch (_) {
9
+ Database = null;
10
+ }
6
11
 
7
12
  const DEFAULT_DB_PATH = path.join(__dirname, 'db', 'social-analytics.db');
8
13
  const SCHEMA_PATH = path.join(__dirname, 'db', 'schema.sql');
9
14
 
15
+ class MemoryStatement {
16
+ constructor(handler) {
17
+ this.handler = handler;
18
+ }
19
+
20
+ run(params) {
21
+ return this.handler.run(params);
22
+ }
23
+
24
+ all(params) {
25
+ return this.handler.all(params);
26
+ }
27
+ }
28
+
29
+ class MemoryDatabase {
30
+ constructor() {
31
+ this.tables = {
32
+ engagement_metrics: [],
33
+ follower_snapshots: [],
34
+ };
35
+ }
36
+
37
+ pragma() {}
38
+
39
+ exec() {}
40
+
41
+ close() {}
42
+
43
+ prepare(sql) {
44
+ const normalized = sql.replace(/\s+/g, ' ').trim();
45
+
46
+ if (normalized.includes('INSERT OR REPLACE INTO engagement_metrics')) {
47
+ return new MemoryStatement({
48
+ run: (params) => {
49
+ const index = this.tables.engagement_metrics.findIndex(
50
+ (row) =>
51
+ row.platform === params.platform &&
52
+ row.post_id === params.post_id &&
53
+ row.metric_date === params.metric_date
54
+ );
55
+ const row = { ...params };
56
+ if (index >= 0) {
57
+ this.tables.engagement_metrics[index] = row;
58
+ } else {
59
+ this.tables.engagement_metrics.push(row);
60
+ }
61
+ return { changes: 1 };
62
+ },
63
+ });
64
+ }
65
+
66
+ if (normalized.includes('INSERT OR REPLACE INTO follower_snapshots')) {
67
+ return new MemoryStatement({
68
+ run: (params) => {
69
+ const index = this.tables.follower_snapshots.findIndex(
70
+ (row) => row.platform === params.platform && row.snapshot_date === params.snapshot_date
71
+ );
72
+ const row = { ...params };
73
+ if (index >= 0) {
74
+ this.tables.follower_snapshots[index] = row;
75
+ } else {
76
+ this.tables.follower_snapshots.push(row);
77
+ }
78
+ return { changes: 1 };
79
+ },
80
+ });
81
+ }
82
+
83
+ if (normalized.includes('SELECT * FROM engagement_metrics WHERE platform = ?')) {
84
+ return new MemoryStatement({
85
+ all: (platform) =>
86
+ this.tables.engagement_metrics.filter((row) => row.platform === platform),
87
+ });
88
+ }
89
+
90
+ const literalPlatformMatch = normalized.match(
91
+ /^SELECT \* FROM engagement_metrics WHERE platform = '([^']+)'$/i
92
+ );
93
+ if (literalPlatformMatch) {
94
+ const [, platform] = literalPlatformMatch;
95
+ return new MemoryStatement({
96
+ all: () =>
97
+ this.tables.engagement_metrics.filter((row) => row.platform === platform),
98
+ });
99
+ }
100
+
101
+ if (normalized.includes('FROM engagement_metrics') && normalized.includes('GROUP BY platform')) {
102
+ return new MemoryStatement({
103
+ all: (params = {}) => {
104
+ const rows = this.tables.engagement_metrics.filter((row) => {
105
+ if (row.metric_date < params.cutoff) return false;
106
+ if (params.platform && row.platform !== params.platform) return false;
107
+ return true;
108
+ });
109
+ const grouped = new Map();
110
+ for (const row of rows) {
111
+ const bucket = grouped.get(row.platform) || {
112
+ platform: row.platform,
113
+ post_count: 0,
114
+ total_impressions: 0,
115
+ total_reach: 0,
116
+ total_likes: 0,
117
+ total_comments: 0,
118
+ total_shares: 0,
119
+ total_saves: 0,
120
+ total_clicks: 0,
121
+ total_video_views: 0,
122
+ _postIds: new Set(),
123
+ _rows: 0,
124
+ };
125
+ bucket._postIds.add(row.post_id);
126
+ bucket.total_impressions += row.impressions || 0;
127
+ bucket.total_reach += row.reach || 0;
128
+ bucket.total_likes += row.likes || 0;
129
+ bucket.total_comments += row.comments || 0;
130
+ bucket.total_shares += row.shares || 0;
131
+ bucket.total_saves += row.saves || 0;
132
+ bucket.total_clicks += row.clicks || 0;
133
+ bucket.total_video_views += row.video_views || 0;
134
+ bucket._rows += 1;
135
+ grouped.set(row.platform, bucket);
136
+ }
137
+ return [...grouped.values()]
138
+ .map((bucket) => ({
139
+ platform: bucket.platform,
140
+ post_count: bucket._postIds.size,
141
+ total_impressions: bucket.total_impressions,
142
+ total_reach: bucket.total_reach,
143
+ total_likes: bucket.total_likes,
144
+ total_comments: bucket.total_comments,
145
+ total_shares: bucket.total_shares,
146
+ total_saves: bucket.total_saves,
147
+ total_clicks: bucket.total_clicks,
148
+ total_video_views: bucket.total_video_views,
149
+ avg_impressions: Number((bucket.total_impressions / bucket._rows).toFixed(2)),
150
+ avg_likes: Number((bucket.total_likes / bucket._rows).toFixed(2)),
151
+ }))
152
+ .sort((a, b) => b.total_impressions - a.total_impressions);
153
+ },
154
+ });
155
+ }
156
+
157
+ if (normalized.includes('FROM engagement_metrics') && normalized.includes('GROUP BY platform, post_id')) {
158
+ return new MemoryStatement({
159
+ all: ({ cutoff, limit }) => {
160
+ const rows = this.tables.engagement_metrics.filter((row) => row.metric_date >= cutoff);
161
+ const grouped = new Map();
162
+ for (const row of rows) {
163
+ const key = `${row.platform}::${row.post_id}`;
164
+ const bucket = grouped.get(key) || {
165
+ platform: row.platform,
166
+ content_type: row.content_type,
167
+ post_id: row.post_id,
168
+ post_url: row.post_url ?? null,
169
+ published_at: row.published_at ?? null,
170
+ total_engagement: 0,
171
+ total_impressions: 0,
172
+ total_likes: 0,
173
+ total_comments: 0,
174
+ total_shares: 0,
175
+ total_saves: 0,
176
+ total_video_views: 0,
177
+ };
178
+ bucket.total_engagement +=
179
+ (row.likes || 0) + (row.comments || 0) + (row.shares || 0) + (row.saves || 0);
180
+ bucket.total_impressions += row.impressions || 0;
181
+ bucket.total_likes += row.likes || 0;
182
+ bucket.total_comments += row.comments || 0;
183
+ bucket.total_shares += row.shares || 0;
184
+ bucket.total_saves += row.saves || 0;
185
+ bucket.total_video_views += row.video_views || 0;
186
+ grouped.set(key, bucket);
187
+ }
188
+ return [...grouped.values()]
189
+ .sort((a, b) => b.total_engagement - a.total_engagement)
190
+ .slice(0, limit);
191
+ },
192
+ });
193
+ }
194
+
195
+ if (normalized.includes('FROM follower_snapshots')) {
196
+ return new MemoryStatement({
197
+ all: ({ platform, cutoff }) =>
198
+ this.tables.follower_snapshots
199
+ .filter((row) => row.platform === platform && row.snapshot_date >= cutoff)
200
+ .sort((a, b) => a.snapshot_date.localeCompare(b.snapshot_date)),
201
+ });
202
+ }
203
+
204
+ throw new Error(`MemoryDatabase does not support query: ${normalized}`);
205
+ }
206
+ }
207
+
10
208
  /**
11
209
  * Opens the SQLite database, applies the schema, and returns the db instance.
12
210
  * Idempotent — safe to call multiple times; schema uses IF NOT EXISTS guards.
@@ -23,7 +221,7 @@ function initDb(dbPath = DEFAULT_DB_PATH) {
23
221
  fs.mkdirSync(dir, { recursive: true });
24
222
  }
25
223
 
26
- const db = new Database(resolvedPath);
224
+ const db = Database ? new Database(resolvedPath) : new MemoryDatabase();
27
225
  db.pragma('busy_timeout = 3000');
28
226
 
29
227
  // Enable WAL mode for better concurrent read performance.
@@ -2,21 +2,24 @@
2
2
  'use strict';
3
3
 
4
4
  const path = require('path');
5
- const { resolveFeedbackDir } = require('./feedback-paths');
5
+ const {
6
+ listFeedbackArtifactPaths,
7
+ resolveFeedbackDir,
8
+ resolveProjectDir,
9
+ } = require('./feedback-paths');
6
10
 
7
11
  function unique(values = []) {
8
12
  return [...new Set(values.filter(Boolean).map((value) => path.resolve(value)))];
9
13
  }
10
14
 
11
15
  function getStatuslineCacheCandidates(options = {}) {
12
- const cwd = options.cwd || process.cwd();
13
- const home = options.home || process.env.HOME || process.env.USERPROFILE || '';
14
- const feedbackDir = resolveFeedbackDir({ cwd, env: options.env || process.env });
16
+ const env = options.env || process.env;
17
+ const projectDir = resolveProjectDir({ cwd: options.cwd, env });
18
+ const feedbackDir = resolveFeedbackDir({ projectDir, env });
15
19
 
16
20
  return unique([
21
+ ...listFeedbackArtifactPaths('statusline_cache.json', { projectDir, env }),
17
22
  path.join(feedbackDir, 'statusline_cache.json'),
18
- path.join(cwd, '.thumbgate', 'statusline_cache.json'),
19
- home ? path.join(home, '.thumbgate', 'statusline_cache.json') : null,
20
23
  ]);
21
24
  }
22
25
 
@@ -18,6 +18,7 @@ const fs = require('fs');
18
18
  const path = require('path');
19
19
 
20
20
  const PROJECT_ROOT = path.join(__dirname, '..');
21
+ const VERSION_PATTERN = '\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?';
21
22
 
22
23
  function explicitPinnedServeArgs(version) {
23
24
  return ['--yes', '--package', `thumbgate@${version}`, 'thumbgate', 'serve'];
@@ -308,7 +309,7 @@ function syncVersion(opts) {
308
309
  'plugins/codex-profile/INSTALL.md',
309
310
  'plugins/opencode-profile/INSTALL.md',
310
311
  ];
311
- const pinnedPackagePattern = /thumbgate@\d+\.\d+\.\d+/g;
312
+ const pinnedPackagePattern = new RegExp(`thumbgate@${VERSION_PATTERN}`, 'g');
312
313
  for (const relPath of pinnedPackageTargets) {
313
314
  const filePath = path.join(PROJECT_ROOT, relPath);
314
315
  if (!fs.existsSync(filePath)) continue;
@@ -329,7 +330,7 @@ function syncVersion(opts) {
329
330
  if (fs.existsSync(path.join(PROJECT_ROOT, landingPath))) {
330
331
  const landingContent = fs.readFileSync(path.join(PROJECT_ROOT, landingPath), 'utf-8');
331
332
  // Match any version pattern in the hero badge
332
- const badgeMatch = landingContent.match(/v(\d+\.\d+\.\d+) — Hosted API/);
333
+ const badgeMatch = landingContent.match(new RegExp(`v(${VERSION_PATTERN}) — Hosted API`));
333
334
  if (badgeMatch && badgeMatch[1] !== version) {
334
335
  drifted.push({ file: landingPath, field: 'hero-badge', current: badgeMatch[1] });
335
336
  if (!checkOnly) {
@@ -337,7 +338,7 @@ function syncVersion(opts) {
337
338
  }
338
339
  }
339
340
  // JSON snippet version
340
- const jsonMatch = landingContent.match(/"version"<\/span><span class="out">: <\/span><span class="val">"(\d+\.\d+\.\d+)"/);
341
+ const jsonMatch = landingContent.match(new RegExp(`"version"<\\/span><span class="out">: <\\/span><span class="val">"(${VERSION_PATTERN})"`));
341
342
  if (jsonMatch && jsonMatch[1] !== version) {
342
343
  drifted.push({ file: landingPath, field: 'json-snippet', current: jsonMatch[1] });
343
344
  if (!checkOnly) {
@@ -351,7 +352,7 @@ function syncVersion(opts) {
351
352
  const mcpSubmPath = 'docs/mcp-hub-submission.md';
352
353
  if (fs.existsSync(path.join(PROJECT_ROOT, mcpSubmPath))) {
353
354
  const mcpContent = fs.readFileSync(path.join(PROJECT_ROOT, mcpSubmPath), 'utf-8');
354
- const versionMatch = mcpContent.match(/## Version\s+(\d+\.\d+\.\d+)/);
355
+ const versionMatch = mcpContent.match(new RegExp(`## Version\\s+(${VERSION_PATTERN})`));
355
356
  if (versionMatch && versionMatch[1] !== version) {
356
357
  drifted.push({ file: mcpSubmPath, field: 'version-heading', current: versionMatch[1] });
357
358
  if (!checkOnly) {
@@ -366,18 +367,18 @@ function syncVersion(opts) {
366
367
  if (fs.existsSync(path.join(PROJECT_ROOT, publicIndexPath))) {
367
368
  const publicIndexFile = path.join(PROJECT_ROOT, publicIndexPath);
368
369
  const publicContent = fs.readFileSync(publicIndexFile, 'utf-8');
369
- const heroVersionMatch = publicContent.match(/New in v(\d+\.\d+\.\d+):?/);
370
+ const heroVersionMatch = publicContent.match(new RegExp(`New in v(${VERSION_PATTERN}):?`));
370
371
  if (heroVersionMatch && heroVersionMatch[1] !== version) {
371
372
  drifted.push({ file: publicIndexPath, field: 'hero-release-note', current: heroVersionMatch[1] });
372
373
  if (!checkOnly) {
373
374
  fs.writeFileSync(
374
375
  publicIndexFile,
375
- publicContent.replace(/New in v\d+\.\d+\.\d+:?/, `New in v${version}`)
376
+ publicContent.replace(new RegExp(`New in v${VERSION_PATTERN}:?`), `New in v${version}`)
376
377
  );
377
378
  }
378
379
  }
379
380
 
380
- const proofMatch = publicContent.match(/Versioned proof: v(\d+\.\d+\.\d+)/);
381
+ const proofMatch = publicContent.match(new RegExp(`Versioned proof: v(${VERSION_PATTERN})`));
381
382
  if (proofMatch && proofMatch[1] !== version) {
382
383
  drifted.push({ file: publicIndexPath, field: 'proof-pill', current: proofMatch[1] });
383
384
  if (!checkOnly) {
@@ -385,11 +386,17 @@ function syncVersion(opts) {
385
386
  }
386
387
  }
387
388
 
388
- const footerMatch = publicContent.match(/Context Gateway v(\d+\.\d+\.\d+)/);
389
+ const footerMatch = publicContent.match(new RegExp(`(?:Context Gateway|MIT License) [•·] v(${VERSION_PATTERN})`));
389
390
  if (footerMatch && footerMatch[1] !== version) {
390
391
  drifted.push({ file: publicIndexPath, field: 'footer-version', current: footerMatch[1] });
391
392
  if (!checkOnly) {
392
- replaceInFile(publicIndexPath, `Context Gateway • v${footerMatch[1]}`, `Context Gateway • v${version}`);
393
+ fs.writeFileSync(
394
+ publicIndexFile,
395
+ publicContent.replace(
396
+ new RegExp(`((?:Context Gateway|MIT License) [•·] )v${VERSION_PATTERN}`),
397
+ `$1v${version}`
398
+ )
399
+ );
393
400
  }
394
401
  }
395
402
  targets.push(publicIndexPath);
@@ -400,13 +407,13 @@ function syncVersion(opts) {
400
407
  const serverStdioFile = path.join(PROJECT_ROOT, serverStdioPath);
401
408
  if (fs.existsSync(serverStdioFile)) {
402
409
  const serverStdioContent = fs.readFileSync(serverStdioFile, 'utf-8');
403
- const serverInfoMatch = serverStdioContent.match(/version:\s*'(\d+\.\d+\.\d+)'/);
410
+ const serverInfoMatch = serverStdioContent.match(new RegExp(`version:\\s*'(${VERSION_PATTERN})'`));
404
411
  if (serverInfoMatch && serverInfoMatch[1] !== version) {
405
412
  drifted.push({ file: serverStdioPath, field: 'server-info-version', current: serverInfoMatch[1] });
406
413
  if (!checkOnly) {
407
414
  fs.writeFileSync(
408
415
  serverStdioFile,
409
- serverStdioContent.replace(/version:\s*'\d+\.\d+\.\d+'/, `version: '${version}'`)
416
+ serverStdioContent.replace(new RegExp(`version:\\s*'${VERSION_PATTERN}'`), `version: '${version}'`)
410
417
  );
411
418
  }
412
419
  }