thumbgate 1.4.1 → 1.4.3
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/README.md +45 -34
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +3 -3
- package/.well-known/llms.txt +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +26 -2
- package/adapters/README.md +4 -1
- package/adapters/chatgpt/INSTALL.md +39 -19
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +10 -4
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/perplexity/.mcp.json +36 -0
- package/adapters/perplexity/config.toml +16 -0
- package/adapters/perplexity/opencode.json +29 -0
- package/bin/cli.js +246 -90
- package/config/mcp-allowlists.json +11 -3
- package/package.json +28 -13
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-codex-bridge/.mcp.json +1 -1
- package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
- package/plugins/codex-profile/.mcp.json +1 -1
- package/plugins/codex-profile/INSTALL.md +1 -1
- package/plugins/codex-profile/README.md +1 -1
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
- package/plugins/opencode-profile/INSTALL.md +1 -1
- package/public/index.html +121 -24
- package/public/llm-context.md +17 -1
- package/scripts/ai-search-visibility.js +10 -36
- package/scripts/audit-trail.js +25 -15
- package/scripts/auto-wire-hooks.js +127 -0
- package/scripts/cli-demo.js +102 -0
- package/scripts/cli-schema.js +285 -0
- package/scripts/cli-status.js +166 -0
- package/scripts/cross-encoder-reranker.js +235 -0
- package/scripts/explore-subcommands.js +277 -0
- package/scripts/explore.js +569 -0
- package/scripts/feedback-loop.js +20 -6
- package/scripts/lesson-inference.js +27 -2
- package/scripts/lesson-reranker.js +263 -0
- package/scripts/lesson-retrieval.js +34 -17
- package/scripts/lesson-search.js +69 -0
- package/scripts/perplexity-client.js +210 -0
- package/scripts/perplexity-command-center.js +644 -0
- package/scripts/perplexity-marketing.js +17 -29
- package/scripts/prove-packaged-runtime.js +5 -4
- package/scripts/ralph-mode-ci.js +122 -19
- package/scripts/reflector-agent.js +2 -2
- package/scripts/session-analyzer.js +533 -0
- package/scripts/social-analytics/db/marketing-db.js +179 -0
- package/scripts/social-analytics/db/schema.sql +23 -0
- package/scripts/social-analytics/generate-instagram-card.js +31 -5
- package/scripts/social-analytics/generate-slides.js +268 -0
- package/scripts/social-analytics/post-video.js +316 -0
- package/scripts/social-analytics/publishers/zernio.js +52 -23
- package/scripts/statusline-local-stats.js +3 -1
- package/scripts/statusline.sh +15 -10
- package/scripts/thumbgate-bench.js +494 -0
- package/src/api/server.js +65 -1
- package/scripts/social-analytics/db/analytics.sqlite +0 -0
|
@@ -94,6 +94,64 @@ function hookAlreadyPresent(hookArray, command) {
|
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* pruneStaleFileHooks — Remove hook entries whose command references a shell
|
|
99
|
+
* script path that no longer exists on disk.
|
|
100
|
+
*
|
|
101
|
+
* Only paths that look like file references (contain a `/` or `\`, or end with
|
|
102
|
+
* `.sh`) are checked. Pure command strings (node calls, npx invocations, etc.)
|
|
103
|
+
* are left untouched.
|
|
104
|
+
*
|
|
105
|
+
* @param {Array} hookArray - The array of hook-entry objects for one lifecycle.
|
|
106
|
+
* @param {string} [baseDir] - Directory used to resolve relative paths
|
|
107
|
+
* (defaults to process.cwd()).
|
|
108
|
+
* @returns {{ hooks: Array, removedPaths: string[] }}
|
|
109
|
+
*/
|
|
110
|
+
function pruneStaleFileHooks(hookArray, baseDir) {
|
|
111
|
+
if (!Array.isArray(hookArray)) {
|
|
112
|
+
return { hooks: [], removedPaths: [] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const resolveBase = baseDir || process.cwd();
|
|
116
|
+
const removedPaths = [];
|
|
117
|
+
|
|
118
|
+
const hooks = hookArray.filter((entry) => {
|
|
119
|
+
const entryHooks = Array.isArray(entry && entry.hooks) ? entry.hooks : [];
|
|
120
|
+
let shouldRemove = false;
|
|
121
|
+
|
|
122
|
+
for (const hook of entryHooks) {
|
|
123
|
+
const command = hook && typeof hook.command === 'string' ? hook.command : '';
|
|
124
|
+
if (!command) continue;
|
|
125
|
+
|
|
126
|
+
// Extract the first token as the potential script path.
|
|
127
|
+
const firstToken = command.split(/\s+/)[0];
|
|
128
|
+
|
|
129
|
+
// Only treat it as a file reference if it looks like a path.
|
|
130
|
+
const looksLikePath =
|
|
131
|
+
firstToken.includes('/') ||
|
|
132
|
+
firstToken.includes('\\') ||
|
|
133
|
+
firstToken.endsWith('.sh');
|
|
134
|
+
|
|
135
|
+
if (!looksLikePath) continue;
|
|
136
|
+
|
|
137
|
+
// Resolve the path (absolute or relative to baseDir).
|
|
138
|
+
const resolved = path.isAbsolute(firstToken)
|
|
139
|
+
? firstToken
|
|
140
|
+
: path.resolve(resolveBase, firstToken);
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(resolved)) {
|
|
143
|
+
removedPaths.push(firstToken);
|
|
144
|
+
shouldRemove = true;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return !shouldRemove;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return { hooks, removedPaths };
|
|
153
|
+
}
|
|
154
|
+
|
|
97
155
|
function pruneLegacyHookEntries(hookArray, expectedCommand, legacyPattern) {
|
|
98
156
|
if (!Array.isArray(hookArray)) {
|
|
99
157
|
return { hooks: [], removed: false };
|
|
@@ -131,12 +189,78 @@ function syncClaudeStatusLine(settingsPath, desiredStatusLine, dryRun) {
|
|
|
131
189
|
return true;
|
|
132
190
|
}
|
|
133
191
|
|
|
192
|
+
/**
|
|
193
|
+
* claudeProjectSettingsPath — returns the project-level .claude/settings.json
|
|
194
|
+
* path relative to the given base directory (defaults to CWD).
|
|
195
|
+
*/
|
|
196
|
+
function claudeProjectSettingsPath(baseDir) {
|
|
197
|
+
return path.join(baseDir || process.cwd(), '.claude', 'settings.json');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* pruneStaleHooksInFile — reads a settings file, removes any hook entries that
|
|
202
|
+
* reference missing shell script files, and writes the file back if changed.
|
|
203
|
+
*
|
|
204
|
+
* @param {string} filePath - Absolute path to the settings JSON file.
|
|
205
|
+
* @param {string} baseDir - Base directory for resolving relative script paths.
|
|
206
|
+
* @param {boolean} dryRun - When true, changes are computed but not persisted.
|
|
207
|
+
* @returns {{ changed: boolean, removedPaths: string[] }}
|
|
208
|
+
*/
|
|
209
|
+
function pruneStaleHooksInFile(filePath, baseDir, dryRun) {
|
|
210
|
+
const settings = loadJsonFile(filePath);
|
|
211
|
+
if (!settings || !settings.hooks || typeof settings.hooks !== 'object') {
|
|
212
|
+
return { changed: false, removedPaths: [] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const allRemovedPaths = [];
|
|
216
|
+
let changed = false;
|
|
217
|
+
|
|
218
|
+
for (const lifecycle of Object.keys(settings.hooks)) {
|
|
219
|
+
const { hooks, removedPaths } = pruneStaleFileHooks(settings.hooks[lifecycle], baseDir);
|
|
220
|
+
if (removedPaths.length > 0) {
|
|
221
|
+
settings.hooks[lifecycle] = hooks;
|
|
222
|
+
allRemovedPaths.push(...removedPaths);
|
|
223
|
+
changed = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (changed && !dryRun) {
|
|
228
|
+
fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { changed, removedPaths: allRemovedPaths };
|
|
232
|
+
}
|
|
233
|
+
|
|
134
234
|
function wireClaudeHooks(options) {
|
|
135
235
|
const settingsPath = options.settingsPath || claudeSettingsPath();
|
|
136
236
|
const sharedSettingsPath = options.sharedSettingsPath || claudeSharedSettingsPath();
|
|
237
|
+
const projectSettingsPath =
|
|
238
|
+
options.projectSettingsPath || claudeProjectSettingsPath(options.projectDir);
|
|
137
239
|
const dryRun = options.dryRun || false;
|
|
240
|
+
const projectDir = options.projectDir || process.cwd();
|
|
138
241
|
const desiredStatusLine = statuslineCommand();
|
|
139
242
|
|
|
243
|
+
// --- Step 0: clean up stale hooks from BOTH settings locations ---
|
|
244
|
+
const staleWarnings = [];
|
|
245
|
+
|
|
246
|
+
// User-level: ~/.claude/settings.local.json
|
|
247
|
+
const userStale = pruneStaleHooksInFile(settingsPath, projectDir, dryRun);
|
|
248
|
+
for (const p of userStale.removedPaths) {
|
|
249
|
+
const msg = `Removed stale hook referencing missing file: ${p}`;
|
|
250
|
+
console.warn(msg);
|
|
251
|
+
staleWarnings.push({ file: settingsPath, path: p });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Project-level: $CWD/.claude/settings.json (takes precedence for some events)
|
|
255
|
+
if (fs.existsSync(projectSettingsPath)) {
|
|
256
|
+
const projStale = pruneStaleHooksInFile(projectSettingsPath, projectDir, dryRun);
|
|
257
|
+
for (const p of projStale.removedPaths) {
|
|
258
|
+
const msg = `Removed stale hook referencing missing file: ${p}`;
|
|
259
|
+
console.warn(msg);
|
|
260
|
+
staleWarnings.push({ file: projectSettingsPath, path: p });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
140
264
|
let settings = loadJsonFile(settingsPath) || {};
|
|
141
265
|
settings.hooks = settings.hooks || {};
|
|
142
266
|
|
|
@@ -426,10 +550,13 @@ module.exports = {
|
|
|
426
550
|
parseFlags,
|
|
427
551
|
claudeSettingsPath,
|
|
428
552
|
claudeSharedSettingsPath,
|
|
553
|
+
claudeProjectSettingsPath,
|
|
429
554
|
codexConfigPath,
|
|
430
555
|
geminiSettingsPath,
|
|
431
556
|
syncClaudeStatusLine,
|
|
432
557
|
forgeConfigPath,
|
|
558
|
+
pruneStaleFileHooks,
|
|
559
|
+
pruneStaleHooksInFile,
|
|
433
560
|
CLAUDE_HOOKS,
|
|
434
561
|
preToolHookCommand,
|
|
435
562
|
userPromptHookCommand,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* cli-demo.js — simulated walkthrough of the ThumbGate value prop.
|
|
6
|
+
*
|
|
7
|
+
* Shows in ~10 seconds:
|
|
8
|
+
* 1. A mock bad action (force-push)
|
|
9
|
+
* 2. Giving it a thumbs-down
|
|
10
|
+
* 3. Lesson created
|
|
11
|
+
* 4. Next session: gate fires, action blocked
|
|
12
|
+
*
|
|
13
|
+
* No actual data is written — this is a pure simulation for onboarding.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const BD = '\x1b[1m';
|
|
17
|
+
const RST = '\x1b[0m';
|
|
18
|
+
const G = '\x1b[32m';
|
|
19
|
+
const R = '\x1b[31m';
|
|
20
|
+
const C = '\x1b[36m';
|
|
21
|
+
const Y = '\x1b[33m';
|
|
22
|
+
const D = '\x1b[90m';
|
|
23
|
+
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
const end = Date.now() + ms;
|
|
26
|
+
while (Date.now() < end) { /* spin — no async needed for short delays */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runDemo(options = {}) {
|
|
30
|
+
const json = options.json || false;
|
|
31
|
+
|
|
32
|
+
if (json) {
|
|
33
|
+
const steps = [
|
|
34
|
+
{ step: 1, event: 'bad_action', description: 'Agent runs: git push --force origin main', result: 'executed', badge: 'ALLOWED' },
|
|
35
|
+
{ step: 2, event: 'thumbs_down', description: 'User gives thumbs-down feedback', signal: 'down', context: 'force-pushed to main, overwrote teammate\'s work' },
|
|
36
|
+
{ step: 3, event: 'lesson_created', description: 'ThumbGate creates a lesson from the feedback', lesson: { id: 'demo-lesson-001', signal: 'negative', context: 'force-pushed to main', whatWentWrong: 'Overwrote teammate\'s commits on main branch', whatToChange: 'Never force-push to protected branches' } },
|
|
37
|
+
{ step: 4, event: 'gate_promoted', description: 'Pattern detected: 2+ failures on force-push → auto-promoted to blocking gate', gate: { id: 'auto-block-force-push', pattern: 'git push --force.*main', action: 'block', occurrences: 2 } },
|
|
38
|
+
{ step: 5, event: 'gate_fires', description: 'Next session: agent tries git push --force origin main', result: 'BLOCKED', reason: 'Auto-promoted gate: force-push to main detected' },
|
|
39
|
+
];
|
|
40
|
+
return { demo: true, steps };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const lines = [];
|
|
44
|
+
const w = (s) => lines.push(s);
|
|
45
|
+
|
|
46
|
+
w('');
|
|
47
|
+
w(`${BD}${C}thumbgate demo${RST} — see ThumbGate in action (simulated)`);
|
|
48
|
+
w('═'.repeat(60));
|
|
49
|
+
w('');
|
|
50
|
+
|
|
51
|
+
// Step 1: Bad action
|
|
52
|
+
w(`${BD}Session 1: Agent runs a risky command${RST}`);
|
|
53
|
+
w(`${D}─────────────────────────────────────${RST}`);
|
|
54
|
+
w(` ${D}Agent>${RST} ${BD}git push --force origin main${RST}`);
|
|
55
|
+
w(` ${G}[ALLOWED]${RST} — No gates configured yet, action proceeds.`);
|
|
56
|
+
w(` ${R}💥 Result: teammate's commits overwritten on main${RST}`);
|
|
57
|
+
w('');
|
|
58
|
+
|
|
59
|
+
// Step 2: Thumbs down
|
|
60
|
+
w(`${BD}You give feedback:${RST}`);
|
|
61
|
+
w(` ${R}👎 thumbs-down${RST} — "force-pushed to main, overwrote teammate's work"`);
|
|
62
|
+
w('');
|
|
63
|
+
|
|
64
|
+
// Step 3: Lesson
|
|
65
|
+
w(`${BD}ThumbGate captures a lesson:${RST}`);
|
|
66
|
+
w(`${D}─────────────────────────────────────${RST}`);
|
|
67
|
+
w(` ${Y}[LEARNING]${RST} Lesson created`);
|
|
68
|
+
w(` Signal : ${R}negative${RST}`);
|
|
69
|
+
w(` Context : force-pushed to main`);
|
|
70
|
+
w(` Root cause : Overwrote teammate's commits on main branch`);
|
|
71
|
+
w(` Corrective : Never force-push to protected branches`);
|
|
72
|
+
w(` Tags : git, deployment, data-loss`);
|
|
73
|
+
w('');
|
|
74
|
+
|
|
75
|
+
// Step 4: Gate promotion
|
|
76
|
+
w(`${BD}Pattern detected (2+ similar failures):${RST}`);
|
|
77
|
+
w(` ${Y}→${RST} Auto-promoted to ${R}blocking gate${RST}`);
|
|
78
|
+
w(` Gate ID : auto-block-force-push`);
|
|
79
|
+
w(` Pattern : git push --force.*main`);
|
|
80
|
+
w(` Action : ${R}block${RST}`);
|
|
81
|
+
w('');
|
|
82
|
+
|
|
83
|
+
// Step 5: Next session — blocked
|
|
84
|
+
w(`${BD}Session 2: Agent tries the same command${RST}`);
|
|
85
|
+
w(`${D}─────────────────────────────────────${RST}`);
|
|
86
|
+
w(` ${D}Agent>${RST} ${BD}git push --force origin main${RST}`);
|
|
87
|
+
w(` ${R}[BLOCKED]${RST} Gate fired: force-push to main detected`);
|
|
88
|
+
w(` ${G}✅ Mistake prevented. Teammate's work is safe.${RST}`);
|
|
89
|
+
w('');
|
|
90
|
+
|
|
91
|
+
w('═'.repeat(60));
|
|
92
|
+
w(`${BD}That's ThumbGate:${RST} one thumbs-down → permanent protection.`);
|
|
93
|
+
w('');
|
|
94
|
+
w(` Get started: ${C}npx thumbgate init${RST}`);
|
|
95
|
+
w(` Capture: ${C}npx thumbgate capture --feedback=down --context="what failed"${RST}`);
|
|
96
|
+
w(` Check gates: ${C}npx thumbgate gate-stats --json${RST}`);
|
|
97
|
+
w('');
|
|
98
|
+
|
|
99
|
+
return lines.join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { runDemo };
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cli-schema.js — single source of truth for thumbgate CLI commands.
|
|
5
|
+
*
|
|
6
|
+
* Inspired by Cloudflare's schema-first CLI architecture: one definition
|
|
7
|
+
* drives both the CLI help text and the explore TUI command browser.
|
|
8
|
+
* MCP tool bindings are listed via `mcpTool` so the two surfaces stay in sync.
|
|
9
|
+
*
|
|
10
|
+
* Groups: capture | discovery | gates | export | ops | advanced
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const CLI_COMMANDS = [
|
|
14
|
+
// -------------------------------------------------------------------------
|
|
15
|
+
// Capture
|
|
16
|
+
// -------------------------------------------------------------------------
|
|
17
|
+
{
|
|
18
|
+
name: 'capture',
|
|
19
|
+
aliases: ['feedback'],
|
|
20
|
+
description: 'Capture an up/down signal — turns feedback into a stored lesson',
|
|
21
|
+
group: 'capture',
|
|
22
|
+
mcpTool: 'capture_feedback',
|
|
23
|
+
flags: [
|
|
24
|
+
{ name: 'feedback', type: 'string', required: true, description: 'Signal: up or down' },
|
|
25
|
+
{ name: 'context', type: 'string', description: 'One-line reason' },
|
|
26
|
+
{ name: 'what-went-wrong', type: 'string', description: 'Root cause (negative feedback)' },
|
|
27
|
+
{ name: 'what-to-change', type: 'string', description: 'Specific fix required' },
|
|
28
|
+
{ name: 'what-worked', type: 'string', description: 'What succeeded (positive feedback)' },
|
|
29
|
+
{ name: 'tags', type: 'string', description: 'Comma-separated tags' },
|
|
30
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// -------------------------------------------------------------------------
|
|
35
|
+
// Discovery
|
|
36
|
+
// -------------------------------------------------------------------------
|
|
37
|
+
{
|
|
38
|
+
name: 'explore',
|
|
39
|
+
description: 'Interactive TUI — browse lessons, gates, stats, and rules keyboard-first',
|
|
40
|
+
group: 'discovery',
|
|
41
|
+
flags: [
|
|
42
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON (non-interactive)' },
|
|
43
|
+
{ name: 'limit', type: 'number', description: 'Max items (default 20)' },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'lessons',
|
|
48
|
+
aliases: ['search-lessons'],
|
|
49
|
+
description: 'Search promoted lessons and show linked corrective actions',
|
|
50
|
+
group: 'discovery',
|
|
51
|
+
mcpTool: 'search_lessons',
|
|
52
|
+
flags: [
|
|
53
|
+
{ name: 'query', type: 'string', description: 'Search query (positional arg also works)' },
|
|
54
|
+
{ name: 'limit', type: 'number', description: 'Max results (default 10)' },
|
|
55
|
+
{ name: 'tags', type: 'string', description: 'Comma-separated tag filter' },
|
|
56
|
+
{ name: 'category', type: 'string', description: 'error | learning | preference' },
|
|
57
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
58
|
+
{ name: 'local', type: 'boolean', description: 'Use local storage (default)' },
|
|
59
|
+
{ name: 'remote', type: 'boolean', description: 'Fetch from hosted Railway instance' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'stats',
|
|
64
|
+
description: 'Feedback analytics — approval rate, Revenue-at-Risk, recent trend',
|
|
65
|
+
group: 'discovery',
|
|
66
|
+
mcpTool: 'feedback_stats',
|
|
67
|
+
flags: [
|
|
68
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
69
|
+
{ name: 'remote', type: 'boolean', description: 'Fetch from hosted Railway instance' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'gate-stats',
|
|
74
|
+
description: 'Gate engine statistics — active gates, blocks, warns, time saved',
|
|
75
|
+
group: 'discovery',
|
|
76
|
+
flags: [
|
|
77
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'summary',
|
|
82
|
+
description: 'Human-readable feedback summary',
|
|
83
|
+
group: 'discovery',
|
|
84
|
+
mcpTool: 'feedback_summary',
|
|
85
|
+
flags: [
|
|
86
|
+
{ name: 'recent', type: 'number', description: 'Number of recent entries (default 20)' },
|
|
87
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'doctor',
|
|
92
|
+
description: 'Audit runtime isolation, bootstrap context, and permission tier',
|
|
93
|
+
group: 'discovery',
|
|
94
|
+
flags: [
|
|
95
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'lesson-health',
|
|
100
|
+
aliases: ['stale'],
|
|
101
|
+
description: 'Report on stale lessons (>60d inactive) with optional auto-archive',
|
|
102
|
+
group: 'discovery',
|
|
103
|
+
flags: [
|
|
104
|
+
{ name: 'archive', type: 'boolean', description: 'Auto-archive lessons >90d inactive' },
|
|
105
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// -------------------------------------------------------------------------
|
|
110
|
+
// Gates
|
|
111
|
+
// -------------------------------------------------------------------------
|
|
112
|
+
{
|
|
113
|
+
name: 'gate-check',
|
|
114
|
+
description: 'PreToolUse hook: pipe tool JSON via stdin, get ALLOW/BLOCK verdict',
|
|
115
|
+
group: 'gates',
|
|
116
|
+
flags: [],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'force-gate',
|
|
120
|
+
description: 'Immediately create a blocking gate from a pattern string',
|
|
121
|
+
group: 'gates',
|
|
122
|
+
flags: [
|
|
123
|
+
{ name: 'pattern', type: 'string', description: 'Pattern to block (positional)' },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'rules',
|
|
128
|
+
description: 'Generate prevention rules from repeated failure patterns',
|
|
129
|
+
group: 'gates',
|
|
130
|
+
mcpTool: 'prevention_rules',
|
|
131
|
+
flags: [
|
|
132
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// -------------------------------------------------------------------------
|
|
137
|
+
// Export
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
{
|
|
140
|
+
name: 'export-dpo',
|
|
141
|
+
aliases: ['dpo'],
|
|
142
|
+
description: 'Export DPO training pairs (prompt/chosen/rejected JSONL)',
|
|
143
|
+
group: 'export',
|
|
144
|
+
flags: [
|
|
145
|
+
{ name: 'output', type: 'string', description: 'Output file path' },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'export-databricks',
|
|
150
|
+
aliases: ['databricks'],
|
|
151
|
+
description: 'Export feedback + proof artifacts as a Databricks-ready analytics bundle',
|
|
152
|
+
group: 'export',
|
|
153
|
+
flags: [],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'obsidian-export',
|
|
157
|
+
description: 'Export all feedback as interlinked Obsidian markdown notes',
|
|
158
|
+
group: 'export',
|
|
159
|
+
flags: [
|
|
160
|
+
{ name: 'vault-path', type: 'string', description: 'Obsidian vault path' },
|
|
161
|
+
{ name: 'output-dir', type: 'string', description: 'Output subdirectory (default: AI-Memories/thumbgate)' },
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// -------------------------------------------------------------------------
|
|
166
|
+
// Ops
|
|
167
|
+
// -------------------------------------------------------------------------
|
|
168
|
+
{
|
|
169
|
+
name: 'status',
|
|
170
|
+
description: 'Agent-friendly health check — gates, lessons, feedback, enforcement',
|
|
171
|
+
group: 'discovery',
|
|
172
|
+
flags: [
|
|
173
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'demo',
|
|
178
|
+
description: 'Simulated walkthrough — see ThumbGate block a bad action in 10 seconds',
|
|
179
|
+
group: 'ops',
|
|
180
|
+
flags: [
|
|
181
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'init',
|
|
186
|
+
description: 'Scaffold .thumbgate/ config and wire agent hooks',
|
|
187
|
+
group: 'ops',
|
|
188
|
+
flags: [
|
|
189
|
+
{ name: 'agent', type: 'string', description: 'Target agent: claude-code | cursor | codex | gemini | amp' },
|
|
190
|
+
{ name: 'wire-hooks', type: 'boolean', description: 'Wire hooks only (skip scaffold)' },
|
|
191
|
+
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'serve',
|
|
196
|
+
description: 'Start MCP server on stdio — connect any MCP-compatible agent',
|
|
197
|
+
group: 'ops',
|
|
198
|
+
flags: [],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'dashboard',
|
|
202
|
+
description: 'Full ThumbGate dashboard — approval rate, gate stats, prevention impact',
|
|
203
|
+
group: 'ops',
|
|
204
|
+
flags: [],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'self-heal',
|
|
208
|
+
description: 'Run self-healing check and auto-fix known issues',
|
|
209
|
+
group: 'ops',
|
|
210
|
+
flags: [
|
|
211
|
+
{ name: 'check', type: 'boolean', description: 'Check only, no fixes' },
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'import-doc',
|
|
216
|
+
aliases: ['import-document'],
|
|
217
|
+
description: 'Import a local policy/runbook and propose reviewable gate candidates',
|
|
218
|
+
group: 'ops',
|
|
219
|
+
flags: [
|
|
220
|
+
{ name: 'file', type: 'string', description: 'Path to document' },
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: 'meta-agent',
|
|
225
|
+
description: 'Run meta-agent loop: generate, evaluate, and promote prevention rules',
|
|
226
|
+
group: 'advanced',
|
|
227
|
+
flags: [
|
|
228
|
+
{ name: 'dry-run', type: 'boolean', description: 'Preview rules without writing' },
|
|
229
|
+
{ name: 'status', type: 'boolean', description: 'Show last run summary' },
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'pro',
|
|
234
|
+
description: `Solo dashboard + exports side lane (${'19'}/mo · ${'149'}/yr)`,
|
|
235
|
+
group: 'ops',
|
|
236
|
+
flags: [
|
|
237
|
+
{ name: 'upgrade', type: 'boolean', description: 'Install Pro configs into .thumbgate/' },
|
|
238
|
+
{ name: 'info', type: 'boolean', description: 'Show Pro feature list' },
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Return the command definition for a given name or alias.
|
|
245
|
+
*/
|
|
246
|
+
function findCommand(name) {
|
|
247
|
+
return CLI_COMMANDS.find(
|
|
248
|
+
(cmd) => cmd.name === name || (cmd.aliases || []).includes(name),
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Return commands grouped by their group field.
|
|
254
|
+
*/
|
|
255
|
+
function groupedCommands() {
|
|
256
|
+
const groups = {};
|
|
257
|
+
for (const cmd of CLI_COMMANDS) {
|
|
258
|
+
const g = cmd.group || 'other';
|
|
259
|
+
if (!groups[g]) groups[g] = [];
|
|
260
|
+
groups[g].push(cmd);
|
|
261
|
+
}
|
|
262
|
+
return groups;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Generate a compact help string for a single command.
|
|
267
|
+
* Format: name [aliases] description [--flag ...]
|
|
268
|
+
*/
|
|
269
|
+
function commandHelpLine(cmd, opts = {}) {
|
|
270
|
+
const { showFlags = false } = opts;
|
|
271
|
+
const nameCol = 22;
|
|
272
|
+
const nameStr = [cmd.name, ...(cmd.aliases || []).slice(0, 1)].join(' | ');
|
|
273
|
+
const pad = ' '.repeat(Math.max(1, nameCol - nameStr.length));
|
|
274
|
+
let line = ` ${nameStr}${pad}${cmd.description}`;
|
|
275
|
+
if (cmd.mcpTool) line += ` [mcp:${cmd.mcpTool}]`;
|
|
276
|
+
if (showFlags && cmd.flags.length > 0) {
|
|
277
|
+
const flagStr = cmd.flags
|
|
278
|
+
.map((f) => `--${f.name}${f.required ? ' (required)' : ''}`)
|
|
279
|
+
.join(' ');
|
|
280
|
+
line += `\n ${flagStr}`;
|
|
281
|
+
}
|
|
282
|
+
return line;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = { CLI_COMMANDS, findCommand, groupedCommands, commandHelpLine };
|