@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.
@@ -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
- // Build base context once
88
- const baseContext = getFullContext('codex');
89
- if (event.cwd) {
90
- baseContext.working_directory = event.cwd;
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 { encrypted_content, content_iv } = encryptPrompt(content.trim(), vaultKey);
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
- ...baseContext,
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
 
@@ -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
- // Override with event data if available
78
- if (event.cwd) {
79
- context.working_directory = event.cwd;
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();
@@ -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
- // Override with event data if available
62
- if (event.cwd) {
63
- context.working_directory = event.cwd;
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.5.4",
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",
@@ -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
@@ -34,7 +34,10 @@ export async function save(options) {
34
34
  content = promptContent;
35
35
  }
36
36
 
37
- const context = getFullContext(options.tool || 'manual', options.model);
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(content.trim(), vaultKey);
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
- async function request(endpoint, options = {}) {
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
- if (!apiKey) {
8
- throw new Error('Not logged in. Run: pc login');
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
- if (!response.ok) {
24
- const data = await response.json().catch(() => ({}));
25
- throw new Error(data.error || `HTTP ${response.status}`);
26
- }
21
+ export async function listCaptured(options) {
22
+ const { apiUrl, apiKey } = requireAuth();
23
+ return coreListCaptured(apiUrl, apiKey, options);
24
+ }
27
25
 
28
- return response.json();
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
- const response = await fetch(`${apiUrl}/health`);
41
- if (response.ok) {
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 capturePrompt(data) {
51
- return request('/api/v1/capture', {
52
- method: 'POST',
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 };
@@ -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
- function execGit(...args) {
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
- const level = getCaptureLevel();
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
- const level = getCaptureLevel();
54
- const sessionId = getSessionId();
55
-
56
- const context = {
14
+ export function getFullContext(tool, model, overrides = {}) {
15
+ return coreFullContext({
57
16
  tool,
58
- captured_at: new Date().toISOString()
59
- };
60
-
61
- // Minimal: just tool and timestamp
62
- if (level === 'minimal') {
63
- return context;
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
- return context;
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
- import { createCipheriv, randomBytes } from 'crypto';
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
- import { generateKeyPair, publicEncrypt, privateDecrypt, constants } from 'crypto';
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';