thumbgate 0.9.10 → 0.9.12

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 (115) hide show
  1. package/.claude-plugin/README.md +2 -2
  2. package/.claude-plugin/marketplace.json +4 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +115 -312
  6. package/adapters/README.md +1 -1
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +4 -4
  9. package/adapters/mcp/server-stdio.js +61 -1
  10. package/adapters/opencode/opencode.json +4 -2
  11. package/bin/cli.js +156 -8
  12. package/bin/memory.sh +3 -3
  13. package/config/e2e-critical-flows.json +4 -0
  14. package/config/gates/default.json +74 -2
  15. package/config/github-about.json +1 -1
  16. package/config/mcp-allowlists.json +27 -0
  17. package/package.json +22 -5
  18. package/plugins/amp-skill/INSTALL.md +1 -0
  19. package/plugins/amp-skill/SKILL.md +1 -0
  20. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  21. package/plugins/claude-codex-bridge/.mcp.json +4 -2
  22. package/plugins/claude-skill/INSTALL.md +1 -0
  23. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  24. package/plugins/codex-profile/.mcp.json +4 -2
  25. package/plugins/codex-profile/INSTALL.md +1 -1
  26. package/plugins/codex-profile/README.md +1 -1
  27. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  28. package/plugins/cursor-marketplace/README.md +3 -3
  29. package/plugins/cursor-marketplace/mcp.json +3 -1
  30. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  31. package/plugins/gemini-extension/INSTALL.md +3 -3
  32. package/plugins/opencode-profile/INSTALL.md +1 -1
  33. package/public/dashboard.html +15 -8
  34. package/public/index.html +125 -185
  35. package/public/js/buyer-intent.js +252 -0
  36. package/public/pro.html +1085 -0
  37. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  38. package/scripts/adk-consolidator.js +14 -2
  39. package/scripts/agent-readiness.js +3 -1
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/auto-promote-gates.js +2 -0
  42. package/scripts/auto-wire-hooks.js +105 -17
  43. package/scripts/behavioral-extraction.js +2 -6
  44. package/scripts/billing.js +107 -3
  45. package/scripts/budget-guard.js +2 -2
  46. package/scripts/build-metadata.js +14 -0
  47. package/scripts/context-engine.js +1 -0
  48. package/scripts/deploy-policy.js +3 -17
  49. package/scripts/dpo-optimizer.js +3 -6
  50. package/scripts/ensure-repo-bootstrap.js +129 -0
  51. package/scripts/export-dpo-pairs.js +2 -3
  52. package/scripts/export-kto-pairs.js +3 -4
  53. package/scripts/export-training.js +8 -6
  54. package/scripts/feedback-attribution.js +23 -11
  55. package/scripts/feedback-loop.js +40 -2
  56. package/scripts/feedback-to-rules.js +2 -1
  57. package/scripts/filesystem-search.js +3 -2
  58. package/scripts/gates-engine.js +760 -29
  59. package/scripts/generate-pretool-hook.sh +0 -0
  60. package/scripts/gtm-revenue-loop.js +20 -1
  61. package/scripts/hook-auto-capture.sh +8 -3
  62. package/scripts/hook-runtime.js +81 -0
  63. package/scripts/hook-stop-self-score.sh +3 -3
  64. package/scripts/hook-thumbgate-cache-updater.js +99 -38
  65. package/scripts/hosted-config.js +4 -16
  66. package/scripts/hybrid-feedback-context.js +54 -14
  67. package/scripts/install-mcp.js +13 -3
  68. package/scripts/intent-router.js +2 -2
  69. package/scripts/license.js +52 -14
  70. package/scripts/local-model-profile.js +3 -2
  71. package/scripts/mcp-config.js +62 -7
  72. package/scripts/meta-policy.js +4 -8
  73. package/scripts/money-watcher.js +166 -16
  74. package/scripts/obsidian-export.js +1 -0
  75. package/scripts/operational-integrity.js +480 -0
  76. package/scripts/post-everywhere.js +35 -12
  77. package/scripts/pr-manager.js +14 -11
  78. package/scripts/profile-router.js +2 -0
  79. package/scripts/prompt-dlp.js +1 -0
  80. package/scripts/publish-decision.js +10 -0
  81. package/scripts/published-cli.js +61 -0
  82. package/scripts/risk-scorer.js +3 -2
  83. package/scripts/rlhf_session_start.sh +32 -0
  84. package/scripts/skill-quality-tracker.js +3 -5
  85. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  86. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  87. package/scripts/social-analytics/engagement-audit.js +202 -0
  88. package/scripts/social-analytics/instagram-thumbgate-post.js +45 -7
  89. package/scripts/social-analytics/install-growth-automation.js +114 -0
  90. package/scripts/social-analytics/load-env.js +46 -0
  91. package/scripts/social-analytics/poll-all.js +23 -23
  92. package/scripts/social-analytics/pollers/plausible.js +2 -4
  93. package/scripts/social-analytics/pollers/zernio.js +3 -0
  94. package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
  95. package/scripts/social-analytics/publish-thumbgate-launch.js +322 -0
  96. package/scripts/social-analytics/publishers/reddit.js +7 -12
  97. package/scripts/social-analytics/publishers/zernio.js +301 -22
  98. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  99. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  100. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  101. package/scripts/social-post-hourly.js +185 -0
  102. package/scripts/social-quality-gate.js +119 -3
  103. package/scripts/social-reply-monitor.js +184 -37
  104. package/scripts/statusline-cache-path.js +27 -0
  105. package/scripts/statusline-local-stats.js +16 -0
  106. package/scripts/statusline-meta.js +22 -0
  107. package/scripts/statusline.sh +40 -33
  108. package/scripts/sync-version.js +24 -3
  109. package/scripts/test-coverage.js +21 -13
  110. package/scripts/tool-registry.js +97 -0
  111. package/scripts/train_from_feedback.py +32 -9
  112. package/scripts/validate-feedback.js +3 -2
  113. package/scripts/vector-store.js +2 -3
  114. package/scripts/verify-obsidian-setup.sh +3 -3
  115. package/src/api/server.js +281 -33
File without changes
@@ -4,13 +4,20 @@
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const { spawnSync } = require('node:child_process');
7
- const { GoogleGenAI } = require('@google/genai');
8
7
  const { resolveHostedBillingConfig } = require('./hosted-config');
9
8
  const { getOperationalBillingSummary } = require('./operational-summary');
10
9
 
11
10
  const COMMERCIAL_TRUTH_LINK = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/COMMERCIAL_TRUTH.md';
12
11
  const VERIFICATION_EVIDENCE_LINK = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md';
13
12
 
13
+ function getGoogleGenAI() {
14
+ try {
15
+ return require('@google/genai').GoogleGenAI;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
14
21
  function parseArgs(argv = []) {
15
22
  const options = {
16
23
  maxTargets: 6,
@@ -310,6 +317,18 @@ async function generateOutreachMessages(targets, motionCatalog = buildMotionCata
310
317
  });
311
318
  }
312
319
 
320
+ const GoogleGenAI = getGoogleGenAI();
321
+ if (!GoogleGenAI) {
322
+ return targets.map((target) => {
323
+ const selectedMotion = selectOutreachMotion(target, motionCatalog);
324
+ return {
325
+ ...target,
326
+ selectedMotion,
327
+ message: buildFallbackMessage(target, selectedMotion, motionCatalog),
328
+ };
329
+ });
330
+ }
331
+
313
332
  const ai = new GoogleGenAI({ apiKey });
314
333
  const results = [];
315
334
 
@@ -7,8 +7,13 @@ PROMPT="$CLAUDE_USER_PROMPT"
7
7
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
8
  CAPTURE="$SCRIPT_DIR/../.claude/scripts/feedback/capture-feedback.js"
9
9
  PROMPT_GUARD="$SCRIPT_DIR/prompt-guard.js"
10
- FEEDBACK_LOG="$SCRIPT_DIR/../.claude/memory/feedback/feedback-log.jsonl"
11
- MEMORY_LOG="$SCRIPT_DIR/../.claude/memory/feedback/memory-log.jsonl"
10
+ ACTIVE_CWD="${CLAUDE_PROJECT_DIR:-${PWD:-$(pwd)}}"
11
+ FEEDBACK_DIR="$(node -e "const path = require('path'); const { resolveFeedbackDir } = require(path.join(process.argv[1], 'feedback-paths.js')); process.stdout.write(resolveFeedbackDir({ cwd: process.argv[2] || process.cwd(), feedbackDir: process.env.THUMBGATE_FEEDBACK_DIR || undefined }));" "$SCRIPT_DIR" "$ACTIVE_CWD" 2>/dev/null)"
12
+ if [ -z "$FEEDBACK_DIR" ]; then
13
+ FEEDBACK_DIR="${THUMBGATE_FEEDBACK_DIR:-$ACTIVE_CWD/.rlhf}"
14
+ fi
15
+ FEEDBACK_LOG="$FEEDBACK_DIR/feedback-log.jsonl"
16
+ MEMORY_LOG="$FEEDBACK_DIR/memory-log.jsonl"
12
17
 
13
18
  # Record the latest user prompt so statusline thumbs can distill lessons
14
19
  # from recent conversation even when the click itself has no body payload.
@@ -49,7 +54,7 @@ capture_and_report() {
49
54
  echo "Storage Proof:"
50
55
  echo " Feedback log : $FEEDBACK_LOG ($(wc -l < "$FEEDBACK_LOG" 2>/dev/null || echo 0) entries)"
51
56
  echo " Memory log : $MEMORY_LOG ($(wc -l < "$MEMORY_LOG" 2>/dev/null || echo 0) entries)"
52
- echo " LanceDB : $SCRIPT_DIR/../.claude/memory/feedback/lancedb/"
57
+ echo " LanceDB : $FEEDBACK_DIR/lancedb/"
53
58
  echo ""
54
59
 
55
60
  # Show last entry written
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ isSourceCheckout,
7
+ publishedCliAvailable,
8
+ } = require('./mcp-config');
9
+ const { publishedCliShellCommand } = require('./published-cli');
10
+
11
+ const PKG_ROOT = path.join(__dirname, '..');
12
+ const featureSupportCache = new Map();
13
+
14
+ function packageVersion() {
15
+ const pkg = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
16
+ return pkg.version;
17
+ }
18
+
19
+ function shellQuote(value) {
20
+ return JSON.stringify(String(value));
21
+ }
22
+
23
+ function publishedHookCommandsAvailable(version) {
24
+ if (!publishedCliAvailable(version)) {
25
+ return false;
26
+ }
27
+ if (featureSupportCache.has(version)) {
28
+ return featureSupportCache.get(version);
29
+ }
30
+
31
+ const available = true;
32
+ featureSupportCache.set(version, available);
33
+ return available;
34
+ }
35
+
36
+ function resolveCliBaseCommand() {
37
+ const version = packageVersion();
38
+ if (publishedHookCommandsAvailable(version)) {
39
+ return publishedCliShellCommand(version);
40
+ }
41
+ if (isSourceCheckout(PKG_ROOT)) {
42
+ return `node ${shellQuote(path.join(PKG_ROOT, 'bin', 'cli.js'))}`;
43
+ }
44
+ return publishedCliShellCommand(version);
45
+ }
46
+
47
+ function buildPortableHookCommand(subcommand) {
48
+ return `${resolveCliBaseCommand()} ${subcommand}`;
49
+ }
50
+
51
+ function preToolHookCommand() {
52
+ return buildPortableHookCommand('gate-check');
53
+ }
54
+
55
+ function userPromptHookCommand() {
56
+ return buildPortableHookCommand('hook-auto-capture');
57
+ }
58
+
59
+ function sessionStartHookCommand() {
60
+ return buildPortableHookCommand('session-start');
61
+ }
62
+
63
+ function cacheUpdateHookCommand() {
64
+ return buildPortableHookCommand('cache-update');
65
+ }
66
+
67
+ function statuslineCommand() {
68
+ return buildPortableHookCommand('statusline-render');
69
+ }
70
+
71
+ module.exports = {
72
+ buildPortableHookCommand,
73
+ cacheUpdateHookCommand,
74
+ packageVersion,
75
+ publishedHookCommandsAvailable,
76
+ preToolHookCommand,
77
+ resolveCliBaseCommand,
78
+ sessionStartHookCommand,
79
+ statuslineCommand,
80
+ userPromptHookCommand,
81
+ };
@@ -18,9 +18,9 @@ node -e '
18
18
  const path = require("path");
19
19
 
20
20
  // Resolve modules relative to ThumbGate package root
21
- const rlhfRoot = process.env.THUMBGATE_ROOT;
22
- const { selfAuditAndLog } = require(path.join(rlhfRoot, "scripts", "rlaif-self-audit"));
23
- const { getFeedbackPaths } = require(path.join(rlhfRoot, "scripts", "feedback-loop"));
21
+ const thumbgateRoot = process.env.THUMBGATE_ROOT;
22
+ const { selfAuditAndLog } = require(path.join(thumbgateRoot, "scripts", "rlaif-self-audit"));
23
+ const { getFeedbackPaths } = require(path.join(thumbgateRoot, "scripts", "feedback-loop"));
24
24
 
25
25
  const stopReason = process.env.CLAUDE_STOP_REASON || "unknown";
26
26
 
@@ -1,48 +1,109 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict';
3
+
2
4
  /**
3
- * PostToolUse hook: updates ThumbGate statusline cache after feedback_stats calls.
4
- * Installed by: npx thumbgate init --agent claude-code
5
+ * PostToolUse hook: updates ThumbGate statusline cache after dashboard/stat calls.
6
+ * Also used directly by the CLI to refresh statusline counters after feedback capture.
5
7
  */
6
- 'use strict';
8
+
7
9
  const fs = require('fs');
8
10
  const path = require('path');
11
+ const { resolveFeedbackDir } = require('./feedback-paths');
9
12
 
10
- const CACHE_DIR = process.env.THUMBGATE_FEEDBACK_DIR || process.cwd();
11
- const CACHE_PATH = path.join(CACHE_DIR, '.thumbgate', 'statusline_cache.json');
13
+ function getCachePath() {
14
+ return path.join(resolveFeedbackDir(), 'statusline_cache.json');
15
+ }
12
16
 
13
- let input = '';
14
- process.stdin.setEncoding('utf8');
15
- process.stdin.on('data', chunk => { input += chunk; });
16
- process.stdin.on('end', () => {
17
+ function readExistingCache(cachePath = getCachePath()) {
17
18
  try {
18
- const event = JSON.parse(input);
19
- const tool = event.tool_name || '';
20
- if (tool !== 'mcp__thumbgate__feedback_stats' && tool !== 'mcp__thumbgate__dashboard') return;
21
-
22
- const raw = event.tool_response;
23
- if (!raw) return;
24
- const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
25
-
26
- const cache = {};
27
- if (tool === 'mcp__thumbgate__feedback_stats') {
28
- cache.thumbs_up = String(data.totalPositive || 0);
29
- cache.thumbs_down = String(data.totalNegative || 0);
30
- cache.lessons = String((data.rubric || {}).samples || 0);
31
- cache.approval_rate = String(Math.round((data.approvalRate || 0) * 1000) / 10);
32
- cache.trend = data.trend || '?';
33
- cache.total_feedback = String(data.total || 0);
34
- } else if (tool === 'mcp__thumbgate__dashboard') {
35
- const approval = data.approval || {};
36
- cache.thumbs_up = String(approval.totalPositive || 0);
37
- cache.thumbs_down = String(approval.totalNegative || 0);
38
- cache.lessons = String((data.rubric || {}).samples || 0);
39
- cache.approval_rate = String(approval.approvalRate || '?');
40
- cache.trend = approval.trendDirection || '?';
41
- cache.total_feedback = String(approval.total || 0);
19
+ return JSON.parse(fs.readFileSync(cachePath, 'utf8'));
20
+ } catch {
21
+ return {};
22
+ }
23
+ }
24
+
25
+ function writeStatuslineCache(nextCache, cachePath = getCachePath()) {
26
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
27
+ fs.writeFileSync(cachePath, JSON.stringify(nextCache));
28
+ }
29
+
30
+ function normalizeDashboardPayload(payload = {}) {
31
+ const approval = payload.approval || {};
32
+ return {
33
+ thumbs_up: String(approval.totalPositive || 0),
34
+ thumbs_down: String(approval.totalNegative || 0),
35
+ lessons: String((payload.rubric || {}).samples || 0),
36
+ approval_rate: String(approval.approvalRate || '?'),
37
+ trend: approval.trendDirection || '?',
38
+ total_feedback: String(approval.total || 0),
39
+ };
40
+ }
41
+
42
+ function normalizeStatsPayload(payload = {}) {
43
+ return {
44
+ thumbs_up: String(payload.totalPositive || 0),
45
+ thumbs_down: String(payload.totalNegative || 0),
46
+ lessons: String((payload.rubric || {}).samples || 0),
47
+ approval_rate: String(Math.round((payload.approvalRate || 0) * 1000) / 10),
48
+ trend: payload.trend || '?',
49
+ total_feedback: String(payload.total || 0),
50
+ };
51
+ }
52
+
53
+ function refreshStatuslineCache(statsPayload = {}, cachePath = getCachePath()) {
54
+ const cache = {
55
+ ...readExistingCache(cachePath),
56
+ ...normalizeStatsPayload(statsPayload),
57
+ updated_at: String(Math.floor(Date.now() / 1000)),
58
+ };
59
+ writeStatuslineCache(cache, cachePath);
60
+ return cache;
61
+ }
62
+
63
+ function updateCacheFromEvent(event = {}, cachePath = getCachePath()) {
64
+ const tool = event.tool_name || '';
65
+ if (tool !== 'mcp__thumbgate__feedback_stats' && tool !== 'mcp__thumbgate__dashboard') {
66
+ return null;
67
+ }
68
+
69
+ const raw = event.tool_response;
70
+ if (!raw) return null;
71
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
72
+ const cache = {
73
+ ...readExistingCache(cachePath),
74
+ ...(tool === 'mcp__thumbgate__feedback_stats'
75
+ ? normalizeStatsPayload(data)
76
+ : normalizeDashboardPayload(data)),
77
+ updated_at: String(Math.floor(Date.now() / 1000)),
78
+ };
79
+ writeStatuslineCache(cache, cachePath);
80
+ return cache;
81
+ }
82
+
83
+ function runFromStdin() {
84
+ let input = '';
85
+ process.stdin.setEncoding('utf8');
86
+ process.stdin.on('data', (chunk) => { input += chunk; });
87
+ process.stdin.on('end', () => {
88
+ try {
89
+ updateCacheFromEvent(input ? JSON.parse(input) : {});
90
+ } catch {
91
+ /* statusline cache is best-effort */
42
92
  }
43
- cache.updated_at = String(Math.floor(Date.now() / 1000));
93
+ });
94
+ }
95
+
96
+ if (require.main === module) {
97
+ runFromStdin();
98
+ }
44
99
 
45
- fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
46
- fs.writeFileSync(CACHE_PATH, JSON.stringify(cache));
47
- } catch (_) { /* silent — statusline cache is best-effort */ }
48
- });
100
+ module.exports = {
101
+ getCachePath,
102
+ normalizeDashboardPayload,
103
+ normalizeStatsPayload,
104
+ readExistingCache,
105
+ refreshStatuslineCache,
106
+ runFromStdin,
107
+ updateCacheFromEvent,
108
+ writeStatuslineCache,
109
+ };
@@ -12,12 +12,6 @@ const DEFAULT_CHECKOUT_FALLBACK_URL = PRO_MONTHLY_PAYMENT_LINK;
12
12
  const DEFAULT_PRO_PRICE_DOLLARS = PRO_MONTHLY_PRICE_DOLLARS;
13
13
  const DEFAULT_PRO_PRICE_LABEL = PRO_PRICE_LABEL;
14
14
  const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]+$/i;
15
- const PUBLIC_APP_ORIGIN_KEYS = ['THUMBGATE_PUBLIC_APP_ORIGIN', 'RLHF_PUBLIC_APP_ORIGIN'];
16
- const BILLING_API_BASE_URL_KEYS = [
17
- 'THUMBGATE_BILLING_API_BASE_URL',
18
- 'RLHF_BILLING_API_BASE_URL',
19
- 'THUMBGATE_CANONICAL_API_BASE_URL',
20
- ];
21
15
 
22
16
  function normalizeOrigin(value) {
23
17
  if (!value || typeof value !== 'string') {
@@ -82,14 +76,8 @@ function normalizeTrackingId(value, pattern) {
82
76
  return trimmed;
83
77
  }
84
78
 
85
- function resolveNormalizedOrigin(env, keys, fallback = '') {
86
- for (const key of keys) {
87
- const normalized = normalizeOrigin(env[key]);
88
- if (normalized) {
89
- return normalized;
90
- }
91
- }
92
- return normalizeOrigin(fallback);
79
+ function resolveNormalizedOrigin(value, fallback = '') {
80
+ return normalizeOrigin(value) || normalizeOrigin(fallback);
93
81
  }
94
82
 
95
83
  function joinPublicUrl(baseOrigin, pathname) {
@@ -130,8 +118,8 @@ function buildHostedCancelUrl(appOrigin, traceId) {
130
118
 
131
119
  function resolveHostedBillingConfig({ requestOrigin } = {}, env = process.env) {
132
120
  const inferredOrigin = normalizeOrigin(requestOrigin) || DEFAULT_PUBLIC_APP_ORIGIN;
133
- const appOrigin = resolveNormalizedOrigin(env, PUBLIC_APP_ORIGIN_KEYS, inferredOrigin) || inferredOrigin;
134
- const billingApiBaseUrl = resolveNormalizedOrigin(env, BILLING_API_BASE_URL_KEYS, appOrigin) || appOrigin;
121
+ const appOrigin = resolveNormalizedOrigin(env.THUMBGATE_PUBLIC_APP_ORIGIN, inferredOrigin) || inferredOrigin;
122
+ const billingApiBaseUrl = resolveNormalizedOrigin(env.THUMBGATE_BILLING_API_BASE_URL, appOrigin) || appOrigin;
135
123
  const proPriceDollars = normalizePriceDollars(env.THUMBGATE_PRO_PRICE_DOLLARS) || DEFAULT_PRO_PRICE_DOLLARS;
136
124
  const proPriceLabel = env.THUMBGATE_PRO_PRICE_LABEL || DEFAULT_PRO_PRICE_LABEL;
137
125
  const gaMeasurementId = normalizeTrackingId(env.THUMBGATE_GA_MEASUREMENT_ID, GA_MEASUREMENT_ID_PATTERN);
@@ -16,21 +16,30 @@
16
16
 
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
- const os = require('os');
19
+ const { resolveFeedbackDir } = require('./feedback-paths');
20
20
 
21
21
  // ---------------------------------------------------------------------------
22
22
  // Paths
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
- const ROOT = path.join(__dirname, '..');
25
+ function getHybridPaths(options = {}) {
26
+ const feedbackDir = resolveFeedbackDir({
27
+ cwd: options.cwd,
28
+ env: options.env,
29
+ feedbackDir: options.feedbackDir,
30
+ home: options.home,
31
+ });
32
+ return {
33
+ feedbackDir,
34
+ feedbackLog: path.join(feedbackDir, 'feedback-log.jsonl'),
35
+ inbox: path.join(feedbackDir, 'inbox.jsonl'),
36
+ pendingSync: path.join(feedbackDir, 'pending_cortex_sync.jsonl'),
37
+ attributedFeedback: path.join(feedbackDir, 'attributed-feedback.jsonl'),
38
+ guardArtifact: path.join(feedbackDir, 'pretool-guards.json'),
39
+ };
40
+ }
26
41
 
27
- const PATHS = {
28
- feedbackLog: path.join(ROOT, '.claude', 'memory', 'feedback', 'feedback-log.jsonl'),
29
- inbox: path.join(ROOT, '.claude', 'memory', 'feedback', 'inbox.jsonl'),
30
- pendingSync: path.join(ROOT, '.claude', 'memory', 'feedback', 'pending_cortex_sync.jsonl'),
31
- attributedFeedback: path.join(ROOT, '.claude', 'memory', 'feedback', 'attributed-feedback.jsonl'),
32
- guardArtifact: path.join(ROOT, '.claude', 'memory', 'feedback', 'pretool-guards.json'),
33
- };
42
+ const PATHS = getHybridPaths();
34
43
 
35
44
  // ---------------------------------------------------------------------------
36
45
  // Constants
@@ -193,10 +202,11 @@ function hashText(text) {
193
202
  */
194
203
  function buildHybridState(opts) {
195
204
  const o = opts || {};
196
- const feedbackLogPath = o.feedbackLogPath || process.env.THUMBGATE_FEEDBACK_LOG || PATHS.feedbackLog;
197
- const inboxPath = o.inboxPath || process.env.THUMBGATE_FEEDBACK_INBOX || PATHS.inbox;
198
- const pendingSyncPath = o.pendingSyncPath || process.env.THUMBGATE_PENDING_SYNC || PATHS.pendingSync;
199
- const attributedFeedbackPath = o.attributedFeedbackPath || process.env.THUMBGATE_ATTRIBUTED_FEEDBACK || PATHS.attributedFeedback;
205
+ const paths = getHybridPaths(o);
206
+ const feedbackLogPath = o.feedbackLogPath || process.env.THUMBGATE_FEEDBACK_LOG || paths.feedbackLog;
207
+ const inboxPath = o.inboxPath || process.env.THUMBGATE_FEEDBACK_INBOX || paths.inbox;
208
+ const pendingSyncPath = o.pendingSyncPath || process.env.THUMBGATE_PENDING_SYNC || paths.pendingSync;
209
+ const attributedFeedbackPath = o.attributedFeedbackPath || process.env.THUMBGATE_ATTRIBUTED_FEEDBACK || paths.attributedFeedback;
200
210
 
201
211
  const feedbackEntries = readJsonl(feedbackLogPath);
202
212
  const inboxEntries = readJsonl(inboxPath);
@@ -237,6 +247,21 @@ function buildHybridState(opts) {
237
247
  entry.context || '',
238
248
  entry.whatWentWrong || entry.what_went_wrong || '',
239
249
  entry.whatToChange || entry.what_to_change || '',
250
+ entry.failureType || '',
251
+ ...(Array.isArray(entry.tags) ? entry.tags : []),
252
+ ...(entry.richContext && Array.isArray(entry.richContext.filePaths) ? entry.richContext.filePaths : []),
253
+ ...(entry.structuredRule && entry.structuredRule.metadata && Array.isArray(entry.structuredRule.metadata.filesInvolved)
254
+ ? entry.structuredRule.metadata.filesInvolved
255
+ : []),
256
+ entry.structuredRule && entry.structuredRule.action ? entry.structuredRule.action.description || '' : '',
257
+ entry.structuredRule && entry.structuredRule.trigger ? entry.structuredRule.trigger.condition || '' : '',
258
+ entry.richContext && entry.richContext.enforcement
259
+ ? [
260
+ entry.richContext.enforcement.scopeViolation ? 'scope violation' : '',
261
+ entry.richContext.enforcement.approvalFailure ? 'approval failure' : '',
262
+ entry.richContext.enforcement.protectedFileViolation ? 'protected file violation' : '',
263
+ ].join(' ')
264
+ : '',
240
265
  ].join(' ');
241
266
  const norm = normalizePatternText(rawText);
242
267
  if (!norm) continue;
@@ -261,6 +286,20 @@ function buildHybridState(opts) {
261
286
  const rawText = [
262
287
  entry.context || '',
263
288
  entry.whatWentWrong || entry.what_went_wrong || '',
289
+ ...(Array.isArray(entry.tags) ? entry.tags : []),
290
+ ...(entry.richContext && Array.isArray(entry.richContext.filePaths) ? entry.richContext.filePaths : []),
291
+ ...(entry.structuredRule && entry.structuredRule.metadata && Array.isArray(entry.structuredRule.metadata.filesInvolved)
292
+ ? entry.structuredRule.metadata.filesInvolved
293
+ : []),
294
+ entry.structuredRule && entry.structuredRule.action ? entry.structuredRule.action.description || '' : '',
295
+ entry.structuredRule && entry.structuredRule.trigger ? entry.structuredRule.trigger.condition || '' : '',
296
+ entry.richContext && entry.richContext.enforcement
297
+ ? [
298
+ entry.richContext.enforcement.scopeViolation ? 'scope violation' : '',
299
+ entry.richContext.enforcement.approvalFailure ? 'approval failure' : '',
300
+ entry.richContext.enforcement.protectedFileViolation ? 'protected file violation' : '',
301
+ ].join(' ')
302
+ : '',
264
303
  ].join(' ');
265
304
  const norm = normalizePatternText(rawText);
266
305
  if (!norm) continue;
@@ -587,7 +626,7 @@ function evaluatePretool(toolName, toolInput, opts) {
587
626
  const o = opts || {};
588
627
 
589
628
  // Fast path: compiled artifact
590
- const artifactPath = o.guardArtifactPath || process.env.THUMBGATE_GUARDS_PATH || PATHS.guardArtifact;
629
+ const artifactPath = o.guardArtifactPath || process.env.THUMBGATE_GUARDS_PATH || getHybridPaths(o).guardArtifact;
591
630
  const artifact = readGuardArtifact(artifactPath);
592
631
  if (artifact) {
593
632
  const result = evaluateCompiledGuards(artifact, toolName, toolInput);
@@ -668,6 +707,7 @@ module.exports = {
668
707
  hashText,
669
708
  hasTwoKeywordHits,
670
709
  readJsonl,
710
+ getHybridPaths,
671
711
  PATHS,
672
712
  };
673
713
 
@@ -17,6 +17,7 @@ const path = require('path');
17
17
  const { resolveMcpEntry } = require('./mcp-config');
18
18
 
19
19
  const MCP_SERVER_KEY = 'thumbgate';
20
+ const LEGACY_MCP_SERVER_KEYS = ['mcp-memory-gateway', 'rlhf'];
20
21
  const PKG_ROOT = path.join(__dirname, '..');
21
22
  const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version;
22
23
 
@@ -29,8 +30,6 @@ function resolveMcpServerConfig(flags = {}) {
29
30
  });
30
31
  }
31
32
 
32
- const MCP_SERVER_CONFIG = resolveMcpServerConfig();
33
-
34
33
  function parseFlags(argv) {
35
34
  const flags = {};
36
35
  for (const arg of argv) {
@@ -80,9 +79,15 @@ function serverConfigMatches(entry, flags = {}) {
80
79
  }
81
80
 
82
81
  function isAlreadyInstalled(settings, flags = {}) {
82
+ const hasLegacyAliases = Boolean(
83
+ settings &&
84
+ settings.mcpServers &&
85
+ LEGACY_MCP_SERVER_KEYS.some((key) => Object.prototype.hasOwnProperty.call(settings.mcpServers, key))
86
+ );
83
87
  return !!(
84
88
  settings &&
85
89
  settings.mcpServers &&
90
+ !hasLegacyAliases &&
86
91
  serverConfigMatches(settings.mcpServers[MCP_SERVER_KEY], flags)
87
92
  );
88
93
  }
@@ -120,6 +125,11 @@ function installMcp(flags) {
120
125
  }
121
126
 
122
127
  settings.mcpServers[MCP_SERVER_KEY] = serverConfig;
128
+ for (const legacyKey of LEGACY_MCP_SERVER_KEYS) {
129
+ if (Object.prototype.hasOwnProperty.call(settings.mcpServers, legacyKey)) {
130
+ delete settings.mcpServers[legacyKey];
131
+ }
132
+ }
123
133
 
124
134
  // Ensure parent directory exists
125
135
  const dir = path.dirname(settingsPath);
@@ -142,7 +152,7 @@ function installMcp(flags) {
142
152
  // Exported for testing
143
153
  module.exports = {
144
154
  MCP_SERVER_KEY,
145
- MCP_SERVER_CONFIG,
155
+ LEGACY_MCP_SERVER_KEYS,
146
156
  resolveMcpServerConfig,
147
157
  resolveSettingsPath,
148
158
  loadSettings,
@@ -13,6 +13,7 @@ const {
13
13
  evaluateDelegation,
14
14
  normalizeDelegationMode,
15
15
  } = require('./delegation-runtime');
16
+ const { resolveFeedbackDir } = require('./feedback-paths');
16
17
 
17
18
  const PROJECT_ROOT = path.join(__dirname, '..');
18
19
  const DEFAULT_BUNDLE_DIR = path.join(PROJECT_ROOT, 'config', 'policy-bundles');
@@ -274,8 +275,7 @@ const ACTION_CATEGORY_MAP = {
274
275
  };
275
276
 
276
277
  function getDefaultModelPath() {
277
- const feedbackDir = process.env.THUMBGATE_FEEDBACK_DIR
278
- || path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
278
+ const feedbackDir = resolveFeedbackDir();
279
279
  return path.join(feedbackDir, 'feedback_model.json');
280
280
  }
281
281
 
@@ -3,42 +3,71 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const crypto = require('crypto');
5
5
 
6
- const LICENSE_PATH = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.thumbgate', 'license.json');
7
6
  const VALID_PREFIXES = ['tg_pro_', 'tg_'];
7
+ const LEGACY_COMPATIBLE_KEY = /^[a-z]{4,16}_[a-f0-9]{24,}$/i;
8
+
9
+ function getLicensePath(homeDir = process.env.HOME || process.env.USERPROFILE || '.') {
10
+ return path.join(homeDir, '.thumbgate', 'license.json');
11
+ }
12
+
13
+ const LICENSE_PATH = getLicensePath();
8
14
 
9
15
  function isValidKey(key) {
10
- return key && VALID_PREFIXES.some((p) => key.startsWith(p));
16
+ return Boolean(
17
+ key
18
+ && (
19
+ VALID_PREFIXES.some((p) => key.startsWith(p))
20
+ || LEGACY_COMPATIBLE_KEY.test(key)
21
+ )
22
+ );
11
23
  }
12
24
 
13
- function verifyLicense() {
14
- const envKey = process.env.THUMBGATE_API_KEY || process.env.THUMBGATE_PRO_KEY;
25
+ function verifyLicense(options = {}) {
26
+ const envKey = [
27
+ process.env.THUMBGATE_API_KEY,
28
+ process.env.THUMBGATE_PRO_KEY,
29
+ ...Object.entries(process.env)
30
+ .filter(([name]) => /(?:_API_KEY|_PRO_KEY)$/.test(name))
31
+ .map(([, value]) => value),
32
+ ].find((value) => isValidKey(value));
15
33
  if (isValidKey(envKey)) {
16
34
  return { valid: true, source: 'env', key: envKey };
17
35
  }
36
+
37
+ const licensePath = getLicensePath(options.homeDir);
18
38
  try {
19
- if (fs.existsSync(LICENSE_PATH)) {
20
- const data = JSON.parse(fs.readFileSync(LICENSE_PATH, 'utf8'));
39
+ if (fs.existsSync(licensePath)) {
40
+ const data = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
21
41
  if (isValidKey(data.key)) {
22
- return { valid: true, source: 'file', key: data.key, activatedAt: data.activatedAt };
42
+ return {
43
+ valid: true,
44
+ source: 'file',
45
+ key: data.key,
46
+ activatedAt: data.activatedAt,
47
+ path: licensePath,
48
+ };
23
49
  }
24
50
  }
25
51
  } catch (_) {}
52
+
26
53
  return { valid: false, source: null };
27
54
  }
28
55
 
29
- function isProLicensed() {
30
- return verifyLicense().valid;
56
+ function isProLicensed(options) {
57
+ return verifyLicense(options).valid;
31
58
  }
32
59
 
33
- function activateLicense(key) {
60
+ function activateLicense(key, options = {}) {
34
61
  if (!isValidKey(key)) {
35
62
  return { success: false, error: 'Invalid key format. Expected tg_... or tg_pro_...' };
36
63
  }
37
- const dir = path.dirname(LICENSE_PATH);
64
+
65
+ const licensePath = getLicensePath(options.homeDir);
66
+ const dir = path.dirname(licensePath);
38
67
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
39
68
  const data = { key, activatedAt: new Date().toISOString(), version: require('../package.json').version };
40
- fs.writeFileSync(LICENSE_PATH, JSON.stringify(data, null, 2));
41
- return { success: true, path: LICENSE_PATH };
69
+ fs.writeFileSync(licensePath, JSON.stringify(data, null, 2));
70
+ return { success: true, path: licensePath };
42
71
  }
43
72
 
44
73
  function generateLicenseKey(email) {
@@ -47,4 +76,13 @@ function generateLicenseKey(email) {
47
76
  return `tg_pro_${hash}`;
48
77
  }
49
78
 
50
- module.exports = { verifyLicense, isProLicensed, activateLicense, generateLicenseKey, isValidKey, VALID_PREFIXES, LICENSE_PATH };
79
+ module.exports = {
80
+ verifyLicense,
81
+ isProLicensed,
82
+ activateLicense,
83
+ generateLicenseKey,
84
+ isValidKey,
85
+ VALID_PREFIXES,
86
+ LICENSE_PATH,
87
+ getLicensePath,
88
+ };
@@ -4,9 +4,10 @@
4
4
  const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
+ const { resolveFeedbackDir: resolveSharedFeedbackDir } = require('./feedback-paths');
7
8
 
8
9
  const PROJECT_ROOT = path.join(__dirname, '..');
9
- const DEFAULT_FEEDBACK_DIR = path.join(PROJECT_ROOT, '.claude', 'memory', 'feedback');
10
+ const DEFAULT_FEEDBACK_DIR = resolveSharedFeedbackDir();
10
11
  const DEFAULT_EMBED_MODEL = 'Xenova/all-MiniLM-L6-v2';
11
12
 
12
13
  // ---------------------------------------------------------------------------
@@ -232,7 +233,7 @@ function recommendInferenceBackend(task = {}, env = process.env) {
232
233
  }
233
234
 
234
235
  function resolveFeedbackDir(explicitDir) {
235
- return explicitDir || process.env.THUMBGATE_FEEDBACK_DIR || DEFAULT_FEEDBACK_DIR;
236
+ return resolveSharedFeedbackDir({ feedbackDir: explicitDir });
236
237
  }
237
238
 
238
239
  function detectHardware(env = process.env) {