@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.
- package/hooks/codex-capture.js +20 -9
- package/hooks/gemini-capture.js +17 -11
- package/hooks/prompt-capture.js +17 -11
- 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 +19 -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
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { capturePrompt } from '../src/lib/api.js';
|
|
19
|
-
import { getFullContext } from '../src/lib/context.js';
|
|
19
|
+
import { getFullContext, collectContextFileContents } from '../src/lib/context.js';
|
|
20
20
|
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
21
21
|
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
22
22
|
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
@@ -84,12 +84,14 @@ async function main() {
|
|
|
84
84
|
process.exit(0);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
// Collect and encrypt context files once for the session
|
|
88
|
+
let encrypted_context_files, context_files_iv;
|
|
89
|
+
const contextFileContents = collectContextFileContents('codex', 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;
|
|
91
94
|
}
|
|
92
|
-
baseContext.session_id = threadId;
|
|
93
95
|
|
|
94
96
|
// Capture each new message
|
|
95
97
|
for (const message of newMessages) {
|
|
@@ -98,12 +100,21 @@ async function main() {
|
|
|
98
100
|
continue;
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
const
|
|
103
|
+
const promptText = content.trim();
|
|
104
|
+
const context = getFullContext('codex', null, {
|
|
105
|
+
cwd: event.cwd || process.cwd(),
|
|
106
|
+
sessionId: threadId,
|
|
107
|
+
promptText,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const { encrypted_content, content_iv } = encryptPrompt(promptText, vaultKey);
|
|
102
111
|
|
|
103
112
|
await capturePrompt({
|
|
104
|
-
...
|
|
113
|
+
...context,
|
|
105
114
|
encrypted_content,
|
|
106
|
-
content_iv
|
|
115
|
+
content_iv,
|
|
116
|
+
encrypted_context_files,
|
|
117
|
+
context_files_iv,
|
|
107
118
|
});
|
|
108
119
|
}
|
|
109
120
|
|
package/hooks/gemini-capture.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { capturePrompt } from '../src/lib/api.js';
|
|
19
|
-
import { getFullContext } from '../src/lib/context.js';
|
|
19
|
+
import { getFullContext, collectContextFileContents } from '../src/lib/context.js';
|
|
20
20
|
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
21
21
|
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
22
22
|
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
@@ -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) {
|
|
@@ -89,10 +85,20 @@ async function main() {
|
|
|
89
85
|
}
|
|
90
86
|
const { encrypted_content, content_iv } = encryptPrompt(content, vaultKey);
|
|
91
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
|
+
|
|
92
96
|
await capturePrompt({
|
|
93
97
|
...context,
|
|
94
98
|
encrypted_content,
|
|
95
|
-
content_iv
|
|
99
|
+
content_iv,
|
|
100
|
+
encrypted_context_files,
|
|
101
|
+
context_files_iv,
|
|
96
102
|
});
|
|
97
103
|
|
|
98
104
|
respond();
|
package/hooks/prompt-capture.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { capturePrompt } from '../src/lib/api.js';
|
|
14
|
-
import { getFullContext } from '../src/lib/context.js';
|
|
14
|
+
import { getFullContext, collectContextFileContents } from '../src/lib/context.js';
|
|
15
15
|
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
16
16
|
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
17
17
|
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
@@ -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) {
|
|
@@ -72,10 +68,20 @@ async function main() {
|
|
|
72
68
|
}
|
|
73
69
|
const { encrypted_content, content_iv } = encryptPrompt(promptContent, vaultKey);
|
|
74
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
|
+
|
|
75
79
|
await capturePrompt({
|
|
76
80
|
...context,
|
|
77
81
|
encrypted_content,
|
|
78
|
-
content_iv
|
|
82
|
+
content_iv,
|
|
83
|
+
encrypted_context_files,
|
|
84
|
+
context_files_iv,
|
|
79
85
|
});
|
|
80
86
|
|
|
81
87
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptcellar/pc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -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,29 @@
|
|
|
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
|
+
collectContextFileContents as coreCollectContextFileContents,
|
|
6
|
+
} from '@promptcellar/core/context';
|
|
3
7
|
|
|
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
|
-
}
|
|
8
|
+
export { sanitizeGitRemote } from '@promptcellar/core/context';
|
|
25
9
|
|
|
26
10
|
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;
|
|
11
|
+
return coreGitContext(process.cwd(), getCaptureLevel());
|
|
50
12
|
}
|
|
51
13
|
|
|
52
|
-
export function getFullContext(tool, model) {
|
|
53
|
-
|
|
54
|
-
const sessionId = getSessionId();
|
|
55
|
-
|
|
56
|
-
const context = {
|
|
14
|
+
export function getFullContext(tool, model, overrides = {}) {
|
|
15
|
+
return coreFullContext({
|
|
57
16
|
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
|
-
}
|
|
17
|
+
model,
|
|
18
|
+
captureLevel: getCaptureLevel(),
|
|
19
|
+
sessionId: getSessionId(),
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
...overrides,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
79
24
|
|
|
80
|
-
|
|
25
|
+
export function collectContextFileContents(tool, cwd) {
|
|
26
|
+
return coreCollectContextFileContents({ tool, cwd });
|
|
81
27
|
}
|
|
82
28
|
|
|
83
|
-
export default { getGitContext, getFullContext };
|
|
29
|
+
export default { getGitContext, getFullContext, collectContextFileContents };
|
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';
|