thumbgate 1.27.18 → 1.27.19
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 +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +249 -30
- package/adapters/chatgpt/openapi.yaml +12 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1429 -121
- package/bin/postinstall.js +1 -8
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +1 -1
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pro.html +34 -11
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/rate-limiter.js +64 -13
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/seo-gsd.js +916 -7
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-registry.js +169 -1
- package/scripts/vector-store.js +45 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/scripts/bot-detector.js +0 -50
package/bin/cli.js
CHANGED
|
@@ -23,14 +23,18 @@
|
|
|
23
23
|
* npx thumbgate background-governance # background-agent run report + risk check
|
|
24
24
|
* npx thumbgate cfo # local operational billing summary
|
|
25
25
|
* npx thumbgate pro # solo dashboard + exports side lane
|
|
26
|
+
* npx thumbgate diagnostic # team workflow diagnostic intake + checkout path
|
|
27
|
+
* npx thumbgate audit <file> # audit an agent transcript for repeat-mistake token waste
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
30
|
'use strict';
|
|
29
31
|
|
|
30
32
|
const fs = require('fs');
|
|
33
|
+
const os = require('os');
|
|
31
34
|
const path = require('path');
|
|
32
35
|
const crypto = require('crypto');
|
|
33
|
-
const
|
|
36
|
+
const http = require('http');
|
|
37
|
+
const { execSync, execFileSync, execFile, spawn } = require('child_process');
|
|
34
38
|
const {
|
|
35
39
|
codexAutoUpdateCliEntry,
|
|
36
40
|
codexAutoUpdateMcpEntry,
|
|
@@ -40,13 +44,6 @@ const {
|
|
|
40
44
|
resolveMcpEntry,
|
|
41
45
|
} = require(path.join(__dirname, '..', 'scripts', 'mcp-config'));
|
|
42
46
|
const { trackEvent } = require(path.join(__dirname, '..', 'scripts', 'cli-telemetry'));
|
|
43
|
-
const {
|
|
44
|
-
cacheUpdateHookCommand,
|
|
45
|
-
preToolHookCommand,
|
|
46
|
-
sessionStartHookCommand,
|
|
47
|
-
statuslineCommand,
|
|
48
|
-
userPromptHookCommand,
|
|
49
|
-
} = require(path.join(__dirname, '..', 'scripts', 'hook-runtime'));
|
|
50
47
|
const {
|
|
51
48
|
PRO_MONTHLY_PAYMENT_LINK,
|
|
52
49
|
PRO_PRICE_LABEL,
|
|
@@ -56,8 +53,74 @@ const COMMAND = process.argv[2];
|
|
|
56
53
|
const CWD = process.cwd();
|
|
57
54
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
58
55
|
|
|
59
|
-
const PRO_URL = 'https://thumbgate
|
|
56
|
+
const PRO_URL = 'https://thumbgate.ai';
|
|
60
57
|
const PRO_CHECKOUT_URL = PRO_MONTHLY_PAYMENT_LINK;
|
|
58
|
+
// Mirrors scripts/rate-limiter.js TRIAL_DAYS — must stay in sync. Gitar
|
|
59
|
+
// caught a 14-vs-7 mismatch on PR #2337 (banner said "7-day trial" but
|
|
60
|
+
// the date label was computed 14 days out).
|
|
61
|
+
const TRIAL_DAYS = 7;
|
|
62
|
+
|
|
63
|
+
function checkoutUrlFor(source, content) {
|
|
64
|
+
try {
|
|
65
|
+
const base = content === 'capture_feedback'
|
|
66
|
+
? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
|
|
67
|
+
: PRO_CHECKOUT_URL;
|
|
68
|
+
const url = new URL(base);
|
|
69
|
+
url.searchParams.set('utm_source', source || 'cli');
|
|
70
|
+
url.searchParams.set('utm_medium', 'cli');
|
|
71
|
+
url.searchParams.set('utm_campaign', 'pro_conversion');
|
|
72
|
+
if (content) url.searchParams.set('utm_content', content);
|
|
73
|
+
return url.toString();
|
|
74
|
+
} catch (_) {
|
|
75
|
+
return content === 'capture_feedback'
|
|
76
|
+
? 'https://buy.stripe.com/7sYfZhaiE1eSbO99uj3sI0d'
|
|
77
|
+
: PRO_CHECKOUT_URL;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function pricingUrlFor(source, content) {
|
|
82
|
+
try {
|
|
83
|
+
const url = new URL('/pricing', PRO_URL);
|
|
84
|
+
url.searchParams.set('utm_source', source || 'cli');
|
|
85
|
+
url.searchParams.set('utm_medium', 'cli');
|
|
86
|
+
url.searchParams.set('utm_campaign', 'pro_education');
|
|
87
|
+
if (content) url.searchParams.set('utm_content', content);
|
|
88
|
+
return url.toString();
|
|
89
|
+
} catch (_) {
|
|
90
|
+
return `${PRO_URL}/pricing`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function diagnosticUrlFor(source, content) {
|
|
95
|
+
try {
|
|
96
|
+
const url = new URL('/diagnostic', PRO_URL);
|
|
97
|
+
url.searchParams.set('utm_source', source || 'cli');
|
|
98
|
+
url.searchParams.set('utm_medium', 'cli');
|
|
99
|
+
url.searchParams.set('utm_campaign', 'workflow_hardening_diagnostic');
|
|
100
|
+
if (content) url.searchParams.set('utm_content', content);
|
|
101
|
+
return url.toString();
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return `${PRO_URL}/diagnostic`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function diagnosticCheckoutUrlFor(source, content) {
|
|
108
|
+
try {
|
|
109
|
+
const url = new URL('/go/diagnostic', PRO_URL);
|
|
110
|
+
url.searchParams.set('utm_source', source || 'cli');
|
|
111
|
+
url.searchParams.set('utm_medium', 'cli');
|
|
112
|
+
url.searchParams.set('utm_campaign', 'workflow_hardening_diagnostic');
|
|
113
|
+
if (content) url.searchParams.set('utm_content', content);
|
|
114
|
+
return url.toString();
|
|
115
|
+
} catch (_) {
|
|
116
|
+
return `${PRO_URL}/go/diagnostic`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function trialDeadlineLabel(now = new Date()) {
|
|
121
|
+
const deadline = new Date(now.getTime() + (TRIAL_DAYS * 24 * 60 * 60 * 1000));
|
|
122
|
+
return deadline.toISOString().slice(0, 10);
|
|
123
|
+
}
|
|
61
124
|
|
|
62
125
|
function upgradeNudge() {
|
|
63
126
|
if (process.env.THUMBGATE_NO_NUDGE === '1') return;
|
|
@@ -65,14 +128,37 @@ function upgradeNudge() {
|
|
|
65
128
|
const { isProTier } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
|
|
66
129
|
if (isProTier()) return;
|
|
67
130
|
} catch (_) { return; }
|
|
131
|
+
const pricingUrl = pricingUrlFor('cli_upgrade_nudge', COMMAND || 'general');
|
|
132
|
+
const diagnosticUrl = diagnosticUrlFor('cli_upgrade_nudge', COMMAND || 'general');
|
|
68
133
|
process.stderr.write(
|
|
69
|
-
'\n Team rollout: start with the Workflow Hardening
|
|
70
|
-
|
|
134
|
+
'\n Team rollout: start with the $499 Workflow Hardening Diagnostic\n' +
|
|
135
|
+
` ${diagnosticUrl}\n` +
|
|
71
136
|
`\n Solo side lane: Pro — ${PRO_PRICE_LABEL}\n` +
|
|
72
|
-
|
|
137
|
+
' Keeps lessons, rules, and the dashboard synced across machines and agent runtimes.\n' +
|
|
138
|
+
` ${pricingUrl}\n\n`
|
|
73
139
|
);
|
|
74
140
|
}
|
|
75
141
|
|
|
142
|
+
function diagnostic() {
|
|
143
|
+
const intakeUrl = diagnosticUrlFor('cli_diagnostic', COMMAND || 'diagnostic');
|
|
144
|
+
const checkoutUrl = diagnosticCheckoutUrlFor('cli_diagnostic', COMMAND || 'diagnostic');
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(' ThumbGate Workflow Hardening Diagnostic');
|
|
147
|
+
console.log(' ---------------------------------------');
|
|
148
|
+
console.log(' Use this when one repeated AI-agent workflow failure is already costing');
|
|
149
|
+
console.log(' review time, release confidence, customer trust, or money.');
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(' Best fit: one workflow, one repeated failure, one owner who can review proof.');
|
|
152
|
+
console.log(' Output: failure taxonomy, block/warn/human-review split, and proof checklist.');
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(' Start with intake:');
|
|
155
|
+
console.log(` ${intakeUrl}`);
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(' Ready to pay now:');
|
|
158
|
+
console.log(` ${checkoutUrl}`);
|
|
159
|
+
console.log('');
|
|
160
|
+
}
|
|
161
|
+
|
|
76
162
|
function appendLocalTelemetry(payload) {
|
|
77
163
|
try {
|
|
78
164
|
const { getFeedbackPaths } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
@@ -126,7 +212,7 @@ function telemetryPing(installId) {
|
|
|
126
212
|
timestamp: new Date().toISOString(),
|
|
127
213
|
};
|
|
128
214
|
appendLocalTelemetry(payloadObject);
|
|
129
|
-
const apiUrl = process.env.THUMBGATE_API_URL || 'https://thumbgate
|
|
215
|
+
const apiUrl = process.env.THUMBGATE_API_URL || 'https://thumbgate.ai';
|
|
130
216
|
const payload = JSON.stringify(payloadObject);
|
|
131
217
|
try {
|
|
132
218
|
const url = new URL('/v1/telemetry/ping', apiUrl);
|
|
@@ -145,25 +231,47 @@ function proNudge(context) {
|
|
|
145
231
|
const { isProTier } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
|
|
146
232
|
if (isProTier()) return;
|
|
147
233
|
} catch (_) { /* if rate-limiter is unavailable, fall through and nudge */ }
|
|
234
|
+
const checkoutUrl = checkoutUrlFor('cli_nudge', context || COMMAND || 'general');
|
|
235
|
+
const pricingUrl = pricingUrlFor('cli_nudge', context || COMMAND || 'general');
|
|
148
236
|
const messages = [
|
|
149
|
-
`\n 💡
|
|
150
|
-
`\n 💡 Pro
|
|
151
|
-
`\n 💡 ThumbGate Pro
|
|
237
|
+
`\n 💡 Pro (${PRO_PRICE_LABEL}): keep lessons, rules, and dashboard state synced across machines and agent runtimes.\n See pricing: ${pricingUrl}\n`,
|
|
238
|
+
`\n 💡 You just taught ThumbGate something locally. Pro keeps that lesson alive on every laptop, CI box, and agent runtime.\n See pricing: ${pricingUrl}\n`,
|
|
239
|
+
`\n 💡 ThumbGate Pro syncs lessons/rules across Claude, Codex, Cursor, containers, and CI. ${PRO_PRICE_LABEL}.\n Start Pro: ${checkoutUrl}\n`,
|
|
152
240
|
];
|
|
153
241
|
// Rotate message daily — no Math.random (security policy)
|
|
154
242
|
const msg = messages[Math.floor(Date.now() / 86400000) % messages.length];
|
|
155
243
|
process.stderr.write(msg);
|
|
156
244
|
}
|
|
157
245
|
|
|
158
|
-
function limitNudge(action) {
|
|
246
|
+
function limitNudge(action, limitResult = {}) {
|
|
159
247
|
if (process.env.THUMBGATE_NO_NUDGE === '1') return;
|
|
248
|
+
const checkoutUrl = checkoutUrlFor('cli_limit', action);
|
|
249
|
+
const usageLine = Number.isFinite(limitResult.used) && Number.isFinite(limitResult.limit)
|
|
250
|
+
? ` Usage: ${limitResult.used}/${limitResult.limit} (${limitResult.limitType || 'limit'}).\n`
|
|
251
|
+
: '';
|
|
252
|
+
const reason = limitResult.message
|
|
253
|
+
? String(limitResult.message).split('\n')[0]
|
|
254
|
+
: 'Free tier limit reached.';
|
|
160
255
|
process.stderr.write(
|
|
161
|
-
`\n
|
|
162
|
-
|
|
163
|
-
` ${
|
|
256
|
+
`\n 🔒 ${reason}\n` +
|
|
257
|
+
usageLine +
|
|
258
|
+
` Upgrade to Pro (${PRO_PRICE_LABEL}) to continue ${action.replace(/_/g, ' ')}:\n` +
|
|
259
|
+
` ${checkoutUrl}\n\n`
|
|
164
260
|
);
|
|
165
261
|
}
|
|
166
262
|
|
|
263
|
+
function printInitConversionPrompt(email) {
|
|
264
|
+
if (process.env.THUMBGATE_NO_NUDGE === '1') return;
|
|
265
|
+
const checkoutUrl = checkoutUrlFor('cli_init', email ? 'init_email' : 'init_no_email');
|
|
266
|
+
console.log('');
|
|
267
|
+
console.log(' ┌──────────────────────────────────────────────────────────┐');
|
|
268
|
+
console.log(` │ 7-day Pro trial active through ${trialDeadlineLabel()}. │`);
|
|
269
|
+
console.log(' │ Pro keeps lessons/rules/dashboard synced everywhere. │');
|
|
270
|
+
console.log(' │ Add onboarding: npx thumbgate init --email you@company.com │');
|
|
271
|
+
console.log(` │ Upgrade: ${checkoutUrl}`);
|
|
272
|
+
console.log(' └──────────────────────────────────────────────────────────┘');
|
|
273
|
+
}
|
|
274
|
+
|
|
167
275
|
function parseArgs(argv) {
|
|
168
276
|
const args = {};
|
|
169
277
|
argv.forEach((arg, index) => {
|
|
@@ -185,6 +293,73 @@ function parseArgs(argv) {
|
|
|
185
293
|
return args;
|
|
186
294
|
}
|
|
187
295
|
|
|
296
|
+
function probeDash(port) {
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
const req = http.get({ hostname: '127.0.0.1', port, path: '/health', timeout: 750 }, (res) => {
|
|
299
|
+
res.resume();
|
|
300
|
+
resolve(res.statusCode >= 200 && res.statusCode < 500);
|
|
301
|
+
});
|
|
302
|
+
req.on('error', () => resolve(false));
|
|
303
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function waitDash(port, timeoutMs = 8000) {
|
|
308
|
+
const deadline = Date.now() + timeoutMs;
|
|
309
|
+
while (Date.now() < deadline) {
|
|
310
|
+
if (await probeDash(port)) return true;
|
|
311
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
312
|
+
}
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function ensureDash(port) {
|
|
317
|
+
if (await probeDash(port)) {
|
|
318
|
+
return { started: false, pid: null };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const serverPath = path.join(PKG_ROOT, 'src', 'api', 'server.js');
|
|
322
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
323
|
+
cwd: PKG_ROOT,
|
|
324
|
+
detached: true,
|
|
325
|
+
env: { ...process.env, PORT: String(port), THUMBGATE_ALLOW_INSECURE: process.env.THUMBGATE_ALLOW_INSECURE || 'true' },
|
|
326
|
+
stdio: 'ignore',
|
|
327
|
+
});
|
|
328
|
+
child.unref();
|
|
329
|
+
|
|
330
|
+
if (!(await waitDash(port))) {
|
|
331
|
+
throw new Error('Dashboard API timeout');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { started: true, pid: child.pid };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function openBrowser(url) {
|
|
338
|
+
if (process.env.THUMBGATE_DASHBOARD_NO_OPEN === '1') {
|
|
339
|
+
console.log(`Ready: ${url}`);
|
|
340
|
+
return Promise.resolve();
|
|
341
|
+
}
|
|
342
|
+
const [command, args] = ({ darwin: ['open', [url]], win32: ['cmd', ['/c', 'start', '', url]] }[process.platform] || ['xdg-open', [url]]);
|
|
343
|
+
return new Promise((resolve, reject) => {
|
|
344
|
+
execFile(command, args, (err) => (err ? reject(err) : resolve()));
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseTtlMs(value, fallbackMs = 5 * 60 * 1000) {
|
|
349
|
+
if (value === undefined || value === null || value === true || value === '') return fallbackMs;
|
|
350
|
+
const raw = String(value).trim().toLowerCase();
|
|
351
|
+
const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
|
|
352
|
+
if (!match) return fallbackMs;
|
|
353
|
+
const amount = Number(match[1]);
|
|
354
|
+
if (!Number.isFinite(amount) || amount <= 0) return fallbackMs;
|
|
355
|
+
const unit = match[2] || 'ms';
|
|
356
|
+
const factor = unit === 'h' ? 60 * 60 * 1000
|
|
357
|
+
: unit === 'm' ? 60 * 1000
|
|
358
|
+
: unit === 's' ? 1000
|
|
359
|
+
: 1;
|
|
360
|
+
return Math.round(amount * factor);
|
|
361
|
+
}
|
|
362
|
+
|
|
188
363
|
function readStdinText() {
|
|
189
364
|
try {
|
|
190
365
|
return fs.readFileSync(0, 'utf8');
|
|
@@ -395,64 +570,24 @@ function whichExists(cmd) {
|
|
|
395
570
|
|
|
396
571
|
function setupClaude() {
|
|
397
572
|
const mcpChanged = mergeMcpJson(path.join(CWD, '.mcp.json'), 'Claude Code', 'project');
|
|
573
|
+
const { wireHooks } = require(path.join(PKG_ROOT, 'scripts', 'auto-wire-hooks'));
|
|
574
|
+
const hookResult = wireHooks({ agent: 'claude-code' });
|
|
398
575
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
let settings = { hooks: {} };
|
|
404
|
-
if (fs.existsSync(settingsPath)) {
|
|
405
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch (_) { /* fresh */ }
|
|
406
|
-
}
|
|
407
|
-
settings.hooks = settings.hooks || {};
|
|
408
|
-
|
|
409
|
-
const stopAlreadyPresent = (settings.hooks.Stop || [])
|
|
410
|
-
.some(entry => (entry.hooks || []).some(h => h.command === stopHookCommand));
|
|
411
|
-
|
|
412
|
-
let hooksChanged = false;
|
|
413
|
-
if (!stopAlreadyPresent) {
|
|
414
|
-
settings.hooks.Stop = settings.hooks.Stop || [];
|
|
415
|
-
settings.hooks.Stop.push({ hooks: [{ type: 'command', command: stopHookCommand }] });
|
|
416
|
-
hooksChanged = true;
|
|
417
|
-
console.log(' Claude Code: installed Stop hook');
|
|
576
|
+
if (hookResult.error) {
|
|
577
|
+
console.log(` Claude Code hooks: ${hookResult.error}`);
|
|
578
|
+
return mcpChanged;
|
|
418
579
|
}
|
|
419
580
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
hooksChanged = true;
|
|
428
|
-
}
|
|
429
|
-
const cacheAlreadyPresent = (settings.hooks.PostToolUse || [])
|
|
430
|
-
.some(entry => (entry.hooks || []).some(h => h.command === cacheHookCommand || (h.command && h.command.includes('cache-update'))));
|
|
431
|
-
|
|
432
|
-
if (!cacheAlreadyPresent) {
|
|
433
|
-
settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
|
|
434
|
-
settings.hooks.PostToolUse.push({
|
|
435
|
-
matcher: 'mcp__thumbgate__feedback_stats|mcp__thumbgate__dashboard',
|
|
436
|
-
hooks: [{ type: 'command', command: cacheHookCommand }]
|
|
437
|
-
});
|
|
438
|
-
hooksChanged = true;
|
|
439
|
-
console.log(' Claude Code: installed ThumbGate cache updater hook');
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Upsert statusLine for ThumbGate feedback display
|
|
443
|
-
const statuslineScript = statuslineCommand();
|
|
444
|
-
if (!settings.statusLine || settings.statusLine.command !== statuslineScript) {
|
|
445
|
-
settings.statusLine = { type: 'command', command: statuslineScript };
|
|
446
|
-
hooksChanged = true;
|
|
447
|
-
console.log(' Claude Code: installed ThumbGate status line');
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (hooksChanged) {
|
|
451
|
-
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
452
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
581
|
+
if (!hookResult.changed) {
|
|
582
|
+
console.log(` Claude Code hooks: already configured at ${hookResult.settingsPath}`);
|
|
583
|
+
} else {
|
|
584
|
+
for (const h of hookResult.added) {
|
|
585
|
+
console.log(` Claude Code: wired ${h.lifecycle} hook`);
|
|
586
|
+
}
|
|
587
|
+
console.log(` Claude Code settings: ${hookResult.settingsPath}`);
|
|
453
588
|
}
|
|
454
589
|
|
|
455
|
-
return mcpChanged ||
|
|
590
|
+
return mcpChanged || hookResult.changed;
|
|
456
591
|
}
|
|
457
592
|
|
|
458
593
|
function setupCodex() {
|
|
@@ -483,6 +618,20 @@ function setupCodex() {
|
|
|
483
618
|
}
|
|
484
619
|
|
|
485
620
|
function setupGemini() {
|
|
621
|
+
// Try to import custom commands as a Gemini plugin if the CLI is installed
|
|
622
|
+
const { execSync } = require('child_process');
|
|
623
|
+
let pluginImported = false;
|
|
624
|
+
for (const binName of ['agy', 'gemini']) {
|
|
625
|
+
try {
|
|
626
|
+
execSync(`${binName} plugin import "${PKG_ROOT}" --force`, { stdio: 'ignore' });
|
|
627
|
+
console.log(` Gemini: imported thumbgate plugin via ${binName}`);
|
|
628
|
+
pluginImported = true;
|
|
629
|
+
break;
|
|
630
|
+
} catch (err) {
|
|
631
|
+
// ignore errors if command doesn't exist or fails
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
486
635
|
const settingsPath = path.join(HOME, '.gemini', 'settings.json');
|
|
487
636
|
if (fs.existsSync(settingsPath)) {
|
|
488
637
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
@@ -503,13 +652,14 @@ function setupGemini() {
|
|
|
503
652
|
}
|
|
504
653
|
}
|
|
505
654
|
|
|
506
|
-
if (!changed) return
|
|
655
|
+
if (!changed) return pluginImported;
|
|
507
656
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
508
657
|
console.log(' Gemini: updated ~/.gemini/settings.json');
|
|
509
658
|
return true;
|
|
510
659
|
}
|
|
511
660
|
// Fallback: project-level .gemini/settings.json
|
|
512
|
-
|
|
661
|
+
const mcpChanged = mergeMcpJson(path.join(CWD, '.gemini', 'settings.json'), 'Gemini', 'project');
|
|
662
|
+
return pluginImported || mcpChanged;
|
|
513
663
|
}
|
|
514
664
|
|
|
515
665
|
function setupAmp() {
|
|
@@ -597,6 +747,104 @@ function detectAgent(projectDir) {
|
|
|
597
747
|
return null;
|
|
598
748
|
}
|
|
599
749
|
|
|
750
|
+
async function setupVertex(options = {}) {
|
|
751
|
+
const { execSync } = require('child_process');
|
|
752
|
+
const dryRun = options.dryRun === true || options['dry-run'] === true;
|
|
753
|
+
console.log(`\nthumbgate setup-vertex v${pkgVersion()}`);
|
|
754
|
+
console.log(' Zero-friction Google Cloud & Vertex AI onboarding...');
|
|
755
|
+
if (dryRun) {
|
|
756
|
+
console.log(' Dry run: will detect gcloud account/project, but will not enable services or write .env.');
|
|
757
|
+
}
|
|
758
|
+
console.log('');
|
|
759
|
+
|
|
760
|
+
// 1. Detect gcloud CLI
|
|
761
|
+
let activeAccount = '';
|
|
762
|
+
let activeProject = '';
|
|
763
|
+
try {
|
|
764
|
+
activeAccount = execSync('gcloud config get-value account', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
765
|
+
activeProject = execSync('gcloud config get-value project', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
766
|
+
} catch (_) {
|
|
767
|
+
console.log(' ⚠️ Google Cloud SDK (gcloud CLI) not detected or not logged in.');
|
|
768
|
+
console.log(' To automate setup, install the Google Cloud CLI and run: gcloud auth login');
|
|
769
|
+
console.log(' Otherwise, manually set the following variables in your .env file:');
|
|
770
|
+
console.log(' THUMBGATE_PROVIDER_MODE=vertex');
|
|
771
|
+
console.log(' VERTEX_PROJECT_ID=<your-gcp-project-id>');
|
|
772
|
+
console.log('');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (!activeAccount) {
|
|
777
|
+
console.log(' ⚠️ No active Google Cloud account set. Run: gcloud auth login');
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
console.log(` Active Account : \x1b[36m${activeAccount}\x1b[0m`);
|
|
782
|
+
if (!activeProject) {
|
|
783
|
+
console.log(' ⚠️ No active Google Cloud project set. Run: gcloud config set project <PROJECT_ID>');
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
console.log(` Active Project : \x1b[36m${activeProject}\x1b[0m`);
|
|
787
|
+
|
|
788
|
+
// Validate project ID matches GCP format before use in shell
|
|
789
|
+
if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(activeProject)) {
|
|
790
|
+
console.log(' ⚠️ Invalid GCP project ID format. Aborting.');
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (dryRun) {
|
|
795
|
+
console.log(` DRY-RUN would enable Vertex AI API for project: ${activeProject}`);
|
|
796
|
+
console.log(` DRY-RUN would write THUMBGATE_PROVIDER_MODE=vertex and VERTEX_PROJECT_ID=${activeProject} to .env.`);
|
|
797
|
+
console.log('');
|
|
798
|
+
console.log(' Dry run complete. Re-run without --dry-run to apply these changes.');
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// 2. Auto-enable Vertex AI API
|
|
803
|
+
console.log(' ⚙️ Enabling Vertex AI API in your project (this can take a few seconds)...');
|
|
804
|
+
try {
|
|
805
|
+
execSync(`gcloud services enable aiplatform.googleapis.com --project=${activeProject}`, { stdio: 'inherit' });
|
|
806
|
+
console.log(' ✅ Vertex AI API successfully enabled.');
|
|
807
|
+
} catch (err) {
|
|
808
|
+
console.log(' ⚠️ Could not programmatically enable Vertex AI API. Please make sure your billing is open.');
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// 3. Write env config to .env
|
|
812
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
813
|
+
let envContent = '';
|
|
814
|
+
if (fs.existsSync(envPath)) {
|
|
815
|
+
envContent = fs.readFileSync(envPath, 'utf8');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Helper to update or append env values
|
|
819
|
+
function updateEnvKey(content, key, value) {
|
|
820
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
821
|
+
if (regex.test(content)) {
|
|
822
|
+
return content.replace(regex, `${key}=${value}`);
|
|
823
|
+
}
|
|
824
|
+
return content.trim() + `\n${key}=${value}\n`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
let updatedContent = updateEnvKey(envContent, 'THUMBGATE_PROVIDER_MODE', 'vertex');
|
|
828
|
+
updatedContent = updateEnvKey(updatedContent, 'VERTEX_PROJECT_ID', activeProject);
|
|
829
|
+
|
|
830
|
+
fs.writeFileSync(envPath, updatedContent, 'utf8');
|
|
831
|
+
console.log(' ✅ Wrote configuration to local .env file.');
|
|
832
|
+
|
|
833
|
+
// 4. Print gorgeous success activation box
|
|
834
|
+
console.log('');
|
|
835
|
+
console.log(' ╭──────────────────────────────────────────────────────────╮');
|
|
836
|
+
console.log(' │ Vertex AI Setup Complete │');
|
|
837
|
+
console.log(' │ │');
|
|
838
|
+
console.log(' │ ThumbGate wrote local Vertex routing config. │');
|
|
839
|
+
console.log(' │ This does not create or verify a Dialogflow CX agent. │');
|
|
840
|
+
console.log(' │ Verify DFCX with the console or Dialogflow CX REST API. │');
|
|
841
|
+
console.log(' │ │');
|
|
842
|
+
console.log(' │ Try a test run: │');
|
|
843
|
+
console.log(' │ npx thumbgate feedback-self-test │');
|
|
844
|
+
console.log(' ╰──────────────────────────────────────────────────────────╯');
|
|
845
|
+
console.log('');
|
|
846
|
+
}
|
|
847
|
+
|
|
600
848
|
function quickStart() {
|
|
601
849
|
const qsArgs = parseArgs(process.argv.slice(3));
|
|
602
850
|
const projectDir = process.cwd();
|
|
@@ -657,8 +905,28 @@ function quickStart() {
|
|
|
657
905
|
console.log('');
|
|
658
906
|
}
|
|
659
907
|
|
|
908
|
+
// Activation walkthrough (guided first rule + live demonstrated block).
|
|
909
|
+
// Implementation lives in scripts/activation-quickstart.js so it can be unit
|
|
910
|
+
// tested without executing the CLI's top-level command switch. `init` is
|
|
911
|
+
// deliberately untouched — this is an additive, separate command.
|
|
912
|
+
function quickstart() {
|
|
913
|
+
return require(path.join(PKG_ROOT, 'scripts', 'activation-quickstart')).quickstart();
|
|
914
|
+
}
|
|
915
|
+
|
|
660
916
|
function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
661
917
|
const args = { ...cliArgs };
|
|
918
|
+
if (args.help || args.h) {
|
|
919
|
+
console.log('Usage: npx thumbgate init [--agent <name>] [--wire-hooks] [--email you@company.com]');
|
|
920
|
+
console.log('');
|
|
921
|
+
console.log('Scaffold ThumbGate in the current project and wire detected agent integrations.');
|
|
922
|
+
console.log('');
|
|
923
|
+
console.log('Options:');
|
|
924
|
+
console.log(' --agent <name> Wire a specific agent: claude-code, codex, gemini, amp, cursor, cline');
|
|
925
|
+
console.log(' --wire-hooks Wire hooks only; do not scaffold project files');
|
|
926
|
+
console.log(' --email <email> Subscribe installer to the setup guide and trial reminders');
|
|
927
|
+
console.log(' --dry-run Show hook changes without writing them');
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
662
930
|
|
|
663
931
|
// --wire-hooks only mode: skip scaffolding, just wire hooks
|
|
664
932
|
if (args['wire-hooks']) {
|
|
@@ -718,12 +986,43 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
|
718
986
|
// Always create .mcp.json (project-level MCP config used by Claude, Codex, Cursor)
|
|
719
987
|
mergeMcpJson(path.join(CWD, '.mcp.json'), 'MCP');
|
|
720
988
|
|
|
989
|
+
// Copy custom slash commands (.claude/commands/*.md) to the project's config directories
|
|
990
|
+
const pkgCommandsDir = path.join(PKG_ROOT, '.claude', 'commands');
|
|
991
|
+
if (fs.existsSync(pkgCommandsDir)) {
|
|
992
|
+
const targets = [
|
|
993
|
+
path.join(CWD, '.claude', 'commands'),
|
|
994
|
+
path.join(CWD, '.gemini', 'commands'),
|
|
995
|
+
path.join(CWD, '.antigravitycli', 'commands')
|
|
996
|
+
];
|
|
997
|
+
for (const projectCommandsDir of targets) {
|
|
998
|
+
if (!fs.existsSync(projectCommandsDir)) {
|
|
999
|
+
fs.mkdirSync(projectCommandsDir, { recursive: true });
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
const files = fs.readdirSync(pkgCommandsDir);
|
|
1003
|
+
for (const file of files) {
|
|
1004
|
+
if (file.endsWith('.md')) {
|
|
1005
|
+
fs.copyFileSync(path.join(pkgCommandsDir, file), path.join(projectCommandsDir, file));
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
console.log(` Failed to copy custom commands to ${path.relative(CWD, projectCommandsDir)}: ${err.message}`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
console.log('Scaffolded custom slash commands directories (.claude, .gemini, .antigravitycli)');
|
|
1013
|
+
}
|
|
1014
|
+
|
|
721
1015
|
// Auto-detect and configure platform-specific locations
|
|
722
1016
|
console.log('');
|
|
723
1017
|
console.log('Detecting platforms...');
|
|
724
1018
|
let configured = 0;
|
|
725
1019
|
|
|
726
1020
|
const platforms = [
|
|
1021
|
+
{ name: 'Claude Code', detect: [
|
|
1022
|
+
() => whichExists('claude'),
|
|
1023
|
+
() => fs.existsSync(path.join(HOME, '.claude')),
|
|
1024
|
+
() => fs.existsSync(path.join(CWD, '.claude')),
|
|
1025
|
+
], setup: setupClaude },
|
|
727
1026
|
{ name: 'Codex', detect: [() => whichExists('codex'), () => fs.existsSync(path.join(HOME, '.codex'))], setup: setupCodex },
|
|
728
1027
|
{ name: 'Gemini', detect: [() => whichExists('gemini'), () => fs.existsSync(path.join(HOME, '.gemini'))], setup: setupGemini },
|
|
729
1028
|
{ name: 'Amp', detect: [() => whichExists('amp'), () => fs.existsSync(path.join(HOME, '.amp'))], setup: setupAmp },
|
|
@@ -746,8 +1045,11 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
|
746
1045
|
}
|
|
747
1046
|
|
|
748
1047
|
// ChatGPT — cannot be automated
|
|
749
|
-
const chatgptSpec =
|
|
750
|
-
|
|
1048
|
+
const chatgptSpec = [
|
|
1049
|
+
path.join(PKG_ROOT, 'openapi', 'openapi.yaml'),
|
|
1050
|
+
path.join(PKG_ROOT, 'adapters', 'chatgpt', 'openapi.yaml'),
|
|
1051
|
+
].find((candidate) => fs.existsSync(candidate));
|
|
1052
|
+
if (chatgptSpec) {
|
|
751
1053
|
const projectChatgptSpec = path.join(thumbgateDir, 'chatgpt-openapi.yaml');
|
|
752
1054
|
fs.copyFileSync(chatgptSpec, projectChatgptSpec);
|
|
753
1055
|
console.log(` ChatGPT: import ${path.relative(CWD, projectChatgptSpec)} in GPT Builder > Actions`);
|
|
@@ -787,16 +1089,43 @@ function init(cliArgs = parseArgs(process.argv.slice(3))) {
|
|
|
787
1089
|
}
|
|
788
1090
|
|
|
789
1091
|
console.log('');
|
|
790
|
-
console.log(
|
|
791
|
-
|
|
1092
|
+
console.log(`✅ thumbgate v${pkgVersion()} initialized.`);
|
|
1093
|
+
const onboardingEmail = typeof args.email === 'string'
|
|
1094
|
+
? args.email.trim()
|
|
1095
|
+
: (typeof args['onboarding-email'] === 'string' ? args['onboarding-email'].trim() : '');
|
|
1096
|
+
if (onboardingEmail) {
|
|
1097
|
+
try {
|
|
1098
|
+
execFileSync(process.execPath, [__filename, 'subscribe', onboardingEmail], {
|
|
1099
|
+
cwd: CWD,
|
|
1100
|
+
env: process.env,
|
|
1101
|
+
stdio: 'inherit',
|
|
1102
|
+
});
|
|
1103
|
+
trackEvent('cli_init_email_subscribed', { command: 'init', source: 'init_email' });
|
|
1104
|
+
} catch (_) {
|
|
1105
|
+
console.log(` Retry onboarding email: npx thumbgate subscribe ${onboardingEmail}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
792
1108
|
trackEvent('cli_init', { command: 'init' });
|
|
793
|
-
|
|
1109
|
+
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
// Activation guide: the ONE thing the user should do next.
|
|
1112
|
+
// 98.5% of init users never promote their first prevention rule.
|
|
1113
|
+
// This is the funnel break — not conversion, not nudges — activation.
|
|
1114
|
+
// ---------------------------------------------------------------------------
|
|
794
1115
|
console.log('');
|
|
795
|
-
console.log('
|
|
796
|
-
console.log(' │
|
|
797
|
-
console.log(' │
|
|
798
|
-
console.log(' │
|
|
799
|
-
console.log('
|
|
1116
|
+
console.log(' ╭──────────────────────────────────────────────────────────╮');
|
|
1117
|
+
console.log(' │ NEXT: Prove feedback capture works │');
|
|
1118
|
+
console.log(' │ │');
|
|
1119
|
+
console.log(' │ npx thumbgate feedback-self-test │');
|
|
1120
|
+
console.log(' │ │');
|
|
1121
|
+
console.log(' │ Then dogfood it in chat: │');
|
|
1122
|
+
console.log(' │ thumbs down: agent skipped verification │');
|
|
1123
|
+
console.log(' ╰──────────────────────────────────────────────────────────╯');
|
|
1124
|
+
console.log('');
|
|
1125
|
+
printInitConversionPrompt(onboardingEmail);
|
|
1126
|
+
if (!onboardingEmail) {
|
|
1127
|
+
console.log(' Get trial reminders: npx thumbgate init --email you@company.com');
|
|
1128
|
+
}
|
|
800
1129
|
|
|
801
1130
|
try {
|
|
802
1131
|
const { appendFunnelEvent } = require(path.join(PKG_ROOT, 'scripts', 'billing'));
|
|
@@ -826,7 +1155,7 @@ function capture() {
|
|
|
826
1155
|
const { getUsage } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
|
|
827
1156
|
const capLimit = checkLimit('capture_feedback');
|
|
828
1157
|
if (!capLimit.allowed) {
|
|
829
|
-
limitNudge('capture_feedback');
|
|
1158
|
+
limitNudge('capture_feedback', capLimit);
|
|
830
1159
|
process.exit(1);
|
|
831
1160
|
}
|
|
832
1161
|
trackEvent('cli_capture', { command: 'capture' });
|
|
@@ -841,23 +1170,88 @@ function capture() {
|
|
|
841
1170
|
return;
|
|
842
1171
|
}
|
|
843
1172
|
|
|
844
|
-
|
|
1173
|
+
// Parse pure positional arguments
|
|
1174
|
+
const rawArgv = process.argv.slice(3);
|
|
1175
|
+
const positionalArgs = [];
|
|
1176
|
+
const BOOLEAN_FLAGS = new Set(['--json', '--verbose', '--quiet', '--dry-run', '--stats', '--summary', '--no-nudge', '--help']);
|
|
1177
|
+
for (let i = 0; i < rawArgv.length; i++) {
|
|
1178
|
+
const arg = rawArgv[i];
|
|
1179
|
+
if (arg.startsWith('--')) {
|
|
1180
|
+
if (!arg.includes('=') && !BOOLEAN_FLAGS.has(arg)) {
|
|
1181
|
+
i++;
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
positionalArgs.push(arg);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
let signal = (args.feedback || '').toLowerCase();
|
|
1189
|
+
let consumedSignalArgs = 0;
|
|
1190
|
+
if (!signal && positionalArgs[0]) {
|
|
1191
|
+
const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
|
|
1192
|
+
const oneWord = positionalArgs[0];
|
|
1193
|
+
const twoWords = positionalArgs.slice(0, 2).join(' ');
|
|
1194
|
+
const detected = detectFeedbackSignal(twoWords) || detectFeedbackSignal(oneWord);
|
|
1195
|
+
if (detected) {
|
|
1196
|
+
signal = detected.signal;
|
|
1197
|
+
consumedSignalArgs = detectFeedbackSignal(twoWords) ? Math.min(2, positionalArgs.length) : 1;
|
|
1198
|
+
} else {
|
|
1199
|
+
const firstPos = positionalArgs[0].toLowerCase();
|
|
1200
|
+
if (['up', 'down', 'thumbsup', 'thumbsdown', 'thumbs_up', 'thumbs_down', 'positive', 'negative'].some(v => firstPos.includes(v))) {
|
|
1201
|
+
signal = firstPos;
|
|
1202
|
+
consumedSignalArgs = 1;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
845
1207
|
const normalized = ['up', 'thumbsup', 'thumbs_up', 'positive'].some(v => signal.includes(v)) ? 'up'
|
|
846
1208
|
: ['down', 'thumbsdown', 'thumbs_down', 'negative'].some(v => signal.includes(v)) ? 'down'
|
|
847
1209
|
: signal;
|
|
848
1210
|
|
|
849
1211
|
if (normalized !== 'up' && normalized !== 'down') {
|
|
850
|
-
console.error('Missing or unrecognized --feedback=up|down');
|
|
1212
|
+
console.error('Missing or unrecognized --feedback=up|down (or positional argument)');
|
|
1213
|
+
process.exit(1);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const gateAction = (args.action || '').toLowerCase();
|
|
1217
|
+
if (gateAction && !['block', 'approve', 'warn'].includes(gateAction)) {
|
|
1218
|
+
console.error('Unrecognized --action. Use: block (default), approve, or warn');
|
|
851
1219
|
process.exit(1);
|
|
852
1220
|
}
|
|
853
1221
|
|
|
1222
|
+
let context = args.context || '';
|
|
1223
|
+
if (!context && consumedSignalArgs > 0) {
|
|
1224
|
+
context = positionalArgs.slice(consumedSignalArgs).join(' ');
|
|
1225
|
+
} else if (!context && positionalArgs[1]) {
|
|
1226
|
+
context = positionalArgs[1];
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
let whatWentWrong = args['what-went-wrong'];
|
|
1230
|
+
if (!whatWentWrong && consumedSignalArgs > 0 && positionalArgs.length > consumedSignalArgs + 1) {
|
|
1231
|
+
whatWentWrong = positionalArgs.slice(consumedSignalArgs + 1).join(' ');
|
|
1232
|
+
} else if (!whatWentWrong && positionalArgs[2]) {
|
|
1233
|
+
whatWentWrong = positionalArgs[2];
|
|
1234
|
+
} else if (!whatWentWrong && normalized === 'down' && context) {
|
|
1235
|
+
whatWentWrong = context;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
let whatToChange = args['what-to-change'];
|
|
1239
|
+
if (!whatToChange && consumedSignalArgs > 0 && positionalArgs.length > consumedSignalArgs + 2) {
|
|
1240
|
+
whatToChange = positionalArgs.slice(consumedSignalArgs + 2).join(' ');
|
|
1241
|
+
} else if (!whatToChange && positionalArgs[3]) {
|
|
1242
|
+
whatToChange = positionalArgs[3];
|
|
1243
|
+
} else if (!whatToChange && normalized === 'down' && context) {
|
|
1244
|
+
whatToChange = `avoid: ${context}`;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
854
1247
|
const result = captureFeedback({
|
|
855
1248
|
signal: normalized,
|
|
856
|
-
context:
|
|
857
|
-
whatWentWrong:
|
|
858
|
-
whatToChange:
|
|
1249
|
+
context: context,
|
|
1250
|
+
whatWentWrong: whatWentWrong,
|
|
1251
|
+
whatToChange: whatToChange,
|
|
859
1252
|
whatWorked: args['what-worked'],
|
|
860
1253
|
tags: args.tags,
|
|
1254
|
+
gateAction: gateAction || undefined,
|
|
861
1255
|
});
|
|
862
1256
|
|
|
863
1257
|
if (result.accepted) {
|
|
@@ -885,10 +1279,21 @@ function capture() {
|
|
|
885
1279
|
const pct = Math.round((capLimit.used / capLimit.limit) * 100);
|
|
886
1280
|
console.log(` Usage : ${capLimit.used}/${capLimit.limit} captures today (${pct}%)`);
|
|
887
1281
|
if (capLimit.remaining <= 1) {
|
|
888
|
-
console.log(` ⚠️ Free tier limit reached. Upgrade to Pro for unlimited: https://thumbgate
|
|
1282
|
+
console.log(` ⚠️ Free tier limit reached. Upgrade to Pro for unlimited: https://thumbgate.ai/pro`);
|
|
889
1283
|
}
|
|
890
1284
|
}
|
|
891
1285
|
console.log('');
|
|
1286
|
+
try {
|
|
1287
|
+
const { buildCaptureReceipt } = require(path.join(PKG_ROOT, 'scripts', 'commercial-offer'));
|
|
1288
|
+
console.log(buildCaptureReceipt({
|
|
1289
|
+
signal: normalized,
|
|
1290
|
+
feedbackId: ev.id,
|
|
1291
|
+
memoryId: mem.id,
|
|
1292
|
+
actionType: ev.actionType,
|
|
1293
|
+
}));
|
|
1294
|
+
} catch (_) {
|
|
1295
|
+
// Receipt is a conversion aid, not part of feedback persistence.
|
|
1296
|
+
}
|
|
892
1297
|
proNudge();
|
|
893
1298
|
} else {
|
|
894
1299
|
if (args.json) {
|
|
@@ -906,13 +1311,130 @@ function capture() {
|
|
|
906
1311
|
}
|
|
907
1312
|
}
|
|
908
1313
|
|
|
1314
|
+
function readJsonlEntries(filePath) {
|
|
1315
|
+
try {
|
|
1316
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
1317
|
+
.split(/\r?\n/)
|
|
1318
|
+
.map((line) => line.trim())
|
|
1319
|
+
.filter(Boolean)
|
|
1320
|
+
.map((line) => JSON.parse(line));
|
|
1321
|
+
} catch (_) {
|
|
1322
|
+
return [];
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function shellSingleQuote(value) {
|
|
1327
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function feedbackSelfTest() {
|
|
1331
|
+
const args = parseArgs(process.argv.slice(3));
|
|
1332
|
+
const signalArg = String(args.feedback || args.signal || 'down').toLowerCase();
|
|
1333
|
+
const normalized = ['up', 'thumbsup', 'thumbs_up', 'positive'].some((v) => signalArg.includes(v)) ? 'up'
|
|
1334
|
+
: ['down', 'thumbsdown', 'thumbs_down', 'negative'].some((v) => signalArg.includes(v)) ? 'down'
|
|
1335
|
+
: signalArg;
|
|
1336
|
+
|
|
1337
|
+
if (normalized !== 'up' && normalized !== 'down') {
|
|
1338
|
+
console.error('feedback-self-test needs --feedback=up|down when overriding the default.');
|
|
1339
|
+
process.exit(1);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const previousFeedbackDir = process.env.THUMBGATE_FEEDBACK_DIR;
|
|
1343
|
+
const previousNoNudge = process.env.THUMBGATE_NO_NUDGE;
|
|
1344
|
+
const feedbackDir = args['feedback-dir']
|
|
1345
|
+
? path.resolve(CWD, args['feedback-dir'])
|
|
1346
|
+
: args.persist
|
|
1347
|
+
? null
|
|
1348
|
+
: fs.mkdtempSync(path.join(os.tmpdir(), 'thumbgate-feedback-self-test-'));
|
|
1349
|
+
const isolated = Boolean(feedbackDir);
|
|
1350
|
+
|
|
1351
|
+
if (feedbackDir) process.env.THUMBGATE_FEEDBACK_DIR = feedbackDir;
|
|
1352
|
+
process.env.THUMBGATE_NO_NUDGE = '1';
|
|
1353
|
+
|
|
1354
|
+
try {
|
|
1355
|
+
const { captureFeedback, getFeedbackPaths } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
1356
|
+
const context = args.context || `feedback self-test: typed thumbs ${normalized} reaches ThumbGate capture`;
|
|
1357
|
+
const result = captureFeedback({
|
|
1358
|
+
signal: normalized,
|
|
1359
|
+
context,
|
|
1360
|
+
whatWentWrong: normalized === 'down'
|
|
1361
|
+
? (args['what-went-wrong'] || 'Need proof that feedback capture is wired in this runtime')
|
|
1362
|
+
: undefined,
|
|
1363
|
+
whatToChange: normalized === 'down'
|
|
1364
|
+
? (args['what-to-change'] || 'Run a one-command self-test before claiming thumbs feedback is captured')
|
|
1365
|
+
: undefined,
|
|
1366
|
+
whatWorked: normalized === 'up'
|
|
1367
|
+
? (args['what-worked'] || 'Feedback capture persisted and was verified by a self-test')
|
|
1368
|
+
: undefined,
|
|
1369
|
+
tags: args.tags || 'self-test,dogfood,feedback-capture',
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
const paths = getFeedbackPaths();
|
|
1373
|
+
const feedbackRows = readJsonlEntries(paths.FEEDBACK_LOG_PATH);
|
|
1374
|
+
const memoryRows = readJsonlEntries(paths.MEMORY_LOG_PATH);
|
|
1375
|
+
const feedbackId = result.feedbackEvent && result.feedbackEvent.id;
|
|
1376
|
+
const memoryId = result.memoryRecord && result.memoryRecord.id;
|
|
1377
|
+
const feedbackStored = Boolean(feedbackId && feedbackRows.some((row) => row.id === feedbackId));
|
|
1378
|
+
const memoryStored = Boolean(memoryId && memoryRows.some((row) => row.id === memoryId));
|
|
1379
|
+
const ok = Boolean(result.accepted && feedbackStored && memoryStored);
|
|
1380
|
+
|
|
1381
|
+
const payload = {
|
|
1382
|
+
ok,
|
|
1383
|
+
command: 'feedback-self-test',
|
|
1384
|
+
signal: normalized,
|
|
1385
|
+
accepted: Boolean(result.accepted),
|
|
1386
|
+
feedbackId: feedbackId || null,
|
|
1387
|
+
memoryId: memoryId || null,
|
|
1388
|
+
feedbackStored,
|
|
1389
|
+
memoryStored,
|
|
1390
|
+
isolated,
|
|
1391
|
+
feedbackDir: paths.FEEDBACK_DIR,
|
|
1392
|
+
nextDogfoodCommand: `npx thumbgate capture --feedback=${normalized} --context=${shellSingleQuote(context)}`,
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
if (args.json) {
|
|
1396
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1397
|
+
} else if (ok) {
|
|
1398
|
+
console.log('\nThumbGate feedback self-test: PASS');
|
|
1399
|
+
console.log('─'.repeat(50));
|
|
1400
|
+
console.log(` Captured : ${normalized} (${feedbackId})`);
|
|
1401
|
+
console.log(` Stored lesson: ${memoryId}`);
|
|
1402
|
+
console.log(` Storage : ${paths.FEEDBACK_DIR}`);
|
|
1403
|
+
console.log(` Mode : ${isolated ? 'isolated test store' : 'active ThumbGate store'}`);
|
|
1404
|
+
console.log('\nDogfood in chat with:');
|
|
1405
|
+
console.log(` thumbs ${normalized}: ${context}`);
|
|
1406
|
+
} else {
|
|
1407
|
+
console.log('\nThumbGate feedback self-test: FAIL');
|
|
1408
|
+
console.log('─'.repeat(50));
|
|
1409
|
+
console.log(` Accepted : ${payload.accepted}`);
|
|
1410
|
+
console.log(` Feedback stored: ${feedbackStored}`);
|
|
1411
|
+
console.log(` Memory stored : ${memoryStored}`);
|
|
1412
|
+
console.log(` Storage : ${paths.FEEDBACK_DIR}`);
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
if (!ok) process.exit(2);
|
|
1416
|
+
} finally {
|
|
1417
|
+
if (previousFeedbackDir === undefined) delete process.env.THUMBGATE_FEEDBACK_DIR;
|
|
1418
|
+
else process.env.THUMBGATE_FEEDBACK_DIR = previousFeedbackDir;
|
|
1419
|
+
if (previousNoNudge === undefined) delete process.env.THUMBGATE_NO_NUDGE;
|
|
1420
|
+
else process.env.THUMBGATE_NO_NUDGE = previousNoNudge;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
909
1424
|
function stats() {
|
|
910
1425
|
trackEvent('cli_stats', { command: 'stats' });
|
|
911
1426
|
const args = parseArgs(process.argv.slice(3));
|
|
912
1427
|
const { analyzeFeedback } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
913
1428
|
const data = analyzeFeedback();
|
|
914
1429
|
|
|
1430
|
+
// Gate enforcement stats — runtime intercepts + configured gates
|
|
1431
|
+
let gateData = { blocked: 0, warned: 0, passed: 0, byGate: {} };
|
|
1432
|
+
try { gateData = require(path.join(PKG_ROOT, 'scripts', 'gates-engine')).loadStats(); } catch {}
|
|
1433
|
+
let gateConfigData = { totalGates: 0, autoPromotedGates: 0, estimatedHoursSaved: '0.0', topBlocked: null, firstTimeFixRate: null };
|
|
1434
|
+
try { gateConfigData = require(path.join(PKG_ROOT, 'scripts', 'gate-stats')).calculateStats(); } catch {}
|
|
1435
|
+
|
|
915
1436
|
const avgCostOfMistake = 2.50;
|
|
1437
|
+
const totalInterceptions = gateData.blocked + gateData.warned;
|
|
916
1438
|
const payload = {
|
|
917
1439
|
total: data.total,
|
|
918
1440
|
positives: data.totalPositive,
|
|
@@ -922,6 +1444,13 @@ function stats() {
|
|
|
922
1444
|
revenueAtRisk: Number((data.totalNegative * avgCostOfMistake).toFixed(2)),
|
|
923
1445
|
topTags: data.topTags || [],
|
|
924
1446
|
recentActivity: data.recentActivity || [],
|
|
1447
|
+
gatesBlocked: gateData.blocked,
|
|
1448
|
+
gatesWarned: gateData.warned,
|
|
1449
|
+
totalGates: gateConfigData.totalGates,
|
|
1450
|
+
autoPromotedGates: gateConfigData.autoPromotedGates,
|
|
1451
|
+
estimatedHoursSaved: gateConfigData.estimatedHoursSaved,
|
|
1452
|
+
topBlockedGate: gateConfigData.topBlocked ? gateConfigData.topBlocked.id : null,
|
|
1453
|
+
firstTimeFixRate: gateConfigData.firstTimeFixRate,
|
|
925
1454
|
};
|
|
926
1455
|
|
|
927
1456
|
if (args.json) {
|
|
@@ -935,17 +1464,36 @@ function stats() {
|
|
|
935
1464
|
console.log(` Approval Rate : ${payload.approvalRate}%`);
|
|
936
1465
|
console.log(` Recent Trend : ${payload.recentTrend}%`);
|
|
937
1466
|
|
|
1467
|
+
// Gate enforcement — the high-ROI section
|
|
1468
|
+
if (totalInterceptions > 0 || payload.totalGates > 0) {
|
|
1469
|
+
console.log('\n🛡️ PRE-ACTION GATE ENFORCEMENT');
|
|
1470
|
+
console.log(` Actions blocked : ${payload.gatesBlocked}`);
|
|
1471
|
+
console.log(` Actions warned : ${payload.gatesWarned}`);
|
|
1472
|
+
console.log(` Active gates : ${payload.totalGates} (${payload.autoPromotedGates} auto-promoted)`);
|
|
1473
|
+
if (payload.topBlockedGate) console.log(` Top blocker : ${payload.topBlockedGate}`);
|
|
1474
|
+
console.log(` Est. time saved : ~${payload.estimatedHoursSaved} hours`);
|
|
1475
|
+
const { formatFirstTimeFixRate } = require(path.join(PKG_ROOT, 'scripts', 'gate-stats'));
|
|
1476
|
+
console.log(` First-time fix : ${formatFirstTimeFixRate(payload.firstTimeFixRate)}`);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
938
1479
|
if (payload.negatives > 0) {
|
|
939
1480
|
console.log('\n⚠️ REVENUE-AT-RISK ANALYSIS');
|
|
940
1481
|
console.log(` Repeated Failures detected: ${payload.negatives}`);
|
|
941
1482
|
console.log(` Estimated Operational Loss: $${payload.revenueAtRisk}`);
|
|
942
1483
|
console.log(' Action Required: Run "npx thumbgate rules" to generate guardrails.');
|
|
943
1484
|
console.log(' Strategic Recommendation: if this is a shared workflow problem, start the Workflow Hardening Sprint.');
|
|
944
|
-
console.log(' Team intake: https://thumbgate
|
|
1485
|
+
console.log(' Team intake: https://thumbgate.ai/#workflow-sprint-intake');
|
|
945
1486
|
console.log(' Solo side lane: npx thumbgate pro');
|
|
946
1487
|
} else {
|
|
947
1488
|
console.log('\n✅ System is currently high-reliability. No immediate revenue loss detected.');
|
|
948
1489
|
}
|
|
1490
|
+
try {
|
|
1491
|
+
const { buildStatsReceipt } = require(path.join(PKG_ROOT, 'scripts', 'commercial-offer'));
|
|
1492
|
+
const receipt = buildStatsReceipt(payload);
|
|
1493
|
+
if (receipt) console.log(receipt);
|
|
1494
|
+
} catch (_) {
|
|
1495
|
+
// Keep stats resilient if the receipt helper is unavailable in old installs.
|
|
1496
|
+
}
|
|
949
1497
|
proNudge();
|
|
950
1498
|
}
|
|
951
1499
|
|
|
@@ -1051,13 +1599,14 @@ function pro() {
|
|
|
1051
1599
|
} = require(path.join(PKG_ROOT, 'scripts', 'pro-local-dashboard'));
|
|
1052
1600
|
|
|
1053
1601
|
function printProInfo() {
|
|
1054
|
-
const hostedUrl = 'https://thumbgate
|
|
1602
|
+
const hostedUrl = 'https://thumbgate.ai';
|
|
1055
1603
|
const truthUrl = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/COMMERCIAL_TRUTH.md';
|
|
1056
1604
|
console.log('\nThumbGate Pro — Local Dashboard');
|
|
1057
1605
|
console.log('─'.repeat(50));
|
|
1058
1606
|
console.log('Self-serve side lane today: Pro ($19/mo or $149/yr).');
|
|
1059
1607
|
console.log('Every licensed Pro user gets a personal local dashboard on localhost.');
|
|
1060
1608
|
console.log('\nWhat is available:');
|
|
1609
|
+
console.log(' - Hosted sync: keep lessons, rules, and dashboard state aligned across laptops, CI, containers, and agent runtimes');
|
|
1061
1610
|
console.log(' - Local Pro dashboard: your own browser dashboard for search, gates, and DPO export');
|
|
1062
1611
|
console.log(' - Team rollout path: shared hosted lessons, org visibility, and workflow proof');
|
|
1063
1612
|
console.log(' - Commercial truth doc: source of truth for traction, pricing, and proof claims');
|
|
@@ -1195,6 +1744,13 @@ function lessons() {
|
|
|
1195
1744
|
const tags = String(args.tags || '').split(',').map((t) => t.trim()).filter(Boolean);
|
|
1196
1745
|
const query = args.query || process.argv.slice(3).find((a) => !a.startsWith('--')) || '';
|
|
1197
1746
|
const limit = Number(args.limit || 10);
|
|
1747
|
+
const { checkLimit } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
|
|
1748
|
+
const searchLimit = checkLimit('search_lessons');
|
|
1749
|
+
if (!searchLimit.allowed) {
|
|
1750
|
+
trackEvent('cli_upgrade_prompt', { command: 'lessons', action: 'search_lessons', limitType: searchLimit.limitType || null });
|
|
1751
|
+
limitNudge('search_lessons', searchLimit);
|
|
1752
|
+
process.exit(1);
|
|
1753
|
+
}
|
|
1198
1754
|
|
|
1199
1755
|
// --remote: fetch from hosted Railway instance
|
|
1200
1756
|
if (args.remote) {
|
|
@@ -1361,6 +1917,7 @@ function modelCandidatesCmd() {
|
|
|
1361
1917
|
const maxCandidates = args.max ? Number(args.max) : undefined;
|
|
1362
1918
|
const { reportPath, report } = writeModelCandidatesReport(undefined, {
|
|
1363
1919
|
workload: args.workload,
|
|
1920
|
+
workloadFile: args['workload-file'] || args.workloadFile,
|
|
1364
1921
|
provider: args.provider,
|
|
1365
1922
|
family: args.family,
|
|
1366
1923
|
gateway: args.gateway,
|
|
@@ -1461,13 +2018,10 @@ function risk() {
|
|
|
1461
2018
|
}
|
|
1462
2019
|
|
|
1463
2020
|
function exportDpo() {
|
|
1464
|
-
const {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
` Your feedback would generate valuable training pairs.\n` +
|
|
1469
|
-
` Upgrade: ${PRO_CHECKOUT_URL}\n\n`
|
|
1470
|
-
);
|
|
2021
|
+
const { checkLimit } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
|
|
2022
|
+
const exportLimit = checkLimit('export_dpo');
|
|
2023
|
+
if (!exportLimit.allowed) {
|
|
2024
|
+
limitNudge('export_dpo', exportLimit);
|
|
1471
2025
|
process.exit(1);
|
|
1472
2026
|
}
|
|
1473
2027
|
const extraArgs = process.argv.slice(3).join(' ');
|
|
@@ -1484,13 +2038,10 @@ function exportDpo() {
|
|
|
1484
2038
|
}
|
|
1485
2039
|
|
|
1486
2040
|
function exportDatabricks() {
|
|
1487
|
-
const {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
` Export feedback logs + proof artifacts for analytics.\n` +
|
|
1492
|
-
` Upgrade: ${PRO_CHECKOUT_URL}\n\n`
|
|
1493
|
-
);
|
|
2041
|
+
const { checkLimit } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
|
|
2042
|
+
const exportLimit = checkLimit('export_databricks');
|
|
2043
|
+
if (!exportLimit.allowed) {
|
|
2044
|
+
limitNudge('export_databricks', exportLimit);
|
|
1494
2045
|
process.exit(1);
|
|
1495
2046
|
}
|
|
1496
2047
|
const extraArgs = process.argv.slice(3).join(' ');
|
|
@@ -1685,6 +2236,26 @@ function pulse() {
|
|
|
1685
2236
|
});
|
|
1686
2237
|
}
|
|
1687
2238
|
|
|
2239
|
+
function checkUpdateCmd() {
|
|
2240
|
+
const { checkUpdate } = require(path.join(PKG_ROOT, 'scripts', 'check-update'));
|
|
2241
|
+
const args = parseArgs(process.argv.slice(3));
|
|
2242
|
+
checkUpdate({ verbose: !args.json, force: args.force }).then((res) => {
|
|
2243
|
+
if (args.json) {
|
|
2244
|
+
console.log(JSON.stringify(res, null, 2));
|
|
2245
|
+
}
|
|
2246
|
+
process.exit(0);
|
|
2247
|
+
}).catch((err) => {
|
|
2248
|
+
console.error(err && err.message ? err.message : err);
|
|
2249
|
+
process.exit(1);
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
function selfUpdateCmd() {
|
|
2254
|
+
const { selfUpdate } = require(path.join(PKG_ROOT, 'scripts', 'check-update'));
|
|
2255
|
+
const success = selfUpdate();
|
|
2256
|
+
process.exit(success ? 0 : 1);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
1688
2259
|
function dispatchBrief() {
|
|
1689
2260
|
const args = parseArgs(process.argv.slice(3));
|
|
1690
2261
|
const {
|
|
@@ -1718,6 +2289,25 @@ function gateStats() {
|
|
|
1718
2289
|
console.log('\n' + formatStats(stats) + '\n');
|
|
1719
2290
|
}
|
|
1720
2291
|
|
|
2292
|
+
function contextPacks() {
|
|
2293
|
+
const args = parseArgs(process.argv.slice(3));
|
|
2294
|
+
const { generateAutoContextPacks } = require(path.join(PKG_ROOT, 'scripts', 'auto-context-packs'));
|
|
2295
|
+
const result = generateAutoContextPacks();
|
|
2296
|
+
if (args.json) {
|
|
2297
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
console.log(`\nGenerated ${result.packCount} context pack(s):`);
|
|
2301
|
+
for (const p of result.packs) {
|
|
2302
|
+
console.log(` [${p.type}] ${p.name}`);
|
|
2303
|
+
console.log(` -> ${p.filePath}`);
|
|
2304
|
+
}
|
|
2305
|
+
if (result.packCount === 0) {
|
|
2306
|
+
console.log(' (No failure patterns found yet — capture some feedback first.)');
|
|
2307
|
+
}
|
|
2308
|
+
console.log('');
|
|
2309
|
+
}
|
|
2310
|
+
|
|
1721
2311
|
function harnessAudit() {
|
|
1722
2312
|
const args = parseArgs(process.argv.slice(3));
|
|
1723
2313
|
const {
|
|
@@ -1998,7 +2588,7 @@ function backgroundGovernance() {
|
|
|
1998
2588
|
console.log(' - Run --check before dispatching an unattended PR job.');
|
|
1999
2589
|
console.log(' - Route protected branches and large blast-radius jobs to human review.');
|
|
2000
2590
|
console.log(' - Convert CI failures into thumbs-down lessons so repeats become Pre-Action Checks.');
|
|
2001
|
-
console.log('\nGuide: https://thumbgate
|
|
2591
|
+
console.log('\nGuide: https://thumbgate.ai/guides/background-agent-governance\n');
|
|
2002
2592
|
}
|
|
2003
2593
|
|
|
2004
2594
|
function optimize() {
|
|
@@ -2006,6 +2596,52 @@ function optimize() {
|
|
|
2006
2596
|
doOptimize();
|
|
2007
2597
|
}
|
|
2008
2598
|
|
|
2599
|
+
function syncGcp() {
|
|
2600
|
+
const { syncToGcp } = require(path.join(PKG_ROOT, 'adapters', 'gcp', 'sync.js'));
|
|
2601
|
+
syncToGcp();
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
function cleanup() {
|
|
2605
|
+
console.log('Cleaning up ThumbGate processes...');
|
|
2606
|
+
try {
|
|
2607
|
+
const { execSync } = require('child_process');
|
|
2608
|
+
// Kill all 'thumbgate serve' and 'thumbgate dashboard' processes except this one
|
|
2609
|
+
const pids = execSync("ps aux | grep -E 'thumbgate (serve|dashboard|mcp)' | grep -v 'grep' | grep -v 'cleanup' | awk '{print $2}'", { encoding: 'utf8' })
|
|
2610
|
+
.split('\n')
|
|
2611
|
+
.filter(Boolean)
|
|
2612
|
+
.map(Number)
|
|
2613
|
+
.filter(pid => pid !== process.pid);
|
|
2614
|
+
|
|
2615
|
+
if (pids.length > 0) {
|
|
2616
|
+
console.log(`Killing ${pids.length} process(es): ${pids.join(', ')}`);
|
|
2617
|
+
pids.forEach(pid => {
|
|
2618
|
+
try { process.kill(pid, 'SIGTERM'); } catch (_) {}
|
|
2619
|
+
});
|
|
2620
|
+
// Give them a moment to die
|
|
2621
|
+
execSync('sleep 1');
|
|
2622
|
+
} else {
|
|
2623
|
+
console.log('No other ThumbGate processes found.');
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
// Check port 3456 specifically
|
|
2627
|
+
try {
|
|
2628
|
+
const portPids = execSync("lsof -ti :3456", { encoding: 'utf8' })
|
|
2629
|
+
.split('\n')
|
|
2630
|
+
.map(s => s.trim())
|
|
2631
|
+
.filter(Boolean)
|
|
2632
|
+
.map(Number);
|
|
2633
|
+
portPids.forEach(pid => {
|
|
2634
|
+
console.log(`Killing process ${pid} holding port 3456`);
|
|
2635
|
+
try { process.kill(pid, 'SIGKILL'); } catch (_) {}
|
|
2636
|
+
});
|
|
2637
|
+
} catch (_) { /* port already free */ }
|
|
2638
|
+
|
|
2639
|
+
console.log('✅ Cleanup complete. Run "npx thumbgate pro" to restart the dashboard.');
|
|
2640
|
+
} catch (err) {
|
|
2641
|
+
console.error(`Cleanup failed: ${err.message}`);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2009
2645
|
function serve() {
|
|
2010
2646
|
try {
|
|
2011
2647
|
const { repairCodexHooks } = require(path.join(PKG_ROOT, 'scripts', 'codex-self-heal'));
|
|
@@ -2042,11 +2678,34 @@ function install() {
|
|
|
2042
2678
|
}
|
|
2043
2679
|
|
|
2044
2680
|
async function gateCheck() {
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2681
|
+
// Explicit emergency escape hatch ONLY. The 2026-06-03 hotfix made this
|
|
2682
|
+
// bypass-by-default, which silently disabled ThumbGate's enforcement entirely
|
|
2683
|
+
// (the firewall approved everything). Restored 2026-06-04: enforcement runs by
|
|
2684
|
+
// default in warn-by-default posture (see gates-engine applyEnforcementPosture);
|
|
2685
|
+
// set THUMBGATE_HOTFIX_BYPASS=1 to disable all checks if a gate ever misfires.
|
|
2686
|
+
if (process.env.THUMBGATE_HOTFIX_BYPASS === '1') {
|
|
2687
|
+
process.stdout.write(JSON.stringify({
|
|
2688
|
+
decision: 'approve',
|
|
2689
|
+
reason: 'hotfix-bypass-opt-in',
|
|
2690
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: '' }
|
|
2691
|
+
}) + '\n');
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
try {
|
|
2695
|
+
const payload = readStdinText();
|
|
2696
|
+
const input = payload ? JSON.parse(payload) : {};
|
|
2697
|
+
const gatesEngine = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
|
|
2698
|
+
const output = await gatesEngine.runAsync(input);
|
|
2699
|
+
process.stdout.write(output + '\n');
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
process.stderr.write(`gate-check error: ${err.message}\n`);
|
|
2702
|
+
process.stdout.write(JSON.stringify({
|
|
2703
|
+
hookSpecificOutput: {
|
|
2704
|
+
hookEventName: 'PreToolUse',
|
|
2705
|
+
additionalContext: `[ThumbGate Error] ${err.message}`,
|
|
2706
|
+
}
|
|
2707
|
+
}) + '\n');
|
|
2708
|
+
}
|
|
2050
2709
|
}
|
|
2051
2710
|
|
|
2052
2711
|
function cacheUpdate() {
|
|
@@ -2073,9 +2732,14 @@ function statuslineRender() {
|
|
|
2073
2732
|
|
|
2074
2733
|
function hookAutoCapture() {
|
|
2075
2734
|
syncActiveProjectContext();
|
|
2076
|
-
const prompt = process.env.CLAUDE_USER_PROMPT
|
|
2735
|
+
const prompt = process.env.CLAUDE_USER_PROMPT
|
|
2736
|
+
|| process.env.THUMBGATE_USER_PROMPT
|
|
2737
|
+
|| process.env.CODEX_USER_PROMPT
|
|
2738
|
+
|| process.env.USER_PROMPT
|
|
2739
|
+
|| readStdinText().trim();
|
|
2077
2740
|
const { evaluatePromptGuard } = require(path.join(PKG_ROOT, 'scripts', 'prompt-guard'));
|
|
2078
2741
|
const { processInlineFeedback, formatCliOutput } = require(path.join(PKG_ROOT, 'scripts', 'cli-feedback'));
|
|
2742
|
+
const { detectFeedbackSignal } = require(path.join(PKG_ROOT, 'scripts', 'feedback-quality'));
|
|
2079
2743
|
const { loadOptionalModule } = require(path.join(PKG_ROOT, 'scripts', 'private-core-boundary'));
|
|
2080
2744
|
const { recordConversationEntry, readRecentConversationWindow } = loadOptionalModule(
|
|
2081
2745
|
path.join(PKG_ROOT, 'scripts', 'feedback-history-distiller'),
|
|
@@ -2097,14 +2761,12 @@ function hookAutoCapture() {
|
|
|
2097
2761
|
return;
|
|
2098
2762
|
}
|
|
2099
2763
|
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
const isDown = /(thumbs?\s*down|that failed|that was wrong|fix this)/i.test(lower);
|
|
2103
|
-
if (!isUp && !isDown) {
|
|
2764
|
+
const detected = detectFeedbackSignal(prompt);
|
|
2765
|
+
if (!detected) {
|
|
2104
2766
|
return;
|
|
2105
2767
|
}
|
|
2106
2768
|
|
|
2107
|
-
const signal =
|
|
2769
|
+
const signal = detected.signal;
|
|
2108
2770
|
const conversationWindow = readRecentConversationWindow({ limit: 8 });
|
|
2109
2771
|
const result = processInlineFeedback({
|
|
2110
2772
|
signal,
|
|
@@ -2185,6 +2847,27 @@ function installMcp() {
|
|
|
2185
2847
|
|
|
2186
2848
|
function dashboard() {
|
|
2187
2849
|
const args = parseArgs(process.argv.slice(3));
|
|
2850
|
+
if (args.open || args.web) {
|
|
2851
|
+
const { resolveProjectDir } = require(path.join(PKG_ROOT, 'scripts', 'feedback-paths'));
|
|
2852
|
+
const projectDir = resolveProjectDir({ cwd: process.cwd(), env: process.env });
|
|
2853
|
+
const port = process.env.PORT || 3456;
|
|
2854
|
+
const url = `http://127.0.0.1:${port}/dashboard?project=${encodeURIComponent(projectDir)}`;
|
|
2855
|
+
|
|
2856
|
+
ensureDash(Number(port))
|
|
2857
|
+
.then((server) => {
|
|
2858
|
+
if (server.started) {
|
|
2859
|
+
console.log(`API ${port} pid ${server.pid}`);
|
|
2860
|
+
}
|
|
2861
|
+
return openBrowser(url);
|
|
2862
|
+
})
|
|
2863
|
+
.then(() => process.exit(0))
|
|
2864
|
+
.catch((err) => {
|
|
2865
|
+
console.error(err && err.message ? err.message : err);
|
|
2866
|
+
process.exit(1);
|
|
2867
|
+
});
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2188
2871
|
const { printDashboard } = require(path.join(PKG_ROOT, 'scripts', 'dashboard'));
|
|
2189
2872
|
const { getOperationalDashboard } = require(path.join(PKG_ROOT, 'scripts', 'operational-dashboard'));
|
|
2190
2873
|
|
|
@@ -2304,6 +2987,64 @@ function startApi() {
|
|
|
2304
2987
|
}
|
|
2305
2988
|
}
|
|
2306
2989
|
|
|
2990
|
+
function breakGlass() {
|
|
2991
|
+
const args = parseArgs(process.argv.slice(3));
|
|
2992
|
+
const positionalReason = process.argv.slice(3).find((arg) => !arg.startsWith('--'));
|
|
2993
|
+
const reason = String(args.reason || positionalReason || '').trim();
|
|
2994
|
+
if (!reason) {
|
|
2995
|
+
console.error('Usage: npx thumbgate break-glass --reason "why this recovery is needed" [--ttl=5m] [--json]');
|
|
2996
|
+
process.exit(1);
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
const ttlMs = parseTtlMs(args.ttl, 5 * 60 * 1000);
|
|
3000
|
+
const { breakGlassEmergency } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
|
|
3001
|
+
const result = breakGlassEmergency({ reason, ttlMs });
|
|
3002
|
+
if (args.json) {
|
|
3003
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
console.log('ThumbGate break-glass active.');
|
|
3008
|
+
console.log(` Reason : ${result.reason}`);
|
|
3009
|
+
console.log(` Expires : ${result.expiresAt}`);
|
|
3010
|
+
console.log(' Unlocked : hook settings edits, pr_create_allowed, pr_threads_checked');
|
|
3011
|
+
console.log(' Still gated: local-only scope, force-push, protected branch push, unsafe chmod, broad rm -rf');
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
function aiInventory() {
|
|
3015
|
+
const args = parseArgs(process.argv.slice(3));
|
|
3016
|
+
const {
|
|
3017
|
+
scanAiComponents,
|
|
3018
|
+
buildCycloneDxMlBom,
|
|
3019
|
+
formatInventoryText,
|
|
3020
|
+
writeOutput,
|
|
3021
|
+
} = require(path.join(PKG_ROOT, 'scripts', 'ai-component-inventory'));
|
|
3022
|
+
const rootDir = path.resolve(String(args.root || args.cwd || CWD));
|
|
3023
|
+
const format = String(args.format || (args.json ? 'json' : 'summary')).toLowerCase();
|
|
3024
|
+
const inventory = scanAiComponents({
|
|
3025
|
+
rootDir,
|
|
3026
|
+
maxFiles: args['max-files'] ? Number(args['max-files']) : undefined,
|
|
3027
|
+
includeSnippets: args.snippets !== false,
|
|
3028
|
+
});
|
|
3029
|
+
|
|
3030
|
+
let payload;
|
|
3031
|
+
if (format === 'cyclonedx' || format === 'ml-bom' || format === 'mlbom') {
|
|
3032
|
+
payload = JSON.stringify(buildCycloneDxMlBom(inventory, { version: pkgVersion() }), null, 2);
|
|
3033
|
+
} else if (format === 'json') {
|
|
3034
|
+
payload = JSON.stringify(inventory, null, 2);
|
|
3035
|
+
} else {
|
|
3036
|
+
payload = formatInventoryText(inventory);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
if (args.output) {
|
|
3040
|
+
writeOutput(path.resolve(String(args.output)), `${payload}\n`);
|
|
3041
|
+
console.log(`Wrote AI inventory evidence to ${path.resolve(String(args.output))}`);
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
console.log(payload);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
2307
3048
|
function help() {
|
|
2308
3049
|
const v = pkgVersion();
|
|
2309
3050
|
const helpArgs = process.argv.slice(3);
|
|
@@ -2319,13 +3060,19 @@ function help() {
|
|
|
2319
3060
|
console.log('');
|
|
2320
3061
|
console.log('Common commands:');
|
|
2321
3062
|
console.log(' init Detect agent and wire ThumbGate hooks');
|
|
3063
|
+
console.log(' feedback-self-test Prove thumbs capture works locally');
|
|
2322
3064
|
console.log(' capture --feedback=up|down --context="<text>" Capture a thumbs signal as a stored lesson');
|
|
2323
3065
|
console.log(' stats Approval rate, recent trend, blocked-pattern count');
|
|
2324
3066
|
console.log(' lessons [query] Search promoted lessons');
|
|
2325
3067
|
console.log(' explore Interactive TUI for lessons, gates, stats');
|
|
2326
3068
|
console.log(' dashboard Open the local ThumbGate dashboard');
|
|
3069
|
+
console.log(' ai-inventory Scan AI/ML components and export ML-BOM evidence');
|
|
2327
3070
|
console.log(' doctor Audit runtime isolation + bootstrap context');
|
|
3071
|
+
console.log(' break-glass --reason="..." Short TTL recovery if gates over-fire');
|
|
3072
|
+
console.log(' brain [--write] Build the agent-readable context brain (lessons + rules + gates)');
|
|
2328
3073
|
console.log(' pro ThumbGate Pro (dashboard, exports, sync)');
|
|
3074
|
+
console.log(' diagnostic $499 Workflow Hardening Diagnostic for one repeated team failure');
|
|
3075
|
+
console.log(' subscribe <email> Get the 5-min setup guide + weekly tips by email');
|
|
2329
3076
|
console.log('');
|
|
2330
3077
|
console.log('More:');
|
|
2331
3078
|
console.log(' thumbgate help all Full subcommand surface (~70 commands)');
|
|
@@ -2371,7 +3118,11 @@ function help() {
|
|
|
2371
3118
|
|
|
2372
3119
|
// Legacy and specialist commands kept visible until they graduate into the schema.
|
|
2373
3120
|
console.log('Also available:');
|
|
2374
|
-
console.log(' install-mcp Install MCP server into Claude Code settings
|
|
3121
|
+
console.log(' install-mcp Install MCP server + PreToolUse hooks into Claude Code settings');
|
|
3122
|
+
console.log(' default: machine-wide (~/.claude/settings.json — shared dashboard)');
|
|
3123
|
+
console.log(' --project: per-repo (<cwd>/.claude/settings.json — isolated dashboard)');
|
|
3124
|
+
console.log(' --no-hooks: MCP only, skip hook wiring');
|
|
3125
|
+
console.log(' break-glass Short TTL recovery if gates over-fire');
|
|
2375
3126
|
console.log(' cfo Hosted billing summary (local fallback JSON)');
|
|
2376
3127
|
console.log(' billing:setup Generate operator key + print Railway setup instructions');
|
|
2377
3128
|
console.log(' repair-github-marketplace Repair legacy GitHub Marketplace amount mappings');
|
|
@@ -2392,6 +3143,7 @@ function help() {
|
|
|
2392
3143
|
console.log(' proxy-pointer-rag-guardrails Map visual document RAG signals to Document RAG Safety gates');
|
|
2393
3144
|
console.log(' rag-precision-guardrails Map retrieval tuning regressions to Document RAG Safety gates');
|
|
2394
3145
|
console.log(' ai-engineering-stack-guardrails Map gateway, MCP, AGENTS.md, LLM wiki, reviewer, and sandbox gaps to stack gates');
|
|
3146
|
+
console.log(' ai-inventory Scan AI/ML components and export JSON or CycloneDX ML-BOM evidence');
|
|
2395
3147
|
console.log(' upstream-contributions Find dependency issues worth fixing without promotional PRs');
|
|
2396
3148
|
console.log(' long-running-agent-context-guardrails Map structured-memory gaps to long-running agent gates');
|
|
2397
3149
|
console.log(' reasoning-efficiency-guardrails Map reasoning compression signals to efficiency gates');
|
|
@@ -2416,6 +3168,7 @@ function help() {
|
|
|
2416
3168
|
|
|
2417
3169
|
console.log('Examples:');
|
|
2418
3170
|
console.log(' npx thumbgate init');
|
|
3171
|
+
console.log(' npx thumbgate feedback-self-test');
|
|
2419
3172
|
console.log(' npx thumbgate status --json');
|
|
2420
3173
|
console.log(' npx thumbgate explore lessons --json');
|
|
2421
3174
|
console.log(' npx thumbgate explore gates --json');
|
|
@@ -2425,6 +3178,7 @@ function help() {
|
|
|
2425
3178
|
console.log(' npx thumbgate proxy-pointer-rag-guardrails --tree-path=.rag/tree.json --image-pointers=paper-1/figures/fig2.png --documents=paper-1 --visual-claims --json');
|
|
2426
3179
|
console.log(' npx thumbgate rag-precision-guardrails --baseline-recall=0.86 --new-recall=0.72 --threshold-change --agentic --structural-near-misses --json');
|
|
2427
3180
|
console.log(' npx thumbgate ai-engineering-stack-guardrails --mcp-tool-count=182 --direct-provider-keys --llm-wiki-pages=24 --context-freshness-days=30 --background-agents --json');
|
|
3181
|
+
console.log(' npx thumbgate ai-inventory --format=cyclonedx --output=.thumbgate/ai-mlbom.json');
|
|
2428
3182
|
console.log(' npx thumbgate long-running-agent-context-guardrails --request-count=80 --output-mb=3 --raw-chat-only --json');
|
|
2429
3183
|
console.log(' npx thumbgate reasoning-efficiency-guardrails --baseline-tokens=1200 --compressed-tokens=980 --baseline-accuracy=0.84 --compressed-accuracy=0.85 --verifier --json');
|
|
2430
3184
|
console.log(' npx thumbgate deepseek-v4-runtime-guardrails --context-tokens=900000 --hybrid-attention --speculative-decoding --accept-length=1.4 --precision-mode=fp8 --json');
|
|
@@ -2446,11 +3200,271 @@ if (COMMAND === 'daemon' || COMMAND === 'serve-daemon') {
|
|
|
2446
3200
|
process.exit(0);
|
|
2447
3201
|
}
|
|
2448
3202
|
|
|
3203
|
+
// ---------------------------------------------------------------------------
|
|
3204
|
+
// Global --help interceptor: `thumbgate <cmd> --help` shows per-command help
|
|
3205
|
+
// instead of running the command. Commands with their own --help guard (init)
|
|
3206
|
+
// handle it internally; everything else falls through here.
|
|
3207
|
+
// ---------------------------------------------------------------------------
|
|
3208
|
+
const _cliSubArgs = process.argv.slice(3);
|
|
3209
|
+
const _wantsHelp = _cliSubArgs.includes('--help') || _cliSubArgs.includes('-h');
|
|
3210
|
+
|
|
3211
|
+
const SUBCOMMAND_HELP = {
|
|
3212
|
+
capture: 'Usage: npx thumbgate capture --feedback=up|down --context="..." [--what-worked="..."] [--what-went-wrong="..."] [--what-to-change="..."] [--tags=a,b]',
|
|
3213
|
+
feedback: 'Usage: npx thumbgate feedback --feedback=up|down --context="..." [--what-worked="..."] [--what-went-wrong="..."] [--what-to-change="..."] [--tags=a,b]',
|
|
3214
|
+
'feedback-self-test': 'Usage: npx thumbgate feedback-self-test [--json] [--persist] [--feedback=up|down]\n\nCapture a synthetic thumbs signal and verify feedback-log + memory-log writes. Defaults to an isolated test store; use --persist to dogfood the active ThumbGate store.',
|
|
3215
|
+
dogfood: 'Usage: npx thumbgate dogfood [--json] [--persist] [--feedback=up|down]\n\nAlias for feedback-self-test.',
|
|
3216
|
+
stats: 'Usage: npx thumbgate stats\n\nShow gate enforcement statistics: blocked/warned counts, active gates, time saved.',
|
|
3217
|
+
trial: 'Usage: npx thumbgate trial\n\nShow Pro trial status, remaining days, and upgrade path.',
|
|
3218
|
+
pro: 'Usage: npx thumbgate pro [--activate <key>]\n\nLaunch the local Pro dashboard or activate a Pro license key.',
|
|
3219
|
+
diagnostic: 'Usage: npx thumbgate diagnostic\n\nShow the $499 Workflow Hardening Diagnostic intake and checkout paths for teams with one repeated AI-agent workflow failure.',
|
|
3220
|
+
subscribe: 'Usage: npx thumbgate subscribe <email>\n\nSubscribe to the 5-minute setup guide + trial reminders.',
|
|
3221
|
+
lessons: 'Usage: npx thumbgate lessons [--query="..."] [--limit=N]\n\nSearch the lesson database (Pro feature).',
|
|
3222
|
+
search: 'Usage: npx thumbgate search <query>\n\nSearch ThumbGate knowledge base (Pro feature).',
|
|
3223
|
+
'gate-check': 'Usage: npx thumbgate gate-check\n\nPreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict.',
|
|
3224
|
+
'hermes-gate': 'Usage: npx thumbgate hermes-gate\n\nNous Research Hermes Agent pre_tool_call shell hook: reads Hermes tool-call JSON from stdin, runs the ThumbGate gate pipeline (strict by default), and outputs {"decision":"block","reason":...} to veto or {} to allow. Gates terminal/patch/skill_manage etc. See adapters/hermes/config.yaml.',
|
|
3225
|
+
'break-glass': 'Usage: npx thumbgate break-glass --reason="why" [--ttl=5m] [--json]\n\nShort-lived recovery path for over-firing gates. Allows hook settings edits and satisfies PR-create/thread-check gates without disabling core destructive-action protections.',
|
|
3226
|
+
serve: 'Usage: npx thumbgate serve\n\nStart the MCP stdio server. This is for agent runtimes, not the local HTTP dashboard.',
|
|
3227
|
+
mcp: 'Usage: npx thumbgate mcp\n\nAlias for `thumbgate serve`.',
|
|
3228
|
+
dashboard: 'Usage: npx thumbgate dashboard [--window=today|7d|30d] [--open]\n\nPrint the operational dashboard summary or open the browser HTTP dashboard (use --open). Defaults to PORT=3456.',
|
|
3229
|
+
'start-api': 'Usage: npx thumbgate start-api\n\nStart the local ThumbGate HTTP API/dashboard. Defaults to PORT=8787; use PORT=3456 for statusline localhost links.',
|
|
3230
|
+
'export-dpo': 'Usage: npx thumbgate export-dpo [--format=jsonl|csv]\n\nExport feedback as DPO training pairs (Pro feature).',
|
|
3231
|
+
status: 'Usage: npx thumbgate status\n\nShow ThumbGate system health and active configuration.',
|
|
3232
|
+
watch: 'Usage: npx thumbgate watch\n\nWatch for feedback changes and auto-regenerate prevention rules.',
|
|
3233
|
+
compact: 'Usage: npx thumbgate compact\n\nCompact the lesson database and reclaim disk space.',
|
|
3234
|
+
'context-packs': 'Usage: npx thumbgate context-packs\n\nGenerate context packs from top failure patterns.',
|
|
3235
|
+
suggest: 'Usage: npx thumbgate suggest <gate-id>\n\nSuggest fixes for a specific gate based on lesson history.',
|
|
3236
|
+
cost: 'Usage: npx thumbgate cost [--json] [--stats <path>] [--mix \'{"claude-sonnet-4-5":0.8,...}\']\n\nShow cumulative $ and tokens saved by PreToolUse gate blocks. Reads ~/.thumbgate/gate-stats.json.',
|
|
3237
|
+
savings: 'Usage: npx thumbgate savings [--json] [--stats <path>] [--mix \'{"claude-sonnet-4-5":0.8,...}\']\n\nAlias for `thumbgate cost`.',
|
|
3238
|
+
'setup-vertex': 'Usage: npx thumbgate setup-vertex [--dry-run]\n\nAuto-enable Vertex AI API on GCP and write local Vertex routing config to .env. With --dry-run, only detect the active account/project and print the planned changes. This does not create or verify a Dialogflow CX agent; use the Dialogflow CX REST API or console for live-agent evidence.',
|
|
3239
|
+
'ai-inventory': 'Usage: npx thumbgate ai-inventory [--root <dir>] [--format=summary|json|cyclonedx] [--output <path>] [--max-files=N]\n\nScan source/manifests/model artifacts for AI, ML, agent-framework, vector DB, Vertex, Gemini, and Dialogflow CX components. Use --format=cyclonedx to produce exportable ML-BOM evidence for enterprise reviews.',
|
|
3240
|
+
brain: 'Usage: npx thumbgate brain [--write] [--json] [--limit=N]\n\nBuild the agent-readable "context brain" — a single artifact consolidating this\nrepo\'s lessons, prevention rules, active gates, and project context for a coding\nagent to read BEFORE acting. --write saves it to .thumbgate/BRAIN.md (versioned,\ndeterministic). --json emits the structured model. --limit caps lessons (default 15).',
|
|
3241
|
+
'team-sync': 'Usage: npx thumbgate team-sync\n\nSynchronize prevention rules and context brain with your team\'s git repository (git pull --rebase & git push), then auto-rebuild the local brain.',
|
|
3242
|
+
};
|
|
3243
|
+
|
|
3244
|
+
if (_wantsHelp && COMMAND && SUBCOMMAND_HELP[COMMAND]) {
|
|
3245
|
+
console.log(SUBCOMMAND_HELP[COMMAND]);
|
|
3246
|
+
process.exit(0);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
// -----------------------------------------------------------------------------
|
|
3250
|
+
// brain — consolidate ThumbGate's institutional memory (lessons + prevention
|
|
3251
|
+
// rules + active gates + project context) into a single, versioned,
|
|
3252
|
+
// agent-readable "context brain". The persistent context a coding agent should
|
|
3253
|
+
// read BEFORE acting, so it stops repeating mistakes. Composes the existing
|
|
3254
|
+
// explore-subcommands primitives; output is deterministic (no volatile
|
|
3255
|
+
// timestamp) so `.thumbgate/BRAIN.md` diffs only when the underlying memory
|
|
3256
|
+
// changes.
|
|
3257
|
+
// -----------------------------------------------------------------------------
|
|
3258
|
+
function buildBrainModel(opts = {}) {
|
|
3259
|
+
const { exploreLessons, exploreRules, exploreGates } = require(path.join(PKG_ROOT, 'scripts', 'explore-subcommands'));
|
|
3260
|
+
let feedbackDir;
|
|
3261
|
+
try {
|
|
3262
|
+
const { getFeedbackPaths } = require(path.join(PKG_ROOT, 'scripts', 'feedback-loop'));
|
|
3263
|
+
feedbackDir = getFeedbackPaths().FEEDBACK_DIR;
|
|
3264
|
+
} catch (_) { feedbackDir = path.join(CWD, '.thumbgate'); }
|
|
3265
|
+
const limit = Math.max(1, Number(opts.limit) || 15);
|
|
3266
|
+
const safe = (fn, fallback) => { try { return fn(); } catch (_) { return fallback; } };
|
|
3267
|
+
const lessons = safe(() => exploreLessons({ feedbackDir, pkgRoot: PKG_ROOT, limit, json: true }), { lessons: [], total: 0 });
|
|
3268
|
+
const rules = safe(() => exploreRules({ feedbackDir, pkgRoot: PKG_ROOT, json: true }), { rules: [], total: 0 });
|
|
3269
|
+
const gates = safe(() => exploreGates({ feedbackDir, pkgRoot: PKG_ROOT, json: true }), { gates: [], total: 0 });
|
|
3270
|
+
const instructionFiles = ['CLAUDE.md', 'AGENTS.md', 'GEMINI.md', '.cursorrules', '.thumbgate/config.json']
|
|
3271
|
+
.filter((f) => { try { return fs.existsSync(path.join(CWD, f)); } catch (_) { return false; } });
|
|
3272
|
+
return { lessons, rules, gates, project: { root: CWD, instructionFiles } };
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
function renderBrainMarkdown(model) {
|
|
3276
|
+
const out = [];
|
|
3277
|
+
out.push('# ThumbGate Context Brain');
|
|
3278
|
+
out.push('');
|
|
3279
|
+
out.push('<!-- The persistent context a coding agent should read BEFORE acting in this repo.');
|
|
3280
|
+
out.push(' Generated by `npx thumbgate brain`. Rebuild after capturing feedback. -->');
|
|
3281
|
+
out.push('');
|
|
3282
|
+
out.push('> Institutional memory for AI coding agents working here: what has been');
|
|
3283
|
+
out.push('> learned, what to avoid, and what is enforced. Read this first.');
|
|
3284
|
+
out.push('');
|
|
3285
|
+
|
|
3286
|
+
out.push('## What this codebase taught its agents (lessons)');
|
|
3287
|
+
out.push('');
|
|
3288
|
+
const lessons = (model.lessons && model.lessons.lessons) || [];
|
|
3289
|
+
if (!lessons.length) {
|
|
3290
|
+
out.push('_No lessons captured yet. Run `npx thumbgate capture --feedback=down --context="what failed"`._');
|
|
3291
|
+
} else {
|
|
3292
|
+
for (const l of lessons) {
|
|
3293
|
+
const sig = String(l.signal || '').toLowerCase();
|
|
3294
|
+
const mark = (sig.includes('positive') || sig === 'up') ? '✅' : ((sig.includes('negative') || sig === 'down') ? '⛔' : '•');
|
|
3295
|
+
const enforced = l.confidence === 'active' ? ' _(enforced)_' : '';
|
|
3296
|
+
const ctx = String(l.context || '').replace(/\s+/g, ' ').trim().slice(0, 220);
|
|
3297
|
+
if (ctx) out.push(`- ${mark} ${ctx}${enforced}`);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
out.push('');
|
|
3301
|
+
|
|
3302
|
+
out.push('## Guardrails — do NOT repeat these (prevention rules)');
|
|
3303
|
+
out.push('');
|
|
3304
|
+
const rules = (model.rules && model.rules.rules) || [];
|
|
3305
|
+
if (!rules.length) {
|
|
3306
|
+
out.push('_No prevention rules yet._');
|
|
3307
|
+
} else {
|
|
3308
|
+
for (const r of rules) {
|
|
3309
|
+
out.push(`- **${String(r.title || '').trim()}**`);
|
|
3310
|
+
const body = String(r.body || '').split('\n')[0].trim();
|
|
3311
|
+
if (body && body !== r.title) out.push(` - ${body.slice(0, 200)}`);
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
out.push('');
|
|
3315
|
+
|
|
3316
|
+
out.push('## Active enforcement (gates)');
|
|
3317
|
+
out.push('');
|
|
3318
|
+
const gates = (model.gates && model.gates.gates) || [];
|
|
3319
|
+
if (!gates.length) {
|
|
3320
|
+
out.push('_No active gates._');
|
|
3321
|
+
} else {
|
|
3322
|
+
for (const g of gates) {
|
|
3323
|
+
const occ = g.occurrences ? ` (${g.occurrences}×)` : '';
|
|
3324
|
+
out.push(`- \`${g.pattern || g.id}\` → **${g.action}**${occ}`);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
out.push('');
|
|
3328
|
+
|
|
3329
|
+
out.push('## Project context');
|
|
3330
|
+
out.push('');
|
|
3331
|
+
out.push(`- Repo root: \`${model.project.root}\``);
|
|
3332
|
+
const files = model.project.instructionFiles;
|
|
3333
|
+
out.push(`- Agent instruction files: ${files.length ? files.map((f) => '`' + f + '`').join(', ') : '_none detected_'}`);
|
|
3334
|
+
out.push('');
|
|
3335
|
+
out.push('---');
|
|
3336
|
+
const lt = (model.lessons && model.lessons.total) || 0;
|
|
3337
|
+
const rt = (model.rules && model.rules.total) || 0;
|
|
3338
|
+
const gt = (model.gates && model.gates.total) || 0;
|
|
3339
|
+
out.push(`_${lt} lessons · ${rt} rules · ${gt} gates. Rebuild with \`npx thumbgate brain --write\`._`);
|
|
3340
|
+
out.push('');
|
|
3341
|
+
return out.join('\n');
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
function cmdBrain(args = {}) {
|
|
3345
|
+
const model = buildBrainModel({ limit: args.limit });
|
|
3346
|
+
if (args.json) { console.log(JSON.stringify(model, null, 2)); return 0; }
|
|
3347
|
+
const md = renderBrainMarkdown(model);
|
|
3348
|
+
if (args.write) {
|
|
3349
|
+
const dir = path.join(CWD, '.thumbgate');
|
|
3350
|
+
const target = path.join(dir, 'BRAIN.md');
|
|
3351
|
+
try {
|
|
3352
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3353
|
+
fs.writeFileSync(target, md);
|
|
3354
|
+
} catch (err) {
|
|
3355
|
+
// Surface a clean, actionable error instead of an uncaught stack trace
|
|
3356
|
+
// (e.g. permission denied, read-only filesystem).
|
|
3357
|
+
console.error(`Could not write ${target}: ${err && err.message ? err.message : err}`);
|
|
3358
|
+
return 1;
|
|
3359
|
+
}
|
|
3360
|
+
const lt = (model.lessons && model.lessons.total) || 0;
|
|
3361
|
+
const rt = (model.rules && model.rules.total) || 0;
|
|
3362
|
+
const gt = (model.gates && model.gates.total) || 0;
|
|
3363
|
+
console.log(`\u{1f9e0} Wrote context brain to .thumbgate/BRAIN.md (${lt} lessons · ${rt} rules · ${gt} gates).`);
|
|
3364
|
+
console.log(' Point your agent at it: add "Read .thumbgate/BRAIN.md first" to CLAUDE.md / AGENTS.md.');
|
|
3365
|
+
return 0;
|
|
3366
|
+
}
|
|
3367
|
+
process.stdout.write(md);
|
|
3368
|
+
return 0;
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
async function teamSync() {
|
|
3372
|
+
const { execSync } = require('child_process');
|
|
3373
|
+
|
|
3374
|
+
// Verify we are in a Git repo
|
|
3375
|
+
try {
|
|
3376
|
+
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore', cwd: CWD });
|
|
3377
|
+
} catch (err) {
|
|
3378
|
+
console.error('❌ Error: The current directory is not a Git repository.');
|
|
3379
|
+
process.exit(1);
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
console.log('🔄 Checking shared prevention rules status...');
|
|
3383
|
+
|
|
3384
|
+
const rulesRelative = '.thumbgate/prevention-rules.md';
|
|
3385
|
+
const brainRelative = '.thumbgate/BRAIN.md';
|
|
3386
|
+
const rulesPath = path.join(CWD, rulesRelative);
|
|
3387
|
+
|
|
3388
|
+
if (!fs.existsSync(rulesPath)) {
|
|
3389
|
+
console.log('⚠️ No local prevention rules file found to sync.');
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
let statusOutput = '';
|
|
3393
|
+
try {
|
|
3394
|
+
statusOutput = execSync('git status --porcelain', { encoding: 'utf8', cwd: CWD }) || '';
|
|
3395
|
+
} catch (_) {}
|
|
3396
|
+
const hasLocalChanges = statusOutput.includes(rulesRelative) || statusOutput.includes(brainRelative) || statusOutput.includes('.thumbgate/');
|
|
3397
|
+
|
|
3398
|
+
if (hasLocalChanges) {
|
|
3399
|
+
console.log('📝 Local changes detected in prevention rules. Committing locally...');
|
|
3400
|
+
try {
|
|
3401
|
+
const filesToAdd = [rulesRelative, brainRelative].filter(f => fs.existsSync(path.join(CWD, f)));
|
|
3402
|
+
if (filesToAdd.length > 0) {
|
|
3403
|
+
const filesStr = filesToAdd.map(f => `"${f}"`).join(' ');
|
|
3404
|
+
execSync(`git add -f ${filesStr}`, { cwd: CWD });
|
|
3405
|
+
execSync('git commit -m "chore(thumbgate): update shared prevention rules [skip ci]"', { cwd: CWD });
|
|
3406
|
+
console.log('✅ Local rules committed successfully.');
|
|
3407
|
+
} else {
|
|
3408
|
+
console.log('✨ No local rules files exist to commit.');
|
|
3409
|
+
}
|
|
3410
|
+
} catch (e) {
|
|
3411
|
+
console.log('✨ No changes to commit (already staged/clean).');
|
|
3412
|
+
}
|
|
3413
|
+
} else {
|
|
3414
|
+
console.log('✨ No local rules changes to commit.');
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// Pull from remote
|
|
3418
|
+
console.log('📥 Pulling rules from teammate remote (git pull --rebase)...');
|
|
3419
|
+
try {
|
|
3420
|
+
execSync('git pull --rebase', { stdio: 'inherit', cwd: CWD });
|
|
3421
|
+
} catch (pullErr) {
|
|
3422
|
+
console.error('❌ Git pull failed. Please resolve conflicts manually.');
|
|
3423
|
+
process.exit(1);
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
// Push to remote
|
|
3427
|
+
console.log('📤 Pushing rules to teammate remote (git push)...');
|
|
3428
|
+
try {
|
|
3429
|
+
execSync('git push', { stdio: 'inherit', cwd: CWD });
|
|
3430
|
+
} catch (pushErr) {
|
|
3431
|
+
console.error('❌ Git push failed. You may need to run git push manually.');
|
|
3432
|
+
process.exit(1);
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
// Rebuild the context brain (.thumbgate/BRAIN.md) from the newly merged rules
|
|
3436
|
+
console.log('🧠 Rebuilding local context brain from merged rules...');
|
|
3437
|
+
try {
|
|
3438
|
+
const brainArgs = { write: true };
|
|
3439
|
+
cmdBrain(brainArgs);
|
|
3440
|
+
} catch (brainErr) {
|
|
3441
|
+
console.warn(`⚠️ Failed to rebuild context brain: ${brainErr.message}`);
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
console.log('\n🚀 Team rules synchronization complete! Your agents now share team-wide learning.');
|
|
3445
|
+
}
|
|
3446
|
+
|
|
2449
3447
|
switch (COMMAND) {
|
|
3448
|
+
case 'team-sync':
|
|
3449
|
+
case 'git-sync':
|
|
3450
|
+
teamSync().catch((err) => {
|
|
3451
|
+
console.error(err && err.message ? err.message : err);
|
|
3452
|
+
process.exit(1);
|
|
3453
|
+
});
|
|
3454
|
+
break;
|
|
3455
|
+
case '--version':
|
|
3456
|
+
case '-v':
|
|
3457
|
+
case 'version':
|
|
3458
|
+
console.log(pkgVersion());
|
|
3459
|
+
break;
|
|
2450
3460
|
case 'init':
|
|
2451
3461
|
init();
|
|
2452
3462
|
upgradeNudge();
|
|
2453
3463
|
break;
|
|
3464
|
+
case 'quickstart':
|
|
3465
|
+
case 'first-rule':
|
|
3466
|
+
quickstart();
|
|
3467
|
+
break;
|
|
2454
3468
|
case 'quick-start':
|
|
2455
3469
|
quickStart();
|
|
2456
3470
|
break;
|
|
@@ -2464,6 +3478,12 @@ switch (COMMAND) {
|
|
|
2464
3478
|
case 'mcp':
|
|
2465
3479
|
serve();
|
|
2466
3480
|
break;
|
|
3481
|
+
case 'cleanup':
|
|
3482
|
+
cleanup();
|
|
3483
|
+
break;
|
|
3484
|
+
case 'sync-gcp':
|
|
3485
|
+
syncGcp();
|
|
3486
|
+
break;
|
|
2467
3487
|
case 'gate-check':
|
|
2468
3488
|
gateCheck().catch((err) => {
|
|
2469
3489
|
console.error(err && err.message ? err.message : err);
|
|
@@ -2487,6 +3507,16 @@ switch (COMMAND) {
|
|
|
2487
3507
|
capture();
|
|
2488
3508
|
upgradeNudge();
|
|
2489
3509
|
break;
|
|
3510
|
+
case 'feedback-self-test':
|
|
3511
|
+
case 'dogfood':
|
|
3512
|
+
feedbackSelfTest();
|
|
3513
|
+
break;
|
|
3514
|
+
case 'setup-vertex':
|
|
3515
|
+
setupVertex(parseArgs(process.argv.slice(3))).catch((err) => {
|
|
3516
|
+
console.error(err && err.message ? err.message : err);
|
|
3517
|
+
process.exit(1);
|
|
3518
|
+
});
|
|
3519
|
+
break;
|
|
2490
3520
|
case 'stats':
|
|
2491
3521
|
stats();
|
|
2492
3522
|
upgradeNudge();
|
|
@@ -2495,6 +3525,27 @@ switch (COMMAND) {
|
|
|
2495
3525
|
case 'revenue':
|
|
2496
3526
|
cfo();
|
|
2497
3527
|
break;
|
|
3528
|
+
case 'cost':
|
|
3529
|
+
case 'savings':
|
|
3530
|
+
case 'costs': {
|
|
3531
|
+
const { main: costMain } = require(path.join(PKG_ROOT, 'scripts', 'cost-cli'));
|
|
3532
|
+
process.exit(costMain(process.argv.slice(3)));
|
|
3533
|
+
// process.exit doesn't return, but keep an explicit break so the switch
|
|
3534
|
+
// cannot accidentally fall through to case 'billing:setup' if a future
|
|
3535
|
+
// refactor wraps costMain in try/finally that intercepts the exit, or
|
|
3536
|
+
// a test runner stubs process.exit (flagged by gitar-bot on PR #2281).
|
|
3537
|
+
break;
|
|
3538
|
+
}
|
|
3539
|
+
case 'brain': {
|
|
3540
|
+
const sub = process.argv.slice(3).find((arg) => !arg.startsWith('--'));
|
|
3541
|
+
if (sub && ['init', 'context', 'remember', 'check', 'cleanup', 'status'].includes(sub)) {
|
|
3542
|
+
brain();
|
|
3543
|
+
} else {
|
|
3544
|
+
const brainArgs = parseArgs(process.argv.slice(3));
|
|
3545
|
+
process.exitCode = cmdBrain(brainArgs);
|
|
3546
|
+
}
|
|
3547
|
+
break;
|
|
3548
|
+
}
|
|
2498
3549
|
case 'billing:setup':
|
|
2499
3550
|
require(path.join(PKG_ROOT, 'scripts', 'billing-setup'));
|
|
2500
3551
|
break;
|
|
@@ -2511,6 +3562,11 @@ switch (COMMAND) {
|
|
|
2511
3562
|
case 'search-lessons':
|
|
2512
3563
|
lessons();
|
|
2513
3564
|
break;
|
|
3565
|
+
case 'notes': {
|
|
3566
|
+
const { cli: notesCli } = require(path.join(PKG_ROOT, 'scripts', 'implementation-notes'));
|
|
3567
|
+
notesCli(process.argv.slice(3));
|
|
3568
|
+
break;
|
|
3569
|
+
}
|
|
2514
3570
|
case 'lesson-health':
|
|
2515
3571
|
case 'stale': {
|
|
2516
3572
|
const { initDB } = require(path.join(PKG_ROOT, 'scripts', 'lesson-db'));
|
|
@@ -2652,6 +3708,9 @@ switch (COMMAND) {
|
|
|
2652
3708
|
case 'rules':
|
|
2653
3709
|
rules();
|
|
2654
3710
|
break;
|
|
3711
|
+
case 'context-packs':
|
|
3712
|
+
contextPacks();
|
|
3713
|
+
break;
|
|
2655
3714
|
case 'harness-audit':
|
|
2656
3715
|
case 'harness':
|
|
2657
3716
|
harnessAudit();
|
|
@@ -2681,6 +3740,12 @@ switch (COMMAND) {
|
|
|
2681
3740
|
case 'llm-wiki-guardrails':
|
|
2682
3741
|
aiEngineeringStackGuardrails();
|
|
2683
3742
|
break;
|
|
3743
|
+
case 'ai-inventory':
|
|
3744
|
+
case 'ai-component-inventory':
|
|
3745
|
+
case 'ml-bom':
|
|
3746
|
+
case 'mlbom':
|
|
3747
|
+
aiInventory();
|
|
3748
|
+
break;
|
|
2684
3749
|
case 'deepseek-v4-runtime-guardrails':
|
|
2685
3750
|
case 'deepseek-runtime-guardrails':
|
|
2686
3751
|
case 'sparse-attention-runtime-guardrails':
|
|
@@ -2745,9 +3810,90 @@ switch (COMMAND) {
|
|
|
2745
3810
|
case 'self-heal':
|
|
2746
3811
|
selfHeal();
|
|
2747
3812
|
break;
|
|
3813
|
+
case 'workflow':
|
|
3814
|
+
case 'swarm': {
|
|
3815
|
+
const args = parseArgs(process.argv.slice(3));
|
|
3816
|
+
let objective = args.objective;
|
|
3817
|
+
if (!objective) {
|
|
3818
|
+
const firstPositional = process.argv.slice(3).find((a, idx, arr) => {
|
|
3819
|
+
if (a.startsWith('--')) return false;
|
|
3820
|
+
const prev = arr[idx - 1];
|
|
3821
|
+
if (prev && prev.startsWith('--') && !prev.includes('=')) return false;
|
|
3822
|
+
return true;
|
|
3823
|
+
});
|
|
3824
|
+
if (firstPositional) objective = firstPositional;
|
|
3825
|
+
}
|
|
3826
|
+
if (!objective) {
|
|
3827
|
+
console.error('Error: objective is required. Run with --objective="your objective" or provide it as a positional argument.');
|
|
3828
|
+
process.exit(1);
|
|
3829
|
+
}
|
|
3830
|
+
const { executeWorkflow } = require(path.join(PKG_ROOT, 'scripts', 'parallel-workflow-orchestrator'));
|
|
3831
|
+
const concurrency = args.concurrency ? Number(args.concurrency) : undefined;
|
|
3832
|
+
const timeoutMs = args.timeoutMs ? Number(args.timeoutMs) : undefined;
|
|
3833
|
+
executeWorkflow(objective, { concurrency, timeoutMs, cwd: CWD })
|
|
3834
|
+
.then((res) => {
|
|
3835
|
+
if (args.json) {
|
|
3836
|
+
console.log(JSON.stringify(res, null, 2));
|
|
3837
|
+
} else {
|
|
3838
|
+
console.log(`\n✅ Parallel workflow execution complete.`);
|
|
3839
|
+
console.log(` Workflow ID: ${res.workflowId}`);
|
|
3840
|
+
console.log(` Objective : ${res.objective}`);
|
|
3841
|
+
console.log(` Duration : ${(res.durationMs / 1000).toFixed(2)}s`);
|
|
3842
|
+
console.log(` Report Path: ${res.reportPath}`);
|
|
3843
|
+
console.log(`\nReport Summary:\n`);
|
|
3844
|
+
console.log(fs.readFileSync(res.reportPath, 'utf8'));
|
|
3845
|
+
}
|
|
3846
|
+
process.exit(0);
|
|
3847
|
+
})
|
|
3848
|
+
.catch((err) => {
|
|
3849
|
+
console.error('Workflow execution failed:', err.message);
|
|
3850
|
+
process.exit(1);
|
|
3851
|
+
});
|
|
3852
|
+
break;
|
|
3853
|
+
}
|
|
3854
|
+
case 'trial': {
|
|
3855
|
+
// Show trial status — connects the 4K monthly npm installers to checkout
|
|
3856
|
+
const { isProTier, isInTrialPeriod, trialDaysRemaining, getInstallAgeDays } = require(path.join(PKG_ROOT, 'scripts', 'rate-limiter'));
|
|
3857
|
+
const ageDays = getInstallAgeDays();
|
|
3858
|
+
const inTrial = isInTrialPeriod();
|
|
3859
|
+
const remaining = trialDaysRemaining();
|
|
3860
|
+
const isPro = isProTier();
|
|
3861
|
+
console.log('');
|
|
3862
|
+
console.log(' ThumbGate Pro Trial');
|
|
3863
|
+
console.log(' ──────────────────');
|
|
3864
|
+
if (isPro && !inTrial) {
|
|
3865
|
+
console.log(' Status: ✅ Pro (active license or API key)');
|
|
3866
|
+
} else if (inTrial) {
|
|
3867
|
+
const expiryDate = new Date(Date.now() + remaining * 86400000).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
|
3868
|
+
console.log(` Status: 🎁 Active (${remaining} day${remaining === 1 ? '' : 's'} remaining)`);
|
|
3869
|
+
console.log(` Expires: ${expiryDate}`);
|
|
3870
|
+
console.log('');
|
|
3871
|
+
console.log(' All Pro features unlocked:');
|
|
3872
|
+
console.log(' • Unlimited prevention rules (free tier: 5)');
|
|
3873
|
+
console.log(' • Recall, lesson search, DPO export');
|
|
3874
|
+
console.log(' • Cross-machine sync via API key');
|
|
3875
|
+
} else {
|
|
3876
|
+
console.log(' Status: ⏰ Expired');
|
|
3877
|
+
if (ageDays !== null) {
|
|
3878
|
+
console.log(` Installed: ${Math.floor(ageDays)} days ago`);
|
|
3879
|
+
}
|
|
3880
|
+
console.log('');
|
|
3881
|
+
console.log(' Free tier: 5 active rules, no recall/search/export');
|
|
3882
|
+
}
|
|
3883
|
+
console.log('');
|
|
3884
|
+
console.log(` Keep Pro: ${PRO_CHECKOUT_URL}`);
|
|
3885
|
+
console.log(` Or set: THUMBGATE_API_KEY=<your-key>`);
|
|
3886
|
+
console.log('');
|
|
3887
|
+
break;
|
|
3888
|
+
}
|
|
2748
3889
|
case 'pro':
|
|
2749
3890
|
pro();
|
|
2750
3891
|
break;
|
|
3892
|
+
case 'diagnostic':
|
|
3893
|
+
case 'workflow-diagnostic':
|
|
3894
|
+
case 'sprint-diagnostic':
|
|
3895
|
+
diagnostic();
|
|
3896
|
+
break;
|
|
2751
3897
|
case 'activate':
|
|
2752
3898
|
// Top-level alias: npx thumbgate activate <key>
|
|
2753
3899
|
process.argv.splice(3, 0, '--activate');
|
|
@@ -2762,12 +3908,24 @@ switch (COMMAND) {
|
|
|
2762
3908
|
case 'status':
|
|
2763
3909
|
status();
|
|
2764
3910
|
break;
|
|
3911
|
+
case 'break-glass':
|
|
3912
|
+
case 'breakglass':
|
|
3913
|
+
breakGlass();
|
|
3914
|
+
break;
|
|
2765
3915
|
case 'funnel':
|
|
2766
3916
|
funnel();
|
|
2767
3917
|
break;
|
|
2768
3918
|
case 'pulse':
|
|
2769
3919
|
pulse();
|
|
2770
3920
|
break;
|
|
3921
|
+
case 'check-update':
|
|
3922
|
+
case 'upgrade-check':
|
|
3923
|
+
checkUpdateCmd();
|
|
3924
|
+
break;
|
|
3925
|
+
case 'self-update':
|
|
3926
|
+
case 'upgrade-cli':
|
|
3927
|
+
selfUpdateCmd();
|
|
3928
|
+
break;
|
|
2771
3929
|
case 'dispatch':
|
|
2772
3930
|
case 'dispatch-brief':
|
|
2773
3931
|
dispatchBrief();
|
|
@@ -2793,6 +3951,53 @@ switch (COMMAND) {
|
|
|
2793
3951
|
});
|
|
2794
3952
|
break;
|
|
2795
3953
|
}
|
|
3954
|
+
case 'hermes-gate': {
|
|
3955
|
+
// Nous Research Hermes Agent `pre_tool_call` shell hook.
|
|
3956
|
+
// Hermes pipes each pending tool call as JSON to stdin and reads a decision from stdout;
|
|
3957
|
+
// {"decision":"block","reason":...} vetoes the call. We reuse the SAME gate pipeline as
|
|
3958
|
+
// `gate-check` (runAsync → secret guard, security scan, force-push / skill_manage / learned
|
|
3959
|
+
// prevention rules) and translate the verdict into Hermes's format.
|
|
3960
|
+
//
|
|
3961
|
+
// Hermes `pre_tool_call` is binary (block or allow) with no warn channel, and the whole point
|
|
3962
|
+
// of wiring it is to gate, so we run STRICT enforcement by default — otherwise ThumbGate's
|
|
3963
|
+
// warn-by-default posture would pass every deny through and the hook would block nothing.
|
|
3964
|
+
// Opt out with THUMBGATE_HERMES_WARN_ONLY=1; THUMBGATE_HOTFIX_BYPASS=1 still disables checks.
|
|
3965
|
+
// Wire it in ~/.hermes/config.yaml — see adapters/hermes/config.yaml.
|
|
3966
|
+
if (process.env.THUMBGATE_HERMES_WARN_ONLY !== '1' && process.env.THUMBGATE_HOTFIX_BYPASS !== '1') {
|
|
3967
|
+
process.env.THUMBGATE_STRICT_ENFORCEMENT = '1';
|
|
3968
|
+
}
|
|
3969
|
+
const { runAsync: hermesGateRun } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
|
|
3970
|
+
let hermesStdin = '';
|
|
3971
|
+
process.stdin.setEncoding('utf8');
|
|
3972
|
+
process.stdin.on('data', (chunk) => { hermesStdin += chunk; });
|
|
3973
|
+
process.stdin.on('end', async () => {
|
|
3974
|
+
try {
|
|
3975
|
+
const payload = JSON.parse(hermesStdin);
|
|
3976
|
+
// Hermes sends snake_case tool_name/tool_input — gates-engine reads these directly.
|
|
3977
|
+
const verdict = await hermesGateRun({ tool_name: payload.tool_name, tool_input: payload.tool_input });
|
|
3978
|
+
let parsed = {};
|
|
3979
|
+
try { parsed = JSON.parse(verdict); } catch (_e) { parsed = {}; }
|
|
3980
|
+
const hso = parsed.hookSpecificOutput || {};
|
|
3981
|
+
if (hso.permissionDecision === 'deny') {
|
|
3982
|
+
process.stdout.write(JSON.stringify({
|
|
3983
|
+
decision: 'block',
|
|
3984
|
+
reason: hso.permissionDecisionReason || 'Blocked by ThumbGate prevention rule.',
|
|
3985
|
+
}) + '\n');
|
|
3986
|
+
} else {
|
|
3987
|
+
// warn / no match → allow. The gate engine already logged the decision.
|
|
3988
|
+
process.stdout.write(JSON.stringify({}) + '\n');
|
|
3989
|
+
}
|
|
3990
|
+
process.exit(0);
|
|
3991
|
+
} catch (err) {
|
|
3992
|
+
// Hermes hooks fail OPEN on error/timeout — emit an explicit allow so a gate fault
|
|
3993
|
+
// never wedges the agent (reliability ≈ enforcement; keep this fast).
|
|
3994
|
+
process.stderr.write(`hermes-gate error: ${err.message}\n`);
|
|
3995
|
+
process.stdout.write(JSON.stringify({}) + '\n');
|
|
3996
|
+
process.exit(0);
|
|
3997
|
+
}
|
|
3998
|
+
});
|
|
3999
|
+
break;
|
|
4000
|
+
}
|
|
2796
4001
|
case 'gate-stats':
|
|
2797
4002
|
gateStats();
|
|
2798
4003
|
break;
|
|
@@ -2847,6 +4052,29 @@ switch (COMMAND) {
|
|
|
2847
4052
|
}
|
|
2848
4053
|
break;
|
|
2849
4054
|
}
|
|
4055
|
+
case 'audit': {
|
|
4056
|
+
const auditFile = process.argv[3];
|
|
4057
|
+
if (!auditFile) {
|
|
4058
|
+
console.error('Usage: npx thumbgate audit <path-to-transcript.txt>');
|
|
4059
|
+
process.exit(1);
|
|
4060
|
+
}
|
|
4061
|
+
const { runAudit } = require(path.join(PKG_ROOT, 'scripts', 'audit'));
|
|
4062
|
+
const { results, totalWaste, error } = runAudit(auditFile);
|
|
4063
|
+
if (error) {
|
|
4064
|
+
console.error(error);
|
|
4065
|
+
process.exit(1);
|
|
4066
|
+
}
|
|
4067
|
+
console.log('\n🔍 AI Bill Audit Results\n');
|
|
4068
|
+
if (results.length === 0) {
|
|
4069
|
+
console.log('✅ No repeat-offender patterns found. Your sessions are efficient!');
|
|
4070
|
+
} else {
|
|
4071
|
+
console.table(results);
|
|
4072
|
+
console.log('\n💰 Total estimated monthly waste: $' + totalWaste);
|
|
4073
|
+
console.log('\nBlock these mistakes permanently with ThumbGate Pro:');
|
|
4074
|
+
console.log(PRO_CHECKOUT_URL);
|
|
4075
|
+
}
|
|
4076
|
+
break;
|
|
4077
|
+
}
|
|
2850
4078
|
case 'dashboard':
|
|
2851
4079
|
dashboard();
|
|
2852
4080
|
break;
|
|
@@ -2870,6 +4098,86 @@ switch (COMMAND) {
|
|
|
2870
4098
|
case 'compact':
|
|
2871
4099
|
compact();
|
|
2872
4100
|
break;
|
|
4101
|
+
case 'subscribe': {
|
|
4102
|
+
// Capture an installer's email so we can send the 5-minute setup
|
|
4103
|
+
// guide + weekly tips. Drops to /v1/marketing/install-email on the
|
|
4104
|
+
// hosted endpoint; the server fires sendNewsletterWelcomeEmail via
|
|
4105
|
+
// Resend and persists the address for follow-up.
|
|
4106
|
+
//
|
|
4107
|
+
// Usage:
|
|
4108
|
+
// npx thumbgate subscribe you@company.com
|
|
4109
|
+
// npx thumbgate subscribe --email=you@company.com
|
|
4110
|
+
//
|
|
4111
|
+
// Never prompts interactively — postinstall must stay non-interactive
|
|
4112
|
+
// for CI safety. This is the deliberate opt-in path the banner points
|
|
4113
|
+
// at; if the operator runs it, they want the email.
|
|
4114
|
+
const args = process.argv.slice(3);
|
|
4115
|
+
let email = '';
|
|
4116
|
+
for (const arg of args) {
|
|
4117
|
+
if (arg.startsWith('--email=')) email = arg.slice('--email='.length).trim();
|
|
4118
|
+
else if (!arg.startsWith('--')) email = arg.trim();
|
|
4119
|
+
}
|
|
4120
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
4121
|
+
console.error('Usage: npx thumbgate subscribe <email>');
|
|
4122
|
+
console.error(' npx thumbgate subscribe --email=you@company.com');
|
|
4123
|
+
console.error('');
|
|
4124
|
+
console.error('We send a 5-minute setup guide + weekly tips. One-click unsubscribe.');
|
|
4125
|
+
process.exit(1);
|
|
4126
|
+
}
|
|
4127
|
+
const endpoint = process.env.THUMBGATE_INSTALL_EMAIL_ENDPOINT
|
|
4128
|
+
|| 'https://thumbgate.ai/v1/marketing/install-email';
|
|
4129
|
+
const payload = JSON.stringify({
|
|
4130
|
+
email,
|
|
4131
|
+
source: 'cli_subscribe',
|
|
4132
|
+
installId: (() => {
|
|
4133
|
+
try {
|
|
4134
|
+
const configPath = path.join(CWD, '.thumbgate', 'config.json');
|
|
4135
|
+
if (!fs.existsSync(configPath)) return null;
|
|
4136
|
+
return (JSON.parse(fs.readFileSync(configPath, 'utf8')).installId) || null;
|
|
4137
|
+
} catch { return null; }
|
|
4138
|
+
})(),
|
|
4139
|
+
cliVersion: pkgVersion(),
|
|
4140
|
+
});
|
|
4141
|
+
const url = new URL(endpoint);
|
|
4142
|
+
const transport = url.protocol === 'https:' ? require('node:https') : require('node:http');
|
|
4143
|
+
const req = transport.request({
|
|
4144
|
+
method: 'POST',
|
|
4145
|
+
hostname: url.hostname,
|
|
4146
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
4147
|
+
path: url.pathname + url.search,
|
|
4148
|
+
headers: {
|
|
4149
|
+
'Content-Type': 'application/json',
|
|
4150
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
4151
|
+
'User-Agent': `thumbgate-cli/${pkgVersion()}`,
|
|
4152
|
+
},
|
|
4153
|
+
timeout: 8000,
|
|
4154
|
+
}, (res) => {
|
|
4155
|
+
let body = '';
|
|
4156
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
4157
|
+
res.on('end', () => {
|
|
4158
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
4159
|
+
console.log(`✓ Subscribed ${email}.`);
|
|
4160
|
+
console.log(' Check your inbox in ~30 seconds for the setup guide.');
|
|
4161
|
+
console.log(' Unsubscribe link is in every email.');
|
|
4162
|
+
process.exit(0);
|
|
4163
|
+
} else {
|
|
4164
|
+
console.error(`✗ Subscribe failed: HTTP ${res.statusCode}${body ? ` — ${body.slice(0, 200)}` : ''}`);
|
|
4165
|
+
process.exit(2);
|
|
4166
|
+
}
|
|
4167
|
+
});
|
|
4168
|
+
});
|
|
4169
|
+
req.on('error', (err) => {
|
|
4170
|
+
console.error(`✗ Subscribe failed: ${err.message}`);
|
|
4171
|
+
console.error(' Network issue? Try again, or email igor.ganapolsky@gmail.com directly.');
|
|
4172
|
+
process.exit(3);
|
|
4173
|
+
});
|
|
4174
|
+
req.on('timeout', () => {
|
|
4175
|
+
req.destroy(new Error('timeout after 8000ms'));
|
|
4176
|
+
});
|
|
4177
|
+
req.write(payload);
|
|
4178
|
+
req.end();
|
|
4179
|
+
break;
|
|
4180
|
+
}
|
|
2873
4181
|
case 'checkin': {
|
|
2874
4182
|
// User check-in command — asks how it's going after install
|
|
2875
4183
|
const thumbgateDir = path.join(CWD, '.thumbgate');
|