thumbgate 1.27.16 → 1.27.18
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 +85 -0
- package/.claude-plugin/plugin.json +3 -4
- package/.well-known/llms.txt +12 -33
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +30 -215
- package/adapters/chatgpt/openapi.yaml +1695 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gemini/function-declarations.json +0 -1
- package/adapters/mcp/server-stdio.js +11 -263
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +122 -1421
- package/bin/postinstall.js +27 -11
- package/config/gate-templates.json +0 -216
- package/config/gates/claim-verification.json +0 -12
- package/config/gates/default.json +2 -31
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +13 -23
- package/config/merge-quality-checks.json +1 -0
- package/config/model-candidates.json +6 -121
- package/openapi/openapi.yaml +0 -12
- package/package.json +98 -228
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +20 -72
- package/public/compare.html +8 -31
- package/public/dashboard.html +166 -930
- package/public/federal.html +2 -2
- package/public/guide.html +9 -33
- package/public/index.html +116 -495
- package/public/learn.html +18 -191
- package/public/lessons.html +10 -168
- package/public/numbers.html +7 -7
- package/public/pro.html +11 -34
- package/scripts/agent-memory-lifecycle.js +0 -211
- package/scripts/agent-readiness.js +3 -167
- package/scripts/agent-reward-model.js +1 -53
- package/scripts/auto-promote-gates.js +10 -82
- package/scripts/auto-wire-hooks.js +0 -14
- package/scripts/billing.js +1 -93
- package/scripts/build-metadata.js +10 -50
- package/scripts/cli-feedback.js +6 -18
- package/scripts/cli-schema.js +0 -90
- package/scripts/cli-telemetry.js +1 -6
- package/scripts/commercial-offer.js +2 -82
- package/scripts/context-manager.js +6 -74
- package/scripts/dashboard.js +2 -68
- package/scripts/durability/step.js +3 -3
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-dpo-pairs.js +2 -7
- package/scripts/feedback-loop.js +1 -123
- package/scripts/feedback-quality.js +0 -87
- package/scripts/filesystem-search.js +10 -35
- package/scripts/gate-stats.js +0 -89
- package/scripts/gates-engine.js +85 -1105
- package/scripts/gemini-embedding-policy.js +1 -2
- package/scripts/hook-runtime.js +14 -20
- package/scripts/hook-thumbgate-cache-updater.js +2 -18
- package/scripts/hybrid-feedback-context.js +7 -142
- package/scripts/lesson-inference.js +3 -8
- package/scripts/lesson-search.js +1 -17
- package/scripts/license.js +1 -0
- package/scripts/llm-client.js +9 -390
- package/scripts/local-model-profile.js +8 -15
- package/scripts/memory-scope-readiness.js +0 -159
- package/scripts/meta-agent-loop.js +0 -36
- package/scripts/operational-integrity.js +5 -39
- package/scripts/oss-pr-opportunity-scout.js +5 -35
- package/scripts/plausible-server-events.js +6 -9
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +1 -42
- package/scripts/rate-limiter.js +13 -64
- package/scripts/secret-scanner.js +5 -44
- package/scripts/security-scanner.js +10 -260
- package/scripts/self-distill-agent.js +1 -3
- package/scripts/seo-gsd.js +2 -916
- package/scripts/statusline-cache-path.js +2 -17
- package/scripts/statusline-local-stats.js +1 -9
- package/scripts/statusline-meta.js +2 -28
- package/scripts/statusline.sh +4 -20
- package/scripts/telemetry-analytics.js +0 -357
- package/scripts/thompson-sampling.js +10 -31
- package/scripts/thumbgate-bench.js +1 -16
- package/scripts/thumbgate-search.js +19 -85
- package/scripts/tool-registry.js +1 -169
- package/scripts/vector-store.js +0 -45
- package/scripts/workflow-sentinel.js +53 -286
- package/scripts/workspace-evolver.js +2 -62
- package/src/api/server.js +290 -2578
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/.well-known/agentic-verify.txt +0 -1
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/config/gate-classifier-routing.json +0 -98
- package/config/post-deploy-marketing-pages.json +0 -61
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- package/scripts/visitor-journey.js +0 -172
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* sync-telemetry-from-prod.js
|
|
6
|
-
*
|
|
7
|
-
* The agentic data pipeline (scripts/agentic-data-pipeline.js →
|
|
8
|
-
* scripts/telemetry-analytics.js) computes `get_business_metrics` from the LOCAL
|
|
9
|
-
* telemetry store at `<feedbackDir>/telemetry-pings.jsonl`. On a developer
|
|
10
|
-
* machine that store is near-empty, so metrics read uniqueVisitors:0 /
|
|
11
|
-
* checkoutStarts:0 even though real web traffic is flowing.
|
|
12
|
-
*
|
|
13
|
-
* The real web funnel lives on the PROD Railway volume and is exposed only via
|
|
14
|
-
* the operator-key-gated endpoint `GET /v1/telemetry/export`. Both stores use the
|
|
15
|
-
* SAME jsonl format/filename and matching fields (receivedAt/event/eventType/
|
|
16
|
-
* installId/visitorId/sessionId/traceId), so a sync is a clean append + dedupe.
|
|
17
|
-
*
|
|
18
|
-
* This script:
|
|
19
|
-
* 1. Resolves the operator (or admin) key from env or ~/.config/thumbgate/operator.json.
|
|
20
|
-
* 2. GETs /v1/telemetry/export?source=both with `Authorization: Bearer <key>`.
|
|
21
|
-
* 3. Merges/dedupes the returned `telemetry` rows into
|
|
22
|
-
* `<feedbackDir>/telemetry-pings.jsonl` (dedupe by a stable sha256 of the
|
|
23
|
-
* canonical row, so repeat runs never double-count).
|
|
24
|
-
* 4. Optionally (`--funnel`) persists the returned `funnel` rows into
|
|
25
|
-
* `<feedbackDir>/funnel-events.jsonl` the same way.
|
|
26
|
-
*
|
|
27
|
-
* After a run, `get_business_metrics` reflects the real funnel because the
|
|
28
|
-
* pipeline reads the same feedback dir on each call.
|
|
29
|
-
*
|
|
30
|
-
* SECURITY: the operator key is never printed. Only its source is reported.
|
|
31
|
-
*
|
|
32
|
-
* Usage:
|
|
33
|
-
* node scripts/sync-telemetry-from-prod.js [--since-days=30] [--since=<iso>]
|
|
34
|
-
* [--limit=10000] [--source=both|telemetry|funnel] [--funnel]
|
|
35
|
-
* [--base-url=<url>] [--feedback-dir=<dir>] [--json] [--dry-run]
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
const fs = require('node:fs');
|
|
39
|
-
const os = require('node:os');
|
|
40
|
-
const path = require('node:path');
|
|
41
|
-
const crypto = require('node:crypto');
|
|
42
|
-
|
|
43
|
-
const { getFeedbackPaths } = require('./feedback-loop');
|
|
44
|
-
|
|
45
|
-
const DEFAULT_BASE_URL = 'https://thumbgate-production.up.railway.app';
|
|
46
|
-
const OPERATOR_CONFIG_PATH = path.join(os.homedir(), '.config', 'thumbgate', 'operator.json');
|
|
47
|
-
const TELEMETRY_FILE_NAME = 'telemetry-pings.jsonl';
|
|
48
|
-
const FUNNEL_FILE_NAME = 'funnel-events.jsonl';
|
|
49
|
-
const EXPORT_PATH = '/v1/telemetry/export';
|
|
50
|
-
const HARD_LIMIT = 10000; // server clamps each stream to this
|
|
51
|
-
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
52
|
-
|
|
53
|
-
// Configure fetch proxy when running behind a corporate/sandbox proxy. Mirrors
|
|
54
|
-
// scripts/operational-summary.js so hosted reads behave the same everywhere.
|
|
55
|
-
(function configureProxy() {
|
|
56
|
-
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy
|
|
57
|
-
|| process.env.HTTP_PROXY || process.env.http_proxy;
|
|
58
|
-
if (!proxyUrl) return;
|
|
59
|
-
try {
|
|
60
|
-
const { ProxyAgent, setGlobalDispatcher } = require('undici');
|
|
61
|
-
setGlobalDispatcher(new ProxyAgent(proxyUrl));
|
|
62
|
-
} catch {
|
|
63
|
-
// undici not available — fetch will use the default dispatcher.
|
|
64
|
-
}
|
|
65
|
-
}());
|
|
66
|
-
|
|
67
|
-
function normalizeText(value) {
|
|
68
|
-
if (value === undefined || value === null) return null;
|
|
69
|
-
const text = String(value).trim();
|
|
70
|
-
return text || null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function loadOperatorConfig(configPath = OPERATOR_CONFIG_PATH) {
|
|
74
|
-
try {
|
|
75
|
-
const parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
76
|
-
return {
|
|
77
|
-
operatorKey: normalizeText(parsed.operatorKey),
|
|
78
|
-
baseUrl: normalizeText(parsed.baseUrl),
|
|
79
|
-
};
|
|
80
|
-
} catch {
|
|
81
|
-
return { operatorKey: null, baseUrl: null };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Resolve the credential + base URL the same way scripts/operational-summary.js
|
|
87
|
-
* does: env THUMBGATE_OPERATOR_KEY > operator.json operatorKey > env
|
|
88
|
-
* THUMBGATE_API_KEY (admin keys are an accepted alternate auth path on the
|
|
89
|
-
* export endpoint). `source` records WHERE the key came from for diagnostics —
|
|
90
|
-
* the key value itself is never returned to callers that print it.
|
|
91
|
-
*/
|
|
92
|
-
function resolveOperatorConfig(options = {}) {
|
|
93
|
-
const operatorConfig = loadOperatorConfig(options.configPath);
|
|
94
|
-
let apiKey = normalizeText(process.env.THUMBGATE_OPERATOR_KEY);
|
|
95
|
-
let keySource = apiKey ? 'env:THUMBGATE_OPERATOR_KEY' : null;
|
|
96
|
-
if (!apiKey && operatorConfig.operatorKey) {
|
|
97
|
-
apiKey = operatorConfig.operatorKey;
|
|
98
|
-
keySource = 'operator.json';
|
|
99
|
-
}
|
|
100
|
-
if (!apiKey) {
|
|
101
|
-
apiKey = normalizeText(process.env.THUMBGATE_API_KEY);
|
|
102
|
-
keySource = apiKey ? 'env:THUMBGATE_API_KEY' : null;
|
|
103
|
-
}
|
|
104
|
-
const baseUrl = normalizeText(options.baseUrl)
|
|
105
|
-
|| normalizeText(process.env.THUMBGATE_SYNC_BASE_URL)
|
|
106
|
-
|| normalizeText(process.env.THUMBGATE_BILLING_API_BASE_URL)
|
|
107
|
-
|| operatorConfig.baseUrl
|
|
108
|
-
|| DEFAULT_BASE_URL;
|
|
109
|
-
return { apiKey, keySource, baseUrl };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Deterministic, LOCALE-INDEPENDENT key comparator (UTF-16 code-unit order).
|
|
113
|
-
// Do NOT switch this to String.prototype.localeCompare (what SonarCloud S2871
|
|
114
|
-
// suggests): localeCompare is locale-sensitive, so it would make the canonical
|
|
115
|
-
// serialization — and therefore the dedupe hash below — differ across machines
|
|
116
|
-
// and CI runners, silently breaking cross-environment dedupe.
|
|
117
|
-
function compareKeys(a, b) {
|
|
118
|
-
if (a < b) return -1;
|
|
119
|
-
if (a > b) return 1;
|
|
120
|
-
return 0;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Recursively sort object keys so structurally-identical rows serialize
|
|
124
|
-
// identically regardless of original key order.
|
|
125
|
-
function canonicalize(value) {
|
|
126
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
127
|
-
if (value && typeof value === 'object') {
|
|
128
|
-
const out = {};
|
|
129
|
-
for (const key of Object.keys(value).sort(compareKeys)) out[key] = canonicalize(value[key]);
|
|
130
|
-
return out;
|
|
131
|
-
}
|
|
132
|
-
return value;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Stable dedupe id. sha256 (not sha1/md5 — SonarCloud S4790) over the canonical
|
|
136
|
-
// JSON of the whole row: identical rows → identical id across runs.
|
|
137
|
-
function stableRowId(row) {
|
|
138
|
-
return crypto.createHash('sha256').update(JSON.stringify(canonicalize(row))).digest('hex');
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// The export endpoint returns each stream as { rows: [...] }; older/simplified
|
|
142
|
-
// shapes may return a bare array. Accept both.
|
|
143
|
-
function extractRows(streamValue) {
|
|
144
|
-
if (Array.isArray(streamValue)) return streamValue;
|
|
145
|
-
if (streamValue && Array.isArray(streamValue.rows)) return streamValue.rows;
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Fetch the telemetry export. `fetchImpl` is injectable so tests run without
|
|
151
|
-
* network or secrets. Auth is `Authorization: Bearer <key>` — the server's
|
|
152
|
-
* extractApiKey reads Bearer / x-api-key, NOT x-operator-key.
|
|
153
|
-
*/
|
|
154
|
-
async function fetchTelemetryExport(params = {}) {
|
|
155
|
-
const {
|
|
156
|
-
baseUrl,
|
|
157
|
-
operatorKey,
|
|
158
|
-
since,
|
|
159
|
-
limit = HARD_LIMIT,
|
|
160
|
-
source = 'both',
|
|
161
|
-
fetchImpl = globalThis.fetch,
|
|
162
|
-
} = params;
|
|
163
|
-
if (!operatorKey) {
|
|
164
|
-
const err = new Error('No operator/admin key available for telemetry export.');
|
|
165
|
-
err.code = 'no_key';
|
|
166
|
-
throw err;
|
|
167
|
-
}
|
|
168
|
-
if (typeof fetchImpl !== 'function') {
|
|
169
|
-
const err = new Error('No fetch implementation available (Node >=18 or inject fetchImpl).');
|
|
170
|
-
err.code = 'no_fetch';
|
|
171
|
-
throw err;
|
|
172
|
-
}
|
|
173
|
-
const url = new URL(EXPORT_PATH, baseUrl || DEFAULT_BASE_URL);
|
|
174
|
-
url.searchParams.set('source', source);
|
|
175
|
-
url.searchParams.set('limit', String(Math.min(Math.max(1, Number(limit) || HARD_LIMIT), HARD_LIMIT)));
|
|
176
|
-
if (since) url.searchParams.set('since', since);
|
|
177
|
-
|
|
178
|
-
const response = await fetchImpl(url, {
|
|
179
|
-
method: 'GET',
|
|
180
|
-
headers: {
|
|
181
|
-
authorization: `Bearer ${operatorKey}`,
|
|
182
|
-
accept: 'application/json',
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
if (!response.ok) {
|
|
186
|
-
const bodyText = await response.text().catch(() => '');
|
|
187
|
-
const err = new Error(`Telemetry export failed: HTTP ${response.status} ${bodyText.slice(0, 200)}`);
|
|
188
|
-
err.code = 'http_error';
|
|
189
|
-
err.status = response.status;
|
|
190
|
-
throw err;
|
|
191
|
-
}
|
|
192
|
-
return response.json();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Read every stable id already present in a jsonl ledger (skips unparseable lines).
|
|
196
|
-
function readExistingIds(filePath) {
|
|
197
|
-
const ids = new Set();
|
|
198
|
-
if (!fs.existsSync(filePath)) return ids;
|
|
199
|
-
const text = fs.readFileSync(filePath, 'utf8');
|
|
200
|
-
for (const line of text.split('\n')) {
|
|
201
|
-
if (!line.trim()) continue;
|
|
202
|
-
try {
|
|
203
|
-
ids.add(stableRowId(JSON.parse(line)));
|
|
204
|
-
} catch {
|
|
205
|
-
// Tolerate malformed historical lines.
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return ids;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Append only rows whose stable id isn't already present. Pure aside from the
|
|
213
|
-
* single append; safe to re-run. Returns { added, skipped }.
|
|
214
|
-
*/
|
|
215
|
-
function mergeRowsIntoLedger(filePath, rows, options = {}) {
|
|
216
|
-
const existing = options.existingIds || readExistingIds(filePath);
|
|
217
|
-
const toAppend = [];
|
|
218
|
-
let skipped = 0;
|
|
219
|
-
for (const row of rows) {
|
|
220
|
-
if (!row || typeof row !== 'object') { skipped += 1; continue; }
|
|
221
|
-
const id = stableRowId(row);
|
|
222
|
-
if (existing.has(id)) { skipped += 1; continue; }
|
|
223
|
-
existing.add(id);
|
|
224
|
-
toAppend.push(JSON.stringify(row));
|
|
225
|
-
}
|
|
226
|
-
if (toAppend.length && !options.dryRun) {
|
|
227
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
228
|
-
fs.appendFileSync(filePath, toAppend.map((l) => `${l}\n`).join(''), 'utf8');
|
|
229
|
-
}
|
|
230
|
-
return { added: toAppend.length, skipped };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function resolveFeedbackDir(options = {}) {
|
|
234
|
-
if (options.feedbackDir) return path.resolve(options.feedbackDir);
|
|
235
|
-
return getFeedbackPaths().FEEDBACK_DIR;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Resolve the `since` ISO timestamp: an explicit --since wins; otherwise derive
|
|
239
|
-
// it from --since-days relative to nowMs (test-injectable) or the current time.
|
|
240
|
-
function computeSinceIso(options = {}) {
|
|
241
|
-
if (options.since) return options.since;
|
|
242
|
-
if (!Number.isFinite(options.sinceDays)) return undefined;
|
|
243
|
-
const baseMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
|
|
244
|
-
return new Date(baseMs - options.sinceDays * MS_PER_DAY).toISOString();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* End-to-end sync: fetch export → merge telemetry (always) and funnel (opt-in).
|
|
249
|
-
* Returns a summary object with counts and resolved paths (no secrets).
|
|
250
|
-
*/
|
|
251
|
-
async function syncTelemetryFromProd(options = {}) {
|
|
252
|
-
const config = resolveOperatorConfig(options);
|
|
253
|
-
const feedbackDir = resolveFeedbackDir(options);
|
|
254
|
-
const source = options.source || (options.funnel ? 'both' : 'telemetry');
|
|
255
|
-
const sinceIso = computeSinceIso(options);
|
|
256
|
-
|
|
257
|
-
const payload = await fetchTelemetryExport({
|
|
258
|
-
baseUrl: config.baseUrl,
|
|
259
|
-
operatorKey: config.apiKey,
|
|
260
|
-
since: sinceIso,
|
|
261
|
-
limit: options.limit || HARD_LIMIT,
|
|
262
|
-
source,
|
|
263
|
-
fetchImpl: options.fetchImpl,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
const telemetryRows = extractRows(payload.telemetry);
|
|
267
|
-
const telemetryPath = path.join(feedbackDir, TELEMETRY_FILE_NAME);
|
|
268
|
-
const telemetryResult = mergeRowsIntoLedger(telemetryPath, telemetryRows, { dryRun: options.dryRun });
|
|
269
|
-
|
|
270
|
-
const summary = {
|
|
271
|
-
feedbackDir,
|
|
272
|
-
baseUrl: config.baseUrl,
|
|
273
|
-
keySource: config.keySource,
|
|
274
|
-
source,
|
|
275
|
-
since: payload.since || sinceIso || null,
|
|
276
|
-
dryRun: !!options.dryRun,
|
|
277
|
-
telemetry: {
|
|
278
|
-
path: telemetryPath,
|
|
279
|
-
fetched: telemetryRows.length,
|
|
280
|
-
added: telemetryResult.added,
|
|
281
|
-
skipped: telemetryResult.skipped,
|
|
282
|
-
},
|
|
283
|
-
funnel: null,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
if (options.funnel) {
|
|
287
|
-
const funnelRows = extractRows(payload.funnel);
|
|
288
|
-
const funnelPath = path.join(feedbackDir, FUNNEL_FILE_NAME);
|
|
289
|
-
const funnelResult = mergeRowsIntoLedger(funnelPath, funnelRows, { dryRun: options.dryRun });
|
|
290
|
-
summary.funnel = {
|
|
291
|
-
path: funnelPath,
|
|
292
|
-
fetched: funnelRows.length,
|
|
293
|
-
added: funnelResult.added,
|
|
294
|
-
skipped: funnelResult.skipped,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return summary;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function parseArgs(argv) {
|
|
302
|
-
const opts = { sinceDays: 30, funnel: false, json: false, dryRun: false };
|
|
303
|
-
for (const arg of argv) {
|
|
304
|
-
if (arg === '--funnel') opts.funnel = true;
|
|
305
|
-
else if (arg === '--json') opts.json = true;
|
|
306
|
-
else if (arg === '--dry-run') opts.dryRun = true;
|
|
307
|
-
else if (arg.startsWith('--since-days=')) opts.sinceDays = Number(arg.split('=')[1]);
|
|
308
|
-
else if (arg.startsWith('--since=')) opts.since = arg.split('=').slice(1).join('=');
|
|
309
|
-
else if (arg.startsWith('--limit=')) opts.limit = Number(arg.split('=')[1]);
|
|
310
|
-
else if (arg.startsWith('--source=')) opts.source = arg.split('=')[1];
|
|
311
|
-
else if (arg.startsWith('--base-url=')) opts.baseUrl = arg.split('=').slice(1).join('=');
|
|
312
|
-
else if (arg.startsWith('--feedback-dir=')) opts.feedbackDir = arg.split('=').slice(1).join('=');
|
|
313
|
-
}
|
|
314
|
-
if (!Number.isFinite(opts.sinceDays)) opts.sinceDays = 30;
|
|
315
|
-
return opts;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function main() {
|
|
319
|
-
const opts = parseArgs(process.argv.slice(2));
|
|
320
|
-
const preflight = resolveOperatorConfig(opts);
|
|
321
|
-
if (!preflight.apiKey) {
|
|
322
|
-
console.error(
|
|
323
|
-
'No operator/admin key found. Set THUMBGATE_OPERATOR_KEY (or THUMBGATE_API_KEY), '
|
|
324
|
-
+ 'or add operatorKey to ~/.config/thumbgate/operator.json. '
|
|
325
|
-
+ 'The key lives in Railway prod env as THUMBGATE_OPERATOR_KEY.'
|
|
326
|
-
);
|
|
327
|
-
process.exitCode = 1;
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
const summary = await syncTelemetryFromProd(opts);
|
|
332
|
-
if (opts.json) {
|
|
333
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
334
|
-
} else {
|
|
335
|
-
const t = summary.telemetry;
|
|
336
|
-
console.log(`✅ Telemetry sync${summary.dryRun ? ' (dry-run)' : ''} complete`);
|
|
337
|
-
console.log(` source=${summary.source} since=${summary.since || 'server-default'} via ${summary.keySource}`);
|
|
338
|
-
console.log(` telemetry: ${t.fetched} fetched → ${t.added} added, ${t.skipped} already present`);
|
|
339
|
-
console.log(` → ${t.path}`);
|
|
340
|
-
if (summary.funnel) {
|
|
341
|
-
const f = summary.funnel;
|
|
342
|
-
console.log(` funnel: ${f.fetched} fetched → ${f.added} added, ${f.skipped} already present`);
|
|
343
|
-
console.log(` → ${f.path}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
} catch (err) {
|
|
347
|
-
console.error(`❌ Telemetry sync failed: ${err.message}`);
|
|
348
|
-
process.exitCode = 1;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Path-based entrypoint check (require.main === module is flagged by SonarCloud S3403).
|
|
353
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
|
|
354
|
-
main();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
module.exports = {
|
|
358
|
-
DEFAULT_BASE_URL,
|
|
359
|
-
TELEMETRY_FILE_NAME,
|
|
360
|
-
FUNNEL_FILE_NAME,
|
|
361
|
-
HARD_LIMIT,
|
|
362
|
-
canonicalize,
|
|
363
|
-
stableRowId,
|
|
364
|
-
extractRows,
|
|
365
|
-
loadOperatorConfig,
|
|
366
|
-
resolveOperatorConfig,
|
|
367
|
-
fetchTelemetryExport,
|
|
368
|
-
readExistingIds,
|
|
369
|
-
mergeRowsIntoLedger,
|
|
370
|
-
resolveFeedbackDir,
|
|
371
|
-
computeSinceIso,
|
|
372
|
-
syncTelemetryFromProd,
|
|
373
|
-
parseArgs,
|
|
374
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Tool Contract Validator
|
|
5
|
-
* Validates tool arguments against the tool's inputSchema.
|
|
6
|
-
*/
|
|
7
|
-
function validateToolContract(schema, args) {
|
|
8
|
-
const errors = [];
|
|
9
|
-
if (!schema) return { valid: true, errors };
|
|
10
|
-
|
|
11
|
-
if (schema.type === 'object') {
|
|
12
|
-
if (typeof args !== 'object' || args === null || Array.isArray(args)) {
|
|
13
|
-
errors.push(`Expected object, got ${args === null ? 'null' : Array.isArray(args) ? 'array' : typeof args}`);
|
|
14
|
-
return { valid: false, errors };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Check required fields
|
|
18
|
-
if (Array.isArray(schema.required)) {
|
|
19
|
-
for (const req of schema.required) {
|
|
20
|
-
if (args[req] === undefined || args[req] === null || args[req] === '') {
|
|
21
|
-
errors.push(`Missing required parameter: '${req}'`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Check properties
|
|
27
|
-
if (schema.properties) {
|
|
28
|
-
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
29
|
-
const value = args[key];
|
|
30
|
-
if (value === undefined || value === null) continue; // Optional field not provided
|
|
31
|
-
|
|
32
|
-
const valType = typeof value;
|
|
33
|
-
if (propSchema.type === 'string') {
|
|
34
|
-
if (valType !== 'string') {
|
|
35
|
-
errors.push(`Parameter '${key}' must be a string (got ${valType})`);
|
|
36
|
-
} else if (Array.isArray(propSchema.enum)) {
|
|
37
|
-
if (!propSchema.enum.includes(value)) {
|
|
38
|
-
errors.push(`Parameter '${key}' must be one of [${propSchema.enum.join(', ')}] (got '${value}')`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
} else if (propSchema.type === 'number') {
|
|
42
|
-
if (valType !== 'number' || isNaN(value)) {
|
|
43
|
-
errors.push(`Parameter '${key}' must be a number (got ${valType})`);
|
|
44
|
-
}
|
|
45
|
-
} else if (propSchema.type === 'boolean') {
|
|
46
|
-
if (valType !== 'boolean') {
|
|
47
|
-
errors.push(`Parameter '${key}' must be a boolean (got ${valType})`);
|
|
48
|
-
}
|
|
49
|
-
} else if (propSchema.type === 'array') {
|
|
50
|
-
if (!Array.isArray(value)) {
|
|
51
|
-
errors.push(`Parameter '${key}' must be an array (got ${valType})`);
|
|
52
|
-
}
|
|
53
|
-
} else if (propSchema.type === 'object') {
|
|
54
|
-
if (valType !== 'object' || value === null || Array.isArray(value)) {
|
|
55
|
-
errors.push(`Parameter '${key}' must be an object (got ${valType})`);
|
|
56
|
-
} else {
|
|
57
|
-
// Recurse for nested objects
|
|
58
|
-
const subRes = validateToolContract(propSchema, value);
|
|
59
|
-
if (!subRes.valid) {
|
|
60
|
-
for (const err of subRes.errors) {
|
|
61
|
-
errors.push(`Parameter '${key}': ${err}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
valid: errors.length === 0,
|
|
72
|
-
errors,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
module.exports = { validateToolContract };
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Trajectory Scorer — Strategic Drift Detection.
|
|
6
|
-
*
|
|
7
|
-
* Measures the "Semantic Distance" between the original user intent
|
|
8
|
-
* (from primer.md) and the current set of changed files.
|
|
9
|
-
*
|
|
10
|
-
* If the agent modifies too many unrelated files, it triggers a safety block.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const fs = require('fs');
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const { execSync } = require('child_process');
|
|
16
|
-
|
|
17
|
-
function getTrajectoryScore(options = {}) {
|
|
18
|
-
const projectRoot = options.projectRoot || process.cwd();
|
|
19
|
-
const primerPath = path.join(projectRoot, 'primer.md');
|
|
20
|
-
|
|
21
|
-
if (!fs.existsSync(primerPath)) return { score: 0, isDrifting: false, drift: false };
|
|
22
|
-
|
|
23
|
-
const intent = fs.readFileSync(primerPath, 'utf8').toLowerCase();
|
|
24
|
-
|
|
25
|
-
// Get currently modified files (unstaged + staged)
|
|
26
|
-
let changedFiles = options.changedFiles;
|
|
27
|
-
if (!changedFiles) {
|
|
28
|
-
try {
|
|
29
|
-
const output = execSync('git diff --name-only HEAD', { cwd: projectRoot, encoding: 'utf8' });
|
|
30
|
-
changedFiles = output.split('\n').filter(f => f.trim());
|
|
31
|
-
} catch {
|
|
32
|
-
return { score: 0, isDrifting: false, drift: false };
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (changedFiles.length === 0) return { score: 0, isDrifting: false, drift: false };
|
|
37
|
-
|
|
38
|
-
// Calculate drift: How many changed files are NOT mentioned in the intent?
|
|
39
|
-
let driftCount = 0;
|
|
40
|
-
for (const file of changedFiles) {
|
|
41
|
-
const base = path.basename(file).toLowerCase();
|
|
42
|
-
if (!intent.includes(base)) {
|
|
43
|
-
driftCount++;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const driftRatio = driftCount / changedFiles.length;
|
|
48
|
-
const isDrifting = driftRatio > 0.6 && changedFiles.length > 3;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
score: Number((1 - driftRatio).toFixed(2)),
|
|
52
|
-
changedCount: changedFiles.length,
|
|
53
|
-
driftCount,
|
|
54
|
-
isDrifting,
|
|
55
|
-
message: isDrifting
|
|
56
|
-
? `🚫 THUMBGATE: Strategic Drift Detected. You have modified ${changedFiles.length} files, but ${driftCount} of them were not mentioned in the original intent. Please refocus or update the intent.`
|
|
57
|
-
: null
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
module.exports = {
|
|
62
|
-
getTrajectoryScore
|
|
63
|
-
};
|