@promptcellar/pc 0.5.4 → 0.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.
@@ -1,501 +0,0 @@
1
- # Multi-Prompt Capture Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Capture all user prompts in real-time (not just the first), grouped by session ID.
6
-
7
- **Architecture:** Switch Claude Code from Stop hook to UserPromptSubmit hook. Add deduplication state tracking for Codex CLI. Gemini already fires per-prompt, just verify session handling.
8
-
9
- **Tech Stack:** Node.js, ES modules, Conf for state storage
10
-
11
- ---
12
-
13
- ## Task 1: Create State Management Utility
14
-
15
- **Files:**
16
- - Create: `src/lib/state.js`
17
-
18
- **Step 1: Create the state utility file**
19
-
20
- ```javascript
21
- import Conf from 'conf';
22
-
23
- const state = new Conf({
24
- projectName: 'promptcellar',
25
- configName: 'capture-state',
26
- schema: {
27
- threads: {
28
- type: 'object',
29
- default: {}
30
- }
31
- }
32
- });
33
-
34
- const MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
35
-
36
- /**
37
- * Get the last captured index for a thread.
38
- * @param {string} threadId
39
- * @returns {number}
40
- */
41
- export function getLastCapturedIndex(threadId) {
42
- const threads = state.get('threads');
43
- return threads[threadId]?.lastIndex || 0;
44
- }
45
-
46
- /**
47
- * Save the last captured index for a thread.
48
- * @param {string} threadId
49
- * @param {number} lastIndex
50
- */
51
- export function saveLastCapturedIndex(threadId, lastIndex) {
52
- const threads = state.get('threads');
53
- threads[threadId] = {
54
- lastIndex,
55
- updatedAt: Date.now()
56
- };
57
- state.set('threads', threads);
58
- }
59
-
60
- /**
61
- * Clean up stale thread entries older than maxAgeMs.
62
- * @param {number} maxAgeMs - Default 24 hours
63
- */
64
- export function cleanupStaleThreads(maxAgeMs = MAX_AGE_MS) {
65
- const threads = state.get('threads');
66
- const now = Date.now();
67
- let changed = false;
68
-
69
- for (const [threadId, data] of Object.entries(threads)) {
70
- if (now - data.updatedAt > maxAgeMs) {
71
- delete threads[threadId];
72
- changed = true;
73
- }
74
- }
75
-
76
- if (changed) {
77
- state.set('threads', threads);
78
- }
79
- }
80
-
81
- export default { getLastCapturedIndex, saveLastCapturedIndex, cleanupStaleThreads };
82
- ```
83
-
84
- **Step 2: Commit**
85
-
86
- ```bash
87
- git add src/lib/state.js
88
- git commit -m "feat: add state management for capture deduplication"
89
- ```
90
-
91
- ---
92
-
93
- ## Task 2: Rewrite Claude Code Hook for UserPromptSubmit
94
-
95
- **Files:**
96
- - Modify: `hooks/prompt-capture.js`
97
-
98
- **Step 1: Rewrite the hook for UserPromptSubmit**
99
-
100
- Replace entire file with:
101
-
102
- ```javascript
103
- #!/usr/bin/env node
104
-
105
- /**
106
- * Claude Code UserPromptSubmit hook for capturing prompts to PromptCellar.
107
- *
108
- * This script is called by Claude Code on every user prompt submission.
109
- * UserPromptSubmit hooks receive JSON via stdin with:
110
- * - session_id: session identifier
111
- * - prompt: the user's prompt text
112
- * - cwd: working directory
113
- */
114
-
115
- import { capturePrompt } from '../src/lib/api.js';
116
- import { getFullContext } from '../src/lib/context.js';
117
- import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
118
- import { encryptPrompt } from '../src/lib/crypto.js';
119
- import { requireVaultKey } from '../src/lib/keychain.js';
120
-
121
- async function readStdin() {
122
- return new Promise((resolve) => {
123
- let data = '';
124
- process.stdin.setEncoding('utf8');
125
- process.stdin.on('data', chunk => data += chunk);
126
- process.stdin.on('end', () => resolve(data));
127
-
128
- // Timeout after 1 second if no input
129
- setTimeout(() => resolve(data), 1000);
130
- });
131
- }
132
-
133
- async function main() {
134
- try {
135
- const input = await readStdin();
136
-
137
- if (!input.trim()) {
138
- process.exit(0);
139
- }
140
-
141
- if (!isLoggedIn()) {
142
- process.exit(0);
143
- }
144
-
145
- if (!isVaultAvailable()) {
146
- process.exit(0);
147
- }
148
-
149
- const event = JSON.parse(input);
150
-
151
- // UserPromptSubmit provides the prompt directly
152
- if (!event.prompt) {
153
- process.exit(0);
154
- }
155
-
156
- const promptContent = event.prompt.trim();
157
- if (!promptContent) {
158
- process.exit(0);
159
- }
160
-
161
- const context = getFullContext('claude-code');
162
-
163
- // Override with event data if available
164
- if (event.cwd) {
165
- context.working_directory = event.cwd;
166
- }
167
- if (event.session_id) {
168
- context.session_id = event.session_id;
169
- }
170
-
171
- const vaultKey = await requireVaultKey({ silent: true });
172
- if (!vaultKey) {
173
- process.exit(0);
174
- }
175
- const { encrypted_content, content_iv } = encryptPrompt(promptContent, vaultKey);
176
-
177
- await capturePrompt({
178
- ...context,
179
- encrypted_content,
180
- content_iv
181
- });
182
-
183
- } catch {
184
- // Fail silently — stderr from hooks can cause issues
185
- process.exit(0);
186
- }
187
- }
188
-
189
- main();
190
- ```
191
-
192
- **Step 2: Commit**
193
-
194
- ```bash
195
- git add hooks/prompt-capture.js
196
- git commit -m "feat: rewrite Claude hook for UserPromptSubmit (real-time capture)"
197
- ```
198
-
199
- ---
200
-
201
- ## Task 3: Update Codex Hook for Multi-Prompt Capture
202
-
203
- **Files:**
204
- - Modify: `hooks/codex-capture.js`
205
-
206
- **Step 1: Update the hook with deduplication**
207
-
208
- Replace entire file with:
209
-
210
- ```javascript
211
- #!/usr/bin/env node
212
-
213
- /**
214
- * Codex CLI notify hook for capturing prompts to PromptCellar.
215
- *
216
- * Codex calls this script with a JSON argument containing:
217
- * - type: event type (e.g., 'agent-turn-complete')
218
- * - thread-id: session identifier
219
- * - turn-id: turn identifier
220
- * - cwd: working directory
221
- * - input-messages: array of user messages (accumulates over session)
222
- * - last-assistant-message: final assistant response
223
- *
224
- * This hook captures ALL new user messages since last capture using
225
- * state tracking to avoid duplicates.
226
- */
227
-
228
- import { capturePrompt } from '../src/lib/api.js';
229
- import { getFullContext } from '../src/lib/context.js';
230
- import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
231
- import { encryptPrompt } from '../src/lib/crypto.js';
232
- import { requireVaultKey } from '../src/lib/keychain.js';
233
- import { getLastCapturedIndex, saveLastCapturedIndex, cleanupStaleThreads } from '../src/lib/state.js';
234
-
235
- function extractContent(message) {
236
- if (typeof message === 'string') {
237
- return message;
238
- }
239
- if (message.content) {
240
- return Array.isArray(message.content)
241
- ? message.content.filter(c => c.type === 'text').map(c => c.text).join('\n')
242
- : message.content;
243
- }
244
- return null;
245
- }
246
-
247
- async function main() {
248
- // Codex passes JSON as first argument
249
- const jsonArg = process.argv[2];
250
-
251
- if (!jsonArg) {
252
- process.exit(0);
253
- }
254
-
255
- if (!isLoggedIn()) {
256
- process.exit(0);
257
- }
258
-
259
- if (!isVaultAvailable()) {
260
- process.exit(0);
261
- }
262
-
263
- try {
264
- const event = JSON.parse(jsonArg);
265
-
266
- // Only capture on agent-turn-complete
267
- if (event.type !== 'agent-turn-complete') {
268
- process.exit(0);
269
- }
270
-
271
- const inputMessages = event['input-messages'] || [];
272
- if (inputMessages.length === 0) {
273
- process.exit(0);
274
- }
275
-
276
- const threadId = event['thread-id'];
277
- if (!threadId) {
278
- process.exit(0);
279
- }
280
-
281
- // Clean up stale threads periodically
282
- cleanupStaleThreads();
283
-
284
- // Get last captured index to avoid duplicates
285
- const lastIndex = getLastCapturedIndex(threadId);
286
- const newMessages = inputMessages.slice(lastIndex);
287
-
288
- if (newMessages.length === 0) {
289
- process.exit(0);
290
- }
291
-
292
- const vaultKey = await requireVaultKey({ silent: true });
293
- if (!vaultKey) {
294
- process.exit(0);
295
- }
296
-
297
- // Build base context once
298
- const baseContext = getFullContext('codex');
299
- if (event.cwd) {
300
- baseContext.working_directory = event.cwd;
301
- }
302
- baseContext.session_id = threadId;
303
-
304
- // Capture each new message
305
- for (const message of newMessages) {
306
- const content = extractContent(message);
307
- if (!content || !content.trim()) {
308
- continue;
309
- }
310
-
311
- const { encrypted_content, content_iv } = encryptPrompt(content.trim(), vaultKey);
312
-
313
- await capturePrompt({
314
- ...baseContext,
315
- encrypted_content,
316
- content_iv
317
- });
318
- }
319
-
320
- // Update state with new index
321
- saveLastCapturedIndex(threadId, inputMessages.length);
322
-
323
- } catch {
324
- // Fail silently to not disrupt Codex
325
- process.exit(0);
326
- }
327
- }
328
-
329
- main();
330
- ```
331
-
332
- **Step 2: Commit**
333
-
334
- ```bash
335
- git add hooks/codex-capture.js
336
- git commit -m "feat: update Codex hook to capture all prompts with deduplication"
337
- ```
338
-
339
- ---
340
-
341
- ## Task 4: Verify Gemini Hook Session Handling
342
-
343
- **Files:**
344
- - Modify: `hooks/gemini-capture.js` (minor update)
345
-
346
- **Step 1: Review and verify session_id handling**
347
-
348
- The Gemini hook already captures per-prompt via BeforeAgent. Verify that session_id is properly included. The current implementation at lines 81-83 already handles this:
349
-
350
- ```javascript
351
- if (event.session_id) {
352
- context.session_id = event.session_id;
353
- }
354
- ```
355
-
356
- No changes needed - the hook already works correctly for multi-prompt capture.
357
-
358
- **Step 2: Commit (documentation only if no changes)**
359
-
360
- No commit needed - Gemini hook already supports multi-prompt capture.
361
-
362
- ---
363
-
364
- ## Task 5: Update Setup Command for UserPromptSubmit Hook
365
-
366
- **Files:**
367
- - Modify: `src/commands/setup.js`
368
-
369
- **Step 1: Update Claude Code hook configuration**
370
-
371
- Change the `setupClaudeCode` function to use `UserPromptSubmit` instead of `Stop`.
372
-
373
- Find and replace in `setupClaudeCode` function (around lines 167-210):
374
-
375
- Change line 61-64 (isClaudeHookInstalled):
376
- ```javascript
377
- function isClaudeHookInstalled(settings) {
378
- const matchers = settings.hooks?.UserPromptSubmit || [];
379
- return matchers.some(matcher =>
380
- matcher.hooks?.some(hook => hook.command?.includes(HOOK_SCRIPT_NAME))
381
- );
382
- }
383
- ```
384
-
385
- Change lines 187-189 (remove existing hook):
386
- ```javascript
387
- // Remove existing hook matchers that contain our hook
388
- settings.hooks.UserPromptSubmit = (settings.hooks.UserPromptSubmit || []).filter(matcher =>
389
- !matcher.hooks?.some(hook => hook.command?.includes(HOOK_SCRIPT_NAME))
390
- );
391
- ```
392
-
393
- Change lines 193-206 (add new hook):
394
- ```javascript
395
- // Ensure hooks.UserPromptSubmit exists
396
- if (!settings.hooks) {
397
- settings.hooks = {};
398
- }
399
- if (!settings.hooks.UserPromptSubmit) {
400
- settings.hooks.UserPromptSubmit = [];
401
- }
402
-
403
- // Add the UserPromptSubmit hook
404
- settings.hooks.UserPromptSubmit.push({
405
- matcher: '*',
406
- hooks: [{
407
- type: 'command',
408
- command: 'pc-capture'
409
- }]
410
- });
411
- ```
412
-
413
- **Step 2: Update unsetup function**
414
-
415
- Change lines 317-324 (unsetup Claude hook):
416
- ```javascript
417
- // Remove Claude hook
418
- const claudeSettings = getClaudeSettings();
419
- if (isClaudeHookInstalled(claudeSettings)) {
420
- claudeSettings.hooks.UserPromptSubmit = (claudeSettings.hooks.UserPromptSubmit || []).filter(matcher =>
421
- !matcher.hooks?.some(hook => hook.command?.includes(HOOK_SCRIPT_NAME))
422
- );
423
- saveClaudeSettings(claudeSettings);
424
- console.log(chalk.green(' Removed Claude Code hook.'));
425
- removed = true;
426
- }
427
- ```
428
-
429
- **Step 3: Commit**
430
-
431
- ```bash
432
- git add src/commands/setup.js
433
- git commit -m "feat: update setup to use UserPromptSubmit hook for Claude Code"
434
- ```
435
-
436
- ---
437
-
438
- ## Task 6: Bump Version and Final Commit
439
-
440
- **Files:**
441
- - Modify: `package.json`
442
-
443
- **Step 1: Bump version to 0.5.0**
444
-
445
- This is a feature release (captures all prompts instead of just first).
446
-
447
- Change line 3:
448
- ```json
449
- "version": "0.5.0",
450
- ```
451
-
452
- **Step 2: Commit version bump**
453
-
454
- ```bash
455
- git add package.json
456
- git commit -m "chore: bump version to 0.5.0"
457
- ```
458
-
459
- ---
460
-
461
- ## Task 7: Test the Implementation
462
-
463
- **Step 1: Verify state.js loads correctly**
464
-
465
- ```bash
466
- node -e "import('./src/lib/state.js').then(m => console.log('state.js OK'))"
467
- ```
468
-
469
- Expected: `state.js OK`
470
-
471
- **Step 2: Verify hooks load correctly**
472
-
473
- ```bash
474
- node -e "import('./hooks/prompt-capture.js').catch(() => console.log('prompt-capture.js OK'))"
475
- node -e "import('./hooks/codex-capture.js').catch(() => console.log('codex-capture.js OK'))"
476
- node -e "import('./hooks/gemini-capture.js').catch(() => console.log('gemini-capture.js OK'))"
477
- ```
478
-
479
- Expected: Each prints OK (the catch is because they exit on no input)
480
-
481
- **Step 3: Verify setup command loads**
482
-
483
- ```bash
484
- node -e "import('./src/commands/setup.js').then(m => console.log('setup.js OK'))"
485
- ```
486
-
487
- Expected: `setup.js OK`
488
-
489
- ---
490
-
491
- ## Summary
492
-
493
- | Task | Description |
494
- |------|-------------|
495
- | 1 | Create `src/lib/state.js` for Codex deduplication |
496
- | 2 | Rewrite `hooks/prompt-capture.js` for UserPromptSubmit |
497
- | 3 | Update `hooks/codex-capture.js` with deduplication |
498
- | 4 | Verify `hooks/gemini-capture.js` (no changes needed) |
499
- | 5 | Update `src/commands/setup.js` for new hook type |
500
- | 6 | Bump version to 0.5.0 |
501
- | 7 | Test all modules load correctly |