@promptcellar/pc 0.6.0 → 0.7.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/hooks/amazonq-capture.js +33 -0
- package/hooks/codex-capture.js +16 -58
- package/hooks/copilot-capture.js +33 -0
- package/hooks/cursor-capture.js +32 -0
- package/hooks/gemini-capture.js +10 -74
- package/hooks/lib/capture-pipeline.js +81 -0
- package/hooks/prompt-capture.js +9 -66
- package/hooks/windsurf-capture.js +33 -0
- package/package.json +11 -3
- package/src/commands/setup.js +256 -242
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Amazon Q Developer CLI hook for capturing prompts to Prompt Cellar.
|
|
5
|
+
*
|
|
6
|
+
* Amazon Q userPromptSubmit hooks receive JSON via stdin with
|
|
7
|
+
* prompt and session context.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readStdin, runCapture } from './lib/capture-pipeline.js';
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
try {
|
|
14
|
+
const input = await readStdin();
|
|
15
|
+
if (!input.trim()) process.exit(0);
|
|
16
|
+
|
|
17
|
+
const event = JSON.parse(input);
|
|
18
|
+
const prompt = event.prompt?.trim();
|
|
19
|
+
if (!prompt) process.exit(0);
|
|
20
|
+
|
|
21
|
+
await runCapture({
|
|
22
|
+
toolName: 'amazon-q',
|
|
23
|
+
prompt,
|
|
24
|
+
cwd: event.cwd,
|
|
25
|
+
sessionId: event.session_id,
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
// Fail silently
|
|
29
|
+
}
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main();
|
package/hooks/codex-capture.js
CHANGED
|
@@ -15,11 +15,8 @@
|
|
|
15
15
|
* state tracking to avoid duplicates.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
import { getFullContext, collectContextFileContents } from '../src/lib/context.js';
|
|
20
|
-
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
18
|
+
import { checkAuth, getVaultKey, buildContext, buildContextFiles, submitCapture } from './lib/capture-pipeline.js';
|
|
21
19
|
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
22
|
-
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
23
20
|
import { getLastCapturedIndex, saveLastCapturedIndex, cleanupStaleThreads } from '../src/lib/state.js';
|
|
24
21
|
|
|
25
22
|
function extractContent(message) {
|
|
@@ -35,96 +32,57 @@ function extractContent(message) {
|
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
async function main() {
|
|
38
|
-
// Codex passes JSON as first argument
|
|
39
35
|
const jsonArg = process.argv[2];
|
|
40
|
-
|
|
41
|
-
if (!
|
|
42
|
-
process.exit(0);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!isLoggedIn()) {
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (!isVaultAvailable()) {
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
36
|
+
if (!jsonArg) process.exit(0);
|
|
37
|
+
if (!checkAuth()) process.exit(0);
|
|
52
38
|
|
|
53
39
|
try {
|
|
54
40
|
const event = JSON.parse(jsonArg);
|
|
55
|
-
|
|
56
|
-
// Only capture on agent-turn-complete
|
|
57
|
-
if (event.type !== 'agent-turn-complete') {
|
|
58
|
-
process.exit(0);
|
|
59
|
-
}
|
|
41
|
+
if (event.type !== 'agent-turn-complete') process.exit(0);
|
|
60
42
|
|
|
61
43
|
const inputMessages = event['input-messages'] || [];
|
|
62
|
-
if (inputMessages.length === 0) {
|
|
63
|
-
process.exit(0);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
44
|
const threadId = event['thread-id'];
|
|
67
|
-
if (!threadId)
|
|
68
|
-
process.exit(0);
|
|
69
|
-
}
|
|
45
|
+
if (!inputMessages.length || !threadId) process.exit(0);
|
|
70
46
|
|
|
71
|
-
// Clean up stale threads periodically
|
|
72
47
|
cleanupStaleThreads();
|
|
73
48
|
|
|
74
|
-
// Get last captured index to avoid duplicates
|
|
75
49
|
const lastIndex = getLastCapturedIndex(threadId);
|
|
76
50
|
const newMessages = inputMessages.slice(lastIndex);
|
|
51
|
+
if (!newMessages.length) process.exit(0);
|
|
77
52
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
53
|
+
const vaultKey = await getVaultKey();
|
|
54
|
+
if (!vaultKey) process.exit(0);
|
|
81
55
|
|
|
82
|
-
const
|
|
83
|
-
if (!vaultKey) {
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
56
|
+
const effectiveCwd = event.cwd || process.cwd();
|
|
86
57
|
|
|
87
|
-
//
|
|
58
|
+
// Encrypt context files once for all messages in this batch
|
|
88
59
|
let encrypted_context_files, context_files_iv;
|
|
89
|
-
const contextFileContents =
|
|
60
|
+
const contextFileContents = buildContextFiles('codex', effectiveCwd);
|
|
90
61
|
if (contextFileContents) {
|
|
91
62
|
const enc = encryptPrompt(JSON.stringify(contextFileContents), vaultKey);
|
|
92
63
|
encrypted_context_files = enc.encrypted_content;
|
|
93
64
|
context_files_iv = enc.content_iv;
|
|
94
65
|
}
|
|
95
66
|
|
|
96
|
-
// Capture each new message
|
|
97
67
|
for (const message of newMessages) {
|
|
98
68
|
const content = extractContent(message);
|
|
99
|
-
if (!content
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
69
|
+
if (!content?.trim()) continue;
|
|
102
70
|
|
|
103
71
|
const promptText = content.trim();
|
|
104
|
-
const context =
|
|
105
|
-
cwd:
|
|
72
|
+
const context = buildContext('codex', {
|
|
73
|
+
cwd: effectiveCwd,
|
|
106
74
|
sessionId: threadId,
|
|
107
75
|
promptText,
|
|
108
76
|
});
|
|
109
77
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
await capturePrompt({
|
|
113
|
-
...context,
|
|
114
|
-
encrypted_content,
|
|
115
|
-
content_iv,
|
|
116
|
-
encrypted_context_files,
|
|
117
|
-
context_files_iv,
|
|
118
|
-
});
|
|
78
|
+
await submitCapture({ context, prompt: promptText, vaultKey, encrypted_context_files, context_files_iv });
|
|
119
79
|
}
|
|
120
80
|
|
|
121
|
-
// Update state with new index
|
|
122
81
|
saveLastCapturedIndex(threadId, inputMessages.length);
|
|
123
|
-
|
|
124
82
|
} catch {
|
|
125
83
|
// Fail silently to not disrupt Codex
|
|
126
|
-
process.exit(0);
|
|
127
84
|
}
|
|
85
|
+
process.exit(0);
|
|
128
86
|
}
|
|
129
87
|
|
|
130
88
|
main();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitHub Copilot CLI hook for capturing prompts to Prompt Cellar.
|
|
5
|
+
*
|
|
6
|
+
* Copilot userPromptSubmitted hooks receive JSON via stdin with
|
|
7
|
+
* prompt and session context.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readStdin, runCapture } from './lib/capture-pipeline.js';
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
try {
|
|
14
|
+
const input = await readStdin();
|
|
15
|
+
if (!input.trim()) process.exit(0);
|
|
16
|
+
|
|
17
|
+
const event = JSON.parse(input);
|
|
18
|
+
const prompt = event.prompt?.trim();
|
|
19
|
+
if (!prompt) process.exit(0);
|
|
20
|
+
|
|
21
|
+
await runCapture({
|
|
22
|
+
toolName: 'copilot',
|
|
23
|
+
prompt,
|
|
24
|
+
cwd: event.cwd,
|
|
25
|
+
sessionId: event.session_id,
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
// Fail silently
|
|
29
|
+
}
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cursor IDE hook for capturing prompts to Prompt Cellar.
|
|
5
|
+
*
|
|
6
|
+
* Cursor hooks receive JSON via stdin with prompt and session context.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readStdin, runCapture } from './lib/capture-pipeline.js';
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
try {
|
|
13
|
+
const input = await readStdin();
|
|
14
|
+
if (!input.trim()) process.exit(0);
|
|
15
|
+
|
|
16
|
+
const event = JSON.parse(input);
|
|
17
|
+
const prompt = event.prompt?.trim();
|
|
18
|
+
if (!prompt) process.exit(0);
|
|
19
|
+
|
|
20
|
+
await runCapture({
|
|
21
|
+
toolName: 'cursor',
|
|
22
|
+
prompt,
|
|
23
|
+
cwd: event.cwd,
|
|
24
|
+
sessionId: event.session_id,
|
|
25
|
+
});
|
|
26
|
+
} catch {
|
|
27
|
+
// Fail silently
|
|
28
|
+
}
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main();
|
package/hooks/gemini-capture.js
CHANGED
|
@@ -15,97 +15,33 @@
|
|
|
15
15
|
* Return {} for pass-through, or { decision: "block", reason: "..." } to block.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
19
|
-
import { getFullContext, collectContextFileContents } from '../src/lib/context.js';
|
|
20
|
-
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
21
|
-
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
22
|
-
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
23
|
-
|
|
24
|
-
async function readStdin() {
|
|
25
|
-
return new Promise((resolve) => {
|
|
26
|
-
let data = '';
|
|
27
|
-
process.stdin.setEncoding('utf8');
|
|
28
|
-
process.stdin.on('data', chunk => data += chunk);
|
|
29
|
-
process.stdin.on('end', () => resolve(data));
|
|
30
|
-
|
|
31
|
-
// Timeout after 1 second if no input
|
|
32
|
-
setTimeout(() => resolve(data), 1000);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
18
|
+
import { readStdin, runCapture } from './lib/capture-pipeline.js';
|
|
35
19
|
|
|
36
20
|
function respond() {
|
|
37
|
-
// Gemini expects JSON output; empty object means pass-through
|
|
38
21
|
console.log('{}');
|
|
39
22
|
}
|
|
40
23
|
|
|
41
24
|
async function main() {
|
|
42
25
|
try {
|
|
43
26
|
const input = await readStdin();
|
|
44
|
-
|
|
45
|
-
if (!input.trim()) {
|
|
46
|
-
respond();
|
|
47
|
-
process.exit(0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (!isLoggedIn()) {
|
|
51
|
-
respond();
|
|
52
|
-
process.exit(0);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!isVaultAvailable()) {
|
|
56
|
-
respond();
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
27
|
+
if (!input.trim()) { respond(); process.exit(0); }
|
|
59
28
|
|
|
60
29
|
const event = JSON.parse(input);
|
|
30
|
+
const prompt = event.prompt?.trim();
|
|
31
|
+
if (!prompt) { respond(); process.exit(0); }
|
|
61
32
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const content = event.prompt.trim();
|
|
69
|
-
if (!content) {
|
|
70
|
-
respond();
|
|
71
|
-
process.exit(0);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Build context
|
|
75
|
-
const context = getFullContext('gemini', null, {
|
|
76
|
-
cwd: event.cwd || process.cwd(),
|
|
33
|
+
await runCapture({
|
|
34
|
+
toolName: 'gemini',
|
|
35
|
+
prompt,
|
|
36
|
+
cwd: event.cwd,
|
|
77
37
|
sessionId: event.session_id,
|
|
78
|
-
promptText: content,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const vaultKey = await requireVaultKey({ silent: true });
|
|
82
|
-
if (!vaultKey) {
|
|
83
|
-
respond();
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
86
|
-
const { encrypted_content, content_iv } = encryptPrompt(content, vaultKey);
|
|
87
|
-
|
|
88
|
-
let encrypted_context_files, context_files_iv;
|
|
89
|
-
const contextFileContents = collectContextFileContents('gemini', event.cwd || process.cwd());
|
|
90
|
-
if (contextFileContents) {
|
|
91
|
-
const enc = encryptPrompt(JSON.stringify(contextFileContents), vaultKey);
|
|
92
|
-
encrypted_context_files = enc.encrypted_content;
|
|
93
|
-
context_files_iv = enc.content_iv;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
await capturePrompt({
|
|
97
|
-
...context,
|
|
98
|
-
encrypted_content,
|
|
99
|
-
content_iv,
|
|
100
|
-
encrypted_context_files,
|
|
101
|
-
context_files_iv,
|
|
102
38
|
});
|
|
103
39
|
|
|
104
40
|
respond();
|
|
105
|
-
} catch
|
|
106
|
-
// Still respond successfully to not block Gemini
|
|
41
|
+
} catch {
|
|
107
42
|
respond();
|
|
108
43
|
}
|
|
44
|
+
process.exit(0);
|
|
109
45
|
}
|
|
110
46
|
|
|
111
47
|
main();
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared capture pipeline for all hook scripts.
|
|
3
|
+
*
|
|
4
|
+
* Provides the common logic: login/vault validation, context building,
|
|
5
|
+
* encryption, and API capture. Each tool's hook script is a thin adapter
|
|
6
|
+
* that parses input and calls runCapture().
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { capturePrompt } from '../../src/lib/api.js';
|
|
10
|
+
import { getFullContext, collectContextFileContents } from '../../src/lib/context.js';
|
|
11
|
+
import { isLoggedIn, isVaultAvailable } from '../../src/lib/config.js';
|
|
12
|
+
import { encryptPrompt } from '../../src/lib/crypto.js';
|
|
13
|
+
import { requireVaultKey } from '../../src/lib/keychain.js';
|
|
14
|
+
|
|
15
|
+
export async function readStdin(timeoutMs = 1000) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
let data = '';
|
|
18
|
+
const onData = chunk => data += chunk;
|
|
19
|
+
const onEnd = () => { cleanup(); resolve(data); };
|
|
20
|
+
const cleanup = () => {
|
|
21
|
+
process.stdin.removeListener('data', onData);
|
|
22
|
+
process.stdin.removeListener('end', onEnd);
|
|
23
|
+
};
|
|
24
|
+
process.stdin.setEncoding('utf8');
|
|
25
|
+
process.stdin.on('data', onData);
|
|
26
|
+
process.stdin.on('end', onEnd);
|
|
27
|
+
setTimeout(() => { cleanup(); resolve(data); }, timeoutMs);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function checkAuth() {
|
|
32
|
+
return isLoggedIn() && isVaultAvailable();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getVaultKey() {
|
|
36
|
+
return requireVaultKey({ silent: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildContext(toolName, { cwd, sessionId, promptText }) {
|
|
40
|
+
return getFullContext(toolName, null, { cwd, sessionId, promptText });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildContextFiles(toolName, cwd) {
|
|
44
|
+
return collectContextFileContents(toolName, cwd);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function submitCapture({ context, prompt, vaultKey, encrypted_context_files, context_files_iv }) {
|
|
48
|
+
const { encrypted_content, content_iv } = encryptPrompt(prompt, vaultKey);
|
|
49
|
+
await capturePrompt({
|
|
50
|
+
...context,
|
|
51
|
+
encrypted_content,
|
|
52
|
+
content_iv,
|
|
53
|
+
encrypted_context_files,
|
|
54
|
+
context_files_iv,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function runCapture({ toolName, prompt, cwd, sessionId }) {
|
|
59
|
+
if (!checkAuth()) return;
|
|
60
|
+
|
|
61
|
+
const effectiveCwd = cwd || process.cwd();
|
|
62
|
+
|
|
63
|
+
const context = buildContext(toolName, {
|
|
64
|
+
cwd: effectiveCwd,
|
|
65
|
+
sessionId,
|
|
66
|
+
promptText: prompt,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const vaultKey = await getVaultKey();
|
|
70
|
+
if (!vaultKey) return;
|
|
71
|
+
|
|
72
|
+
let encrypted_context_files, context_files_iv;
|
|
73
|
+
const contextFileContents = buildContextFiles(toolName, effectiveCwd);
|
|
74
|
+
if (contextFileContents) {
|
|
75
|
+
const enc = encryptPrompt(JSON.stringify(contextFileContents), vaultKey);
|
|
76
|
+
encrypted_context_files = enc.encrypted_content;
|
|
77
|
+
context_files_iv = enc.content_iv;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await submitCapture({ context, prompt, vaultKey, encrypted_context_files, context_files_iv });
|
|
81
|
+
}
|
package/hooks/prompt-capture.js
CHANGED
|
@@ -10,84 +10,27 @@
|
|
|
10
10
|
* - cwd: working directory
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import { getFullContext, collectContextFileContents } from '../src/lib/context.js';
|
|
15
|
-
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
16
|
-
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
17
|
-
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
18
|
-
|
|
19
|
-
async function readStdin() {
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
let data = '';
|
|
22
|
-
process.stdin.setEncoding('utf8');
|
|
23
|
-
process.stdin.on('data', chunk => data += chunk);
|
|
24
|
-
process.stdin.on('end', () => resolve(data));
|
|
25
|
-
|
|
26
|
-
// Timeout after 1 second if no input
|
|
27
|
-
setTimeout(() => resolve(data), 1000);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
13
|
+
import { readStdin, runCapture } from './lib/capture-pipeline.js';
|
|
30
14
|
|
|
31
15
|
async function main() {
|
|
32
16
|
try {
|
|
33
17
|
const input = await readStdin();
|
|
34
|
-
|
|
35
|
-
if (!input.trim()) {
|
|
36
|
-
process.exit(0);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!isLoggedIn()) {
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (!isVaultAvailable()) {
|
|
44
|
-
process.exit(0);
|
|
45
|
-
}
|
|
18
|
+
if (!input.trim()) process.exit(0);
|
|
46
19
|
|
|
47
20
|
const event = JSON.parse(input);
|
|
21
|
+
const prompt = event.prompt?.trim();
|
|
22
|
+
if (!prompt) process.exit(0);
|
|
48
23
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const promptContent = event.prompt.trim();
|
|
55
|
-
if (!promptContent) {
|
|
56
|
-
process.exit(0);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const context = getFullContext('claude-code', null, {
|
|
60
|
-
cwd: event.cwd || process.cwd(),
|
|
24
|
+
await runCapture({
|
|
25
|
+
toolName: 'claude-code',
|
|
26
|
+
prompt,
|
|
27
|
+
cwd: event.cwd,
|
|
61
28
|
sessionId: event.session_id,
|
|
62
|
-
promptText: promptContent,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const vaultKey = await requireVaultKey({ silent: true });
|
|
66
|
-
if (!vaultKey) {
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
const { encrypted_content, content_iv } = encryptPrompt(promptContent, vaultKey);
|
|
70
|
-
|
|
71
|
-
let encrypted_context_files, context_files_iv;
|
|
72
|
-
const contextFileContents = collectContextFileContents('claude-code', event.cwd || process.cwd());
|
|
73
|
-
if (contextFileContents) {
|
|
74
|
-
const enc = encryptPrompt(JSON.stringify(contextFileContents), vaultKey);
|
|
75
|
-
encrypted_context_files = enc.encrypted_content;
|
|
76
|
-
context_files_iv = enc.content_iv;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await capturePrompt({
|
|
80
|
-
...context,
|
|
81
|
-
encrypted_content,
|
|
82
|
-
content_iv,
|
|
83
|
-
encrypted_context_files,
|
|
84
|
-
context_files_iv,
|
|
85
29
|
});
|
|
86
|
-
|
|
87
30
|
} catch {
|
|
88
31
|
// Fail silently — stderr from hooks can cause issues
|
|
89
|
-
process.exit(0);
|
|
90
32
|
}
|
|
33
|
+
process.exit(0);
|
|
91
34
|
}
|
|
92
35
|
|
|
93
36
|
main();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Windsurf IDE (Codeium) hook for capturing prompts to Prompt Cellar.
|
|
5
|
+
*
|
|
6
|
+
* Windsurf pre_user_prompt hooks receive JSON via stdin with prompt
|
|
7
|
+
* and session context including trajectory_id and execution_id.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readStdin, runCapture } from './lib/capture-pipeline.js';
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
try {
|
|
14
|
+
const input = await readStdin();
|
|
15
|
+
if (!input.trim()) process.exit(0);
|
|
16
|
+
|
|
17
|
+
const event = JSON.parse(input);
|
|
18
|
+
const prompt = event.prompt?.trim();
|
|
19
|
+
if (!prompt) process.exit(0);
|
|
20
|
+
|
|
21
|
+
await runCapture({
|
|
22
|
+
toolName: 'windsurf',
|
|
23
|
+
prompt,
|
|
24
|
+
cwd: event.cwd,
|
|
25
|
+
sessionId: event.session_id || event.trajectory_id,
|
|
26
|
+
});
|
|
27
|
+
} catch {
|
|
28
|
+
// Fail silently
|
|
29
|
+
}
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptcellar/pc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI for Prompt Cellar - sync prompts between your terminal and the cloud",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
"pc": "bin/pc.js",
|
|
9
9
|
"pc-capture": "hooks/prompt-capture.js",
|
|
10
10
|
"pc-codex-capture": "hooks/codex-capture.js",
|
|
11
|
-
"pc-gemini-capture": "hooks/gemini-capture.js"
|
|
11
|
+
"pc-gemini-capture": "hooks/gemini-capture.js",
|
|
12
|
+
"pc-cursor-capture": "hooks/cursor-capture.js",
|
|
13
|
+
"pc-windsurf-capture": "hooks/windsurf-capture.js",
|
|
14
|
+
"pc-copilot-capture": "hooks/copilot-capture.js",
|
|
15
|
+
"pc-amazonq-capture": "hooks/amazonq-capture.js"
|
|
12
16
|
},
|
|
13
17
|
"scripts": {
|
|
14
18
|
"test": "node --test"
|
|
@@ -21,7 +25,11 @@
|
|
|
21
25
|
"llm",
|
|
22
26
|
"claude",
|
|
23
27
|
"codex",
|
|
24
|
-
"gemini"
|
|
28
|
+
"gemini",
|
|
29
|
+
"cursor",
|
|
30
|
+
"windsurf",
|
|
31
|
+
"copilot",
|
|
32
|
+
"amazon-q"
|
|
25
33
|
],
|
|
26
34
|
"author": "Welded Anvil Technologies LLC",
|
|
27
35
|
"license": "MIT",
|
package/src/commands/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
@@ -9,152 +9,138 @@ import { execFileSync } from 'child_process';
|
|
|
9
9
|
const CLAUDE_SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
10
10
|
const CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
|
|
11
11
|
const GEMINI_SETTINGS_PATH = join(homedir(), '.gemini', 'settings.json');
|
|
12
|
+
const CURSOR_HOOKS_PATH = join(homedir(), '.cursor', 'hooks.json');
|
|
13
|
+
const WINDSURF_HOOKS_PATH = join(homedir(), '.codeium', 'windsurf', 'hooks.json');
|
|
14
|
+
const COPILOT_HOOKS_DIR = join(homedir(), '.copilot', 'hooks');
|
|
15
|
+
const COPILOT_HOOK_FILE = join(COPILOT_HOOKS_DIR, 'promptcellar-capture.json');
|
|
16
|
+
const AMAZONQ_AGENTS_DIR = join(homedir(), '.aws', 'amazonq', 'cli-agents');
|
|
17
|
+
const AMAZONQ_AGENT_FILE = join(AMAZONQ_AGENTS_DIR, 'promptcellar.json');
|
|
18
|
+
|
|
19
|
+
// Hook script names
|
|
20
|
+
const HOOK_SCRIPTS = {
|
|
21
|
+
claude: 'pc-capture',
|
|
22
|
+
codex: 'pc-codex-capture',
|
|
23
|
+
gemini: 'pc-gemini-capture',
|
|
24
|
+
cursor: 'pc-cursor-capture',
|
|
25
|
+
windsurf: 'pc-windsurf-capture',
|
|
26
|
+
copilot: 'pc-copilot-capture',
|
|
27
|
+
'amazon-q': 'pc-amazonq-capture',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// --- Shared helpers ---
|
|
31
|
+
|
|
32
|
+
function readJsonConfig(path) {
|
|
33
|
+
if (existsSync(path)) {
|
|
34
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
12
38
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Check for Claude Code
|
|
20
|
-
try {
|
|
21
|
-
execFileSync('which', ['claude'], { stdio: 'pipe' });
|
|
22
|
-
tools.push('claude');
|
|
23
|
-
} catch { /* not installed */ }
|
|
39
|
+
function writeConfigFile(filePath, content) {
|
|
40
|
+
const dir = dirname(filePath);
|
|
41
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
42
|
+
writeFileSync(filePath, typeof content === 'string' ? content : JSON.stringify(content, null, 2));
|
|
43
|
+
}
|
|
24
44
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
async function confirmReinstall() {
|
|
46
|
+
const { reinstall } = await inquirer.prompt([{
|
|
47
|
+
type: 'confirm',
|
|
48
|
+
name: 'reinstall',
|
|
49
|
+
message: 'Reinstall the hook?',
|
|
50
|
+
default: false
|
|
51
|
+
}]);
|
|
52
|
+
return reinstall;
|
|
53
|
+
}
|
|
30
54
|
|
|
31
|
-
|
|
55
|
+
function isCommandAvailable(cmd) {
|
|
32
56
|
try {
|
|
33
|
-
execFileSync('which', [
|
|
34
|
-
|
|
35
|
-
} catch {
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
execFileSync('which', [cmd], { stdio: 'pipe' });
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
38
62
|
}
|
|
39
63
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
return JSON.parse(readFileSync(CLAUDE_SETTINGS_PATH, 'utf8'));
|
|
45
|
-
} catch {
|
|
46
|
-
return {};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return {};
|
|
64
|
+
function hasJsonHook(config, eventKey, scriptName) {
|
|
65
|
+
const matchers = config.hooks?.[eventKey] || [];
|
|
66
|
+
return matchers.some(m => m.hooks?.some(h => h.command?.includes(scriptName)));
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
function removeJsonHook(config, eventKey, scriptName) {
|
|
70
|
+
if (config.hooks?.[eventKey]) {
|
|
71
|
+
config.hooks[eventKey] = config.hooks[eventKey].filter(m =>
|
|
72
|
+
!m.hooks?.some(h => h.command?.includes(scriptName))
|
|
73
|
+
);
|
|
56
74
|
}
|
|
57
|
-
writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
58
75
|
}
|
|
59
76
|
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
77
|
+
function addJsonHook(config, eventKey, hookEntry) {
|
|
78
|
+
if (!config.hooks) config.hooks = {};
|
|
79
|
+
if (!config.hooks[eventKey]) config.hooks[eventKey] = [];
|
|
80
|
+
config.hooks[eventKey].push(hookEntry);
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
//
|
|
68
|
-
function hasLegacyClaudeHook(settings) {
|
|
69
|
-
const stopMatchers = settings.hooks?.Stop || [];
|
|
70
|
-
return stopMatchers.some(matcher =>
|
|
71
|
-
matcher.hooks?.some(hook => hook.command?.includes(HOOK_SCRIPT_NAME))
|
|
72
|
-
);
|
|
73
|
-
}
|
|
83
|
+
// --- Tool detection ---
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
function detectInstalledTools() {
|
|
86
|
+
const tools = [];
|
|
87
|
+
const simpleBinaries = [
|
|
88
|
+
{ binary: 'claude', key: 'claude' },
|
|
89
|
+
{ binary: 'codex', key: 'codex' },
|
|
90
|
+
{ binary: 'gemini', key: 'gemini' },
|
|
91
|
+
{ binary: 'cursor', key: 'cursor' },
|
|
92
|
+
{ binary: 'windsurf', key: 'windsurf' },
|
|
93
|
+
{ binary: 'q', key: 'amazon-q' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
for (const { binary, key } of simpleBinaries) {
|
|
97
|
+
if (isCommandAvailable(binary)) tools.push(key);
|
|
81
98
|
}
|
|
82
|
-
}
|
|
83
99
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
return readFileSync(CODEX_CONFIG_PATH, 'utf8');
|
|
89
|
-
} catch {
|
|
90
|
-
return '';
|
|
91
|
-
}
|
|
100
|
+
// Copilot: gh binary + ~/.copilot directory
|
|
101
|
+
if (isCommandAvailable('gh') && existsSync(join(homedir(), '.copilot'))) {
|
|
102
|
+
tools.push('copilot');
|
|
92
103
|
}
|
|
93
|
-
return '';
|
|
94
|
-
}
|
|
95
104
|
|
|
96
|
-
|
|
97
|
-
return config.includes('pc-codex-capture');
|
|
105
|
+
return tools;
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
|
|
101
|
-
const dir = dirname(CODEX_CONFIG_PATH);
|
|
102
|
-
if (!existsSync(dir)) {
|
|
103
|
-
mkdirSync(dir, { recursive: true });
|
|
104
|
-
}
|
|
105
|
-
writeFileSync(CODEX_CONFIG_PATH, content);
|
|
106
|
-
}
|
|
108
|
+
// --- Codex (TOML — special case) ---
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
return JSON.parse(readFileSync(GEMINI_SETTINGS_PATH, 'utf8'));
|
|
113
|
-
} catch {
|
|
114
|
-
return {};
|
|
115
|
-
}
|
|
110
|
+
function getCodexConfig() {
|
|
111
|
+
if (existsSync(CODEX_CONFIG_PATH)) {
|
|
112
|
+
try { return readFileSync(CODEX_CONFIG_PATH, 'utf8'); } catch { return ''; }
|
|
116
113
|
}
|
|
117
|
-
return
|
|
114
|
+
return '';
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
return hooks.some(h =>
|
|
123
|
-
h.hooks?.some(hook => hook.command?.includes('pc-gemini-capture'))
|
|
124
|
-
);
|
|
117
|
+
function isCodexHookInstalled(config) {
|
|
118
|
+
return config.includes(HOOK_SCRIPTS.codex);
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
|
|
128
|
-
const dir = dirname(GEMINI_SETTINGS_PATH);
|
|
129
|
-
if (!existsSync(dir)) {
|
|
130
|
-
mkdirSync(dir, { recursive: true });
|
|
131
|
-
}
|
|
132
|
-
writeFileSync(GEMINI_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
133
|
-
}
|
|
121
|
+
// --- Setup functions ---
|
|
134
122
|
|
|
135
123
|
export async function setup() {
|
|
136
124
|
console.log(chalk.bold('\nPrompt Cellar CLI Setup\n'));
|
|
137
125
|
|
|
138
126
|
const installedTools = detectInstalledTools();
|
|
139
127
|
|
|
140
|
-
const
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
value: 'codex',
|
|
149
|
-
checked: installedTools.includes('codex')
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
name: `Gemini CLI${installedTools.includes('gemini') ? chalk.green(' (detected)') : ''}`,
|
|
153
|
-
value: 'gemini',
|
|
154
|
-
checked: installedTools.includes('gemini')
|
|
155
|
-
}
|
|
128
|
+
const toolDefs = [
|
|
129
|
+
{ label: 'Claude Code', value: 'claude' },
|
|
130
|
+
{ label: 'Codex CLI', value: 'codex' },
|
|
131
|
+
{ label: 'Gemini CLI', value: 'gemini' },
|
|
132
|
+
{ label: 'Cursor', value: 'cursor' },
|
|
133
|
+
{ label: 'Windsurf', value: 'windsurf' },
|
|
134
|
+
{ label: 'GitHub Copilot CLI', value: 'copilot' },
|
|
135
|
+
{ label: 'Amazon Q Developer', value: 'amazon-q' },
|
|
156
136
|
];
|
|
157
137
|
|
|
138
|
+
const tools = toolDefs.map(t => ({
|
|
139
|
+
name: `${t.label}${installedTools.includes(t.value) ? chalk.green(' (detected)') : ''}`,
|
|
140
|
+
value: t.value,
|
|
141
|
+
checked: installedTools.includes(t.value),
|
|
142
|
+
}));
|
|
143
|
+
|
|
158
144
|
const { selectedTools } = await inquirer.prompt([{
|
|
159
145
|
type: 'checkbox',
|
|
160
146
|
name: 'selectedTools',
|
|
@@ -167,14 +153,19 @@ export async function setup() {
|
|
|
167
153
|
return;
|
|
168
154
|
}
|
|
169
155
|
|
|
156
|
+
const setupFns = {
|
|
157
|
+
claude: setupClaudeCode,
|
|
158
|
+
codex: setupCodex,
|
|
159
|
+
gemini: setupGemini,
|
|
160
|
+
cursor: setupCursor,
|
|
161
|
+
windsurf: setupWindsurf,
|
|
162
|
+
copilot: setupCopilot,
|
|
163
|
+
'amazon-q': setupAmazonQ,
|
|
164
|
+
};
|
|
165
|
+
|
|
170
166
|
for (const tool of selectedTools) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
} else if (tool === 'codex') {
|
|
174
|
-
await setupCodex();
|
|
175
|
-
} else if (tool === 'gemini') {
|
|
176
|
-
await setupGemini();
|
|
177
|
-
}
|
|
167
|
+
const fn = setupFns[tool];
|
|
168
|
+
if (fn) await fn();
|
|
178
169
|
}
|
|
179
170
|
|
|
180
171
|
console.log(chalk.green('\nSetup complete!'));
|
|
@@ -184,59 +175,33 @@ export async function setup() {
|
|
|
184
175
|
async function setupClaudeCode() {
|
|
185
176
|
console.log(chalk.cyan('\nConfiguring Claude Code...'));
|
|
186
177
|
|
|
187
|
-
const settings =
|
|
188
|
-
const
|
|
189
|
-
const
|
|
178
|
+
const settings = readJsonConfig(CLAUDE_SETTINGS_PATH);
|
|
179
|
+
const script = HOOK_SCRIPTS.claude;
|
|
180
|
+
const hasLegacy = hasJsonHook(settings, 'Stop', script);
|
|
181
|
+
const hasCurrent = hasJsonHook(settings, 'UserPromptSubmit', script);
|
|
190
182
|
|
|
191
183
|
if (hasCurrent) {
|
|
192
184
|
console.log(chalk.yellow(' Hook already installed.'));
|
|
193
|
-
|
|
194
|
-
const { reinstall } = await inquirer.prompt([{
|
|
195
|
-
type: 'confirm',
|
|
196
|
-
name: 'reinstall',
|
|
197
|
-
message: 'Reinstall the hook?',
|
|
198
|
-
default: false
|
|
199
|
-
}]);
|
|
200
|
-
|
|
201
|
-
if (!reinstall) {
|
|
202
|
-
// Still remove legacy hook if present
|
|
185
|
+
if (!await confirmReinstall()) {
|
|
203
186
|
if (hasLegacy) {
|
|
204
|
-
|
|
205
|
-
|
|
187
|
+
removeJsonHook(settings, 'Stop', script);
|
|
188
|
+
writeConfigFile(CLAUDE_SETTINGS_PATH, settings);
|
|
206
189
|
console.log(chalk.green(' Removed legacy Stop hook.'));
|
|
207
190
|
}
|
|
208
191
|
return;
|
|
209
192
|
}
|
|
210
|
-
|
|
211
|
-
// Remove existing hook matchers that contain our hook
|
|
212
|
-
settings.hooks.UserPromptSubmit = (settings.hooks.UserPromptSubmit || []).filter(matcher =>
|
|
213
|
-
!matcher.hooks?.some(hook => hook.command?.includes(HOOK_SCRIPT_NAME))
|
|
214
|
-
);
|
|
193
|
+
removeJsonHook(settings, 'UserPromptSubmit', script);
|
|
215
194
|
} else if (hasLegacy) {
|
|
216
195
|
console.log(chalk.yellow(' Migrating from legacy Stop hook to UserPromptSubmit...'));
|
|
217
196
|
}
|
|
218
197
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// Ensure hooks.UserPromptSubmit exists
|
|
223
|
-
if (!settings.hooks) {
|
|
224
|
-
settings.hooks = {};
|
|
225
|
-
}
|
|
226
|
-
if (!settings.hooks.UserPromptSubmit) {
|
|
227
|
-
settings.hooks.UserPromptSubmit = [];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Add the UserPromptSubmit hook
|
|
231
|
-
settings.hooks.UserPromptSubmit.push({
|
|
198
|
+
removeJsonHook(settings, 'Stop', script);
|
|
199
|
+
addJsonHook(settings, 'UserPromptSubmit', {
|
|
232
200
|
matcher: '*',
|
|
233
|
-
hooks: [{
|
|
234
|
-
type: 'command',
|
|
235
|
-
command: 'pc-capture'
|
|
236
|
-
}]
|
|
201
|
+
hooks: [{ type: 'command', command: script }]
|
|
237
202
|
});
|
|
238
203
|
|
|
239
|
-
|
|
204
|
+
writeConfigFile(CLAUDE_SETTINGS_PATH, settings);
|
|
240
205
|
console.log(chalk.green(' Hook installed successfully.'));
|
|
241
206
|
}
|
|
242
207
|
|
|
@@ -244,35 +209,22 @@ async function setupCodex() {
|
|
|
244
209
|
console.log(chalk.cyan('\nConfiguring Codex CLI...'));
|
|
245
210
|
|
|
246
211
|
let config = getCodexConfig();
|
|
212
|
+
const script = HOOK_SCRIPTS.codex;
|
|
247
213
|
|
|
248
214
|
if (isCodexHookInstalled(config)) {
|
|
249
215
|
console.log(chalk.yellow(' Hook already installed.'));
|
|
216
|
+
if (!await confirmReinstall()) return;
|
|
250
217
|
|
|
251
|
-
const { reinstall } = await inquirer.prompt([{
|
|
252
|
-
type: 'confirm',
|
|
253
|
-
name: 'reinstall',
|
|
254
|
-
message: 'Reinstall the hook?',
|
|
255
|
-
default: false
|
|
256
|
-
}]);
|
|
257
|
-
|
|
258
|
-
if (!reinstall) {
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Remove existing notify line and comment
|
|
263
218
|
config = config.split('\n')
|
|
264
|
-
.filter(line => !line.includes(
|
|
219
|
+
.filter(line => !line.includes(script) && !line.includes('# Prompt Cellar capture hook'))
|
|
265
220
|
.join('\n');
|
|
266
221
|
}
|
|
267
222
|
|
|
268
|
-
|
|
269
|
-
const notifyLine = 'notify = ["pc-codex-capture"]';
|
|
223
|
+
const notifyLine = `notify = ["${script}"]`;
|
|
270
224
|
|
|
271
225
|
if (config.match(/^notify\s*=/m)) {
|
|
272
|
-
// Replace existing root-level notify
|
|
273
226
|
config = config.replace(/^notify\s*=.*$/m, notifyLine);
|
|
274
227
|
} else {
|
|
275
|
-
// Insert at root level — before the first [table] section
|
|
276
228
|
const firstTableMatch = config.match(/^\[/m);
|
|
277
229
|
if (firstTableMatch) {
|
|
278
230
|
const insertPos = firstTableMatch.index;
|
|
@@ -280,112 +232,174 @@ async function setupCodex() {
|
|
|
280
232
|
const after = config.slice(insertPos);
|
|
281
233
|
config = before + '\n\n# Prompt Cellar capture hook\n' + notifyLine + '\n\n' + after;
|
|
282
234
|
} else {
|
|
283
|
-
// No table sections — safe to append
|
|
284
235
|
config = config.trimEnd() + '\n\n# Prompt Cellar capture hook\n' + notifyLine + '\n';
|
|
285
236
|
}
|
|
286
237
|
}
|
|
287
238
|
|
|
288
|
-
|
|
239
|
+
writeConfigFile(CODEX_CONFIG_PATH, config);
|
|
289
240
|
console.log(chalk.green(' Hook installed successfully.'));
|
|
290
241
|
}
|
|
291
242
|
|
|
292
|
-
async function
|
|
293
|
-
console.log(chalk.cyan(
|
|
243
|
+
async function setupJsonHookTool({ label, configPath, eventKey, hookEntry, scriptName }) {
|
|
244
|
+
console.log(chalk.cyan(`\nConfiguring ${label}...`));
|
|
294
245
|
|
|
295
|
-
const
|
|
246
|
+
const config = readJsonConfig(configPath);
|
|
296
247
|
|
|
297
|
-
if (
|
|
248
|
+
if (hasJsonHook(config, eventKey, scriptName)) {
|
|
298
249
|
console.log(chalk.yellow(' Hook already installed.'));
|
|
250
|
+
if (!await confirmReinstall()) return;
|
|
251
|
+
removeJsonHook(config, eventKey, scriptName);
|
|
252
|
+
}
|
|
299
253
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
default: false
|
|
305
|
-
}]);
|
|
254
|
+
addJsonHook(config, eventKey, hookEntry);
|
|
255
|
+
writeConfigFile(configPath, config);
|
|
256
|
+
console.log(chalk.green(' Hook installed successfully.'));
|
|
257
|
+
}
|
|
306
258
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
259
|
+
async function setupGemini() {
|
|
260
|
+
return setupJsonHookTool({
|
|
261
|
+
label: 'Gemini CLI',
|
|
262
|
+
configPath: GEMINI_SETTINGS_PATH,
|
|
263
|
+
eventKey: 'BeforeAgent',
|
|
264
|
+
scriptName: HOOK_SCRIPTS.gemini,
|
|
265
|
+
hookEntry: {
|
|
266
|
+
matcher: '*',
|
|
267
|
+
hooks: [{
|
|
268
|
+
name: 'promptcellar-capture',
|
|
269
|
+
type: 'command',
|
|
270
|
+
command: HOOK_SCRIPTS.gemini,
|
|
271
|
+
timeout: 5000
|
|
272
|
+
}]
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
310
276
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
277
|
+
async function setupCursor() {
|
|
278
|
+
return setupJsonHookTool({
|
|
279
|
+
label: 'Cursor',
|
|
280
|
+
configPath: CURSOR_HOOKS_PATH,
|
|
281
|
+
eventKey: 'UserPromptSubmit',
|
|
282
|
+
scriptName: HOOK_SCRIPTS.cursor,
|
|
283
|
+
hookEntry: {
|
|
284
|
+
matcher: '*',
|
|
285
|
+
hooks: [{ type: 'command', command: HOOK_SCRIPTS.cursor }]
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
318
289
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
290
|
+
async function setupWindsurf() {
|
|
291
|
+
return setupJsonHookTool({
|
|
292
|
+
label: 'Windsurf',
|
|
293
|
+
configPath: WINDSURF_HOOKS_PATH,
|
|
294
|
+
eventKey: 'pre_user_prompt',
|
|
295
|
+
scriptName: HOOK_SCRIPTS.windsurf,
|
|
296
|
+
hookEntry: {
|
|
297
|
+
matcher: '*',
|
|
298
|
+
hooks: [{ type: 'command', command: HOOK_SCRIPTS.windsurf }]
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function setupFileTool({ label, filePath, content }) {
|
|
304
|
+
console.log(chalk.cyan(`\nConfiguring ${label}...`));
|
|
305
|
+
|
|
306
|
+
if (existsSync(filePath)) {
|
|
307
|
+
console.log(chalk.yellow(' Hook already installed.'));
|
|
308
|
+
if (!await confirmReinstall()) return;
|
|
325
309
|
}
|
|
326
310
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
311
|
+
writeConfigFile(filePath, content);
|
|
312
|
+
console.log(chalk.green(' Hook installed successfully.'));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function setupCopilot() {
|
|
316
|
+
return setupFileTool({
|
|
317
|
+
label: 'GitHub Copilot CLI',
|
|
318
|
+
filePath: COPILOT_HOOK_FILE,
|
|
319
|
+
content: {
|
|
330
320
|
name: 'promptcellar-capture',
|
|
331
|
-
|
|
332
|
-
command:
|
|
321
|
+
event: 'userPromptSubmitted',
|
|
322
|
+
command: HOOK_SCRIPTS.copilot,
|
|
333
323
|
timeout: 5000
|
|
334
|
-
}
|
|
324
|
+
},
|
|
335
325
|
});
|
|
326
|
+
}
|
|
336
327
|
|
|
337
|
-
|
|
338
|
-
|
|
328
|
+
async function setupAmazonQ() {
|
|
329
|
+
return setupFileTool({
|
|
330
|
+
label: 'Amazon Q Developer',
|
|
331
|
+
filePath: AMAZONQ_AGENT_FILE,
|
|
332
|
+
content: {
|
|
333
|
+
name: 'promptcellar',
|
|
334
|
+
hooks: {
|
|
335
|
+
userPromptSubmit: [{
|
|
336
|
+
command: HOOK_SCRIPTS['amazon-q'],
|
|
337
|
+
timeout_ms: 5000
|
|
338
|
+
}]
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
});
|
|
339
342
|
}
|
|
340
343
|
|
|
344
|
+
// --- Unsetup ---
|
|
345
|
+
|
|
341
346
|
export async function unsetup() {
|
|
342
347
|
console.log(chalk.bold('\nRemoving Prompt Cellar hooks...\n'));
|
|
343
348
|
|
|
344
349
|
let removed = false;
|
|
345
350
|
|
|
346
|
-
//
|
|
347
|
-
const claudeSettings =
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
claudeSettings.hooks.UserPromptSubmit = (claudeSettings.hooks.UserPromptSubmit || []).filter(matcher =>
|
|
354
|
-
!matcher.hooks?.some(hook => hook.command?.includes(HOOK_SCRIPT_NAME))
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
if (hasLegacy) {
|
|
358
|
-
removeLegacyClaudeHook(claudeSettings);
|
|
359
|
-
}
|
|
360
|
-
saveClaudeSettings(claudeSettings);
|
|
351
|
+
// Claude (both current and legacy)
|
|
352
|
+
const claudeSettings = readJsonConfig(CLAUDE_SETTINGS_PATH);
|
|
353
|
+
const claudeScript = HOOK_SCRIPTS.claude;
|
|
354
|
+
if (hasJsonHook(claudeSettings, 'UserPromptSubmit', claudeScript) || hasJsonHook(claudeSettings, 'Stop', claudeScript)) {
|
|
355
|
+
removeJsonHook(claudeSettings, 'UserPromptSubmit', claudeScript);
|
|
356
|
+
removeJsonHook(claudeSettings, 'Stop', claudeScript);
|
|
357
|
+
writeConfigFile(CLAUDE_SETTINGS_PATH, claudeSettings);
|
|
361
358
|
console.log(chalk.green(' Removed Claude Code hook.'));
|
|
362
359
|
removed = true;
|
|
363
360
|
}
|
|
364
361
|
|
|
365
|
-
//
|
|
362
|
+
// Codex
|
|
366
363
|
let codexConfig = getCodexConfig();
|
|
367
364
|
if (isCodexHookInstalled(codexConfig)) {
|
|
368
365
|
codexConfig = codexConfig.split('\n')
|
|
369
|
-
.filter(line => !line.includes(
|
|
370
|
-
.join('\n')
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
saveCodexConfig(codexConfig);
|
|
366
|
+
.filter(line => !line.includes(HOOK_SCRIPTS.codex) && !line.includes('# Prompt Cellar capture hook'))
|
|
367
|
+
.join('\n')
|
|
368
|
+
.replace(/\n{3,}/g, '\n\n');
|
|
369
|
+
writeConfigFile(CODEX_CONFIG_PATH, codexConfig);
|
|
374
370
|
console.log(chalk.green(' Removed Codex CLI hook.'));
|
|
375
371
|
removed = true;
|
|
376
372
|
}
|
|
377
373
|
|
|
378
|
-
//
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
374
|
+
// JSON hook tools (Gemini, Cursor, Windsurf)
|
|
375
|
+
const jsonHookTools = [
|
|
376
|
+
{ label: 'Gemini CLI', path: GEMINI_SETTINGS_PATH, eventKey: 'BeforeAgent', key: 'gemini' },
|
|
377
|
+
{ label: 'Cursor', path: CURSOR_HOOKS_PATH, eventKey: 'UserPromptSubmit', key: 'cursor' },
|
|
378
|
+
{ label: 'Windsurf', path: WINDSURF_HOOKS_PATH, eventKey: 'pre_user_prompt', key: 'windsurf' },
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
for (const { label, path, eventKey, key } of jsonHookTools) {
|
|
382
|
+
const config = readJsonConfig(path);
|
|
383
|
+
if (hasJsonHook(config, eventKey, HOOK_SCRIPTS[key])) {
|
|
384
|
+
removeJsonHook(config, eventKey, HOOK_SCRIPTS[key]);
|
|
385
|
+
writeConfigFile(path, config);
|
|
386
|
+
console.log(chalk.green(` Removed ${label} hook.`));
|
|
387
|
+
removed = true;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// File-based tools (Copilot, Amazon Q)
|
|
392
|
+
const fileTools = [
|
|
393
|
+
{ label: 'GitHub Copilot CLI', path: COPILOT_HOOK_FILE },
|
|
394
|
+
{ label: 'Amazon Q Developer', path: AMAZONQ_AGENT_FILE },
|
|
395
|
+
];
|
|
396
|
+
|
|
397
|
+
for (const { label, path } of fileTools) {
|
|
398
|
+
if (existsSync(path)) {
|
|
399
|
+
try { unlinkSync(path); } catch { /* already removed */ }
|
|
400
|
+
console.log(chalk.green(` Removed ${label} hook.`));
|
|
401
|
+
removed = true;
|
|
385
402
|
}
|
|
386
|
-
saveGeminiSettings(geminiSettings);
|
|
387
|
-
console.log(chalk.green(' Removed Gemini CLI hook.'));
|
|
388
|
-
removed = true;
|
|
389
403
|
}
|
|
390
404
|
|
|
391
405
|
if (!removed) {
|