thumbgate 0.9.14 → 1.1.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 (64) 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 +1 -0
  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 +41 -1
  11. package/adapters/opencode/opencode.json +1 -1
  12. package/bin/cli.js +18 -3
  13. package/config/mcp-allowlists.json +11 -0
  14. package/openapi/openapi.yaml +105 -0
  15. package/package.json +7 -5
  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 +8 -4
  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 +1 -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/export-hf-dataset.js +293 -0
  48. package/scripts/gates-engine.js +96 -10
  49. package/scripts/hook-auto-capture.sh +1 -1
  50. package/scripts/hosted-job-launcher.js +260 -0
  51. package/scripts/managed-dpo-export.js +91 -0
  52. package/scripts/obsidian-export.js +0 -1
  53. package/scripts/operational-integrity.js +50 -7
  54. package/scripts/prove-lancedb.js +62 -4
  55. package/scripts/publish-decision.js +16 -0
  56. package/scripts/self-healing-check.js +6 -1
  57. package/scripts/social-analytics/load-env.js +33 -2
  58. package/scripts/social-analytics/store.js +200 -2
  59. package/scripts/sync-version.js +18 -11
  60. package/scripts/tool-registry.js +48 -0
  61. package/scripts/train_from_feedback.py +0 -4
  62. package/scripts/workflow-sentinel.js +793 -0
  63. package/src/api/server.js +205 -27
  64. /package/scripts/{rlhf_session_start.sh → thumbgate_session_start.sh} +0 -0
@@ -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.
@@ -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
  }
@@ -399,6 +399,17 @@ const TOOLS = [
399
399
  },
400
400
  },
401
401
  }),
402
+ destructiveTool({
403
+ name: 'export_hf_dataset',
404
+ description: 'Export ThumbGate agent traces and DPO preference pairs as a HuggingFace-compatible dataset. Produces traces.jsonl, preferences.jsonl, and dataset_info.json with PII-redacted paths. Ready for huggingface-cli upload.',
405
+ inputSchema: {
406
+ type: 'object',
407
+ properties: {
408
+ outputDir: { type: 'string', description: 'Output directory (default: feedback-dir/hf-dataset)' },
409
+ includeProvenance: { type: 'boolean', description: 'Include provenance events in traces (default: true)' },
410
+ },
411
+ },
412
+ }),
402
413
  destructiveTool({
403
414
  name: 'export_databricks_bundle',
404
415
  description: 'Export ThumbGate logs and proof artifacts as a Databricks-ready analytics bundle',
@@ -490,6 +501,21 @@ const TOOLS = [
490
501
  },
491
502
  },
492
503
  }),
504
+ readOnlyTool({
505
+ name: 'unified_context',
506
+ description: 'Assemble a complete, role-aware context object in one call. Combines session state, user profile, relevant lessons, prevention guards, context pack, and code-graph impact — with tiered graceful degradation (full → warm → cold). Replaces multiple recall/retrieve/session_primer calls with a single orchestrated request.',
507
+ inputSchema: {
508
+ type: 'object',
509
+ required: ['query'],
510
+ properties: {
511
+ query: { type: 'string', description: 'Describe the current task to find relevant context' },
512
+ toolName: { type: 'string', description: 'Current tool being invoked (improves lesson matching)' },
513
+ toolInput: { type: 'object', description: 'Current tool input (for guard evaluation)' },
514
+ agentType: { type: 'string', enum: ['claude', 'cursor', 'forgecode', 'codex'], description: 'Agent type — shapes context budget and feature inclusion' },
515
+ repoPath: { type: 'string', description: 'Repository path for code-graph impact analysis' },
516
+ },
517
+ },
518
+ }),
493
519
  destructiveTool({
494
520
  name: 'satisfy_gate',
495
521
  description: 'Satisfy a gate condition with optional structured reasoning. Evidence is stored with a 5-minute TTL. When structuredReasoning is provided, the premise/evidence/conclusion chain is stored in the audit trail.',
@@ -632,6 +658,28 @@ const TOOLS = [
632
658
  },
633
659
  },
634
660
  }),
661
+ readOnlyTool({
662
+ name: 'workflow_sentinel',
663
+ description: 'Predict pre-action workflow risk, blast radius, and remediations before a tool call executes.',
664
+ inputSchema: {
665
+ type: 'object',
666
+ required: ['toolName'],
667
+ properties: {
668
+ toolName: { type: 'string', description: 'Tool being assessed, such as Bash, Edit, or Write' },
669
+ command: { type: 'string', description: 'Optional shell command when toolName is Bash' },
670
+ filePath: { type: 'string', description: 'Optional primary file path for edit-like tools' },
671
+ changedFiles: {
672
+ type: 'array',
673
+ items: { type: 'string' },
674
+ description: 'Optional affected-file list used to estimate blast radius',
675
+ },
676
+ repoPath: { type: 'string', description: 'Optional repository path used for git-aware integrity checks' },
677
+ baseBranch: { type: 'string', description: 'Optional protected base branch override (defaults to main)' },
678
+ requirePrForReleaseSensitive: { type: 'boolean', description: 'When true, release-sensitive changes on non-base branches require an open PR' },
679
+ requireVersionNotBehindBase: { type: 'boolean', description: 'When true, release-sensitive changes cannot lag behind the base branch package version' },
680
+ },
681
+ },
682
+ }),
635
683
  destructiveTool({
636
684
  name: 'register_claim_gate',
637
685
  description: 'Register a custom claim verification rule in local runtime state without editing tracked repo config.',
@@ -40,10 +40,6 @@ def resolve_feedback_dir() -> Path:
40
40
  if local_thumbgate.exists():
41
41
  return local_thumbgate
42
42
 
43
- local_rlhf = PROJECT_ROOT / ".rlhf"
44
- if local_rlhf.exists():
45
- return local_rlhf
46
-
47
43
  local_legacy = PROJECT_ROOT / ".claude" / "memory" / "feedback"
48
44
  if local_legacy.exists():
49
45
  return local_legacy