thumbgate 1.15.0 → 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 (129) 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 +59 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +210 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +157 -8
  12. package/config/evals/agent-safety-eval.json +338 -22
  13. package/config/gates/routine.json +43 -0
  14. package/config/github-about.json +3 -3
  15. package/config/model-candidates.json +131 -0
  16. package/openapi/openapi.yaml +118 -2
  17. package/package.json +55 -48
  18. package/public/blog.html +7 -7
  19. package/public/codex-plugin.html +6 -6
  20. package/public/compare.html +29 -23
  21. package/public/dashboard.html +82 -10
  22. package/public/guide.html +28 -28
  23. package/public/index.html +216 -98
  24. package/public/learn.html +50 -22
  25. package/public/lessons.html +1 -1
  26. package/public/numbers.html +17 -17
  27. package/public/pro.html +82 -18
  28. package/scripts/agent-audit-trace.js +55 -0
  29. package/scripts/agent-memory-lifecycle.js +96 -0
  30. package/scripts/agent-readiness-plan.js +118 -0
  31. package/scripts/agentic-data-pipeline.js +21 -1
  32. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  33. package/scripts/ai-org-governance.js +98 -0
  34. package/scripts/ai-search-distribution.js +43 -0
  35. package/scripts/artifact-agent-plan.js +81 -0
  36. package/scripts/billing.js +27 -8
  37. package/scripts/cli-schema.js +18 -2
  38. package/scripts/code-mode-mcp-plan.js +71 -0
  39. package/scripts/context-engine.js +1 -2
  40. package/scripts/context-manager.js +4 -1
  41. package/scripts/dashboard-render-spec.js +1 -1
  42. package/scripts/dashboard.js +275 -9
  43. package/scripts/decision-journal.js +13 -3
  44. package/scripts/document-workflow-governance.js +62 -0
  45. package/scripts/enterprise-agent-rollout.js +34 -0
  46. package/scripts/experience-replay-governance.js +69 -0
  47. package/scripts/export-hf-dataset.js +1 -1
  48. package/scripts/feedback-loop.js +92 -4
  49. package/scripts/feedback-to-rules.js +17 -23
  50. package/scripts/gates-engine.js +4 -6
  51. package/scripts/growth-campaigns.js +49 -0
  52. package/scripts/harness-selector.js +16 -4
  53. package/scripts/hybrid-supervisor-agent.js +64 -0
  54. package/scripts/inference-cache-policy.js +72 -0
  55. package/scripts/inference-economics.js +53 -0
  56. package/scripts/internal-agent-bootstrap.js +12 -2
  57. package/scripts/knowledge-layer-plan.js +108 -0
  58. package/scripts/lesson-inference.js +183 -44
  59. package/scripts/lesson-search.js +4 -1
  60. package/scripts/llm-client.js +157 -26
  61. package/scripts/mailer/resend-mailer.js +112 -1
  62. package/scripts/mcp-transport-strategy.js +66 -0
  63. package/scripts/memory-store-governance.js +60 -0
  64. package/scripts/meta-agent-loop.js +7 -13
  65. package/scripts/model-access-eligibility.js +38 -0
  66. package/scripts/model-migration-readiness.js +55 -0
  67. package/scripts/operational-integrity.js +96 -3
  68. package/scripts/otel-declarative-config.js +56 -0
  69. package/scripts/perplexity-client.js +1 -1
  70. package/scripts/post-training-governance.js +34 -0
  71. package/scripts/private-core-boundary.js +72 -0
  72. package/scripts/production-agent-readiness.js +40 -0
  73. package/scripts/prompt-eval.js +564 -32
  74. package/scripts/prompt-programs.js +93 -0
  75. package/scripts/provider-action-normalizer.js +585 -0
  76. package/scripts/scaling-law-claims.js +60 -0
  77. package/scripts/security-scanner.js +1 -1
  78. package/scripts/self-distill-agent.js +7 -32
  79. package/scripts/seo-gsd.js +232 -55
  80. package/scripts/skill-rag-router.js +53 -0
  81. package/scripts/spec-gate.js +1 -1
  82. package/scripts/student-consistent-training.js +73 -0
  83. package/scripts/synthetic-data-provenance.js +98 -0
  84. package/scripts/task-context-result.js +81 -0
  85. package/scripts/telemetry-analytics.js +149 -0
  86. package/scripts/thompson-sampling.js +2 -2
  87. package/scripts/token-savings.js +7 -6
  88. package/scripts/token-tco.js +46 -0
  89. package/scripts/tool-registry.js +63 -3
  90. package/scripts/verification-loop.js +10 -1
  91. package/scripts/verifier-scoring.js +71 -0
  92. package/scripts/workflow-sentinel.js +284 -28
  93. package/scripts/workspace-agent-routines.js +118 -0
  94. package/src/api/server.js +381 -120
  95. package/scripts/analytics-report.js +0 -328
  96. package/scripts/autonomous-workflow.js +0 -377
  97. package/scripts/billing-setup.js +0 -109
  98. package/scripts/creator-campaigns.js +0 -239
  99. package/scripts/cross-encoder-reranker.js +0 -235
  100. package/scripts/daemon-manager.js +0 -108
  101. package/scripts/decision-trace.js +0 -354
  102. package/scripts/delegation-runtime.js +0 -896
  103. package/scripts/dispatch-brief.js +0 -159
  104. package/scripts/distribution-surfaces.js +0 -110
  105. package/scripts/feedback-history-distiller.js +0 -382
  106. package/scripts/funnel-analytics.js +0 -35
  107. package/scripts/history-distiller.js +0 -200
  108. package/scripts/hosted-job-launcher.js +0 -256
  109. package/scripts/intent-router.js +0 -392
  110. package/scripts/lesson-reranker.js +0 -263
  111. package/scripts/lesson-retrieval.js +0 -148
  112. package/scripts/managed-lesson-agent.js +0 -183
  113. package/scripts/operational-dashboard.js +0 -103
  114. package/scripts/operational-summary.js +0 -129
  115. package/scripts/operator-artifacts.js +0 -608
  116. package/scripts/optimize-context.js +0 -17
  117. package/scripts/org-dashboard.js +0 -206
  118. package/scripts/partner-orchestration.js +0 -146
  119. package/scripts/predictive-insights.js +0 -356
  120. package/scripts/pulse.js +0 -80
  121. package/scripts/reflector-agent.js +0 -221
  122. package/scripts/sales-pipeline.js +0 -681
  123. package/scripts/session-episode-store.js +0 -329
  124. package/scripts/session-health-sensor.js +0 -242
  125. package/scripts/session-report.js +0 -120
  126. package/scripts/swarm-coordinator.js +0 -81
  127. package/scripts/tool-kpi-tracker.js +0 -12
  128. package/scripts/webhook-delivery.js +0 -62
  129. package/scripts/workflow-sprint-intake.js +0 -475
@@ -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
- };