thumbgate 1.14.1 → 1.16.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 (150) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +60 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +217 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +211 -8
  12. package/config/enforcement.json +59 -7
  13. package/config/evals/agent-safety-eval.json +338 -22
  14. package/config/gates/default.json +33 -0
  15. package/config/gates/routine.json +43 -0
  16. package/config/github-about.json +3 -3
  17. package/config/mcp-allowlists.json +4 -0
  18. package/config/merge-quality-checks.json +2 -1
  19. package/config/model-candidates.json +131 -0
  20. package/openapi/openapi.yaml +118 -2
  21. package/package.json +70 -51
  22. package/public/blog.html +7 -7
  23. package/public/codex-plugin.html +13 -7
  24. package/public/compare.html +29 -23
  25. package/public/dashboard.html +105 -12
  26. package/public/guide.html +28 -28
  27. package/public/index.html +233 -97
  28. package/public/learn.html +87 -20
  29. package/public/lessons.html +26 -2
  30. package/public/numbers.html +271 -0
  31. package/public/pro.html +89 -19
  32. package/scripts/agent-audit-trace.js +55 -0
  33. package/scripts/agent-memory-lifecycle.js +96 -0
  34. package/scripts/agent-readiness-plan.js +118 -0
  35. package/scripts/agentic-data-pipeline.js +21 -1
  36. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  37. package/scripts/ai-org-governance.js +98 -0
  38. package/scripts/ai-search-distribution.js +43 -0
  39. package/scripts/artifact-agent-plan.js +81 -0
  40. package/scripts/billing.js +27 -8
  41. package/scripts/cli-feedback.js +2 -1
  42. package/scripts/cli-schema.js +60 -5
  43. package/scripts/code-mode-mcp-plan.js +71 -0
  44. package/scripts/commercial-offer.js +1 -1
  45. package/scripts/context-engine.js +1 -2
  46. package/scripts/context-manager.js +4 -1
  47. package/scripts/contextfs.js +214 -32
  48. package/scripts/dashboard-render-spec.js +1 -1
  49. package/scripts/dashboard.js +275 -9
  50. package/scripts/decision-journal.js +13 -3
  51. package/scripts/document-workflow-governance.js +62 -0
  52. package/scripts/enterprise-agent-rollout.js +34 -0
  53. package/scripts/experience-replay-governance.js +69 -0
  54. package/scripts/export-hf-dataset.js +1 -1
  55. package/scripts/feedback-loop.js +141 -9
  56. package/scripts/feedback-to-rules.js +17 -23
  57. package/scripts/gates-engine.js +4 -6
  58. package/scripts/growth-campaigns.js +49 -0
  59. package/scripts/harness-selector.js +145 -1
  60. package/scripts/hybrid-supervisor-agent.js +64 -0
  61. package/scripts/inference-cache-policy.js +72 -0
  62. package/scripts/inference-economics.js +53 -0
  63. package/scripts/internal-agent-bootstrap.js +12 -2
  64. package/scripts/knowledge-layer-plan.js +108 -0
  65. package/scripts/lesson-canonical.js +181 -0
  66. package/scripts/lesson-db.js +71 -10
  67. package/scripts/lesson-inference.js +183 -44
  68. package/scripts/lesson-search.js +4 -1
  69. package/scripts/lesson-synthesis.js +23 -2
  70. package/scripts/llm-client.js +157 -26
  71. package/scripts/mailer/resend-mailer.js +112 -1
  72. package/scripts/mcp-transport-strategy.js +66 -0
  73. package/scripts/memory-store-governance.js +60 -0
  74. package/scripts/meta-agent-loop.js +7 -13
  75. package/scripts/model-access-eligibility.js +38 -0
  76. package/scripts/model-migration-readiness.js +55 -0
  77. package/scripts/native-messaging-audit.js +514 -0
  78. package/scripts/operational-integrity.js +96 -3
  79. package/scripts/otel-declarative-config.js +56 -0
  80. package/scripts/perplexity-client.js +1 -1
  81. package/scripts/post-training-governance.js +34 -0
  82. package/scripts/pr-manager.js +47 -7
  83. package/scripts/private-core-boundary.js +72 -0
  84. package/scripts/production-agent-readiness.js +40 -0
  85. package/scripts/profile-router.js +16 -1
  86. package/scripts/prompt-eval.js +564 -32
  87. package/scripts/prompt-programs.js +93 -0
  88. package/scripts/provider-action-normalizer.js +585 -0
  89. package/scripts/rule-validator.js +285 -0
  90. package/scripts/scaling-law-claims.js +60 -0
  91. package/scripts/security-scanner.js +1 -1
  92. package/scripts/self-distill-agent.js +7 -32
  93. package/scripts/seo-gsd.js +400 -43
  94. package/scripts/skill-rag-router.js +53 -0
  95. package/scripts/spec-gate.js +1 -1
  96. package/scripts/student-consistent-training.js +73 -0
  97. package/scripts/synthetic-data-provenance.js +98 -0
  98. package/scripts/task-context-result.js +81 -0
  99. package/scripts/telemetry-analytics.js +149 -0
  100. package/scripts/thompson-sampling.js +2 -2
  101. package/scripts/token-savings.js +7 -6
  102. package/scripts/token-tco.js +46 -0
  103. package/scripts/tool-registry.js +75 -3
  104. package/scripts/verification-loop.js +10 -1
  105. package/scripts/verifier-scoring.js +71 -0
  106. package/scripts/workflow-sentinel.js +284 -28
  107. package/scripts/workspace-agent-routines.js +118 -0
  108. package/skills/thumbgate/SKILL.md +1 -1
  109. package/src/api/server.js +434 -120
  110. package/.claude-plugin/README.md +0 -170
  111. package/adapters/README.md +0 -12
  112. package/scripts/analytics-report.js +0 -328
  113. package/scripts/autonomous-workflow.js +0 -377
  114. package/scripts/billing-setup.js +0 -109
  115. package/scripts/creator-campaigns.js +0 -239
  116. package/scripts/cross-encoder-reranker.js +0 -235
  117. package/scripts/daemon-manager.js +0 -108
  118. package/scripts/decision-trace.js +0 -354
  119. package/scripts/delegation-runtime.js +0 -896
  120. package/scripts/dispatch-brief.js +0 -159
  121. package/scripts/distribution-surfaces.js +0 -110
  122. package/scripts/feedback-history-distiller.js +0 -382
  123. package/scripts/funnel-analytics.js +0 -35
  124. package/scripts/history-distiller.js +0 -200
  125. package/scripts/hosted-job-launcher.js +0 -256
  126. package/scripts/intent-router.js +0 -392
  127. package/scripts/lesson-reranker.js +0 -263
  128. package/scripts/lesson-retrieval.js +0 -148
  129. package/scripts/managed-lesson-agent.js +0 -183
  130. package/scripts/operational-dashboard.js +0 -103
  131. package/scripts/operational-summary.js +0 -129
  132. package/scripts/operator-artifacts.js +0 -608
  133. package/scripts/optimize-context.js +0 -17
  134. package/scripts/org-dashboard.js +0 -206
  135. package/scripts/partner-orchestration.js +0 -146
  136. package/scripts/predictive-insights.js +0 -356
  137. package/scripts/pulse.js +0 -80
  138. package/scripts/reflector-agent.js +0 -221
  139. package/scripts/sales-pipeline.js +0 -681
  140. package/scripts/session-episode-store.js +0 -329
  141. package/scripts/session-health-sensor.js +0 -242
  142. package/scripts/session-report.js +0 -120
  143. package/scripts/swarm-coordinator.js +0 -81
  144. package/scripts/tool-kpi-tracker.js +0 -12
  145. package/scripts/webhook-delivery.js +0 -62
  146. package/scripts/workflow-sprint-intake.js +0 -475
  147. package/skills/agent-memory/SKILL.md +0 -97
  148. package/skills/solve-architecture-autonomy/SKILL.md +0 -17
  149. package/skills/solve-architecture-autonomy/tool.js +0 -33
  150. package/skills/thumbgate-feedback/SKILL.md +0 -49
@@ -1,148 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * Per-action lesson retrieval.
6
- * v3: bi-encoder retrieval → cross-encoder reranking
7
- *
8
- * Stage 1 (bi-encoder): score all memories independently using token overlap,
9
- * bigram Jaccard, tool-name matching, and recency decay. Retrieve top-50.
10
- * Stage 2 (cross-encoder): rerank the top-50 candidates by computing a
11
- * field-weighted BM25 score that processes (query, lesson) jointly, then
12
- * blend with the original bi-encoder score. Return top-maxResults.
13
- */
14
-
15
- const RECENCY_DECAY_DAYS = 30;
16
- const RERANK_CANDIDATE_POOL = 50; // bi-encoder retrieves this many; reranker picks topK
17
-
18
- function retrieveRelevantLessons(toolName, actionContext, options = {}) {
19
- const { maxResults = 5, feedbackDir } = options;
20
- const { getFeedbackPaths, readJSONL } = require('./feedback-loop');
21
- const { rerankLessons } = require('./lesson-reranker');
22
- const pathMod = require('path');
23
- const paths = feedbackDir
24
- ? { MEMORY_LOG_PATH: pathMod.join(feedbackDir, 'memory-log.jsonl') }
25
- : getFeedbackPaths();
26
-
27
- const memories = readJSONL(paths.MEMORY_LOG_PATH, { maxLines: 200 });
28
- if (memories.length === 0) return [];
29
-
30
- const actionSig = buildActionSignature(toolName, actionContext);
31
-
32
- // Stage 1 — bi-encoder: score all memories independently, take top-50 candidates
33
- const candidates = memories
34
- .map((mem) => ({
35
- ...mem,
36
- relevanceScore: scoreRelevance(mem, toolName, actionContext, actionSig),
37
- }))
38
- .filter((m) => m.relevanceScore > 0.1)
39
- .sort((a, b) => b.relevanceScore - a.relevanceScore)
40
- .slice(0, RERANK_CANDIDATE_POOL);
41
-
42
- if (candidates.length === 0) return [];
43
-
44
- // Stage 2 — cross-encoder reranker: rerank candidates by joint (query, lesson) score
45
- const reranked = rerankLessons(actionContext, candidates, {
46
- topK: maxResults,
47
- toolName,
48
- });
49
-
50
- return reranked.map((m) => ({
51
- id: m.id,
52
- title: m.title,
53
- content: m.content,
54
- signal: m.tags?.includes('negative') ? 'negative' : 'positive',
55
- rule: m.structuredRule || null,
56
- relevanceScore: m.rerankedScore ?? m.relevanceScore,
57
- timestamp: m.timestamp,
58
- }));
59
- }
60
-
61
- function buildActionSignature(toolName, actionContext) {
62
- const toolLower = (toolName || '').toLowerCase();
63
- const contextLower = (actionContext || '').toLowerCase();
64
- const sigPaths = extractPaths(actionContext);
65
- const tokens = tokenize(contextLower);
66
- const ngramSet = textBigrams(contextLower);
67
- return { toolLower, contextLower, paths: sigPaths, tokens, ngramSet };
68
- }
69
-
70
- function textBigrams(text) {
71
- const normalized = (text || '')
72
- .toLowerCase()
73
- .replace(/[^a-z0-9\s]/g, ' ')
74
- .replace(/\s+/g, ' ')
75
- .trim();
76
- const set = new Set();
77
- for (let i = 0; i < normalized.length - 1; i++) {
78
- set.add(normalized.slice(i, i + 2));
79
- }
80
- return set;
81
- }
82
-
83
- function bigramJaccard(setA, setB) {
84
- if (setA.size === 0 && setB.size === 0) return 1;
85
- if (setA.size === 0 || setB.size === 0) return 0;
86
- let intersection = 0;
87
- for (const item of setA) {
88
- if (setB.has(item)) intersection++;
89
- }
90
- const union = setA.size + setB.size - intersection;
91
- return union === 0 ? 0 : intersection / union;
92
- }
93
-
94
- function scoreRelevance(memory, toolName, actionContext, actionSig) {
95
- const sig = actionSig || buildActionSignature(toolName, actionContext);
96
- let score = 0;
97
-
98
- const memText = ((memory.title || '') + ' ' + (memory.content || '') + ' ' + (memory.tags || []).join(' ')).toLowerCase();
99
-
100
- if (memory.metadata?.toolsUsed?.some((t) => t.toLowerCase() === sig.toolLower)) score += 0.4;
101
- if (memText.includes(sig.toolLower)) score += 0.2;
102
-
103
- const memPaths = memory.metadata?.filesInvolved || extractPaths(memText);
104
- const pathOverlap = sig.paths.filter((p) =>
105
- memPaths.some((mp) => mp.includes(p) || p.includes(mp)),
106
- );
107
- if (pathOverlap.length > 0) score += 0.3;
108
-
109
- const memTokens = tokenize(memText);
110
- const overlap = sig.tokens.filter((t) => memTokens.includes(t));
111
- score += Math.min(overlap.length * 0.05, 0.3);
112
-
113
- // Fuzzy n-gram matching (only when there is already signal)
114
- if (score > 0) {
115
- const memBigrams = textBigrams(memText);
116
- const fuzzyScore = bigramJaccard(sig.ngramSet, memBigrams);
117
- score += fuzzyScore * 0.2;
118
- }
119
-
120
- if (memory.tags?.includes('negative')) score += 0.1;
121
-
122
- if (memory.timestamp) {
123
- const ageMs = Date.now() - new Date(memory.timestamp).getTime();
124
- const ageDays = ageMs / (1000 * 60 * 60 * 24);
125
- const decay = Math.max(0, 1 - ageDays / RECENCY_DECAY_DAYS);
126
- score *= 0.5 + 0.5 * decay;
127
- }
128
-
129
- if (memory.structuredRule) score += 0.15;
130
-
131
- return score;
132
- }
133
-
134
- function extractPaths(text) {
135
- return [...new Set((text || '').match(/(?:src\/|scripts\/|tests\/)[^\s,)'"<>]+/g) || [])];
136
- }
137
-
138
- function tokenize(text) {
139
- return (text || '').split(/[\s.,;:!?()\[\]{}"'`]+/).filter((t) => t.length > 3);
140
- }
141
-
142
- module.exports = {
143
- retrieveRelevantLessons,
144
- scoreRelevance,
145
- buildActionSignature,
146
- textBigrams,
147
- bigramJaccard,
148
- };
@@ -1,183 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
- const { resolveFeedbackDir } = require('./feedback-paths');
8
- const { parseFeedbackFile, classifySignal, analyzeWithLLM, analyze, promoteToGates } = require('./feedback-to-rules');
9
- const { inferStructuredLessonLLM, inferStructuredLesson, createLesson } = require('./lesson-inference');
10
- const { isAvailable } = require('./llm-client');
11
-
12
- const MAX_ENTRIES_PER_RUN = 20;
13
- const DELAY_BETWEEN_CALLS_MS = 500;
14
- const MANIFEST_DIR = path.join(os.homedir(), '.thumbgate');
15
- const MANIFEST_PATH = path.join(MANIFEST_DIR, 'managed-agent-runs.jsonl');
16
-
17
- function sleep(ms) {
18
- return new Promise((resolve) => setTimeout(resolve, ms));
19
- }
20
-
21
- function getProcessedIds() {
22
- if (!fs.existsSync(MANIFEST_PATH)) return new Set();
23
- const ids = new Set();
24
- for (const line of fs.readFileSync(MANIFEST_PATH, 'utf8').split('\n')) {
25
- const trimmed = line.trim();
26
- if (!trimmed) continue;
27
- try {
28
- const run = JSON.parse(trimmed);
29
- if (Array.isArray(run.processedIds)) {
30
- for (const id of run.processedIds) ids.add(id);
31
- }
32
- } catch { /* skip */ }
33
- }
34
- return ids;
35
- }
36
-
37
- function writeManifest(manifest) {
38
- fs.mkdirSync(MANIFEST_DIR, { recursive: true });
39
- fs.appendFileSync(MANIFEST_PATH, JSON.stringify(manifest) + '\n');
40
- }
41
-
42
- function getManagedAgentStatus() {
43
- if (!fs.existsSync(MANIFEST_PATH)) return null;
44
- const lines = fs.readFileSync(MANIFEST_PATH, 'utf8').split('\n').filter(Boolean);
45
- if (lines.length === 0) return null;
46
- try {
47
- const last = JSON.parse(lines[lines.length - 1]);
48
- return {
49
- lastRun: last.runAt,
50
- entriesProcessed: last.entriesProcessed,
51
- lessonsCreated: last.lessonsCreated,
52
- gatesPromoted: last.gatesPromoted,
53
- model: last.model,
54
- durationMs: last.durationMs,
55
- totalRuns: lines.length,
56
- };
57
- } catch {
58
- return null;
59
- }
60
- }
61
-
62
- async function runManagedAgent({ dryRun = false, limit, model } = {}) {
63
- const startTime = Date.now();
64
- const feedbackDir = resolveFeedbackDir();
65
- const logPath = path.join(feedbackDir, 'feedback-log.jsonl');
66
- const entries = parseFeedbackFile(logPath);
67
-
68
- if (entries.length === 0) {
69
- return { entriesProcessed: 0, lessonsCreated: 0, gatesPromoted: 0, model: 'none', durationMs: 0, message: 'No feedback entries found' };
70
- }
71
-
72
- const processedIds = getProcessedIds();
73
- const pending = entries
74
- .filter((e) => {
75
- const id = e.id || e.feedbackId || e.timestamp;
76
- return id && !processedIds.has(id);
77
- })
78
- .slice(0, limit || MAX_ENTRIES_PER_RUN);
79
-
80
- if (pending.length === 0) {
81
- return { entriesProcessed: 0, lessonsCreated: 0, gatesPromoted: 0, model: 'none', durationMs: Date.now() - startTime, message: 'All entries already processed' };
82
- }
83
-
84
- const useLLM = isAvailable();
85
- const modelUsed = useLLM ? 'claude-haiku-4-5' : 'heuristic';
86
- let lessonsCreated = 0;
87
- const newProcessedIds = [];
88
-
89
- for (const entry of pending) {
90
- const id = entry.id || entry.feedbackId || entry.timestamp;
91
- const signal = classifySignal(entry);
92
- if (!signal) {
93
- newProcessedIds.push(id);
94
- continue;
95
- }
96
-
97
- const window = Array.isArray(entry.conversationWindow) ? entry.conversationWindow : [];
98
- const context = entry.context || '';
99
-
100
- let structuredLesson = null;
101
- if (useLLM) {
102
- structuredLesson = await inferStructuredLessonLLM(window, signal, context);
103
- if (structuredLesson && !dryRun) {
104
- await sleep(DELAY_BETWEEN_CALLS_MS);
105
- }
106
- }
107
-
108
- if (!structuredLesson) {
109
- structuredLesson = inferStructuredLesson(window, signal, context);
110
- }
111
-
112
- if (!dryRun && structuredLesson) {
113
- try {
114
- createLesson({
115
- feedbackId: id,
116
- signal,
117
- inferredLesson: structuredLesson.action?.description || '',
118
- triggerMessage: structuredLesson.examples?.[0]?.assistantAction || '',
119
- priorSummary: '',
120
- confidence: structuredLesson.confidence || 0.5,
121
- tags: structuredLesson.tags || entry.tags || [],
122
- metadata: { ...structuredLesson.metadata, managedAgent: true, format: structuredLesson.format },
123
- });
124
- lessonsCreated++;
125
- } catch { /* lesson creation is best-effort */ }
126
- } else if (dryRun && structuredLesson) {
127
- lessonsCreated++;
128
- }
129
-
130
- newProcessedIds.push(id);
131
- }
132
-
133
- // Rule generation pass
134
- let gatesPromoted = 0;
135
- if (useLLM) {
136
- const llmIssues = await analyzeWithLLM(entries);
137
- if (llmIssues && llmIssues.length > 0) {
138
- if (!dryRun) {
139
- promoteToGates(llmIssues);
140
- }
141
- gatesPromoted = llmIssues.filter((i) => i.severity === 'critical').length;
142
- }
143
- } else {
144
- const report = analyze(entries);
145
- gatesPromoted = report.recurringIssues.filter((i) => i.severity === 'critical').length;
146
- }
147
-
148
- const manifest = {
149
- runAt: new Date().toISOString(),
150
- entriesProcessed: pending.length,
151
- lessonsCreated,
152
- gatesPromoted,
153
- model: modelUsed,
154
- dryRun,
155
- durationMs: Date.now() - startTime,
156
- processedIds: newProcessedIds,
157
- };
158
-
159
- if (!dryRun) {
160
- writeManifest(manifest);
161
- }
162
-
163
- return manifest;
164
- }
165
-
166
- if (require.main === module) {
167
- const args = process.argv.slice(2);
168
- const dryRun = args.includes('--dry-run');
169
- const limitFlag = args.find((a) => a.startsWith('--limit'));
170
- const limit = limitFlag ? Number(args[args.indexOf(limitFlag) + 1]) || MAX_ENTRIES_PER_RUN : undefined;
171
-
172
- runManagedAgent({ dryRun, limit })
173
- .then((result) => {
174
- console.log(JSON.stringify(result, null, 2));
175
- process.exit(0);
176
- })
177
- .catch((err) => {
178
- console.error('Managed agent error:', err.message);
179
- process.exit(1);
180
- });
181
- }
182
-
183
- module.exports = { runManagedAgent, getManagedAgentStatus };
@@ -1,103 +0,0 @@
1
- 'use strict';
2
-
3
- const { resolveAnalyticsWindow } = require('./analytics-window');
4
- const { getBillingSummaryLive } = require('./billing');
5
- const { generateDashboard } = require('./dashboard');
6
- const { getFeedbackPaths } = require('./feedback-loop');
7
- const { resolveHostedBillingConfig } = require('./hosted-config');
8
-
9
- function normalizeText(value) {
10
- if (value === undefined || value === null) return null;
11
- const text = String(value).trim();
12
- return text || null;
13
- }
14
-
15
- function shouldPreferHostedDashboard() {
16
- return String(process.env.THUMBGATE_METRICS_SOURCE || '').trim().toLowerCase() !== 'local';
17
- }
18
-
19
- function resolveHostedDashboardConfig() {
20
- const runtimeConfig = resolveHostedBillingConfig();
21
- const apiBaseUrl = normalizeText(process.env.THUMBGATE_BILLING_API_BASE_URL) || runtimeConfig.billingApiBaseUrl;
22
- const apiKey = normalizeText(process.env.THUMBGATE_API_KEY);
23
- return {
24
- apiBaseUrl,
25
- apiKey,
26
- };
27
- }
28
-
29
- async function buildOperationalDashboard(options = {}) {
30
- const analyticsWindow = resolveAnalyticsWindow(options);
31
- const feedbackDir = options.feedbackDir || getFeedbackPaths().FEEDBACK_DIR;
32
- const billingSummary = await getBillingSummaryLive(analyticsWindow);
33
-
34
- return generateDashboard(feedbackDir, {
35
- analyticsWindow,
36
- billingSummary,
37
- billingSource: 'live',
38
- billingFallbackReason: null,
39
- });
40
- }
41
-
42
- async function fetchHostedDashboard(options = {}, config = resolveHostedDashboardConfig()) {
43
- const analyticsWindow = resolveAnalyticsWindow(options);
44
- if (!shouldPreferHostedDashboard()) {
45
- const err = new Error('Hosted operational dashboard is disabled.');
46
- err.code = 'hosted_dashboard_disabled';
47
- throw err;
48
- }
49
- if (!config.apiBaseUrl || !config.apiKey) {
50
- const err = new Error('Hosted operational dashboard is not configured.');
51
- err.code = 'hosted_dashboard_unconfigured';
52
- throw err;
53
- }
54
-
55
- const requestUrl = new URL('/v1/dashboard', config.apiBaseUrl);
56
- requestUrl.searchParams.set('window', analyticsWindow.window);
57
- requestUrl.searchParams.set('timezone', analyticsWindow.timeZone);
58
- requestUrl.searchParams.set('now', analyticsWindow.now);
59
-
60
- const response = await fetch(requestUrl, {
61
- method: 'GET',
62
- headers: {
63
- authorization: `Bearer ${config.apiKey}`,
64
- accept: 'application/json',
65
- },
66
- });
67
-
68
- if (!response.ok) {
69
- const detail = await response.text().catch(() => '');
70
- const err = new Error(`Hosted operational dashboard request failed (${response.status}): ${detail || 'unknown error'}`);
71
- err.code = 'hosted_dashboard_http_error';
72
- err.status = response.status;
73
- throw err;
74
- }
75
-
76
- return response.json();
77
- }
78
-
79
- async function getOperationalDashboard(options = {}) {
80
- const analyticsWindow = resolveAnalyticsWindow(options);
81
- try {
82
- const data = await fetchHostedDashboard(analyticsWindow);
83
- return {
84
- source: 'hosted',
85
- data,
86
- fallbackReason: null,
87
- };
88
- } catch (err) {
89
- return {
90
- source: 'local',
91
- data: await buildOperationalDashboard(analyticsWindow),
92
- fallbackReason: err && err.message ? err.message : 'hosted_dashboard_unavailable',
93
- };
94
- }
95
- }
96
-
97
- module.exports = {
98
- buildOperationalDashboard,
99
- fetchHostedDashboard,
100
- getOperationalDashboard,
101
- resolveHostedDashboardConfig,
102
- shouldPreferHostedDashboard,
103
- };
@@ -1,129 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const os = require('node:os');
6
- const { getBillingSummaryLive } = require('./billing');
7
- const { resolveAnalyticsWindow } = require('./analytics-window');
8
- const { resolveHostedBillingConfig } = require('./hosted-config');
9
-
10
- // Configure fetch proxy when running behind a corporate/sandbox proxy
11
- (function configureProxy() {
12
- const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy
13
- || process.env.HTTP_PROXY || process.env.http_proxy;
14
- if (!proxyUrl) return;
15
- try {
16
- const { ProxyAgent, setGlobalDispatcher } = require('undici');
17
- setGlobalDispatcher(new ProxyAgent(proxyUrl));
18
- } catch {
19
- // undici not available — fetch will use default dispatcher
20
- }
21
- }());
22
-
23
- const OPERATOR_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
24
-
25
- function normalizeText(value) {
26
- if (value === undefined || value === null) return null;
27
- const text = String(value).trim();
28
- return text || null;
29
- }
30
-
31
- function loadOperatorConfig(configPath = OPERATOR_CONFIG_PATH) {
32
- try {
33
- const raw = fs.readFileSync(configPath, 'utf8');
34
- const parsed = JSON.parse(raw);
35
- return {
36
- operatorKey: normalizeText(parsed.operatorKey),
37
- baseUrl: normalizeText(parsed.baseUrl),
38
- };
39
- } catch {
40
- return { operatorKey: null, baseUrl: null };
41
- }
42
- }
43
-
44
- function shouldPreferHostedSummary() {
45
- return String(process.env.THUMBGATE_METRICS_SOURCE || '').trim().toLowerCase() !== 'local';
46
- }
47
-
48
- function resolveHostedSummaryConfig() {
49
- const runtimeConfig = resolveHostedBillingConfig();
50
- const operatorConfig = loadOperatorConfig();
51
- // Priority: env THUMBGATE_OPERATOR_KEY > local config file > env THUMBGATE_API_KEY
52
- const apiKey = normalizeText(process.env.THUMBGATE_OPERATOR_KEY)
53
- || operatorConfig.operatorKey
54
- || normalizeText(process.env.THUMBGATE_API_KEY);
55
- const apiBaseUrl = normalizeText(process.env.THUMBGATE_BILLING_API_BASE_URL)
56
- || operatorConfig.baseUrl
57
- || runtimeConfig.billingApiBaseUrl;
58
- return {
59
- apiBaseUrl,
60
- apiKey,
61
- };
62
- }
63
-
64
- async function fetchHostedBillingSummary(options = {}, config = resolveHostedSummaryConfig()) {
65
- const analyticsWindow = resolveAnalyticsWindow(options);
66
- if (!shouldPreferHostedSummary()) {
67
- const err = new Error('Hosted operational summary is disabled.');
68
- err.code = 'hosted_summary_disabled';
69
- throw err;
70
- }
71
- if (!config.apiBaseUrl || !config.apiKey) {
72
- const err = new Error('Hosted operational summary is not configured.');
73
- err.code = 'hosted_summary_unconfigured';
74
- throw err;
75
- }
76
-
77
- const requestUrl = new URL('/v1/billing/summary', config.apiBaseUrl);
78
- requestUrl.searchParams.set('window', analyticsWindow.window);
79
- requestUrl.searchParams.set('timezone', analyticsWindow.timeZone);
80
- if (options.now !== undefined && options.now !== null && options.now !== '') {
81
- requestUrl.searchParams.set('now', analyticsWindow.now);
82
- }
83
-
84
- const response = await fetch(requestUrl, {
85
- method: 'GET',
86
- headers: {
87
- authorization: `Bearer ${config.apiKey}`,
88
- accept: 'application/json',
89
- },
90
- });
91
-
92
- if (!response.ok) {
93
- const detail = await response.text().catch(() => '');
94
- const err = new Error(`Hosted operational summary request failed (${response.status}): ${detail || 'unknown error'}`);
95
- err.code = 'hosted_summary_http_error';
96
- err.status = response.status;
97
- throw err;
98
- }
99
-
100
- return response.json();
101
- }
102
-
103
- async function getOperationalBillingSummary(options = {}) {
104
- const analyticsWindow = resolveAnalyticsWindow(options);
105
- try {
106
- const summary = await fetchHostedBillingSummary(analyticsWindow);
107
- return {
108
- source: 'hosted',
109
- summary,
110
- fallbackReason: null,
111
- };
112
- } catch (err) {
113
- const reason = err && err.message ? err.message : 'hosted_summary_unavailable';
114
- console.warn(`[operational-summary] Hosted billing unavailable — falling back to local state. Reason: ${reason}`);
115
- return {
116
- source: 'local',
117
- summary: await getBillingSummaryLive(analyticsWindow),
118
- fallbackReason: reason,
119
- };
120
- }
121
- }
122
-
123
- module.exports = {
124
- fetchHostedBillingSummary,
125
- getOperationalBillingSummary,
126
- resolveHostedSummaryConfig,
127
- shouldPreferHostedSummary,
128
- loadOperatorConfig,
129
- };