@promptcellar/pc 0.3.3 → 0.4.1

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.
@@ -14,7 +14,9 @@
14
14
 
15
15
  import { capturePrompt } from '../src/lib/api.js';
16
16
  import { getFullContext } from '../src/lib/context.js';
17
- import { isLoggedIn } from '../src/lib/config.js';
17
+ import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
18
+ import { encryptPrompt } from '../src/lib/crypto.js';
19
+ import { requireVaultKey } from '../src/lib/keychain.js';
18
20
 
19
21
  async function main() {
20
22
  // Codex passes JSON as first argument
@@ -30,6 +32,10 @@ async function main() {
30
32
  process.exit(0);
31
33
  }
32
34
 
35
+ if (!isVaultAvailable()) {
36
+ process.exit(0);
37
+ }
38
+
33
39
  try {
34
40
  const event = JSON.parse(jsonArg);
35
41
 
@@ -80,9 +86,16 @@ async function main() {
80
86
  context.session_id = event['thread-id'];
81
87
  }
82
88
 
89
+ const vaultKey = await requireVaultKey({ silent: true });
90
+ if (!vaultKey) {
91
+ process.exit(0);
92
+ }
93
+ const { encrypted_content, content_iv } = encryptPrompt(content.trim(), vaultKey);
94
+
83
95
  await capturePrompt({
84
- content: content.trim(),
85
- ...context
96
+ ...context,
97
+ encrypted_content,
98
+ content_iv
86
99
  });
87
100
 
88
101
  } catch (error) {
@@ -1,21 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Gemini CLI AfterAgent hook for capturing prompts to PromptCellar.
4
+ * Gemini CLI BeforeAgent hook for capturing prompts to PromptCellar.
5
5
  *
6
6
  * Gemini hooks receive JSON via stdin with:
7
- * - event_type: the hook event (e.g., 'AfterAgent')
7
+ * - hook_event_name: the hook event (e.g., 'BeforeAgent')
8
8
  * - prompt: the user's prompt
9
- * - prompt_response: the agent's response (for AfterAgent)
10
9
  * - session_id: session identifier
11
- * - working_directory: current working directory
10
+ * - cwd: working directory
11
+ * - transcript_path: path to session transcript
12
+ * - timestamp: event timestamp
12
13
  *
13
14
  * The hook must output valid JSON to stdout.
15
+ * Return {} for pass-through, or { decision: "block", reason: "..." } to block.
14
16
  */
15
17
 
16
18
  import { capturePrompt } from '../src/lib/api.js';
17
19
  import { getFullContext } from '../src/lib/context.js';
18
- import { isLoggedIn } from '../src/lib/config.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';
19
23
 
20
24
  async function readStdin() {
21
25
  return new Promise((resolve) => {
@@ -29,13 +33,9 @@ async function readStdin() {
29
33
  });
30
34
  }
31
35
 
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));
36
+ function respond() {
37
+ // Gemini expects JSON output; empty object means pass-through
38
+ console.log('{}');
39
39
  }
40
40
 
41
41
  async function main() {
@@ -43,26 +43,31 @@ async function main() {
43
43
  const input = await readStdin();
44
44
 
45
45
  if (!input.trim()) {
46
- respond(true);
46
+ respond();
47
47
  process.exit(0);
48
48
  }
49
49
 
50
50
  if (!isLoggedIn()) {
51
- respond(true);
51
+ respond();
52
+ process.exit(0);
53
+ }
54
+
55
+ if (!isVaultAvailable()) {
56
+ respond();
52
57
  process.exit(0);
53
58
  }
54
59
 
55
60
  const event = JSON.parse(input);
56
61
 
57
- // Only capture AfterAgent events with a prompt
58
- if (event.event_type !== 'AfterAgent' || !event.prompt) {
59
- respond(true);
62
+ // Extract the user's prompt
63
+ if (!event.prompt) {
64
+ respond();
60
65
  process.exit(0);
61
66
  }
62
67
 
63
68
  const content = event.prompt.trim();
64
69
  if (!content) {
65
- respond(true);
70
+ respond();
66
71
  process.exit(0);
67
72
  }
68
73
 
@@ -70,22 +75,30 @@ async function main() {
70
75
  const context = getFullContext('gemini');
71
76
 
72
77
  // Override with event data if available
73
- if (event.working_directory) {
74
- context.working_directory = event.working_directory;
78
+ if (event.cwd) {
79
+ context.working_directory = event.cwd;
75
80
  }
76
81
  if (event.session_id) {
77
82
  context.session_id = event.session_id;
78
83
  }
79
84
 
85
+ const vaultKey = await requireVaultKey({ silent: true });
86
+ if (!vaultKey) {
87
+ respond();
88
+ process.exit(0);
89
+ }
90
+ const { encrypted_content, content_iv } = encryptPrompt(content, vaultKey);
91
+
80
92
  await capturePrompt({
81
- content,
82
- ...context
93
+ ...context,
94
+ encrypted_content,
95
+ content_iv
83
96
  });
84
97
 
85
- respond(true);
98
+ respond();
86
99
  } catch (error) {
87
100
  // Still respond successfully to not block Gemini
88
- respond(true);
101
+ respond();
89
102
  }
90
103
  }
91
104
 
@@ -4,33 +4,56 @@
4
4
  * Claude Code Stop hook for capturing prompts to PromptCellar.
5
5
  *
6
6
  * This script is called by Claude Code when a session ends.
7
- * It parses the transcript file and extracts user prompts to capture.
7
+ * Claude Code Stop hooks receive JSON via stdin with:
8
+ * - transcript_path: path to the session JSONL transcript
9
+ * - session_id: session identifier
10
+ * - cwd: working directory
11
+ *
12
+ * It reads the transcript file and extracts the initial user prompt to capture.
8
13
  */
9
14
 
10
15
  import { readFileSync, existsSync } from 'fs';
11
16
  import { capturePrompt } from '../src/lib/api.js';
12
17
  import { getFullContext } from '../src/lib/context.js';
13
- import { isLoggedIn } from '../src/lib/config.js';
18
+ import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
19
+ import { encryptPrompt } from '../src/lib/crypto.js';
20
+ import { requireVaultKey } from '../src/lib/keychain.js';
21
+
22
+ async function readStdin() {
23
+ return new Promise((resolve) => {
24
+ let data = '';
25
+ process.stdin.setEncoding('utf8');
26
+ process.stdin.on('data', chunk => data += chunk);
27
+ process.stdin.on('end', () => resolve(data));
28
+
29
+ // Timeout after 1 second if no input
30
+ setTimeout(() => resolve(data), 1000);
31
+ });
32
+ }
14
33
 
15
34
  async function main() {
16
- const transcriptPath = process.argv[2];
35
+ try {
36
+ const input = await readStdin();
17
37
 
18
- if (!transcriptPath) {
19
- console.error('Usage: pc-capture <transcript-file>');
20
- process.exit(1);
21
- }
38
+ if (!input.trim()) {
39
+ process.exit(0);
40
+ }
22
41
 
23
- if (!existsSync(transcriptPath)) {
24
- console.error('Transcript file not found:', transcriptPath);
25
- process.exit(1);
26
- }
42
+ if (!isLoggedIn()) {
43
+ process.exit(0);
44
+ }
27
45
 
28
- if (!isLoggedIn()) {
29
- // Silently exit if not logged in
30
- process.exit(0);
31
- }
46
+ if (!isVaultAvailable()) {
47
+ process.exit(0);
48
+ }
49
+
50
+ const event = JSON.parse(input);
51
+ const transcriptPath = event.transcript_path;
52
+
53
+ if (!transcriptPath || !existsSync(transcriptPath)) {
54
+ process.exit(0);
55
+ }
32
56
 
33
- try {
34
57
  const content = readFileSync(transcriptPath, 'utf8');
35
58
  const lines = content.split('\n').filter(line => line.trim());
36
59
 
@@ -58,18 +81,39 @@ async function main() {
58
81
  const initialPrompt = messages[0];
59
82
  const context = getFullContext('claude-code');
60
83
 
84
+ // Override with event data if available
85
+ if (event.cwd) {
86
+ context.working_directory = event.cwd;
87
+ }
88
+ if (event.session_id) {
89
+ context.session_id = event.session_id;
90
+ }
91
+
92
+ // Content can be a string or an array of content blocks
93
+ let promptContent = initialPrompt.content;
94
+ if (Array.isArray(promptContent)) {
95
+ promptContent = promptContent
96
+ .filter(block => block.type === 'text')
97
+ .map(block => block.text)
98
+ .join('\n');
99
+ }
100
+
101
+ const vaultKey = await requireVaultKey({ silent: true });
102
+ if (!vaultKey) {
103
+ process.exit(0);
104
+ }
105
+ const { encrypted_content, content_iv } = encryptPrompt(promptContent, vaultKey);
106
+
61
107
  await capturePrompt({
62
- content: initialPrompt.content,
63
108
  ...context,
109
+ encrypted_content,
110
+ content_iv,
64
111
  captured_at: initialPrompt.timestamp
65
112
  });
66
113
 
67
- // Optionally capture all prompts if configured
68
- // For now, just capture the initial prompt
69
-
70
- } catch (error) {
71
- console.error('Error capturing prompt:', error.message);
72
- process.exit(1);
114
+ } catch {
115
+ // Fail silently stderr from hooks can cause issues
116
+ process.exit(0);
73
117
  }
74
118
  }
75
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptcellar/pc",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "CLI for PromptCellar - sync prompts between your terminal and the cloud",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -11,7 +11,7 @@
11
11
  "pc-gemini-capture": "hooks/gemini-capture.js"
12
12
  },
13
13
  "scripts": {
14
- "test": "echo \"Error: no test specified\" && exit 1"
14
+ "test": "node --test"
15
15
  },
16
16
  "keywords": [
17
17
  "promptcellar",
@@ -39,7 +39,7 @@
39
39
  "chalk": "^5.3.0",
40
40
  "ora": "^8.0.0",
41
41
  "inquirer": "^9.2.0",
42
- "socket.io-client": "^4.6.0",
43
- "node-fetch": "^3.3.0"
42
+ "keytar": "^7.9.0",
43
+ "socket.io-client": "^4.6.0"
44
44
  }
45
45
  }
@@ -1,68 +1,132 @@
1
+ import { randomBytes } from 'crypto';
1
2
  import ora from 'ora';
2
3
  import chalk from 'chalk';
3
- import { setApiKey, setApiUrl, isLoggedIn } from '../lib/config.js';
4
+ import {
5
+ setApiKey, setApiUrl, setAccountUrl,
6
+ setVaultAvailable,
7
+ isLoggedIn
8
+ } from '../lib/config.js';
9
+ import { generateTransferKeyPair, decryptTransfer } from '../lib/device-transfer.js';
10
+ import { b64urlEncode } from '../lib/crypto.js';
11
+ import { storeVaultKey } from '../lib/keychain.js';
4
12
 
5
13
  const DEFAULT_API_URL = 'https://prompts.weldedanvil.com';
14
+ const DEFAULT_ACCOUNT_URL = 'https://account.weldedanvil.com';
15
+ let clientId = 'prompts-prod';
6
16
 
7
- async function initiateDeviceAuth(apiUrl) {
8
- const response = await fetch(`${apiUrl}/auth/device`, {
17
+ export function setClientId(id) { clientId = id; }
18
+
19
+ export async function requestDeviceCode(accountUrl, transferPublicKey) {
20
+ const payload = { client_id: clientId };
21
+ if (transferPublicKey) {
22
+ payload.transfer_public_key = transferPublicKey;
23
+ }
24
+
25
+ const response = await fetch(`${accountUrl}/device/code`, {
9
26
  method: 'POST',
10
27
  headers: { 'Content-Type': 'application/json' },
11
- body: JSON.stringify({ device_name: getDeviceName() })
28
+ body: JSON.stringify(payload),
12
29
  });
13
30
 
14
31
  if (!response.ok) {
15
- throw new Error('Failed to initiate device authentication');
32
+ const data = await response.json().catch(() => ({}));
33
+ throw new Error(data.error || 'Failed to request device code');
16
34
  }
17
35
 
18
36
  return response.json();
19
37
  }
20
38
 
21
- async function pollForApproval(apiUrl, deviceCode, interval, expiresIn) {
22
- const startTime = Date.now();
23
- const expiresAt = startTime + (expiresIn * 1000);
39
+ async function pollForApproval(accountUrl, deviceCode, expiresIn) {
40
+ const expiresAt = Date.now() + (expiresIn || 600) * 1000;
41
+ const interval = 5000;
24
42
 
25
43
  while (Date.now() < expiresAt) {
26
- await sleep(interval * 1000);
44
+ await sleep(interval);
27
45
 
28
- const response = await fetch(`${apiUrl}/auth/device/poll`, {
46
+ const response = await fetch(`${accountUrl}/device/poll`, {
29
47
  method: 'POST',
30
48
  headers: { 'Content-Type': 'application/json' },
31
- body: JSON.stringify({ device_code: deviceCode })
49
+ body: JSON.stringify({ device_code: deviceCode }),
32
50
  });
33
51
 
34
52
  const data = await response.json();
35
53
 
36
- if (response.ok && data.access_token) {
54
+ if (data.status === 'approved' && data.access_token) {
37
55
  return data;
38
56
  }
39
57
 
40
- if (data.error === 'authorization_pending') {
41
- continue;
42
- }
43
-
44
- if (data.error === 'expired_token') {
58
+ if (data.status === 'expired') {
45
59
  throw new Error('Authorization request expired. Please try again.');
46
60
  }
47
61
 
48
- if (data.error === 'access_denied') {
49
- throw new Error('Authorization denied.');
62
+ if (data.status === 'rejected') {
63
+ const error = data.error || 'unknown';
64
+ if (error.startsWith('vault_missing')) {
65
+ throw new Error(`Vault error: ${error}`);
66
+ }
67
+ if (error === 'missing_transfer_key') {
68
+ throw new Error('Transfer key missing. Please try again.');
69
+ }
70
+ throw new Error(`Authorization rejected: ${error}`);
50
71
  }
72
+
73
+ // status === 'pending' -- keep polling
51
74
  }
52
75
 
53
76
  throw new Error('Authorization request timed out. Please try again.');
54
77
  }
55
78
 
79
+ async function exchangeTokenForApiKey(apiUrl, jwt, deviceName) {
80
+ const response = await fetch(`${apiUrl}/api/v1/auth/device-token`, {
81
+ method: 'POST',
82
+ headers: {
83
+ 'Authorization': `Bearer ${jwt}`,
84
+ 'Content-Type': 'application/json',
85
+ },
86
+ body: JSON.stringify({ device_name: deviceName }),
87
+ });
88
+
89
+ if (!response.ok) {
90
+ const data = await response.json().catch(() => ({}));
91
+ throw new Error(data.error || 'Failed to exchange token');
92
+ }
93
+
94
+ return response.json();
95
+ }
96
+
97
+ export async function finalizeDeviceLogin({
98
+ approval,
99
+ privateKeyPem,
100
+ apiUrl,
101
+ deviceName,
102
+ decryptTransferFn = decryptTransfer,
103
+ storeVaultKeyFn = storeVaultKey,
104
+ exchangeTokenForApiKeyFn = exchangeTokenForApiKey,
105
+ }) {
106
+ if (!approval?.vault_transfer) {
107
+ throw new Error('Vault transfer missing. Please reauthorize.');
108
+ }
109
+
110
+ if (!approval?.access_token) {
111
+ throw new Error('Missing access token. Please reauthorize.');
112
+ }
113
+
114
+ let masterKeyB64;
115
+ if (approval.vault_transfer === 'cli_generate') {
116
+ // No browser extensions available — generate master key locally
117
+ masterKeyB64 = b64urlEncode(randomBytes(32));
118
+ } else {
119
+ masterKeyB64 = decryptTransferFn(approval.vault_transfer, privateKeyPem);
120
+ }
121
+ await storeVaultKeyFn(masterKeyB64);
122
+
123
+ return exchangeTokenForApiKeyFn(apiUrl, approval.access_token, deviceName);
124
+ }
125
+
56
126
  function getDeviceName() {
57
127
  const os = process.platform;
58
128
  const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || 'CLI';
59
-
60
- const osNames = {
61
- darwin: 'macOS',
62
- linux: 'Linux',
63
- win32: 'Windows'
64
- };
65
-
129
+ const osNames = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
66
130
  return `${osNames[os] || os} - ${hostname}`;
67
131
  }
68
132
 
@@ -72,6 +136,8 @@ function sleep(ms) {
72
136
 
73
137
  export async function login(options) {
74
138
  const apiUrl = options?.url || DEFAULT_API_URL;
139
+ const accountUrl = options?.accountUrl || DEFAULT_ACCOUNT_URL;
140
+ if (options?.clientId) setClientId(options.clientId);
75
141
 
76
142
  if (isLoggedIn()) {
77
143
  console.log(chalk.yellow('Already logged in.'));
@@ -84,33 +150,51 @@ export async function login(options) {
84
150
  const spinner = ora('Connecting...').start();
85
151
 
86
152
  try {
87
- // Step 1: Request device code
88
- const authData = await initiateDeviceAuth(apiUrl);
153
+ // Step 1: Request device code from wa-auth
154
+ const { publicKeySpkiB64url, privateKeyPem } = await generateTransferKeyPair();
155
+ const authData = await requestDeviceCode(accountUrl, publicKeySpkiB64url);
89
156
  spinner.stop();
90
157
 
91
- // Step 2: Show login URL and code
92
- const verifyUrl = `${authData.verification_url}?code=${authData.user_code}`;
93
-
158
+ // Step 2: Show login URL
159
+ const code = authData.user_code;
160
+ const displayCode = code.length === 8
161
+ ? `${code.slice(0, 4)}-${code.slice(4)}`.toUpperCase()
162
+ : code.toUpperCase();
163
+ const verifyUrl = `${authData.verification_url}?code=${code}`;
94
164
  console.log('Open this URL in your browser to log in:\n');
95
165
  console.log(chalk.cyan.bold(` ${verifyUrl}\n`));
96
- console.log(chalk.dim(`Or go to ${authData.verification_url} and enter code: ${authData.user_code}\n`));
166
+ console.log(chalk.dim(`Or go to ${authData.verification_url}`));
167
+ console.log(chalk.dim(`and enter code: `) + chalk.white.bold(displayCode) + '\n');
97
168
 
98
169
  // Step 3: Poll for approval
99
170
  spinner.start('Waiting for you to authorize in browser...');
100
-
101
- const result = await pollForApproval(
171
+ const approval = await pollForApproval(accountUrl, authData.device_code, authData.expires_in);
172
+ spinner.text = 'Securing vault key...';
173
+
174
+ // Step 4: Decrypt vault transfer and store key before exchanging JWT
175
+ const deviceName = getDeviceName();
176
+ const result = await finalizeDeviceLogin({
177
+ approval,
178
+ privateKeyPem,
102
179
  apiUrl,
103
- authData.device_code,
104
- authData.interval,
105
- authData.expires_in
106
- );
180
+ deviceName,
181
+ });
107
182
 
108
- // Step 4: Save credentials
109
- setApiKey(result.access_token);
183
+ // Step 5: Store credentials
184
+ setApiKey(result.api_key);
110
185
  setApiUrl(apiUrl);
186
+ setAccountUrl(accountUrl);
187
+ setVaultAvailable(true);
111
188
 
112
189
  spinner.succeed(chalk.green('Logged in successfully!'));
113
- console.log(`\nWelcome, ${chalk.cyan(result.user_email)}!`);
190
+ console.log(`\nWelcome, ${chalk.cyan(result.email)}!`);
191
+
192
+ if (result.vault && result.vault.available) {
193
+ console.log(chalk.green('\nVault ready — prompts will be encrypted end-to-end.'));
194
+ } else {
195
+ console.log(chalk.dim('\nSet up your vault at: ' + `${apiUrl}/dashboard/settings`));
196
+ }
197
+
114
198
  console.log('\nRun ' + chalk.cyan('pc setup') + ' to configure auto-capture for your CLI tools.\n');
115
199
 
116
200
  } catch (error) {
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import { clearApiKey, isLoggedIn } from '../lib/config.js';
3
+ import { clearVaultKey } from '../lib/keychain.js';
3
4
 
4
5
  export async function logout() {
5
6
  if (!isLoggedIn()) {
@@ -7,6 +8,11 @@ export async function logout() {
7
8
  return;
8
9
  }
9
10
 
11
+ try {
12
+ await clearVaultKey();
13
+ } catch (error) {
14
+ console.log(chalk.yellow('Warning: failed to clear vault key.'));
15
+ }
10
16
  clearApiKey();
11
17
  console.log(chalk.green('Logged out successfully.'));
12
18
  }
@@ -1,7 +1,6 @@
1
1
  import ora from 'ora';
2
2
  import chalk from 'chalk';
3
3
  import { getPrompt } from '../lib/api.js';
4
- import { connect, disconnect } from '../lib/websocket.js';
5
4
  import { isLoggedIn } from '../lib/config.js';
6
5
 
7
6
  export async function push(slug, options) {
@@ -14,42 +13,15 @@ export async function push(slug, options) {
14
13
 
15
14
  try {
16
15
  const prompt = await getPrompt(slug);
17
- spinner.text = 'Connecting to session...';
18
16
 
19
- const socket = connect(options.tool || 'claude');
20
-
21
- // Wait for connection
22
- await new Promise((resolve, reject) => {
23
- const timeout = setTimeout(() => {
24
- reject(new Error('Connection timeout'));
25
- }, 10000);
26
-
27
- socket.on('registered', () => {
28
- clearTimeout(timeout);
29
- resolve();
30
- });
31
-
32
- socket.on('error', (err) => {
33
- clearTimeout(timeout);
34
- reject(new Error(err.message));
35
- });
36
- });
37
-
38
- spinner.text = 'Pushing prompt...';
39
-
40
- // Push the prompt to ourselves (for now, just copy to clipboard simulation)
17
+ spinner.succeed(chalk.green('Prompt fetched!'));
41
18
  console.log('\n' + chalk.cyan('Prompt content:'));
42
19
  console.log(chalk.dim('─'.repeat(50)));
43
20
  console.log(prompt.content);
44
21
  console.log(chalk.dim('─'.repeat(50)));
45
-
46
- spinner.succeed(chalk.green('Prompt fetched!'));
47
22
  console.log('\nCopy the prompt above to use it in your session.');
48
-
49
- disconnect();
50
23
  } catch (error) {
51
24
  spinner.fail(chalk.red('Failed: ' + error.message));
52
- disconnect();
53
25
  }
54
26
  }
55
27
 
@@ -3,7 +3,9 @@ import ora from 'ora';
3
3
  import chalk from 'chalk';
4
4
  import { capturePrompt } from '../lib/api.js';
5
5
  import { getFullContext } from '../lib/context.js';
6
- import { isLoggedIn } from '../lib/config.js';
6
+ import { isLoggedIn, isVaultAvailable, getApiUrl } from '../lib/config.js';
7
+ import { encryptPrompt } from '../lib/crypto.js';
8
+ import { requireVaultKey } from '../lib/keychain.js';
7
9
 
8
10
  export async function save(options) {
9
11
  if (!isLoggedIn()) {
@@ -11,6 +13,12 @@ export async function save(options) {
11
13
  return;
12
14
  }
13
15
 
16
+ if (!isVaultAvailable()) {
17
+ console.log(chalk.red('Vault not set up.') + ' Prompt capture requires a vault.');
18
+ console.log('Set up your vault at: ' + chalk.cyan(`${getApiUrl()}/dashboard/settings`));
19
+ return;
20
+ }
21
+
14
22
  let content = options.message;
15
23
 
16
24
  if (!content) {
@@ -31,8 +39,19 @@ export async function save(options) {
31
39
  const spinner = ora('Saving prompt...').start();
32
40
 
33
41
  try {
42
+ let vaultKey;
43
+ try {
44
+ vaultKey = await requireVaultKey();
45
+ } catch (error) {
46
+ spinner.stop();
47
+ console.log(chalk.red(error.message));
48
+ return;
49
+ }
50
+ const { encrypted_content, content_iv } = encryptPrompt(content.trim(), vaultKey);
51
+
34
52
  const result = await capturePrompt({
35
- content: content.trim(),
53
+ encrypted_content,
54
+ content_iv,
36
55
  ...context
37
56
  });
38
57