thumbgate 1.15.0 → 1.16.1
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.
- package/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +3 -3
- package/.well-known/llms.txt +5 -5
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +59 -35
- package/adapters/chatgpt/openapi.yaml +118 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +210 -84
- package/adapters/opencode/opencode.json +1 -1
- package/bench/prompt-eval-suite.json +5 -1
- package/bin/cli.js +157 -8
- package/config/evals/agent-safety-eval.json +338 -22
- package/config/gates/routine.json +43 -0
- package/config/github-about.json +3 -3
- package/config/model-candidates.json +131 -0
- package/openapi/openapi.yaml +118 -2
- package/package.json +57 -49
- package/public/blog.html +7 -7
- package/public/codex-plugin.html +6 -6
- package/public/compare.html +29 -23
- package/public/dashboard.html +82 -10
- package/public/guide.html +28 -28
- package/public/index.html +216 -98
- package/public/learn.html +50 -22
- package/public/lessons.html +1 -1
- package/public/numbers.html +17 -17
- package/public/pro.html +82 -18
- package/scripts/agent-audit-trace.js +55 -0
- package/scripts/agent-memory-lifecycle.js +96 -0
- package/scripts/agent-readiness-plan.js +118 -0
- package/scripts/agentic-data-pipeline.js +21 -1
- package/scripts/agents-sdk-sandbox-plan.js +57 -0
- package/scripts/ai-org-governance.js +98 -0
- package/scripts/ai-search-distribution.js +43 -0
- package/scripts/artifact-agent-plan.js +81 -0
- package/scripts/billing.js +27 -8
- package/scripts/cli-schema.js +18 -2
- package/scripts/code-mode-mcp-plan.js +71 -0
- package/scripts/context-engine.js +1 -2
- package/scripts/context-manager.js +4 -1
- package/scripts/dashboard-render-spec.js +1 -1
- package/scripts/dashboard.js +275 -9
- package/scripts/decision-journal.js +13 -3
- package/scripts/document-workflow-governance.js +62 -0
- package/scripts/enterprise-agent-rollout.js +34 -0
- package/scripts/experience-replay-governance.js +69 -0
- package/scripts/export-hf-dataset.js +1 -1
- package/scripts/feedback-loop.js +92 -4
- package/scripts/feedback-to-rules.js +17 -23
- package/scripts/gates-engine.js +4 -6
- package/scripts/growth-campaigns.js +49 -0
- package/scripts/harness-selector.js +16 -4
- package/scripts/hybrid-supervisor-agent.js +64 -0
- package/scripts/inference-cache-policy.js +72 -0
- package/scripts/inference-economics.js +53 -0
- package/scripts/internal-agent-bootstrap.js +12 -2
- package/scripts/knowledge-layer-plan.js +108 -0
- package/scripts/lesson-inference.js +183 -44
- package/scripts/lesson-search.js +4 -1
- package/scripts/llm-client.js +157 -26
- package/scripts/mailer/resend-mailer.js +112 -1
- package/scripts/mcp-transport-strategy.js +66 -0
- package/scripts/memory-store-governance.js +60 -0
- package/scripts/meta-agent-loop.js +7 -13
- package/scripts/model-access-eligibility.js +38 -0
- package/scripts/model-migration-readiness.js +55 -0
- package/scripts/operational-integrity.js +96 -3
- package/scripts/otel-declarative-config.js +56 -0
- package/scripts/perplexity-client.js +1 -1
- package/scripts/post-training-governance.js +34 -0
- package/scripts/private-core-boundary.js +72 -0
- package/scripts/production-agent-readiness.js +40 -0
- package/scripts/prompt-eval.js +564 -32
- package/scripts/prompt-programs.js +93 -0
- package/scripts/provider-action-normalizer.js +585 -0
- package/scripts/scaling-law-claims.js +60 -0
- package/scripts/security-scanner.js +1 -1
- package/scripts/self-distill-agent.js +7 -32
- package/scripts/seo-gsd.js +232 -55
- package/scripts/skill-rag-router.js +53 -0
- package/scripts/spec-gate.js +1 -1
- package/scripts/student-consistent-training.js +73 -0
- package/scripts/synthetic-data-provenance.js +98 -0
- package/scripts/task-context-result.js +81 -0
- package/scripts/telemetry-analytics.js +149 -0
- package/scripts/thompson-sampling.js +2 -2
- package/scripts/token-savings.js +7 -6
- package/scripts/token-tco.js +46 -0
- package/scripts/tool-registry.js +63 -3
- package/scripts/verification-loop.js +10 -1
- package/scripts/verifier-scoring.js +71 -0
- package/scripts/workflow-sentinel.js +284 -28
- package/scripts/workspace-agent-routines.js +118 -0
- package/src/api/server.js +381 -120
- package/scripts/analytics-report.js +0 -328
- package/scripts/autonomous-workflow.js +0 -377
- package/scripts/billing-setup.js +0 -109
- package/scripts/creator-campaigns.js +0 -239
- package/scripts/cross-encoder-reranker.js +0 -235
- package/scripts/daemon-manager.js +0 -108
- package/scripts/decision-trace.js +0 -354
- package/scripts/delegation-runtime.js +0 -896
- package/scripts/dispatch-brief.js +0 -159
- package/scripts/distribution-surfaces.js +0 -110
- package/scripts/feedback-history-distiller.js +0 -382
- package/scripts/funnel-analytics.js +0 -35
- package/scripts/history-distiller.js +0 -200
- package/scripts/hosted-job-launcher.js +0 -256
- package/scripts/intent-router.js +0 -392
- package/scripts/lesson-reranker.js +0 -263
- package/scripts/lesson-retrieval.js +0 -148
- package/scripts/managed-lesson-agent.js +0 -183
- package/scripts/operational-dashboard.js +0 -103
- package/scripts/operational-summary.js +0 -129
- package/scripts/operator-artifacts.js +0 -608
- package/scripts/optimize-context.js +0 -17
- package/scripts/org-dashboard.js +0 -206
- package/scripts/partner-orchestration.js +0 -146
- package/scripts/predictive-insights.js +0 -356
- package/scripts/pulse.js +0 -80
- package/scripts/reflector-agent.js +0 -221
- package/scripts/sales-pipeline.js +0 -681
- package/scripts/session-episode-store.js +0 -329
- package/scripts/session-health-sensor.js +0 -242
- package/scripts/session-report.js +0 -120
- package/scripts/swarm-coordinator.js +0 -81
- package/scripts/tool-kpi-tracker.js +0 -12
- package/scripts/webhook-delivery.js +0 -62
- 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
|
-
};
|