@promptcellar/pc 0.5.3 → 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 +34 -115
- package/src/commands/save.js +5 -2
- package/src/lib/api.js +28 -47
- package/src/lib/config.js +22 -2
- package/src/lib/context.js +14 -59
- package/src/lib/crypto.js +1 -39
- package/src/lib/device-transfer.js +1 -43
- package/src/lib/keychain.js +7 -2
- package/tests/config.test.js +29 -0
- package/tests/context.test.js +23 -0
- package/tests/device-login.test.js +28 -0
- package/tests/keychain.test.js +34 -1
- package/README.md +0 -114
- 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
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { sanitizeGitRemote } from '../src/lib/context.js';
|
|
4
|
+
|
|
5
|
+
describe('sanitizeGitRemote', () => {
|
|
6
|
+
it('strips credentials and query/fragment', () => {
|
|
7
|
+
const input = 'https://token:secret@github.com/org/repo.git?foo=bar#frag';
|
|
8
|
+
const output = sanitizeGitRemote(input);
|
|
9
|
+
assert.equal(output, 'https://github.com/org/repo.git');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('removes userinfo from https URL', () => {
|
|
13
|
+
const input = 'https://token@github.com/org/repo.git';
|
|
14
|
+
const output = sanitizeGitRemote(input);
|
|
15
|
+
assert.equal(output, 'https://github.com/org/repo.git');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('leaves scp-style remotes intact', () => {
|
|
19
|
+
const input = 'git@github.com:org/repo.git';
|
|
20
|
+
const output = sanitizeGitRemote(input);
|
|
21
|
+
assert.equal(output, input);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -118,4 +118,32 @@ describe('device login helpers', () => {
|
|
|
118
118
|
assert.equal(exchangeCalled, false);
|
|
119
119
|
});
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
describe('persistLoginState', () => {
|
|
123
|
+
it('does not persist when validation fails', () => {
|
|
124
|
+
const { persistLoginState } = login;
|
|
125
|
+
assert.equal(typeof persistLoginState, 'function', 'persistLoginState should be exported');
|
|
126
|
+
|
|
127
|
+
const calls = [];
|
|
128
|
+
const normalizeFn = () => {
|
|
129
|
+
throw new Error('invalid url');
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
assert.throws(
|
|
133
|
+
() => persistLoginState({
|
|
134
|
+
apiUrl: 'http://bad',
|
|
135
|
+
accountUrl: 'http://bad',
|
|
136
|
+
apiKey: 'api-key',
|
|
137
|
+
normalizeFn,
|
|
138
|
+
setApiUrlFn: () => calls.push('apiUrl'),
|
|
139
|
+
setAccountUrlFn: () => calls.push('accountUrl'),
|
|
140
|
+
setApiKeyFn: () => calls.push('apiKey'),
|
|
141
|
+
setVaultAvailableFn: () => calls.push('vaultAvailable'),
|
|
142
|
+
}),
|
|
143
|
+
/invalid url/i,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
assert.deepEqual(calls, []);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
121
149
|
});
|
package/tests/keychain.test.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
1
|
+
import { describe, it, mock } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import keytar from 'keytar';
|
|
3
4
|
import * as keychain from '../src/lib/keychain.js';
|
|
5
|
+
import { loadVaultKey, storeVaultKey } from '../src/lib/keychain.js';
|
|
6
|
+
|
|
7
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
8
|
+
|
|
9
|
+
function resetEnv() {
|
|
10
|
+
process.env = { ...ORIGINAL_ENV };
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
describe('requireVaultKey', () => {
|
|
6
14
|
it('returns key when available', async () => {
|
|
@@ -38,3 +46,28 @@ describe('requireVaultKey', () => {
|
|
|
38
46
|
assert.equal(key, null);
|
|
39
47
|
});
|
|
40
48
|
});
|
|
49
|
+
|
|
50
|
+
describe('vault key env handling', () => {
|
|
51
|
+
it('prefers PC_VAULT_KEY when set', async () => {
|
|
52
|
+
resetEnv();
|
|
53
|
+
process.env.PC_VAULT_KEY = 'env-key';
|
|
54
|
+
mock.method(keytar, 'getPassword', async () => {
|
|
55
|
+
throw new Error('should not call keytar');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const key = await loadVaultKey();
|
|
59
|
+
assert.equal(key, 'env-key');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('throws when keytar fails and fallback disabled', async () => {
|
|
63
|
+
resetEnv();
|
|
64
|
+
mock.method(keytar, 'setPassword', async () => {
|
|
65
|
+
throw new Error('keytar locked');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
() => storeVaultKey('vault-key'),
|
|
70
|
+
/keytar locked/i,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|
package/README.md
DELETED
|
@@ -1,114 +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
|
-
## API
|
|
107
|
-
|
|
108
|
-
The CLI communicates with your Prompt Cellar instance. Get your API key from:
|
|
109
|
-
|
|
110
|
-
Settings > API Keys in your Prompt Cellar dashboard.
|
|
111
|
-
|
|
112
|
-
## License
|
|
113
|
-
|
|
114
|
-
MIT
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# wa-auth Integration Design
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Migrate pc-cli authentication from prompt-registry's removed device auth endpoints to wa-auth, and add client-side encryption so captured prompts match prompt-registry's zero-knowledge architecture.
|
|
6
|
-
|
|
7
|
-
## Login Flow
|
|
8
|
-
|
|
9
|
-
Four stages:
|
|
10
|
-
|
|
11
|
-
1. **Device code request** -- CLI calls `POST /device/code` on wa-auth with `{ client_id: "prompts-prod" }`. Gets back `device_code`, `user_code`, and `verification_url`.
|
|
12
|
-
|
|
13
|
-
2. **User approval** -- User opens the verification URL in browser, authenticates with their passkey, approves the device. CLI polls `POST /device/poll` with `{ device_code }`.
|
|
14
|
-
|
|
15
|
-
3. **JWT received** -- On approval, wa-auth returns `{ status: "approved", access_token: "<JWT>", user_id: "..." }`. The JWT has `aud: "prompts-prod"`, `sub: "<user_id>"`, standard exp/iss claims.
|
|
16
|
-
|
|
17
|
-
4. **API key exchange** -- CLI calls `POST /api/v1/auth/device-token` on prompt-registry with `Authorization: Bearer <JWT>` and `{ device_name: "Linux - myhostname" }`. Prompt-registry validates the JWT, auto-revokes any previous key with the same device name, creates a new `pk_*` API key, and returns:
|
|
18
|
-
|
|
19
|
-
```json
|
|
20
|
-
{
|
|
21
|
-
"api_key": "pk_...",
|
|
22
|
-
"email": "user@example.com",
|
|
23
|
-
"vault": {
|
|
24
|
-
"available": true,
|
|
25
|
-
"encrypted_key": "base64url(...)"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
If `vault.available` is false, the CLI warns that capture requires vault setup at the web UI.
|
|
31
|
-
|
|
32
|
-
## Changes to wa-auth
|
|
33
|
-
|
|
34
|
-
Two modifications to the device auth flow:
|
|
35
|
-
|
|
36
|
-
1. **`POST /device/code` accepts `client_id`** -- Validates that `client_id` exists in `OIDC_CLIENTS`. Stores `client_id` on the `DeviceAuthRequest` record (new column). Rejects unknown client IDs.
|
|
37
|
-
|
|
38
|
-
2. **`POST /device/poll` returns a JWT on approval** -- When status is `approved`, generates a JWT with `sub: user_id`, `aud: client_id` (from the stored device request), `iss: account.weldedanvil.com`, and standard `iat`/`exp`. Returns it as `access_token` alongside `status` and `user_id`.
|
|
39
|
-
|
|
40
|
-
The `DeviceAuthRequest` model gets one new field:
|
|
41
|
-
|
|
42
|
-
```
|
|
43
|
-
client_id: String(64) -- stored from the initial code request
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
No other wa-auth changes needed.
|
|
47
|
-
|
|
48
|
-
## Changes to prompt-registry
|
|
49
|
-
|
|
50
|
-
One new endpoint:
|
|
51
|
-
|
|
52
|
-
**`POST /api/v1/auth/device-token`** -- Exchanges a wa-auth JWT for a `pk_*` API key.
|
|
53
|
-
|
|
54
|
-
- Reads JWT from `Authorization: Bearer <jwt>` header
|
|
55
|
-
- Validates with `verify_account_token()` (existing function)
|
|
56
|
-
- Calls `create_or_get_user(sub, email)` to resolve the user
|
|
57
|
-
- Reads `device_name` from the request body
|
|
58
|
-
- Deletes any existing `UserApiKey` with the same `user_id` and `name` (auto-revoke)
|
|
59
|
-
- Creates a new `UserApiKey` with `generate_api_key()`, stores the hash
|
|
60
|
-
- Returns the plaintext key, user email, and vault status
|
|
61
|
-
|
|
62
|
-
Vault status is determined by checking if a vault record exists for the user. If it does, returns `encrypted_metadata`. If not, `vault.available` is false.
|
|
63
|
-
|
|
64
|
-
No changes to existing endpoints or middleware.
|
|
65
|
-
|
|
66
|
-
## Changes to pc-cli
|
|
67
|
-
|
|
68
|
-
### `src/commands/login.js` -- Rewritten login flow
|
|
69
|
-
|
|
70
|
-
- Calls wa-auth `POST /device/code` with `client_id`
|
|
71
|
-
- Polls wa-auth `POST /device/poll`
|
|
72
|
-
- On approval, exchanges JWT at prompt-registry `POST /api/v1/auth/device-token` with device name
|
|
73
|
-
- Stores API key, vault key, and account URL in config
|
|
74
|
-
- Warns if vault not available
|
|
75
|
-
|
|
76
|
-
### `src/lib/config.js` -- New config fields
|
|
77
|
-
|
|
78
|
-
- `accountUrl` -- wa-auth base URL (default `https://account.weldedanvil.com`)
|
|
79
|
-
- `vaultKey` -- encrypted vault metadata from login (empty string default)
|
|
80
|
-
- `vaultAvailable` -- boolean, whether capture is enabled
|
|
81
|
-
|
|
82
|
-
### `src/lib/crypto.js` -- New file
|
|
83
|
-
|
|
84
|
-
- Derives AES-256-GCM key from vault metadata
|
|
85
|
-
- `encryptPrompt(plaintext, key)` returns `{ encrypted_content, content_iv }`
|
|
86
|
-
- Uses Node.js built-in `crypto` module (no new dependencies)
|
|
87
|
-
|
|
88
|
-
### Hook scripts -- Updated capture payload
|
|
89
|
-
|
|
90
|
-
`hooks/prompt-capture.js`, `codex-capture.js`, `gemini-capture.js`:
|
|
91
|
-
|
|
92
|
-
- Check `vaultAvailable` before attempting capture
|
|
93
|
-
- Encrypt prompt content before sending
|
|
94
|
-
- Send `{ encrypted_content, content_iv, context_hash }` instead of plaintext
|
|
95
|
-
|
|
96
|
-
### `src/lib/api.js` -- No changes
|
|
97
|
-
|
|
98
|
-
Already sends `Authorization: Bearer <key>` which works with `pk_*` keys.
|
|
99
|
-
|
|
100
|
-
## Error Handling
|
|
101
|
-
|
|
102
|
-
| Scenario | Behavior |
|
|
103
|
-
|----------|----------|
|
|
104
|
-
| Expired JWT during key exchange | 401 from prompt-registry, CLI says re-run `pc login` |
|
|
105
|
-
| Vault not set up | Login succeeds with warning. Hooks silently skip capture. `pc save` shows actionable error. |
|
|
106
|
-
| Vault set up after login | Re-run `pc login` to pick up vault key |
|
|
107
|
-
| API key revoked from web UI | 401 on API calls, existing "Not logged in" error |
|
|
108
|
-
| wa-auth down during login | Connection error, no partial state saved |
|
|
109
|
-
| prompt-registry down during exchange | JWT expires, re-run `pc login` |
|
|
110
|
-
|
|
111
|
-
## Out of Scope
|
|
112
|
-
|
|
113
|
-
- Search index encryption from CLI (web UI concern)
|
|
114
|
-
- `pc push` decryption (separate effort)
|
|
115
|
-
- Vault creation from CLI (stays in web UI)
|
|
116
|
-
- Token refresh (API keys don't expire)
|
|
117
|
-
- Migration of existing plaintext captures
|
|
118
|
-
- Changes to `pc setup` (hook registration unchanged)
|