@promptcellar/pc 0.3.3 → 0.4.1
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/README.md +3 -3
- package/bin/pc.js +3 -1
- package/docs/plans/2026-01-30-wa-auth-integration-design.md +118 -0
- package/docs/plans/2026-01-30-wa-auth-integration-plan.md +776 -0
- package/hooks/codex-capture.js +16 -3
- package/hooks/gemini-capture.js +37 -24
- package/hooks/prompt-capture.js +67 -23
- package/package.json +4 -4
- package/src/commands/login.js +125 -41
- package/src/commands/logout.js +6 -0
- package/src/commands/push.js +1 -29
- package/src/commands/save.js +21 -2
- package/src/commands/setup.js +44 -44
- package/src/commands/status.js +11 -0
- package/src/lib/api.js +0 -1
- package/src/lib/config.js +28 -1
- package/src/lib/crypto.js +39 -0
- package/src/lib/device-transfer.js +43 -0
- package/src/lib/keychain.js +80 -0
- package/src/lib/websocket.js +2 -6
- package/tests/device-login.test.js +121 -0
- package/tests/device-transfer.test.js +69 -0
- package/tests/keychain.test.js +40 -0
package/hooks/codex-capture.js
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
import { capturePrompt } from '../src/lib/api.js';
|
|
16
16
|
import { getFullContext } from '../src/lib/context.js';
|
|
17
|
-
import { isLoggedIn } from '../src/lib/config.js';
|
|
17
|
+
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
18
|
+
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
19
|
+
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
18
20
|
|
|
19
21
|
async function main() {
|
|
20
22
|
// Codex passes JSON as first argument
|
|
@@ -30,6 +32,10 @@ async function main() {
|
|
|
30
32
|
process.exit(0);
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
if (!isVaultAvailable()) {
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
try {
|
|
34
40
|
const event = JSON.parse(jsonArg);
|
|
35
41
|
|
|
@@ -80,9 +86,16 @@ async function main() {
|
|
|
80
86
|
context.session_id = event['thread-id'];
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
const vaultKey = await requireVaultKey({ silent: true });
|
|
90
|
+
if (!vaultKey) {
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
const { encrypted_content, content_iv } = encryptPrompt(content.trim(), vaultKey);
|
|
94
|
+
|
|
83
95
|
await capturePrompt({
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
...context,
|
|
97
|
+
encrypted_content,
|
|
98
|
+
content_iv
|
|
86
99
|
});
|
|
87
100
|
|
|
88
101
|
} catch (error) {
|
package/hooks/gemini-capture.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Gemini CLI
|
|
4
|
+
* Gemini CLI BeforeAgent hook for capturing prompts to PromptCellar.
|
|
5
5
|
*
|
|
6
6
|
* Gemini hooks receive JSON via stdin with:
|
|
7
|
-
* -
|
|
7
|
+
* - hook_event_name: the hook event (e.g., 'BeforeAgent')
|
|
8
8
|
* - prompt: the user's prompt
|
|
9
|
-
* - prompt_response: the agent's response (for AfterAgent)
|
|
10
9
|
* - session_id: session identifier
|
|
11
|
-
* -
|
|
10
|
+
* - cwd: working directory
|
|
11
|
+
* - transcript_path: path to session transcript
|
|
12
|
+
* - timestamp: event timestamp
|
|
12
13
|
*
|
|
13
14
|
* The hook must output valid JSON to stdout.
|
|
15
|
+
* Return {} for pass-through, or { decision: "block", reason: "..." } to block.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import { capturePrompt } from '../src/lib/api.js';
|
|
17
19
|
import { getFullContext } from '../src/lib/context.js';
|
|
18
|
-
import { isLoggedIn } from '../src/lib/config.js';
|
|
20
|
+
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
21
|
+
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
22
|
+
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
19
23
|
|
|
20
24
|
async function readStdin() {
|
|
21
25
|
return new Promise((resolve) => {
|
|
@@ -29,13 +33,9 @@ async function readStdin() {
|
|
|
29
33
|
});
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
function respond(
|
|
33
|
-
// Gemini expects JSON output
|
|
34
|
-
|
|
35
|
-
action: 'continue',
|
|
36
|
-
success
|
|
37
|
-
};
|
|
38
|
-
console.log(JSON.stringify(response));
|
|
36
|
+
function respond() {
|
|
37
|
+
// Gemini expects JSON output; empty object means pass-through
|
|
38
|
+
console.log('{}');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
async function main() {
|
|
@@ -43,26 +43,31 @@ async function main() {
|
|
|
43
43
|
const input = await readStdin();
|
|
44
44
|
|
|
45
45
|
if (!input.trim()) {
|
|
46
|
-
respond(
|
|
46
|
+
respond();
|
|
47
47
|
process.exit(0);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (!isLoggedIn()) {
|
|
51
|
-
respond(
|
|
51
|
+
respond();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!isVaultAvailable()) {
|
|
56
|
+
respond();
|
|
52
57
|
process.exit(0);
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
const event = JSON.parse(input);
|
|
56
61
|
|
|
57
|
-
//
|
|
58
|
-
if (
|
|
59
|
-
respond(
|
|
62
|
+
// Extract the user's prompt
|
|
63
|
+
if (!event.prompt) {
|
|
64
|
+
respond();
|
|
60
65
|
process.exit(0);
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
const content = event.prompt.trim();
|
|
64
69
|
if (!content) {
|
|
65
|
-
respond(
|
|
70
|
+
respond();
|
|
66
71
|
process.exit(0);
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -70,22 +75,30 @@ async function main() {
|
|
|
70
75
|
const context = getFullContext('gemini');
|
|
71
76
|
|
|
72
77
|
// Override with event data if available
|
|
73
|
-
if (event.
|
|
74
|
-
context.working_directory = event.
|
|
78
|
+
if (event.cwd) {
|
|
79
|
+
context.working_directory = event.cwd;
|
|
75
80
|
}
|
|
76
81
|
if (event.session_id) {
|
|
77
82
|
context.session_id = event.session_id;
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
const vaultKey = await requireVaultKey({ silent: true });
|
|
86
|
+
if (!vaultKey) {
|
|
87
|
+
respond();
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
const { encrypted_content, content_iv } = encryptPrompt(content, vaultKey);
|
|
91
|
+
|
|
80
92
|
await capturePrompt({
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
...context,
|
|
94
|
+
encrypted_content,
|
|
95
|
+
content_iv
|
|
83
96
|
});
|
|
84
97
|
|
|
85
|
-
respond(
|
|
98
|
+
respond();
|
|
86
99
|
} catch (error) {
|
|
87
100
|
// Still respond successfully to not block Gemini
|
|
88
|
-
respond(
|
|
101
|
+
respond();
|
|
89
102
|
}
|
|
90
103
|
}
|
|
91
104
|
|
package/hooks/prompt-capture.js
CHANGED
|
@@ -4,33 +4,56 @@
|
|
|
4
4
|
* Claude Code Stop hook for capturing prompts to PromptCellar.
|
|
5
5
|
*
|
|
6
6
|
* This script is called by Claude Code when a session ends.
|
|
7
|
-
*
|
|
7
|
+
* Claude Code Stop hooks receive JSON via stdin with:
|
|
8
|
+
* - transcript_path: path to the session JSONL transcript
|
|
9
|
+
* - session_id: session identifier
|
|
10
|
+
* - cwd: working directory
|
|
11
|
+
*
|
|
12
|
+
* It reads the transcript file and extracts the initial user prompt to capture.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { readFileSync, existsSync } from 'fs';
|
|
11
16
|
import { capturePrompt } from '../src/lib/api.js';
|
|
12
17
|
import { getFullContext } from '../src/lib/context.js';
|
|
13
|
-
import { isLoggedIn } from '../src/lib/config.js';
|
|
18
|
+
import { isLoggedIn, isVaultAvailable } from '../src/lib/config.js';
|
|
19
|
+
import { encryptPrompt } from '../src/lib/crypto.js';
|
|
20
|
+
import { requireVaultKey } from '../src/lib/keychain.js';
|
|
21
|
+
|
|
22
|
+
async function readStdin() {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let data = '';
|
|
25
|
+
process.stdin.setEncoding('utf8');
|
|
26
|
+
process.stdin.on('data', chunk => data += chunk);
|
|
27
|
+
process.stdin.on('end', () => resolve(data));
|
|
28
|
+
|
|
29
|
+
// Timeout after 1 second if no input
|
|
30
|
+
setTimeout(() => resolve(data), 1000);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
14
33
|
|
|
15
34
|
async function main() {
|
|
16
|
-
|
|
35
|
+
try {
|
|
36
|
+
const input = await readStdin();
|
|
17
37
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
38
|
+
if (!input.trim()) {
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
42
|
+
if (!isLoggedIn()) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
46
|
+
if (!isVaultAvailable()) {
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const event = JSON.parse(input);
|
|
51
|
+
const transcriptPath = event.transcript_path;
|
|
52
|
+
|
|
53
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
32
56
|
|
|
33
|
-
try {
|
|
34
57
|
const content = readFileSync(transcriptPath, 'utf8');
|
|
35
58
|
const lines = content.split('\n').filter(line => line.trim());
|
|
36
59
|
|
|
@@ -58,18 +81,39 @@ async function main() {
|
|
|
58
81
|
const initialPrompt = messages[0];
|
|
59
82
|
const context = getFullContext('claude-code');
|
|
60
83
|
|
|
84
|
+
// Override with event data if available
|
|
85
|
+
if (event.cwd) {
|
|
86
|
+
context.working_directory = event.cwd;
|
|
87
|
+
}
|
|
88
|
+
if (event.session_id) {
|
|
89
|
+
context.session_id = event.session_id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Content can be a string or an array of content blocks
|
|
93
|
+
let promptContent = initialPrompt.content;
|
|
94
|
+
if (Array.isArray(promptContent)) {
|
|
95
|
+
promptContent = promptContent
|
|
96
|
+
.filter(block => block.type === 'text')
|
|
97
|
+
.map(block => block.text)
|
|
98
|
+
.join('\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const vaultKey = await requireVaultKey({ silent: true });
|
|
102
|
+
if (!vaultKey) {
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
const { encrypted_content, content_iv } = encryptPrompt(promptContent, vaultKey);
|
|
106
|
+
|
|
61
107
|
await capturePrompt({
|
|
62
|
-
content: initialPrompt.content,
|
|
63
108
|
...context,
|
|
109
|
+
encrypted_content,
|
|
110
|
+
content_iv,
|
|
64
111
|
captured_at: initialPrompt.timestamp
|
|
65
112
|
});
|
|
66
113
|
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error('Error capturing prompt:', error.message);
|
|
72
|
-
process.exit(1);
|
|
114
|
+
} catch {
|
|
115
|
+
// Fail silently — stderr from hooks can cause issues
|
|
116
|
+
process.exit(0);
|
|
73
117
|
}
|
|
74
118
|
}
|
|
75
119
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptcellar/pc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "CLI for PromptCellar - sync prompts between your terminal and the cloud",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"pc-gemini-capture": "hooks/gemini-capture.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "
|
|
14
|
+
"test": "node --test"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"promptcellar",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"chalk": "^5.3.0",
|
|
40
40
|
"ora": "^8.0.0",
|
|
41
41
|
"inquirer": "^9.2.0",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
42
|
+
"keytar": "^7.9.0",
|
|
43
|
+
"socket.io-client": "^4.6.0"
|
|
44
44
|
}
|
|
45
45
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -1,68 +1,132 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
1
2
|
import ora from 'ora';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
setApiKey, setApiUrl, setAccountUrl,
|
|
6
|
+
setVaultAvailable,
|
|
7
|
+
isLoggedIn
|
|
8
|
+
} from '../lib/config.js';
|
|
9
|
+
import { generateTransferKeyPair, decryptTransfer } from '../lib/device-transfer.js';
|
|
10
|
+
import { b64urlEncode } from '../lib/crypto.js';
|
|
11
|
+
import { storeVaultKey } from '../lib/keychain.js';
|
|
4
12
|
|
|
5
13
|
const DEFAULT_API_URL = 'https://prompts.weldedanvil.com';
|
|
14
|
+
const DEFAULT_ACCOUNT_URL = 'https://account.weldedanvil.com';
|
|
15
|
+
let clientId = 'prompts-prod';
|
|
6
16
|
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
export function setClientId(id) { clientId = id; }
|
|
18
|
+
|
|
19
|
+
export async function requestDeviceCode(accountUrl, transferPublicKey) {
|
|
20
|
+
const payload = { client_id: clientId };
|
|
21
|
+
if (transferPublicKey) {
|
|
22
|
+
payload.transfer_public_key = transferPublicKey;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const response = await fetch(`${accountUrl}/device/code`, {
|
|
9
26
|
method: 'POST',
|
|
10
27
|
headers: { 'Content-Type': 'application/json' },
|
|
11
|
-
body: JSON.stringify(
|
|
28
|
+
body: JSON.stringify(payload),
|
|
12
29
|
});
|
|
13
30
|
|
|
14
31
|
if (!response.ok) {
|
|
15
|
-
|
|
32
|
+
const data = await response.json().catch(() => ({}));
|
|
33
|
+
throw new Error(data.error || 'Failed to request device code');
|
|
16
34
|
}
|
|
17
35
|
|
|
18
36
|
return response.json();
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
async function pollForApproval(
|
|
22
|
-
const
|
|
23
|
-
const
|
|
39
|
+
async function pollForApproval(accountUrl, deviceCode, expiresIn) {
|
|
40
|
+
const expiresAt = Date.now() + (expiresIn || 600) * 1000;
|
|
41
|
+
const interval = 5000;
|
|
24
42
|
|
|
25
43
|
while (Date.now() < expiresAt) {
|
|
26
|
-
await sleep(interval
|
|
44
|
+
await sleep(interval);
|
|
27
45
|
|
|
28
|
-
const response = await fetch(`${
|
|
46
|
+
const response = await fetch(`${accountUrl}/device/poll`, {
|
|
29
47
|
method: 'POST',
|
|
30
48
|
headers: { 'Content-Type': 'application/json' },
|
|
31
|
-
body: JSON.stringify({ device_code: deviceCode })
|
|
49
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
32
50
|
});
|
|
33
51
|
|
|
34
52
|
const data = await response.json();
|
|
35
53
|
|
|
36
|
-
if (
|
|
54
|
+
if (data.status === 'approved' && data.access_token) {
|
|
37
55
|
return data;
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
if (data.
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (data.error === 'expired_token') {
|
|
58
|
+
if (data.status === 'expired') {
|
|
45
59
|
throw new Error('Authorization request expired. Please try again.');
|
|
46
60
|
}
|
|
47
61
|
|
|
48
|
-
if (data.
|
|
49
|
-
|
|
62
|
+
if (data.status === 'rejected') {
|
|
63
|
+
const error = data.error || 'unknown';
|
|
64
|
+
if (error.startsWith('vault_missing')) {
|
|
65
|
+
throw new Error(`Vault error: ${error}`);
|
|
66
|
+
}
|
|
67
|
+
if (error === 'missing_transfer_key') {
|
|
68
|
+
throw new Error('Transfer key missing. Please try again.');
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Authorization rejected: ${error}`);
|
|
50
71
|
}
|
|
72
|
+
|
|
73
|
+
// status === 'pending' -- keep polling
|
|
51
74
|
}
|
|
52
75
|
|
|
53
76
|
throw new Error('Authorization request timed out. Please try again.');
|
|
54
77
|
}
|
|
55
78
|
|
|
79
|
+
async function exchangeTokenForApiKey(apiUrl, jwt, deviceName) {
|
|
80
|
+
const response = await fetch(`${apiUrl}/api/v1/auth/device-token`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: {
|
|
83
|
+
'Authorization': `Bearer ${jwt}`,
|
|
84
|
+
'Content-Type': 'application/json',
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({ device_name: deviceName }),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
const data = await response.json().catch(() => ({}));
|
|
91
|
+
throw new Error(data.error || 'Failed to exchange token');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return response.json();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function finalizeDeviceLogin({
|
|
98
|
+
approval,
|
|
99
|
+
privateKeyPem,
|
|
100
|
+
apiUrl,
|
|
101
|
+
deviceName,
|
|
102
|
+
decryptTransferFn = decryptTransfer,
|
|
103
|
+
storeVaultKeyFn = storeVaultKey,
|
|
104
|
+
exchangeTokenForApiKeyFn = exchangeTokenForApiKey,
|
|
105
|
+
}) {
|
|
106
|
+
if (!approval?.vault_transfer) {
|
|
107
|
+
throw new Error('Vault transfer missing. Please reauthorize.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!approval?.access_token) {
|
|
111
|
+
throw new Error('Missing access token. Please reauthorize.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let masterKeyB64;
|
|
115
|
+
if (approval.vault_transfer === 'cli_generate') {
|
|
116
|
+
// No browser extensions available — generate master key locally
|
|
117
|
+
masterKeyB64 = b64urlEncode(randomBytes(32));
|
|
118
|
+
} else {
|
|
119
|
+
masterKeyB64 = decryptTransferFn(approval.vault_transfer, privateKeyPem);
|
|
120
|
+
}
|
|
121
|
+
await storeVaultKeyFn(masterKeyB64);
|
|
122
|
+
|
|
123
|
+
return exchangeTokenForApiKeyFn(apiUrl, approval.access_token, deviceName);
|
|
124
|
+
}
|
|
125
|
+
|
|
56
126
|
function getDeviceName() {
|
|
57
127
|
const os = process.platform;
|
|
58
128
|
const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || 'CLI';
|
|
59
|
-
|
|
60
|
-
const osNames = {
|
|
61
|
-
darwin: 'macOS',
|
|
62
|
-
linux: 'Linux',
|
|
63
|
-
win32: 'Windows'
|
|
64
|
-
};
|
|
65
|
-
|
|
129
|
+
const osNames = { darwin: 'macOS', linux: 'Linux', win32: 'Windows' };
|
|
66
130
|
return `${osNames[os] || os} - ${hostname}`;
|
|
67
131
|
}
|
|
68
132
|
|
|
@@ -72,6 +136,8 @@ function sleep(ms) {
|
|
|
72
136
|
|
|
73
137
|
export async function login(options) {
|
|
74
138
|
const apiUrl = options?.url || DEFAULT_API_URL;
|
|
139
|
+
const accountUrl = options?.accountUrl || DEFAULT_ACCOUNT_URL;
|
|
140
|
+
if (options?.clientId) setClientId(options.clientId);
|
|
75
141
|
|
|
76
142
|
if (isLoggedIn()) {
|
|
77
143
|
console.log(chalk.yellow('Already logged in.'));
|
|
@@ -84,33 +150,51 @@ export async function login(options) {
|
|
|
84
150
|
const spinner = ora('Connecting...').start();
|
|
85
151
|
|
|
86
152
|
try {
|
|
87
|
-
// Step 1: Request device code
|
|
88
|
-
const
|
|
153
|
+
// Step 1: Request device code from wa-auth
|
|
154
|
+
const { publicKeySpkiB64url, privateKeyPem } = await generateTransferKeyPair();
|
|
155
|
+
const authData = await requestDeviceCode(accountUrl, publicKeySpkiB64url);
|
|
89
156
|
spinner.stop();
|
|
90
157
|
|
|
91
|
-
// Step 2: Show login URL
|
|
92
|
-
const
|
|
93
|
-
|
|
158
|
+
// Step 2: Show login URL
|
|
159
|
+
const code = authData.user_code;
|
|
160
|
+
const displayCode = code.length === 8
|
|
161
|
+
? `${code.slice(0, 4)}-${code.slice(4)}`.toUpperCase()
|
|
162
|
+
: code.toUpperCase();
|
|
163
|
+
const verifyUrl = `${authData.verification_url}?code=${code}`;
|
|
94
164
|
console.log('Open this URL in your browser to log in:\n');
|
|
95
165
|
console.log(chalk.cyan.bold(` ${verifyUrl}\n`));
|
|
96
|
-
console.log(chalk.dim(`Or go to ${authData.verification_url}
|
|
166
|
+
console.log(chalk.dim(`Or go to ${authData.verification_url}`));
|
|
167
|
+
console.log(chalk.dim(`and enter code: `) + chalk.white.bold(displayCode) + '\n');
|
|
97
168
|
|
|
98
169
|
// Step 3: Poll for approval
|
|
99
170
|
spinner.start('Waiting for you to authorize in browser...');
|
|
100
|
-
|
|
101
|
-
|
|
171
|
+
const approval = await pollForApproval(accountUrl, authData.device_code, authData.expires_in);
|
|
172
|
+
spinner.text = 'Securing vault key...';
|
|
173
|
+
|
|
174
|
+
// Step 4: Decrypt vault transfer and store key before exchanging JWT
|
|
175
|
+
const deviceName = getDeviceName();
|
|
176
|
+
const result = await finalizeDeviceLogin({
|
|
177
|
+
approval,
|
|
178
|
+
privateKeyPem,
|
|
102
179
|
apiUrl,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
authData.expires_in
|
|
106
|
-
);
|
|
180
|
+
deviceName,
|
|
181
|
+
});
|
|
107
182
|
|
|
108
|
-
// Step
|
|
109
|
-
setApiKey(result.
|
|
183
|
+
// Step 5: Store credentials
|
|
184
|
+
setApiKey(result.api_key);
|
|
110
185
|
setApiUrl(apiUrl);
|
|
186
|
+
setAccountUrl(accountUrl);
|
|
187
|
+
setVaultAvailable(true);
|
|
111
188
|
|
|
112
189
|
spinner.succeed(chalk.green('Logged in successfully!'));
|
|
113
|
-
console.log(`\nWelcome, ${chalk.cyan(result.
|
|
190
|
+
console.log(`\nWelcome, ${chalk.cyan(result.email)}!`);
|
|
191
|
+
|
|
192
|
+
if (result.vault && result.vault.available) {
|
|
193
|
+
console.log(chalk.green('\nVault ready — prompts will be encrypted end-to-end.'));
|
|
194
|
+
} else {
|
|
195
|
+
console.log(chalk.dim('\nSet up your vault at: ' + `${apiUrl}/dashboard/settings`));
|
|
196
|
+
}
|
|
197
|
+
|
|
114
198
|
console.log('\nRun ' + chalk.cyan('pc setup') + ' to configure auto-capture for your CLI tools.\n');
|
|
115
199
|
|
|
116
200
|
} catch (error) {
|
package/src/commands/logout.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { clearApiKey, isLoggedIn } from '../lib/config.js';
|
|
3
|
+
import { clearVaultKey } from '../lib/keychain.js';
|
|
3
4
|
|
|
4
5
|
export async function logout() {
|
|
5
6
|
if (!isLoggedIn()) {
|
|
@@ -7,6 +8,11 @@ export async function logout() {
|
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
try {
|
|
12
|
+
await clearVaultKey();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.log(chalk.yellow('Warning: failed to clear vault key.'));
|
|
15
|
+
}
|
|
10
16
|
clearApiKey();
|
|
11
17
|
console.log(chalk.green('Logged out successfully.'));
|
|
12
18
|
}
|
package/src/commands/push.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { getPrompt } from '../lib/api.js';
|
|
4
|
-
import { connect, disconnect } from '../lib/websocket.js';
|
|
5
4
|
import { isLoggedIn } from '../lib/config.js';
|
|
6
5
|
|
|
7
6
|
export async function push(slug, options) {
|
|
@@ -14,42 +13,15 @@ export async function push(slug, options) {
|
|
|
14
13
|
|
|
15
14
|
try {
|
|
16
15
|
const prompt = await getPrompt(slug);
|
|
17
|
-
spinner.text = 'Connecting to session...';
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Wait for connection
|
|
22
|
-
await new Promise((resolve, reject) => {
|
|
23
|
-
const timeout = setTimeout(() => {
|
|
24
|
-
reject(new Error('Connection timeout'));
|
|
25
|
-
}, 10000);
|
|
26
|
-
|
|
27
|
-
socket.on('registered', () => {
|
|
28
|
-
clearTimeout(timeout);
|
|
29
|
-
resolve();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
socket.on('error', (err) => {
|
|
33
|
-
clearTimeout(timeout);
|
|
34
|
-
reject(new Error(err.message));
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
spinner.text = 'Pushing prompt...';
|
|
39
|
-
|
|
40
|
-
// Push the prompt to ourselves (for now, just copy to clipboard simulation)
|
|
17
|
+
spinner.succeed(chalk.green('Prompt fetched!'));
|
|
41
18
|
console.log('\n' + chalk.cyan('Prompt content:'));
|
|
42
19
|
console.log(chalk.dim('─'.repeat(50)));
|
|
43
20
|
console.log(prompt.content);
|
|
44
21
|
console.log(chalk.dim('─'.repeat(50)));
|
|
45
|
-
|
|
46
|
-
spinner.succeed(chalk.green('Prompt fetched!'));
|
|
47
22
|
console.log('\nCopy the prompt above to use it in your session.');
|
|
48
|
-
|
|
49
|
-
disconnect();
|
|
50
23
|
} catch (error) {
|
|
51
24
|
spinner.fail(chalk.red('Failed: ' + error.message));
|
|
52
|
-
disconnect();
|
|
53
25
|
}
|
|
54
26
|
}
|
|
55
27
|
|
package/src/commands/save.js
CHANGED
|
@@ -3,7 +3,9 @@ import ora from 'ora';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { capturePrompt } from '../lib/api.js';
|
|
5
5
|
import { getFullContext } from '../lib/context.js';
|
|
6
|
-
import { isLoggedIn } from '../lib/config.js';
|
|
6
|
+
import { isLoggedIn, isVaultAvailable, getApiUrl } from '../lib/config.js';
|
|
7
|
+
import { encryptPrompt } from '../lib/crypto.js';
|
|
8
|
+
import { requireVaultKey } from '../lib/keychain.js';
|
|
7
9
|
|
|
8
10
|
export async function save(options) {
|
|
9
11
|
if (!isLoggedIn()) {
|
|
@@ -11,6 +13,12 @@ export async function save(options) {
|
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
if (!isVaultAvailable()) {
|
|
17
|
+
console.log(chalk.red('Vault not set up.') + ' Prompt capture requires a vault.');
|
|
18
|
+
console.log('Set up your vault at: ' + chalk.cyan(`${getApiUrl()}/dashboard/settings`));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
let content = options.message;
|
|
15
23
|
|
|
16
24
|
if (!content) {
|
|
@@ -31,8 +39,19 @@ export async function save(options) {
|
|
|
31
39
|
const spinner = ora('Saving prompt...').start();
|
|
32
40
|
|
|
33
41
|
try {
|
|
42
|
+
let vaultKey;
|
|
43
|
+
try {
|
|
44
|
+
vaultKey = await requireVaultKey();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
spinner.stop();
|
|
47
|
+
console.log(chalk.red(error.message));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const { encrypted_content, content_iv } = encryptPrompt(content.trim(), vaultKey);
|
|
51
|
+
|
|
34
52
|
const result = await capturePrompt({
|
|
35
|
-
|
|
53
|
+
encrypted_content,
|
|
54
|
+
content_iv,
|
|
36
55
|
...context
|
|
37
56
|
});
|
|
38
57
|
|