@promptcellar/pc 0.1.0 → 0.3.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.
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install -g @promptcellar/pc
14
14
  # Login with your API key
15
15
  pc login
16
16
 
17
- # Set up auto-capture for Claude Code
17
+ # Set up auto-capture for your AI CLI tools
18
18
  pc setup
19
19
 
20
20
  # Check status
@@ -38,12 +38,15 @@ pc setup # Configure auto-capture for CLI tools
38
38
  pc unsetup # Remove auto-capture hooks
39
39
  ```
40
40
 
41
- Currently supports:
42
- - Claude Code (via Stop hooks)
41
+ Supported tools (auto-detected during setup):
42
+ - **Claude Code** - via Stop hooks
43
+ - **Codex CLI** - via notify hook
44
+ - **Gemini CLI** - via AfterAgent hook
43
45
 
44
46
  Coming soon:
45
47
  - Cursor
46
48
  - Windsurf
49
+ - Aider
47
50
 
48
51
  ### Manual Capture
49
52
 
package/bin/pc.js CHANGED
@@ -13,11 +13,12 @@ import { update } from '../src/commands/update.js';
13
13
  program
14
14
  .name('pc')
15
15
  .description('PromptCellar CLI - sync prompts between your terminal and the cloud')
16
- .version('0.1.0');
16
+ .version('0.3.0');
17
17
 
18
18
  program
19
19
  .command('login')
20
- .description('Authenticate with PromptCellar')
20
+ .description('Authenticate with PromptCellar via browser')
21
+ .option('-u, --url <url>', 'API URL (for self-hosted instances)')
21
22
  .action(login);
22
23
 
23
24
  program
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Codex CLI notify hook for capturing prompts to PromptCellar.
5
+ *
6
+ * Codex calls this script with a JSON argument containing:
7
+ * - type: event type (e.g., 'agent-turn-complete')
8
+ * - thread-id: session identifier
9
+ * - turn-id: turn identifier
10
+ * - cwd: working directory
11
+ * - input-messages: array of user messages
12
+ * - last-assistant-message: final assistant response
13
+ */
14
+
15
+ import { capturePrompt } from '../src/lib/api.js';
16
+ import { getFullContext } from '../src/lib/context.js';
17
+ import { isLoggedIn } from '../src/lib/config.js';
18
+
19
+ async function main() {
20
+ // Codex passes JSON as first argument
21
+ const jsonArg = process.argv[2];
22
+
23
+ if (!jsonArg) {
24
+ // No argument, nothing to do
25
+ process.exit(0);
26
+ }
27
+
28
+ if (!isLoggedIn()) {
29
+ // Silently exit if not logged in
30
+ process.exit(0);
31
+ }
32
+
33
+ try {
34
+ const event = JSON.parse(jsonArg);
35
+
36
+ // Only capture on agent-turn-complete
37
+ if (event.type !== 'agent-turn-complete') {
38
+ process.exit(0);
39
+ }
40
+
41
+ // Extract the user's input messages
42
+ const inputMessages = event['input-messages'] || [];
43
+ if (inputMessages.length === 0) {
44
+ process.exit(0);
45
+ }
46
+
47
+ // Get the first user message as the prompt
48
+ const userMessage = inputMessages.find(m => m.role === 'user');
49
+ if (!userMessage || !userMessage.content) {
50
+ process.exit(0);
51
+ }
52
+
53
+ // Extract content (handle both string and array formats)
54
+ let content = userMessage.content;
55
+ if (Array.isArray(content)) {
56
+ content = content
57
+ .filter(c => c.type === 'text')
58
+ .map(c => c.text)
59
+ .join('\n');
60
+ }
61
+
62
+ if (!content.trim()) {
63
+ process.exit(0);
64
+ }
65
+
66
+ // Build context
67
+ const context = getFullContext('codex');
68
+
69
+ // Override working directory if provided
70
+ if (event.cwd) {
71
+ context.working_directory = event.cwd;
72
+ }
73
+
74
+ // Add session info
75
+ if (event['thread-id']) {
76
+ context.session_id = event['thread-id'];
77
+ }
78
+
79
+ await capturePrompt({
80
+ content: content.trim(),
81
+ ...context
82
+ });
83
+
84
+ } catch (error) {
85
+ // Fail silently to not disrupt Codex
86
+ process.exit(0);
87
+ }
88
+ }
89
+
90
+ main();
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Gemini CLI AfterAgent hook for capturing prompts to PromptCellar.
5
+ *
6
+ * Gemini hooks receive JSON via stdin with:
7
+ * - event_type: the hook event (e.g., 'AfterAgent')
8
+ * - prompt: the user's prompt
9
+ * - prompt_response: the agent's response (for AfterAgent)
10
+ * - session_id: session identifier
11
+ * - working_directory: current working directory
12
+ *
13
+ * The hook must output valid JSON to stdout.
14
+ */
15
+
16
+ import { capturePrompt } from '../src/lib/api.js';
17
+ import { getFullContext } from '../src/lib/context.js';
18
+ import { isLoggedIn } from '../src/lib/config.js';
19
+
20
+ async function readStdin() {
21
+ return new Promise((resolve) => {
22
+ let data = '';
23
+ process.stdin.setEncoding('utf8');
24
+ process.stdin.on('data', chunk => data += chunk);
25
+ process.stdin.on('end', () => resolve(data));
26
+
27
+ // Timeout after 1 second if no input
28
+ setTimeout(() => resolve(data), 1000);
29
+ });
30
+ }
31
+
32
+ function respond(success = true) {
33
+ // Gemini expects JSON output
34
+ const response = {
35
+ action: 'continue',
36
+ success
37
+ };
38
+ console.log(JSON.stringify(response));
39
+ }
40
+
41
+ async function main() {
42
+ try {
43
+ const input = await readStdin();
44
+
45
+ if (!input.trim()) {
46
+ respond(true);
47
+ process.exit(0);
48
+ }
49
+
50
+ if (!isLoggedIn()) {
51
+ respond(true);
52
+ process.exit(0);
53
+ }
54
+
55
+ const event = JSON.parse(input);
56
+
57
+ // Only capture AfterAgent events with a prompt
58
+ if (event.event_type !== 'AfterAgent' || !event.prompt) {
59
+ respond(true);
60
+ process.exit(0);
61
+ }
62
+
63
+ const content = event.prompt.trim();
64
+ if (!content) {
65
+ respond(true);
66
+ process.exit(0);
67
+ }
68
+
69
+ // Build context
70
+ const context = getFullContext('gemini');
71
+
72
+ // Override with event data if available
73
+ if (event.working_directory) {
74
+ context.working_directory = event.working_directory;
75
+ }
76
+ if (event.session_id) {
77
+ context.session_id = event.session_id;
78
+ }
79
+
80
+ await capturePrompt({
81
+ content,
82
+ ...context
83
+ });
84
+
85
+ respond(true);
86
+ } catch (error) {
87
+ // Still respond successfully to not block Gemini
88
+ respond(true);
89
+ }
90
+ }
91
+
92
+ main();
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@promptcellar/pc",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for PromptCellar - sync prompts between your terminal and the cloud",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
8
- "pc": "bin/pc.js",
9
- "pc-capture": "hooks/prompt-capture.js"
8
+ "pc": "./bin/pc.js",
9
+ "pc-capture": "./hooks/prompt-capture.js",
10
+ "pc-codex-capture": "./hooks/codex-capture.js",
11
+ "pc-gemini-capture": "./hooks/gemini-capture.js"
10
12
  },
11
13
  "scripts": {
12
14
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -38,6 +40,7 @@
38
40
  "ora": "^8.0.0",
39
41
  "inquirer": "^9.2.0",
40
42
  "socket.io-client": "^4.6.0",
41
- "node-fetch": "^3.3.0"
43
+ "node-fetch": "^3.3.0",
44
+ "open": "^10.0.0"
42
45
  }
43
46
  }
@@ -1,56 +1,127 @@
1
- import inquirer from 'inquirer';
2
1
  import ora from 'ora';
3
2
  import chalk from 'chalk';
4
- import { setApiKey, setApiUrl, isLoggedIn } from '../lib/config.js';
5
- import { testConnection } from '../lib/api.js';
3
+ import open from 'open';
4
+ import { setApiKey, setApiUrl, isLoggedIn, getApiUrl } from '../lib/config.js';
6
5
 
7
- export async function login() {
8
- if (isLoggedIn()) {
9
- const { overwrite } = await inquirer.prompt([{
10
- type: 'confirm',
11
- name: 'overwrite',
12
- message: 'Already logged in. Replace existing credentials?',
13
- default: false
14
- }]);
15
-
16
- if (!overwrite) {
17
- console.log('Login cancelled.');
18
- return;
19
- }
6
+ const DEFAULT_API_URL = 'https://prompts.weldedanvil.com';
7
+
8
+ async function initiateDeviceAuth(apiUrl) {
9
+ const response = await fetch(`${apiUrl}/auth/device`, {
10
+ method: 'POST',
11
+ headers: { 'Content-Type': 'application/json' },
12
+ body: JSON.stringify({ device_name: getDeviceName() })
13
+ });
14
+
15
+ if (!response.ok) {
16
+ throw new Error('Failed to initiate device authentication');
20
17
  }
21
18
 
22
- const answers = await inquirer.prompt([
23
- {
24
- type: 'input',
25
- name: 'apiKey',
26
- message: 'Enter your PromptCellar API key:',
27
- validate: (input) => {
28
- if (!input.trim()) return 'API key is required';
29
- if (!input.startsWith('pk_')) return 'Invalid API key format (should start with pk_)';
30
- return true;
31
- }
32
- },
33
- {
34
- type: 'input',
35
- name: 'apiUrl',
36
- message: 'API URL (press enter for default):',
37
- default: 'https://prompts.weldedanvil.com'
19
+ return response.json();
20
+ }
21
+
22
+ async function pollForApproval(apiUrl, deviceCode, interval, expiresIn) {
23
+ const startTime = Date.now();
24
+ const expiresAt = startTime + (expiresIn * 1000);
25
+
26
+ while (Date.now() < expiresAt) {
27
+ await sleep(interval * 1000);
28
+
29
+ const response = await fetch(`${apiUrl}/auth/device/poll`, {
30
+ method: 'POST',
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: JSON.stringify({ device_code: deviceCode })
33
+ });
34
+
35
+ const data = await response.json();
36
+
37
+ if (response.ok && data.access_token) {
38
+ return data;
39
+ }
40
+
41
+ if (data.error === 'authorization_pending') {
42
+ continue;
43
+ }
44
+
45
+ if (data.error === 'expired_token') {
46
+ throw new Error('Authorization request expired. Please try again.');
38
47
  }
39
- ]);
40
48
 
41
- const spinner = ora('Testing connection...').start();
49
+ if (data.error === 'access_denied') {
50
+ throw new Error('Authorization denied.');
51
+ }
52
+ }
53
+
54
+ throw new Error('Authorization request timed out. Please try again.');
55
+ }
42
56
 
43
- setApiKey(answers.apiKey.trim());
44
- setApiUrl(answers.apiUrl.trim());
57
+ function getDeviceName() {
58
+ const os = process.platform;
59
+ const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || 'Unknown';
45
60
 
46
- const result = await testConnection();
61
+ const osNames = {
62
+ darwin: 'macOS',
63
+ linux: 'Linux',
64
+ win32: 'Windows'
65
+ };
66
+
67
+ return `${osNames[os] || os} - ${hostname}`;
68
+ }
69
+
70
+ function sleep(ms) {
71
+ return new Promise(resolve => setTimeout(resolve, ms));
72
+ }
73
+
74
+ export async function login(options) {
75
+ const apiUrl = options?.url || DEFAULT_API_URL;
76
+
77
+ if (isLoggedIn()) {
78
+ console.log(chalk.yellow('Already logged in.'));
79
+ console.log('Run ' + chalk.cyan('pc logout') + ' first to switch accounts.\n');
80
+ return;
81
+ }
82
+
83
+ console.log(chalk.bold('\nPromptCellar CLI Login\n'));
84
+
85
+ const spinner = ora('Initiating login...').start();
86
+
87
+ try {
88
+ // Step 1: Request device code
89
+ const authData = await initiateDeviceAuth(apiUrl);
90
+ spinner.stop();
91
+
92
+ // Step 2: Show code and open browser
93
+ console.log(chalk.bold('Your verification code:\n'));
94
+ console.log(chalk.cyan.bold(` ${authData.user_code}\n`));
95
+ console.log('Opening your browser to authorize this device...\n');
96
+ console.log(chalk.dim(`If the browser doesn't open, visit: ${authData.verification_url}\n`));
97
+
98
+ // Open browser with the code pre-filled
99
+ const verifyUrl = `${authData.verification_url}?code=${authData.user_code}`;
100
+ await open(verifyUrl).catch(() => {
101
+ // Browser failed to open, user will have to do it manually
102
+ });
103
+
104
+ // Step 3: Poll for approval
105
+ spinner.start('Waiting for authorization...');
106
+
107
+ const result = await pollForApproval(
108
+ apiUrl,
109
+ authData.device_code,
110
+ authData.interval,
111
+ authData.expires_in
112
+ );
113
+
114
+ // Step 4: Save credentials
115
+ setApiKey(result.access_token);
116
+ setApiUrl(apiUrl);
47
117
 
48
- if (result.success) {
49
118
  spinner.succeed(chalk.green('Logged in successfully!'));
50
- console.log('\nRun ' + chalk.cyan('pc setup') + ' to configure auto-capture for your CLI tools.');
51
- } else {
52
- spinner.fail(chalk.red('Connection failed: ' + result.error));
53
- console.log('Check your API key and try again.');
119
+ console.log(`\nWelcome, ${chalk.cyan(result.user_email)}!`);
120
+ console.log('\nRun ' + chalk.cyan('pc setup') + ' to configure auto-capture for your CLI tools.\n');
121
+
122
+ } catch (error) {
123
+ spinner.fail(chalk.red('Login failed: ' + error.message));
124
+ console.log('\nPlease try again or visit ' + chalk.cyan(apiUrl) + ' to sign up.\n');
54
125
  }
55
126
  }
56
127
 
@@ -3,11 +3,42 @@ import chalk from 'chalk';
3
3
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
5
  import { homedir } from 'os';
6
+ import { execFileSync } from 'child_process';
6
7
 
8
+ // Config paths
7
9
  const CLAUDE_HOOKS_PATH = join(homedir(), '.claude', 'hooks.json');
10
+ const CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
11
+ const GEMINI_SETTINGS_PATH = join(homedir(), '.gemini', 'settings.json');
12
+
8
13
  const HOOK_SCRIPT_NAME = 'pc-capture';
9
14
 
10
- function getHooksConfig() {
15
+ // Detect which tools are installed
16
+ function detectInstalledTools() {
17
+ const tools = [];
18
+
19
+ // Check for Claude Code
20
+ try {
21
+ execFileSync('which', ['claude'], { stdio: 'pipe' });
22
+ tools.push('claude');
23
+ } catch { /* not installed */ }
24
+
25
+ // Check for Codex CLI
26
+ try {
27
+ execFileSync('which', ['codex'], { stdio: 'pipe' });
28
+ tools.push('codex');
29
+ } catch { /* not installed */ }
30
+
31
+ // Check for Gemini CLI
32
+ try {
33
+ execFileSync('which', ['gemini'], { stdio: 'pipe' });
34
+ tools.push('gemini');
35
+ } catch { /* not installed */ }
36
+
37
+ return tools;
38
+ }
39
+
40
+ // Claude Code hooks
41
+ function getClaudeHooksConfig() {
11
42
  if (existsSync(CLAUDE_HOOKS_PATH)) {
12
43
  try {
13
44
  return JSON.parse(readFileSync(CLAUDE_HOOKS_PATH, 'utf8'));
@@ -18,7 +49,7 @@ function getHooksConfig() {
18
49
  return {};
19
50
  }
20
51
 
21
- function saveHooksConfig(config) {
52
+ function saveClaudeHooksConfig(config) {
22
53
  const dir = dirname(CLAUDE_HOOKS_PATH);
23
54
  if (!existsSync(dir)) {
24
55
  mkdirSync(dir, { recursive: true });
@@ -26,20 +57,85 @@ function saveHooksConfig(config) {
26
57
  writeFileSync(CLAUDE_HOOKS_PATH, JSON.stringify(config, null, 2));
27
58
  }
28
59
 
29
- function isHookInstalled(config) {
60
+ function isClaudeHookInstalled(config) {
30
61
  const stopHooks = config.Stop || [];
31
62
  return stopHooks.some(hook =>
32
63
  hook.command && hook.command.includes(HOOK_SCRIPT_NAME)
33
64
  );
34
65
  }
35
66
 
67
+ // Codex config (TOML)
68
+ function getCodexConfig() {
69
+ if (existsSync(CODEX_CONFIG_PATH)) {
70
+ try {
71
+ return readFileSync(CODEX_CONFIG_PATH, 'utf8');
72
+ } catch {
73
+ return '';
74
+ }
75
+ }
76
+ return '';
77
+ }
78
+
79
+ function isCodexHookInstalled(config) {
80
+ return config.includes('pc-codex-capture');
81
+ }
82
+
83
+ function saveCodexConfig(content) {
84
+ const dir = dirname(CODEX_CONFIG_PATH);
85
+ if (!existsSync(dir)) {
86
+ mkdirSync(dir, { recursive: true });
87
+ }
88
+ writeFileSync(CODEX_CONFIG_PATH, content);
89
+ }
90
+
91
+ // Gemini settings (JSON)
92
+ function getGeminiSettings() {
93
+ if (existsSync(GEMINI_SETTINGS_PATH)) {
94
+ try {
95
+ return JSON.parse(readFileSync(GEMINI_SETTINGS_PATH, 'utf8'));
96
+ } catch {
97
+ return {};
98
+ }
99
+ }
100
+ return {};
101
+ }
102
+
103
+ function isGeminiHookInstalled(settings) {
104
+ const hooks = settings.hooks?.AfterAgent || [];
105
+ return hooks.some(h =>
106
+ h.hooks?.some(hook => hook.command?.includes('pc-gemini-capture'))
107
+ );
108
+ }
109
+
110
+ function saveGeminiSettings(settings) {
111
+ const dir = dirname(GEMINI_SETTINGS_PATH);
112
+ if (!existsSync(dir)) {
113
+ mkdirSync(dir, { recursive: true });
114
+ }
115
+ writeFileSync(GEMINI_SETTINGS_PATH, JSON.stringify(settings, null, 2));
116
+ }
117
+
36
118
  export async function setup() {
37
119
  console.log(chalk.bold('\nPromptCellar CLI Setup\n'));
38
120
 
121
+ const installedTools = detectInstalledTools();
122
+
39
123
  const tools = [
40
- { name: 'Claude Code', value: 'claude', checked: true },
41
- { name: 'Cursor (coming soon)', value: 'cursor', disabled: true },
42
- { name: 'Windsurf (coming soon)', value: 'windsurf', disabled: true }
124
+ {
125
+ name: `Claude Code${installedTools.includes('claude') ? chalk.green(' (detected)') : ''}`,
126
+ value: 'claude',
127
+ checked: installedTools.includes('claude')
128
+ },
129
+ {
130
+ name: `Codex CLI${installedTools.includes('codex') ? chalk.green(' (detected)') : ''}`,
131
+ value: 'codex',
132
+ checked: installedTools.includes('codex')
133
+ },
134
+ {
135
+ name: `Gemini CLI${installedTools.includes('gemini') ? chalk.green(' (detected)') : ''}`,
136
+ value: 'gemini',
137
+ checked: installedTools.includes('gemini')
138
+ }
43
139
  ];
44
140
 
45
141
  const { selectedTools } = await inquirer.prompt([{
@@ -57,6 +153,10 @@ export async function setup() {
57
153
  for (const tool of selectedTools) {
58
154
  if (tool === 'claude') {
59
155
  await setupClaudeCode();
156
+ } else if (tool === 'codex') {
157
+ await setupCodex();
158
+ } else if (tool === 'gemini') {
159
+ await setupGemini();
60
160
  }
61
161
  }
62
162
 
@@ -67,9 +167,9 @@ export async function setup() {
67
167
  async function setupClaudeCode() {
68
168
  console.log(chalk.cyan('\nConfiguring Claude Code...'));
69
169
 
70
- const config = getHooksConfig();
170
+ const config = getClaudeHooksConfig();
71
171
 
72
- if (isHookInstalled(config)) {
172
+ if (isClaudeHookInstalled(config)) {
73
173
  console.log(chalk.yellow(' Hook already installed.'));
74
174
 
75
175
  const { reinstall } = await inquirer.prompt([{
@@ -99,26 +199,150 @@ async function setupClaudeCode() {
99
199
  description: 'Capture prompts to PromptCellar'
100
200
  });
101
201
 
102
- saveHooksConfig(config);
202
+ saveClaudeHooksConfig(config);
203
+ console.log(chalk.green(' Hook installed successfully.'));
204
+ }
205
+
206
+ async function setupCodex() {
207
+ console.log(chalk.cyan('\nConfiguring Codex CLI...'));
208
+
209
+ let config = getCodexConfig();
210
+
211
+ if (isCodexHookInstalled(config)) {
212
+ console.log(chalk.yellow(' Hook already installed.'));
213
+
214
+ const { reinstall } = await inquirer.prompt([{
215
+ type: 'confirm',
216
+ name: 'reinstall',
217
+ message: 'Reinstall the hook?',
218
+ default: false
219
+ }]);
220
+
221
+ if (!reinstall) {
222
+ return;
223
+ }
224
+
225
+ // Remove existing notify line
226
+ config = config.split('\n')
227
+ .filter(line => !line.includes('pc-codex-capture'))
228
+ .join('\n');
229
+ }
230
+
231
+ // Add the notify hook
232
+ const notifyLine = 'notify = ["pc-codex-capture"]';
233
+
234
+ if (config.includes('notify')) {
235
+ // Replace existing notify
236
+ config = config.replace(/notify\s*=\s*\[.*\]/, notifyLine);
237
+ } else {
238
+ // Add new notify line
239
+ config = config.trim() + '\n\n# PromptCellar capture hook\n' + notifyLine + '\n';
240
+ }
241
+
242
+ saveCodexConfig(config);
243
+ console.log(chalk.green(' Hook installed successfully.'));
244
+ }
245
+
246
+ async function setupGemini() {
247
+ console.log(chalk.cyan('\nConfiguring Gemini CLI...'));
248
+
249
+ const settings = getGeminiSettings();
250
+
251
+ if (isGeminiHookInstalled(settings)) {
252
+ console.log(chalk.yellow(' Hook already installed.'));
253
+
254
+ const { reinstall } = await inquirer.prompt([{
255
+ type: 'confirm',
256
+ name: 'reinstall',
257
+ message: 'Reinstall the hook?',
258
+ default: false
259
+ }]);
260
+
261
+ if (!reinstall) {
262
+ return;
263
+ }
264
+
265
+ // Remove existing hook
266
+ if (settings.hooks?.AfterAgent) {
267
+ settings.hooks.AfterAgent = settings.hooks.AfterAgent.filter(h =>
268
+ !h.hooks?.some(hook => hook.command?.includes('pc-gemini-capture'))
269
+ );
270
+ }
271
+ }
272
+
273
+ // Ensure hooks are enabled
274
+ if (!settings.hooksConfig) {
275
+ settings.hooksConfig = {};
276
+ }
277
+ settings.hooksConfig.enabled = true;
278
+
279
+ // Add the AfterAgent hook
280
+ if (!settings.hooks) {
281
+ settings.hooks = {};
282
+ }
283
+ if (!settings.hooks.AfterAgent) {
284
+ settings.hooks.AfterAgent = [];
285
+ }
286
+
287
+ settings.hooks.AfterAgent.push({
288
+ matcher: '*',
289
+ hooks: [{
290
+ name: 'promptcellar-capture',
291
+ type: 'command',
292
+ command: 'pc-gemini-capture',
293
+ timeout: 5000
294
+ }]
295
+ });
296
+
297
+ saveGeminiSettings(settings);
103
298
  console.log(chalk.green(' Hook installed successfully.'));
104
299
  }
105
300
 
106
301
  export async function unsetup() {
107
302
  console.log(chalk.bold('\nRemoving PromptCellar hooks...\n'));
108
303
 
109
- const config = getHooksConfig();
304
+ let removed = false;
110
305
 
111
- if (!isHookInstalled(config)) {
112
- console.log(chalk.yellow('No hooks installed.'));
113
- return;
306
+ // Remove Claude hook
307
+ const claudeConfig = getClaudeHooksConfig();
308
+ if (isClaudeHookInstalled(claudeConfig)) {
309
+ claudeConfig.Stop = (claudeConfig.Stop || []).filter(hook =>
310
+ !hook.command || !hook.command.includes(HOOK_SCRIPT_NAME)
311
+ );
312
+ saveClaudeHooksConfig(claudeConfig);
313
+ console.log(chalk.green(' Removed Claude Code hook.'));
314
+ removed = true;
114
315
  }
115
316
 
116
- config.Stop = (config.Stop || []).filter(hook =>
117
- !hook.command || !hook.command.includes(HOOK_SCRIPT_NAME)
118
- );
317
+ // Remove Codex hook
318
+ let codexConfig = getCodexConfig();
319
+ if (isCodexHookInstalled(codexConfig)) {
320
+ codexConfig = codexConfig.split('\n')
321
+ .filter(line => !line.includes('pc-codex-capture') && !line.includes('# PromptCellar'))
322
+ .join('\n');
323
+ saveCodexConfig(codexConfig);
324
+ console.log(chalk.green(' Removed Codex CLI hook.'));
325
+ removed = true;
326
+ }
327
+
328
+ // Remove Gemini hook
329
+ const geminiSettings = getGeminiSettings();
330
+ if (isGeminiHookInstalled(geminiSettings)) {
331
+ if (geminiSettings.hooks?.AfterAgent) {
332
+ geminiSettings.hooks.AfterAgent = geminiSettings.hooks.AfterAgent.filter(h =>
333
+ !h.hooks?.some(hook => hook.command?.includes('pc-gemini-capture'))
334
+ );
335
+ }
336
+ saveGeminiSettings(geminiSettings);
337
+ console.log(chalk.green(' Removed Gemini CLI hook.'));
338
+ removed = true;
339
+ }
119
340
 
120
- saveHooksConfig(config);
121
- console.log(chalk.green('Hooks removed successfully.'));
341
+ if (!removed) {
342
+ console.log(chalk.yellow('No hooks installed.'));
343
+ } else {
344
+ console.log(chalk.green('\nHooks removed successfully.'));
345
+ }
122
346
  }
123
347
 
124
348
  export default setup;