@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.
@@ -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 { encrypted_content, content_iv } = encryptPrompt(content.trim(), vaultKey);
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
- ...baseContext,
104
+ ...context,
105
105
  encrypted_content,
106
106
  content_iv
107
107
  });
@@ -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) {
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptcellar/pc",
3
- "version": "0.5.4",
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",
@@ -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,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
- 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
- }
7
+ export { sanitizeGitRemote } from '@promptcellar/core/context';
25
8
 
26
9
  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;
10
+ return coreGitContext(process.cwd(), getCaptureLevel());
50
11
  }
51
12
 
52
- export function getFullContext(tool, model) {
53
- const level = getCaptureLevel();
54
- const sessionId = getSessionId();
55
-
56
- const context = {
13
+ export function getFullContext(tool, model, overrides = {}) {
14
+ return coreFullContext({
57
15
  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
- }
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
- 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';
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