@promptcellar/pc 0.5.4 → 0.5.5
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/codex-capture.js +9 -9
- package/hooks/gemini-capture.js +5 -9
- package/hooks/prompt-capture.js +5 -9
- package/package.json +2 -1
- package/src/commands/login.js +12 -116
- package/src/commands/save.js +5 -2
- package/src/lib/api.js +28 -47
- package/src/lib/context.js +14 -73
- package/src/lib/crypto.js +1 -39
- package/src/lib/device-transfer.js +1 -43
- package/README.md +0 -119
- package/docs/plans/2026-01-30-wa-auth-integration-design.md +0 -118
- package/docs/plans/2026-01-30-wa-auth-integration-plan.md +0 -776
- package/docs/plans/2026-02-03-multi-prompt-capture-design.md +0 -501
package/hooks/codex-capture.js
CHANGED
|
@@ -84,13 +84,6 @@ async function main() {
|
|
|
84
84
|
process.exit(0);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
// Build base context once
|
|
88
|
-
const baseContext = getFullContext('codex');
|
|
89
|
-
if (event.cwd) {
|
|
90
|
-
baseContext.working_directory = event.cwd;
|
|
91
|
-
}
|
|
92
|
-
baseContext.session_id = threadId;
|
|
93
|
-
|
|
94
87
|
// Capture each new message
|
|
95
88
|
for (const message of newMessages) {
|
|
96
89
|
const content = extractContent(message);
|
|
@@ -98,10 +91,17 @@ async function main() {
|
|
|
98
91
|
continue;
|
|
99
92
|
}
|
|
100
93
|
|
|
101
|
-
const
|
|
94
|
+
const promptText = content.trim();
|
|
95
|
+
const context = getFullContext('codex', null, {
|
|
96
|
+
cwd: event.cwd || process.cwd(),
|
|
97
|
+
sessionId: threadId,
|
|
98
|
+
promptText,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const { encrypted_content, content_iv } = encryptPrompt(promptText, vaultKey);
|
|
102
102
|
|
|
103
103
|
await capturePrompt({
|
|
104
|
-
...
|
|
104
|
+
...context,
|
|
105
105
|
encrypted_content,
|
|
106
106
|
content_iv
|
|
107
107
|
});
|
package/hooks/gemini-capture.js
CHANGED
|
@@ -72,15 +72,11 @@ async function main() {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// Build context
|
|
75
|
-
const context = getFullContext('gemini'
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
if (event.session_id) {
|
|
82
|
-
context.session_id = event.session_id;
|
|
83
|
-
}
|
|
75
|
+
const context = getFullContext('gemini', null, {
|
|
76
|
+
cwd: event.cwd || process.cwd(),
|
|
77
|
+
sessionId: event.session_id,
|
|
78
|
+
promptText: content,
|
|
79
|
+
});
|
|
84
80
|
|
|
85
81
|
const vaultKey = await requireVaultKey({ silent: true });
|
|
86
82
|
if (!vaultKey) {
|
package/hooks/prompt-capture.js
CHANGED
|
@@ -56,15 +56,11 @@ async function main() {
|
|
|
56
56
|
process.exit(0);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const context = getFullContext('claude-code'
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
if (event.session_id) {
|
|
66
|
-
context.session_id = event.session_id;
|
|
67
|
-
}
|
|
59
|
+
const context = getFullContext('claude-code', null, {
|
|
60
|
+
cwd: event.cwd || process.cwd(),
|
|
61
|
+
sessionId: event.session_id,
|
|
62
|
+
promptText: promptContent,
|
|
63
|
+
});
|
|
68
64
|
|
|
69
65
|
const vaultKey = await requireVaultKey({ silent: true });
|
|
70
66
|
if (!vaultKey) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptcellar/pc",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"node": ">=18.0.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@promptcellar/core": "*",
|
|
37
38
|
"commander": "^12.0.0",
|
|
38
39
|
"conf": "^12.0.0",
|
|
39
40
|
"chalk": "^5.3.0",
|
package/src/commands/login.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from 'crypto';
|
|
2
1
|
import ora from 'ora';
|
|
3
2
|
import chalk from 'chalk';
|
|
4
3
|
import {
|
|
@@ -8,121 +7,20 @@ import {
|
|
|
8
7
|
isLoggedIn
|
|
9
8
|
} from '../lib/config.js';
|
|
10
9
|
import { generateTransferKeyPair, decryptTransfer } from '../lib/device-transfer.js';
|
|
11
|
-
import { b64urlEncode } from '../lib/crypto.js';
|
|
12
10
|
import { storeVaultKey } from '../lib/keychain.js';
|
|
11
|
+
import {
|
|
12
|
+
setClientId,
|
|
13
|
+
requestDeviceCode,
|
|
14
|
+
pollForApproval,
|
|
15
|
+
exchangeTokenForApiKey,
|
|
16
|
+
finalizeDeviceLogin,
|
|
17
|
+
} from '@promptcellar/core/auth';
|
|
18
|
+
|
|
19
|
+
// Re-export auth functions so existing CLI tests can import from this module
|
|
20
|
+
export { setClientId, requestDeviceCode, finalizeDeviceLogin };
|
|
13
21
|
|
|
14
22
|
const DEFAULT_API_URL = 'https://prompts.weldedanvil.com';
|
|
15
23
|
const DEFAULT_ACCOUNT_URL = 'https://account.weldedanvil.com';
|
|
16
|
-
let clientId = 'prompts-prod';
|
|
17
|
-
|
|
18
|
-
export function setClientId(id) { clientId = id; }
|
|
19
|
-
|
|
20
|
-
export async function requestDeviceCode(accountUrl, transferPublicKey) {
|
|
21
|
-
const payload = { client_id: clientId };
|
|
22
|
-
if (transferPublicKey) {
|
|
23
|
-
payload.transfer_public_key = transferPublicKey;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const response = await fetch(`${accountUrl}/device/code`, {
|
|
27
|
-
method: 'POST',
|
|
28
|
-
headers: { 'Content-Type': 'application/json' },
|
|
29
|
-
body: JSON.stringify(payload),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
if (!response.ok) {
|
|
33
|
-
const data = await response.json().catch(() => ({}));
|
|
34
|
-
throw new Error(data.error || 'Failed to request device code');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return response.json();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function pollForApproval(accountUrl, deviceCode, expiresIn) {
|
|
41
|
-
const expiresAt = Date.now() + (expiresIn || 600) * 1000;
|
|
42
|
-
const interval = 5000;
|
|
43
|
-
|
|
44
|
-
while (Date.now() < expiresAt) {
|
|
45
|
-
await sleep(interval);
|
|
46
|
-
|
|
47
|
-
const response = await fetch(`${accountUrl}/device/poll`, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: { 'Content-Type': 'application/json' },
|
|
50
|
-
body: JSON.stringify({ device_code: deviceCode }),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const data = await response.json();
|
|
54
|
-
|
|
55
|
-
if (data.status === 'approved' && data.access_token) {
|
|
56
|
-
return data;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (data.status === 'expired') {
|
|
60
|
-
throw new Error('Authorization request expired. Please try again.');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (data.status === 'rejected') {
|
|
64
|
-
const error = data.error || 'unknown';
|
|
65
|
-
if (error.startsWith('vault_missing')) {
|
|
66
|
-
throw new Error(`Vault error: ${error}`);
|
|
67
|
-
}
|
|
68
|
-
if (error === 'missing_transfer_key') {
|
|
69
|
-
throw new Error('Transfer key missing. Please try again.');
|
|
70
|
-
}
|
|
71
|
-
throw new Error(`Authorization rejected: ${error}`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// status === 'pending' -- keep polling
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
throw new Error('Authorization request timed out. Please try again.');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function exchangeTokenForApiKey(apiUrl, jwt, deviceName) {
|
|
81
|
-
const response = await fetch(`${apiUrl}/api/v1/auth/device-token`, {
|
|
82
|
-
method: 'POST',
|
|
83
|
-
headers: {
|
|
84
|
-
'Authorization': `Bearer ${jwt}`,
|
|
85
|
-
'Content-Type': 'application/json',
|
|
86
|
-
},
|
|
87
|
-
body: JSON.stringify({ device_name: deviceName }),
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (!response.ok) {
|
|
91
|
-
const data = await response.json().catch(() => ({}));
|
|
92
|
-
throw new Error(data.error || 'Failed to exchange token');
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return response.json();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function finalizeDeviceLogin({
|
|
99
|
-
approval,
|
|
100
|
-
privateKeyPem,
|
|
101
|
-
apiUrl,
|
|
102
|
-
deviceName,
|
|
103
|
-
decryptTransferFn = decryptTransfer,
|
|
104
|
-
storeVaultKeyFn = storeVaultKey,
|
|
105
|
-
exchangeTokenForApiKeyFn = exchangeTokenForApiKey,
|
|
106
|
-
}) {
|
|
107
|
-
if (!approval?.vault_transfer) {
|
|
108
|
-
throw new Error('Vault transfer missing. Please reauthorize.');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (!approval?.access_token) {
|
|
112
|
-
throw new Error('Missing access token. Please reauthorize.');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let masterKeyB64;
|
|
116
|
-
if (approval.vault_transfer === 'cli_generate') {
|
|
117
|
-
// No browser extensions available — generate master key locally
|
|
118
|
-
masterKeyB64 = b64urlEncode(randomBytes(32));
|
|
119
|
-
} else {
|
|
120
|
-
masterKeyB64 = decryptTransferFn(approval.vault_transfer, privateKeyPem);
|
|
121
|
-
}
|
|
122
|
-
await storeVaultKeyFn(masterKeyB64);
|
|
123
|
-
|
|
124
|
-
return exchangeTokenForApiKeyFn(apiUrl, approval.access_token, deviceName);
|
|
125
|
-
}
|
|
126
24
|
|
|
127
25
|
export function persistLoginState({
|
|
128
26
|
apiUrl,
|
|
@@ -151,10 +49,6 @@ function getDeviceName() {
|
|
|
151
49
|
return `${osNames[os] || os} - ${hostname}`;
|
|
152
50
|
}
|
|
153
51
|
|
|
154
|
-
function sleep(ms) {
|
|
155
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
52
|
export async function login(options) {
|
|
159
53
|
const apiUrl = options?.url || DEFAULT_API_URL;
|
|
160
54
|
const accountUrl = options?.accountUrl || DEFAULT_ACCOUNT_URL;
|
|
@@ -199,6 +93,8 @@ export async function login(options) {
|
|
|
199
93
|
privateKeyPem,
|
|
200
94
|
apiUrl,
|
|
201
95
|
deviceName,
|
|
96
|
+
decryptTransferFn: decryptTransfer,
|
|
97
|
+
storeVaultKeyFn: storeVaultKey,
|
|
202
98
|
});
|
|
203
99
|
|
|
204
100
|
// Step 5: Store credentials
|
package/src/commands/save.js
CHANGED
|
@@ -34,7 +34,10 @@ export async function save(options) {
|
|
|
34
34
|
content = promptContent;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const
|
|
37
|
+
const trimmedContent = content.trim();
|
|
38
|
+
const context = getFullContext(options.tool || 'manual', options.model, {
|
|
39
|
+
promptText: trimmedContent,
|
|
40
|
+
});
|
|
38
41
|
|
|
39
42
|
const spinner = ora('Saving prompt...').start();
|
|
40
43
|
|
|
@@ -47,7 +50,7 @@ export async function save(options) {
|
|
|
47
50
|
console.log(chalk.red(error.message));
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
|
-
const { encrypted_content, content_iv } = encryptPrompt(
|
|
53
|
+
const { encrypted_content, content_iv } = encryptPrompt(trimmedContent, vaultKey);
|
|
51
54
|
|
|
52
55
|
const result = await capturePrompt({
|
|
53
56
|
encrypted_content,
|
package/src/lib/api.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import { getApiKey, getApiUrl } from './config.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
capturePrompt as coreCapturePrompt,
|
|
4
|
+
listCaptured as coreListCaptured,
|
|
5
|
+
getPrompt as coreGetPrompt,
|
|
6
|
+
healthCheck as coreHealthCheck,
|
|
7
|
+
} from '@promptcellar/core/api';
|
|
8
|
+
|
|
9
|
+
function requireAuth() {
|
|
4
10
|
const apiKey = getApiKey();
|
|
5
11
|
const apiUrl = getApiUrl();
|
|
12
|
+
if (!apiKey) throw new Error('Not logged in. Run: pc login');
|
|
13
|
+
return { apiKey, apiUrl };
|
|
14
|
+
}
|
|
6
15
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const url = `${apiUrl}${endpoint}`;
|
|
12
|
-
const headers = {
|
|
13
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
14
|
-
'Content-Type': 'application/json',
|
|
15
|
-
...options.headers
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const response = await fetch(url, {
|
|
19
|
-
...options,
|
|
20
|
-
headers
|
|
21
|
-
});
|
|
16
|
+
export async function capturePrompt(data) {
|
|
17
|
+
const { apiUrl, apiKey } = requireAuth();
|
|
18
|
+
return coreCapturePrompt(apiUrl, apiKey, data);
|
|
19
|
+
}
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
export async function listCaptured(options) {
|
|
22
|
+
const { apiUrl, apiKey } = requireAuth();
|
|
23
|
+
return coreListCaptured(apiUrl, apiKey, options);
|
|
24
|
+
}
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
export async function getPrompt(slug) {
|
|
27
|
+
const { apiUrl, apiKey } = requireAuth();
|
|
28
|
+
return coreGetPrompt(apiUrl, apiKey, slug);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export async function testConnection() {
|
|
@@ -37,35 +37,16 @@ export async function testConnection() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return { success: true };
|
|
43
|
-
}
|
|
44
|
-
return { success: false, error: 'Server not responding' };
|
|
40
|
+
await coreHealthCheck(apiUrl);
|
|
41
|
+
return { success: true };
|
|
45
42
|
} catch (error) {
|
|
46
43
|
return { success: false, error: error.message };
|
|
47
44
|
}
|
|
48
45
|
}
|
|
49
46
|
|
|
50
|
-
export async function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
body: JSON.stringify(data)
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function listCaptured(options = {}) {
|
|
58
|
-
const params = new URLSearchParams();
|
|
59
|
-
if (options.page) params.set('page', options.page);
|
|
60
|
-
if (options.tool) params.set('tool', options.tool);
|
|
61
|
-
if (options.starred) params.set('starred', 'true');
|
|
62
|
-
|
|
63
|
-
const query = params.toString();
|
|
64
|
-
return request(`/api/v1/captured${query ? '?' + query : ''}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function getPrompt(slug) {
|
|
68
|
-
return request(`/api/v1/prompts/${slug}`);
|
|
47
|
+
export async function healthCheck() {
|
|
48
|
+
const apiUrl = getApiUrl();
|
|
49
|
+
return coreHealthCheck(apiUrl);
|
|
69
50
|
}
|
|
70
51
|
|
|
71
|
-
export default { testConnection, capturePrompt, listCaptured, getPrompt };
|
|
52
|
+
export default { testConnection, capturePrompt, listCaptured, getPrompt, healthCheck };
|
package/src/lib/context.js
CHANGED
|
@@ -1,83 +1,24 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
1
|
import { getCaptureLevel, getSessionId } from './config.js';
|
|
2
|
+
import {
|
|
3
|
+
getGitContext as coreGitContext,
|
|
4
|
+
getFullContext as coreFullContext,
|
|
5
|
+
} from '@promptcellar/core/context';
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
try {
|
|
6
|
-
return execFileSync('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
7
|
-
} catch {
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function sanitizeGitRemote(remote) {
|
|
13
|
-
if (!remote) return remote;
|
|
14
|
-
try {
|
|
15
|
-
const url = new URL(remote);
|
|
16
|
-
url.username = '';
|
|
17
|
-
url.password = '';
|
|
18
|
-
url.search = '';
|
|
19
|
-
url.hash = '';
|
|
20
|
-
return `${url.origin}${url.pathname}`;
|
|
21
|
-
} catch {
|
|
22
|
-
return remote.replace(/\/\/[^@]+@/, '//');
|
|
23
|
-
}
|
|
24
|
-
}
|
|
7
|
+
export { sanitizeGitRemote } from '@promptcellar/core/context';
|
|
25
8
|
|
|
26
9
|
export function getGitContext() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Minimal: no git context
|
|
30
|
-
if (level === 'minimal') {
|
|
31
|
-
return {};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const context = {};
|
|
35
|
-
|
|
36
|
-
// Standard: repo name and branch
|
|
37
|
-
const topLevel = execGit('rev-parse', '--show-toplevel');
|
|
38
|
-
if (topLevel) {
|
|
39
|
-
context.git_repo = topLevel.split('/').pop();
|
|
40
|
-
context.git_branch = execGit('rev-parse', '--abbrev-ref', 'HEAD');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Rich: add remote URL and commit hash
|
|
44
|
-
if (level === 'rich' && topLevel) {
|
|
45
|
-
context.git_remote_url = sanitizeGitRemote(execGit('remote', 'get-url', 'origin'));
|
|
46
|
-
context.git_commit = execGit('rev-parse', 'HEAD');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return context;
|
|
10
|
+
return coreGitContext(process.cwd(), getCaptureLevel());
|
|
50
11
|
}
|
|
51
12
|
|
|
52
|
-
export function getFullContext(tool, model) {
|
|
53
|
-
|
|
54
|
-
const sessionId = getSessionId();
|
|
55
|
-
|
|
56
|
-
const context = {
|
|
13
|
+
export function getFullContext(tool, model, overrides = {}) {
|
|
14
|
+
return coreFullContext({
|
|
57
15
|
tool,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Standard: add working directory and model
|
|
67
|
-
context.working_directory = process.cwd();
|
|
68
|
-
if (model) {
|
|
69
|
-
context.model = model;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Add git context
|
|
73
|
-
Object.assign(context, getGitContext());
|
|
74
|
-
|
|
75
|
-
// Rich: add session ID
|
|
76
|
-
if (level === 'rich') {
|
|
77
|
-
context.session_id = sessionId;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return context;
|
|
16
|
+
model,
|
|
17
|
+
captureLevel: getCaptureLevel(),
|
|
18
|
+
sessionId: getSessionId(),
|
|
19
|
+
cwd: process.cwd(),
|
|
20
|
+
...overrides,
|
|
21
|
+
});
|
|
81
22
|
}
|
|
82
23
|
|
|
83
24
|
export default { getGitContext, getFullContext };
|
package/src/lib/crypto.js
CHANGED
|
@@ -1,39 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function b64urlEncode(buffer) {
|
|
4
|
-
return Buffer.from(buffer)
|
|
5
|
-
.toString('base64')
|
|
6
|
-
.replace(/\+/g, '-')
|
|
7
|
-
.replace(/\//g, '_')
|
|
8
|
-
.replace(/=+$/, '');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function b64urlDecode(str) {
|
|
12
|
-
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
13
|
-
while (base64.length % 4) base64 += '=';
|
|
14
|
-
return Buffer.from(base64, 'base64');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function encryptPrompt(plaintext, keyBase64url) {
|
|
18
|
-
const key = b64urlDecode(keyBase64url);
|
|
19
|
-
if (key.length !== 32) {
|
|
20
|
-
throw new Error('Invalid vault key length');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const iv = randomBytes(12);
|
|
24
|
-
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
25
|
-
|
|
26
|
-
const encoded = Buffer.from(plaintext, 'utf8');
|
|
27
|
-
const encrypted = Buffer.concat([cipher.update(encoded), cipher.final()]);
|
|
28
|
-
const authTag = cipher.getAuthTag();
|
|
29
|
-
|
|
30
|
-
// AES-GCM ciphertext = encrypted + authTag (WebCrypto format)
|
|
31
|
-
const ciphertext = Buffer.concat([encrypted, authTag]);
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
encrypted_content: b64urlEncode(ciphertext),
|
|
35
|
-
content_iv: b64urlEncode(iv),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export { b64urlEncode, b64urlDecode };
|
|
1
|
+
export { encryptPrompt, decryptPrompt, b64urlEncode, b64urlDecode } from '@promptcellar/core/crypto';
|
|
@@ -1,43 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { promisify } from 'util';
|
|
3
|
-
import { b64urlEncode, b64urlDecode } from './crypto.js';
|
|
4
|
-
|
|
5
|
-
const generateKeyPairAsync = promisify(generateKeyPair);
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Generate an RSA-OAEP 2048 keypair for device-transfer envelope encryption.
|
|
9
|
-
* Returns { publicKeySpkiB64url, privateKeyPem }.
|
|
10
|
-
*/
|
|
11
|
-
export async function generateTransferKeyPair() {
|
|
12
|
-
const { publicKey, privateKey } = await generateKeyPairAsync('rsa', {
|
|
13
|
-
modulusLength: 2048,
|
|
14
|
-
publicKeyEncoding: { type: 'spki', format: 'der' },
|
|
15
|
-
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
publicKeySpkiB64url: b64urlEncode(publicKey),
|
|
20
|
-
privateKeyPem: privateKey,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Decrypt a transfer payload encrypted with RSA-OAEP + SHA-256.
|
|
26
|
-
* @param {string} ciphertextB64url - base64url-encoded ciphertext
|
|
27
|
-
* @param {string} privateKeyPem - PEM-encoded PKCS#8 private key
|
|
28
|
-
* @returns {string} base64url-encoded master key
|
|
29
|
-
*/
|
|
30
|
-
export function decryptTransfer(ciphertextB64url, privateKeyPem) {
|
|
31
|
-
const ciphertext = b64urlDecode(ciphertextB64url);
|
|
32
|
-
|
|
33
|
-
const plaintext = privateDecrypt(
|
|
34
|
-
{
|
|
35
|
-
key: privateKeyPem,
|
|
36
|
-
oaepHash: 'sha256',
|
|
37
|
-
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
|
38
|
-
},
|
|
39
|
-
ciphertext,
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return b64urlEncode(plaintext);
|
|
43
|
-
}
|
|
1
|
+
export { generateTransferKeyPair, decryptTransfer } from '@promptcellar/core/auth';
|
package/README.md
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# Prompt Cellar CLI
|
|
2
|
-
|
|
3
|
-
Command-line tool for capturing, managing, and reusing AI prompts with [Prompt Cellar](https://prompts.weldedanvil.com).
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g @promptcellar/pc
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# Login via browser authorization
|
|
15
|
-
pc login
|
|
16
|
-
|
|
17
|
-
# Set up auto-capture for your AI CLI tools
|
|
18
|
-
pc setup
|
|
19
|
-
|
|
20
|
-
# Check status
|
|
21
|
-
pc status
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Commands
|
|
25
|
-
|
|
26
|
-
### Authentication
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
pc login # Login via browser authorization
|
|
30
|
-
pc logout # Remove stored credentials
|
|
31
|
-
pc status # Show current status and connection info
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
### Auto-Capture Setup
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
pc setup # Configure auto-capture for CLI tools
|
|
38
|
-
pc unsetup # Remove auto-capture hooks
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Supported tools (auto-detected during setup):
|
|
42
|
-
- **Claude Code** - via Stop hooks
|
|
43
|
-
- **Codex CLI** - via notify hook
|
|
44
|
-
- **Gemini CLI** - via BeforeAgent hook
|
|
45
|
-
|
|
46
|
-
Coming soon:
|
|
47
|
-
- Cursor
|
|
48
|
-
- Windsurf
|
|
49
|
-
- Aider
|
|
50
|
-
|
|
51
|
-
### Manual Capture
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
# Save a prompt manually
|
|
55
|
-
pc save -m "Your prompt text here"
|
|
56
|
-
|
|
57
|
-
# Open editor to write prompt
|
|
58
|
-
pc save
|
|
59
|
-
|
|
60
|
-
# Specify tool and model
|
|
61
|
-
pc save -m "prompt" -t aider -M gpt-4
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Push Prompts
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
# Fetch and display a prompt by slug
|
|
68
|
-
pc push my-project/useful-prompt
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### Configuration
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
# View/change capture level
|
|
75
|
-
pc config
|
|
76
|
-
|
|
77
|
-
# Set capture level directly
|
|
78
|
-
pc config --level minimal # Tool + timestamp only
|
|
79
|
-
pc config --level standard # + working directory, git repo/branch
|
|
80
|
-
pc config --level rich # + session ID, git commit, remote URL
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Update
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
pc update # Update to latest version
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Capture Levels
|
|
90
|
-
|
|
91
|
-
Control how much context is captured with your prompts:
|
|
92
|
-
|
|
93
|
-
| Level | Captured Data |
|
|
94
|
-
|-------|--------------|
|
|
95
|
-
| `minimal` | Tool, timestamp |
|
|
96
|
-
| `standard` | + Working directory, git repo, git branch |
|
|
97
|
-
| `rich` | + Session ID, git commit hash, remote URL |
|
|
98
|
-
|
|
99
|
-
## Environment
|
|
100
|
-
|
|
101
|
-
Configuration is stored in:
|
|
102
|
-
- macOS: `~/Library/Preferences/promptcellar-nodejs/`
|
|
103
|
-
- Linux: `~/.config/promptcellar-nodejs/`
|
|
104
|
-
- Windows: `%APPDATA%/promptcellar-nodejs/`
|
|
105
|
-
|
|
106
|
-
Security flags and requirements:
|
|
107
|
-
- `PC_VAULT_KEY`: overrides the vault key used by the CLI.
|
|
108
|
-
- `PC_VAULT_KEY_FALLBACK=1`: enables file fallback when the keychain is locked.
|
|
109
|
-
- `apiUrl` and `accountUrl` must use HTTPS (`http://localhost` is allowed).
|
|
110
|
-
|
|
111
|
-
## API
|
|
112
|
-
|
|
113
|
-
The CLI communicates with your Prompt Cellar instance. Get your API key from:
|
|
114
|
-
|
|
115
|
-
Settings > API Keys in your Prompt Cellar dashboard.
|
|
116
|
-
|
|
117
|
-
## License
|
|
118
|
-
|
|
119
|
-
MIT
|