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.
Files changed (60) hide show
  1. package/.claude-plugin/README.md +45 -34
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +3 -3
  4. package/.well-known/llms.txt +1 -1
  5. package/.well-known/mcp/server-card.json +1 -1
  6. package/README.md +26 -2
  7. package/adapters/README.md +4 -1
  8. package/adapters/chatgpt/INSTALL.md +39 -19
  9. package/adapters/claude/.mcp.json +2 -2
  10. package/adapters/codex/config.toml +2 -2
  11. package/adapters/mcp/server-stdio.js +10 -4
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/adapters/perplexity/.mcp.json +36 -0
  14. package/adapters/perplexity/config.toml +16 -0
  15. package/adapters/perplexity/opencode.json +29 -0
  16. package/bin/cli.js +246 -90
  17. package/config/mcp-allowlists.json +11 -3
  18. package/package.json +28 -13
  19. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  20. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  21. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  22. package/plugins/codex-profile/.mcp.json +1 -1
  23. package/plugins/codex-profile/INSTALL.md +1 -1
  24. package/plugins/codex-profile/README.md +1 -1
  25. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  26. package/plugins/opencode-profile/INSTALL.md +1 -1
  27. package/public/index.html +121 -24
  28. package/public/llm-context.md +17 -1
  29. package/scripts/ai-search-visibility.js +10 -36
  30. package/scripts/audit-trail.js +25 -15
  31. package/scripts/auto-wire-hooks.js +127 -0
  32. package/scripts/cli-demo.js +102 -0
  33. package/scripts/cli-schema.js +285 -0
  34. package/scripts/cli-status.js +166 -0
  35. package/scripts/cross-encoder-reranker.js +235 -0
  36. package/scripts/explore-subcommands.js +277 -0
  37. package/scripts/explore.js +569 -0
  38. package/scripts/feedback-loop.js +20 -6
  39. package/scripts/lesson-inference.js +27 -2
  40. package/scripts/lesson-reranker.js +263 -0
  41. package/scripts/lesson-retrieval.js +34 -17
  42. package/scripts/lesson-search.js +69 -0
  43. package/scripts/perplexity-client.js +210 -0
  44. package/scripts/perplexity-command-center.js +644 -0
  45. package/scripts/perplexity-marketing.js +17 -29
  46. package/scripts/prove-packaged-runtime.js +5 -4
  47. package/scripts/ralph-mode-ci.js +122 -19
  48. package/scripts/reflector-agent.js +2 -2
  49. package/scripts/session-analyzer.js +533 -0
  50. package/scripts/social-analytics/db/marketing-db.js +179 -0
  51. package/scripts/social-analytics/db/schema.sql +23 -0
  52. package/scripts/social-analytics/generate-instagram-card.js +31 -5
  53. package/scripts/social-analytics/generate-slides.js +268 -0
  54. package/scripts/social-analytics/post-video.js +316 -0
  55. package/scripts/social-analytics/publishers/zernio.js +52 -23
  56. package/scripts/statusline-local-stats.js +3 -1
  57. package/scripts/statusline.sh +15 -10
  58. package/scripts/thumbgate-bench.js +494 -0
  59. package/src/api/server.js +65 -1
  60. 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 };