thumbgate 1.5.4 → 1.6.0

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.
@@ -17,6 +17,11 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const {
19
19
  cacheUpdateHookCommand,
20
+ codexCacheUpdateHookCommand,
21
+ codexPreToolHookCommand,
22
+ codexSessionStartHookCommand,
23
+ codexStatuslineCommand,
24
+ codexUserPromptHookCommand,
20
25
  preToolHookCommand,
21
26
  sessionStartHookCommand,
22
27
  statuslineCommand,
@@ -48,17 +53,17 @@ const CLAUDE_HOOKS = {
48
53
  const CODEX_HOOKS = {
49
54
  PreToolUse: {
50
55
  matcher: 'Bash',
51
- hooks: [{ type: 'command', command: preToolHookCommand() }],
56
+ hooks: [{ type: 'command', command: codexPreToolHookCommand() }],
52
57
  },
53
58
  UserPromptSubmit: {
54
- hooks: [{ type: 'command', command: userPromptHookCommand() }],
59
+ hooks: [{ type: 'command', command: codexUserPromptHookCommand() }],
55
60
  },
56
61
  PostToolUse: {
57
62
  matcher: 'mcp__thumbgate__feedback_stats|mcp__thumbgate__dashboard',
58
- hooks: [{ type: 'command', command: cacheUpdateHookCommand() }],
63
+ hooks: [{ type: 'command', command: codexCacheUpdateHookCommand() }],
59
64
  },
60
65
  SessionStart: {
61
- hooks: [{ type: 'command', command: sessionStartHookCommand() }],
66
+ hooks: [{ type: 'command', command: codexSessionStartHookCommand() }],
62
67
  },
63
68
  };
64
69
 
@@ -71,6 +76,7 @@ function detectAgent(flagAgent) {
71
76
  if (['codex'].includes(normalized)) return 'codex';
72
77
  if (['gemini'].includes(normalized)) return 'gemini';
73
78
  if (['forge', 'forgecode', 'forge-code'].includes(normalized)) return 'forge';
79
+ if (['cursor'].includes(normalized)) return 'cursor';
74
80
  return null;
75
81
  }
76
82
 
@@ -80,9 +86,65 @@ function detectAgent(flagAgent) {
80
86
  if (fs.existsSync(path.join(home, '.codex'))) return 'codex';
81
87
  if (fs.existsSync(path.join(home, '.gemini'))) return 'gemini';
82
88
  if (fs.existsSync(path.join(process.cwd(), 'forge.yaml'))) return 'forge';
89
+ if (fs.existsSync(path.join(process.cwd(), '.cursor'))) return 'cursor';
83
90
  return null;
84
91
  }
85
92
 
93
+ // --- Cursor wiring ---
94
+ // Cursor uses .cursor/mcp.json in the project root. We write the ThumbGate MCP
95
+ // server config there so Cursor picks up the server on next restart. Cursor's
96
+ // native hook model is different from Claude Code's — we rely on the MCP
97
+ // server's PreToolUse-equivalent enforcement via the gate-check tool.
98
+
99
+ function cursorMcpConfigPath() {
100
+ return path.join(process.cwd(), '.cursor', 'mcp.json');
101
+ }
102
+
103
+ function wireCursorHooks(options = {}) {
104
+ const mcpPath = cursorMcpConfigPath();
105
+ const dir = path.dirname(mcpPath);
106
+ const thumbgateServer = {
107
+ command: 'npx',
108
+ args: ['--yes', '--package', 'thumbgate@latest', 'thumbgate', 'serve'],
109
+ };
110
+
111
+ let existing = { mcpServers: {} };
112
+ if (fs.existsSync(mcpPath)) {
113
+ try {
114
+ existing = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
115
+ if (!existing.mcpServers) existing.mcpServers = {};
116
+ } catch {
117
+ return { changed: false, error: `Could not parse ${mcpPath}` };
118
+ }
119
+ }
120
+
121
+ const before = JSON.stringify(existing.mcpServers.thumbgate || null);
122
+ existing.mcpServers.thumbgate = thumbgateServer;
123
+ const after = JSON.stringify(existing.mcpServers.thumbgate);
124
+
125
+ const addedEntry = {
126
+ lifecycle: 'mcpServers.thumbgate',
127
+ command: `${thumbgateServer.command} ${thumbgateServer.args.join(' ')}`,
128
+ };
129
+
130
+ if (options.dryRun) {
131
+ return {
132
+ changed: before !== after,
133
+ dryRun: true,
134
+ settingsPath: mcpPath,
135
+ added: before === after ? [] : [addedEntry],
136
+ };
137
+ }
138
+
139
+ if (before === after) {
140
+ return { changed: false, settingsPath: mcpPath, added: [] };
141
+ }
142
+
143
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
144
+ fs.writeFileSync(mcpPath, JSON.stringify(existing, null, 2) + '\n');
145
+ return { changed: true, settingsPath: mcpPath, added: [addedEntry] };
146
+ }
147
+
86
148
  // --- Claude Code wiring ---
87
149
 
88
150
  function claudeSettingsPath() {
@@ -402,7 +464,7 @@ function syncCodexStatusLine(config, desiredStatusLine) {
402
464
  function wireCodexHooks(options) {
403
465
  const configPath = options.settingsPath || codexConfigPath();
404
466
  const dryRun = options.dryRun || false;
405
- const desiredStatusLine = statuslineCommand();
467
+ const desiredStatusLine = codexStatuslineCommand();
406
468
 
407
469
  let config = loadJsonFile(configPath) || {};
408
470
  config.hooks = config.hooks || {};
@@ -543,7 +605,7 @@ function wireHooks(options) {
543
605
  const agent = detectAgent(options.agent);
544
606
  if (!agent) {
545
607
  return {
546
- error: 'Could not detect AI agent. Use --agent=claude-code|codex|gemini|forge',
608
+ error: 'Could not detect AI agent. Use --agent=claude-code|codex|gemini|forge|cursor',
547
609
  agent: null,
548
610
  changed: false,
549
611
  };
@@ -563,6 +625,9 @@ function wireHooks(options) {
563
625
  case 'forge':
564
626
  result = wireForgeHooks(options);
565
627
  break;
628
+ case 'cursor':
629
+ result = wireCursorHooks(options);
630
+ break;
566
631
  default:
567
632
  return { error: `Unsupported agent: ${agent}`, agent, changed: false };
568
633
  }