@promptcellar/pc 0.1.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/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # PromptCellar CLI
2
+
3
+ Command-line tool for capturing, managing, and reusing AI prompts with [PromptCellar](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 with your API key
15
+ pc login
16
+
17
+ # Set up auto-capture for Claude Code
18
+ pc setup
19
+
20
+ # Check status
21
+ pc status
22
+ ```
23
+
24
+ ## Commands
25
+
26
+ ### Authentication
27
+
28
+ ```bash
29
+ pc login # Login with your API key
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
+ Currently supports:
42
+ - Claude Code (via Stop hooks)
43
+
44
+ Coming soon:
45
+ - Cursor
46
+ - Windsurf
47
+
48
+ ### Manual Capture
49
+
50
+ ```bash
51
+ # Save a prompt manually
52
+ pc save -m "Your prompt text here"
53
+
54
+ # Open editor to write prompt
55
+ pc save
56
+
57
+ # Specify tool and model
58
+ pc save -m "prompt" -t aider -M gpt-4
59
+ ```
60
+
61
+ ### Push Prompts
62
+
63
+ ```bash
64
+ # Fetch and display a prompt by slug
65
+ pc push my-project/useful-prompt
66
+ ```
67
+
68
+ ### Configuration
69
+
70
+ ```bash
71
+ # View/change capture level
72
+ pc config
73
+
74
+ # Set capture level directly
75
+ pc config --level minimal # Tool + timestamp only
76
+ pc config --level standard # + working directory, git repo/branch
77
+ pc config --level rich # + session ID, git commit, remote URL
78
+ ```
79
+
80
+ ### Update
81
+
82
+ ```bash
83
+ pc update # Update to latest version
84
+ ```
85
+
86
+ ## Capture Levels
87
+
88
+ Control how much context is captured with your prompts:
89
+
90
+ | Level | Captured Data |
91
+ |-------|--------------|
92
+ | `minimal` | Tool, timestamp |
93
+ | `standard` | + Working directory, git repo, git branch |
94
+ | `rich` | + Session ID, git commit hash, remote URL |
95
+
96
+ ## Environment
97
+
98
+ Configuration is stored in:
99
+ - macOS: `~/Library/Preferences/promptcellar-nodejs/`
100
+ - Linux: `~/.config/promptcellar-nodejs/`
101
+ - Windows: `%APPDATA%/promptcellar-nodejs/`
102
+
103
+ ## API
104
+
105
+ The CLI communicates with your PromptCellar instance. Get your API key from:
106
+
107
+ Settings > API Keys in your PromptCellar dashboard.
108
+
109
+ ## License
110
+
111
+ MIT
package/bin/pc.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { login } from '../src/commands/login.js';
5
+ import { logout } from '../src/commands/logout.js';
6
+ import { status } from '../src/commands/status.js';
7
+ import { setup, unsetup } from '../src/commands/setup.js';
8
+ import { save } from '../src/commands/save.js';
9
+ import { push } from '../src/commands/push.js';
10
+ import { config } from '../src/commands/config.js';
11
+ import { update } from '../src/commands/update.js';
12
+
13
+ program
14
+ .name('pc')
15
+ .description('PromptCellar CLI - sync prompts between your terminal and the cloud')
16
+ .version('0.1.0');
17
+
18
+ program
19
+ .command('login')
20
+ .description('Authenticate with PromptCellar')
21
+ .action(login);
22
+
23
+ program
24
+ .command('logout')
25
+ .description('Clear stored credentials')
26
+ .action(logout);
27
+
28
+ program
29
+ .command('status')
30
+ .description('Show connection status and active sessions')
31
+ .action(status);
32
+
33
+ program
34
+ .command('setup')
35
+ .description('Auto-detect installed tools and configure hooks/wrappers')
36
+ .option('--force', 'Overwrite existing hooks/wrappers')
37
+ .action(setup);
38
+
39
+ program
40
+ .command('save')
41
+ .description('Manually save a prompt to current project')
42
+ .option('-m, --message <prompt>', 'Prompt content (opens editor if not provided)')
43
+ .option('-t, --tool <tool>', 'Tool name (default: manual)')
44
+ .option('-M, --model <model>', 'Model used')
45
+ .action(save);
46
+
47
+ program
48
+ .command('unsetup')
49
+ .description('Remove auto-capture hooks')
50
+ .action(unsetup);
51
+
52
+ program
53
+ .command('push <slug>')
54
+ .description('Push a prompt to the active CLI session')
55
+ .option('-t, --tool <tool>', 'Target tool (default: claude)')
56
+ .action(push);
57
+
58
+ program
59
+ .command('config')
60
+ .description('Adjust privacy/capture settings')
61
+ .option('-l, --level <level>', 'Set capture level: minimal, standard, or rich')
62
+ .action(config);
63
+
64
+ program
65
+ .command('update')
66
+ .description('Self-update to latest version')
67
+ .action(update);
68
+
69
+ program.parse();
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Claude Code Stop hook for capturing prompts to PromptCellar.
5
+ *
6
+ * This script is called by Claude Code when a session ends.
7
+ * It parses the transcript file and extracts user prompts to capture.
8
+ */
9
+
10
+ import { readFileSync, existsSync } from 'fs';
11
+ import { capturePrompt } from '../src/lib/api.js';
12
+ import { getFullContext } from '../src/lib/context.js';
13
+ import { isLoggedIn } from '../src/lib/config.js';
14
+
15
+ async function main() {
16
+ const transcriptPath = process.argv[2];
17
+
18
+ if (!transcriptPath) {
19
+ console.error('Usage: pc-capture <transcript-file>');
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!existsSync(transcriptPath)) {
24
+ console.error('Transcript file not found:', transcriptPath);
25
+ process.exit(1);
26
+ }
27
+
28
+ if (!isLoggedIn()) {
29
+ // Silently exit if not logged in
30
+ process.exit(0);
31
+ }
32
+
33
+ try {
34
+ const content = readFileSync(transcriptPath, 'utf8');
35
+ const lines = content.split('\n').filter(line => line.trim());
36
+
37
+ // Parse JSONL format
38
+ const messages = [];
39
+ for (const line of lines) {
40
+ try {
41
+ const entry = JSON.parse(line);
42
+ if (entry.type === 'human' && entry.message?.content) {
43
+ messages.push({
44
+ content: entry.message.content,
45
+ timestamp: entry.timestamp
46
+ });
47
+ }
48
+ } catch {
49
+ // Skip invalid JSON lines
50
+ }
51
+ }
52
+
53
+ if (messages.length === 0) {
54
+ process.exit(0);
55
+ }
56
+
57
+ // Capture the first user message (initial prompt)
58
+ const initialPrompt = messages[0];
59
+ const context = getFullContext('claude-code');
60
+
61
+ await capturePrompt({
62
+ content: initialPrompt.content,
63
+ ...context,
64
+ captured_at: initialPrompt.timestamp
65
+ });
66
+
67
+ // Optionally capture all prompts if configured
68
+ // For now, just capture the initial prompt
69
+
70
+ } catch (error) {
71
+ console.error('Error capturing prompt:', error.message);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ main();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@promptcellar/pc",
3
+ "version": "0.1.0",
4
+ "description": "CLI for PromptCellar - sync prompts between your terminal and the cloud",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "pc": "bin/pc.js",
9
+ "pc-capture": "hooks/prompt-capture.js"
10
+ },
11
+ "scripts": {
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "keywords": [
15
+ "promptcellar",
16
+ "prompts",
17
+ "cli",
18
+ "ai",
19
+ "llm",
20
+ "claude",
21
+ "codex",
22
+ "gemini"
23
+ ],
24
+ "author": "Welded Anvil Technologies LLC",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/WeldedAnvil/pc-cli.git"
29
+ },
30
+ "homepage": "https://prompts.weldedanvil.com",
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ },
34
+ "dependencies": {
35
+ "commander": "^12.0.0",
36
+ "conf": "^12.0.0",
37
+ "chalk": "^5.3.0",
38
+ "ora": "^8.0.0",
39
+ "inquirer": "^9.2.0",
40
+ "socket.io-client": "^4.6.0",
41
+ "node-fetch": "^3.3.0"
42
+ }
43
+ }
@@ -0,0 +1,45 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { getCaptureLevel, setCaptureLevel } from '../lib/config.js';
4
+
5
+ const CAPTURE_LEVELS = {
6
+ minimal: 'Just tool and timestamp',
7
+ standard: 'Tool, timestamp, working directory, git repo/branch',
8
+ rich: 'Everything including session ID, git commit, remote URL'
9
+ };
10
+
11
+ export async function config(options) {
12
+ if (options.level) {
13
+ try {
14
+ setCaptureLevel(options.level);
15
+ console.log(chalk.green(`Capture level set to: ${options.level}`));
16
+ console.log(chalk.dim(CAPTURE_LEVELS[options.level]));
17
+ } catch (error) {
18
+ console.log(chalk.red(error.message));
19
+ }
20
+ return;
21
+ }
22
+
23
+ const currentLevel = getCaptureLevel();
24
+
25
+ console.log(chalk.bold('\nCapture Level Configuration\n'));
26
+ console.log(`Current level: ${chalk.cyan(currentLevel)}\n`);
27
+
28
+ const choices = Object.entries(CAPTURE_LEVELS).map(([level, desc]) => ({
29
+ name: `${level} - ${desc}`,
30
+ value: level
31
+ }));
32
+
33
+ const { level } = await inquirer.prompt([{
34
+ type: 'list',
35
+ name: 'level',
36
+ message: 'Select capture level:',
37
+ choices,
38
+ default: currentLevel
39
+ }]);
40
+
41
+ setCaptureLevel(level);
42
+ console.log(chalk.green(`\nCapture level set to: ${level}`));
43
+ }
44
+
45
+ export default config;
@@ -0,0 +1,57 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+ import { setApiKey, setApiUrl, isLoggedIn } from '../lib/config.js';
5
+ import { testConnection } from '../lib/api.js';
6
+
7
+ export async function login() {
8
+ if (isLoggedIn()) {
9
+ const { overwrite } = await inquirer.prompt([{
10
+ type: 'confirm',
11
+ name: 'overwrite',
12
+ message: 'Already logged in. Replace existing credentials?',
13
+ default: false
14
+ }]);
15
+
16
+ if (!overwrite) {
17
+ console.log('Login cancelled.');
18
+ return;
19
+ }
20
+ }
21
+
22
+ const answers = await inquirer.prompt([
23
+ {
24
+ type: 'input',
25
+ name: 'apiKey',
26
+ message: 'Enter your PromptCellar API key:',
27
+ validate: (input) => {
28
+ if (!input.trim()) return 'API key is required';
29
+ if (!input.startsWith('pk_')) return 'Invalid API key format (should start with pk_)';
30
+ return true;
31
+ }
32
+ },
33
+ {
34
+ type: 'input',
35
+ name: 'apiUrl',
36
+ message: 'API URL (press enter for default):',
37
+ default: 'https://prompts.weldedanvil.com'
38
+ }
39
+ ]);
40
+
41
+ const spinner = ora('Testing connection...').start();
42
+
43
+ setApiKey(answers.apiKey.trim());
44
+ setApiUrl(answers.apiUrl.trim());
45
+
46
+ const result = await testConnection();
47
+
48
+ if (result.success) {
49
+ spinner.succeed(chalk.green('Logged in successfully!'));
50
+ console.log('\nRun ' + chalk.cyan('pc setup') + ' to configure auto-capture for your CLI tools.');
51
+ } else {
52
+ spinner.fail(chalk.red('Connection failed: ' + result.error));
53
+ console.log('Check your API key and try again.');
54
+ }
55
+ }
56
+
57
+ export default login;
@@ -0,0 +1,14 @@
1
+ import chalk from 'chalk';
2
+ import { clearApiKey, isLoggedIn } from '../lib/config.js';
3
+
4
+ export async function logout() {
5
+ if (!isLoggedIn()) {
6
+ console.log('Not logged in.');
7
+ return;
8
+ }
9
+
10
+ clearApiKey();
11
+ console.log(chalk.green('Logged out successfully.'));
12
+ }
13
+
14
+ export default logout;
@@ -0,0 +1,56 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import { getPrompt } from '../lib/api.js';
4
+ import { connect, disconnect } from '../lib/websocket.js';
5
+ import { isLoggedIn } from '../lib/config.js';
6
+
7
+ export async function push(slug, options) {
8
+ if (!isLoggedIn()) {
9
+ console.log(chalk.red('Not logged in.') + ' Run: pc login');
10
+ return;
11
+ }
12
+
13
+ const spinner = ora('Fetching prompt...').start();
14
+
15
+ try {
16
+ const prompt = await getPrompt(slug);
17
+ spinner.text = 'Connecting to session...';
18
+
19
+ const socket = connect(options.tool || 'claude');
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)
41
+ console.log('\n' + chalk.cyan('Prompt content:'));
42
+ console.log(chalk.dim('─'.repeat(50)));
43
+ console.log(prompt.content);
44
+ console.log(chalk.dim('─'.repeat(50)));
45
+
46
+ spinner.succeed(chalk.green('Prompt fetched!'));
47
+ console.log('\nCopy the prompt above to use it in your session.');
48
+
49
+ disconnect();
50
+ } catch (error) {
51
+ spinner.fail(chalk.red('Failed: ' + error.message));
52
+ disconnect();
53
+ }
54
+ }
55
+
56
+ export default push;
@@ -0,0 +1,46 @@
1
+ import inquirer from 'inquirer';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+ import { capturePrompt } from '../lib/api.js';
5
+ import { getFullContext } from '../lib/context.js';
6
+ import { isLoggedIn } from '../lib/config.js';
7
+
8
+ export async function save(options) {
9
+ if (!isLoggedIn()) {
10
+ console.log(chalk.red('Not logged in.') + ' Run: pc login');
11
+ return;
12
+ }
13
+
14
+ let content = options.message;
15
+
16
+ if (!content) {
17
+ const { promptContent } = await inquirer.prompt([{
18
+ type: 'editor',
19
+ name: 'promptContent',
20
+ message: 'Enter your prompt (opens editor):',
21
+ validate: (input) => {
22
+ if (!input.trim()) return 'Prompt content is required';
23
+ return true;
24
+ }
25
+ }]);
26
+ content = promptContent;
27
+ }
28
+
29
+ const context = getFullContext(options.tool || 'manual', options.model);
30
+
31
+ const spinner = ora('Saving prompt...').start();
32
+
33
+ try {
34
+ const result = await capturePrompt({
35
+ content: content.trim(),
36
+ ...context
37
+ });
38
+
39
+ spinner.succeed(chalk.green('Prompt saved!'));
40
+ console.log(` ID: ${chalk.dim(result.id)}`);
41
+ } catch (error) {
42
+ spinner.fail(chalk.red('Failed to save: ' + error.message));
43
+ }
44
+ }
45
+
46
+ export default save;
@@ -0,0 +1,124 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { homedir } from 'os';
6
+
7
+ const CLAUDE_HOOKS_PATH = join(homedir(), '.claude', 'hooks.json');
8
+ const HOOK_SCRIPT_NAME = 'pc-capture';
9
+
10
+ function getHooksConfig() {
11
+ if (existsSync(CLAUDE_HOOKS_PATH)) {
12
+ try {
13
+ return JSON.parse(readFileSync(CLAUDE_HOOKS_PATH, 'utf8'));
14
+ } catch {
15
+ return {};
16
+ }
17
+ }
18
+ return {};
19
+ }
20
+
21
+ function saveHooksConfig(config) {
22
+ const dir = dirname(CLAUDE_HOOKS_PATH);
23
+ if (!existsSync(dir)) {
24
+ mkdirSync(dir, { recursive: true });
25
+ }
26
+ writeFileSync(CLAUDE_HOOKS_PATH, JSON.stringify(config, null, 2));
27
+ }
28
+
29
+ function isHookInstalled(config) {
30
+ const stopHooks = config.Stop || [];
31
+ return stopHooks.some(hook =>
32
+ hook.command && hook.command.includes(HOOK_SCRIPT_NAME)
33
+ );
34
+ }
35
+
36
+ export async function setup() {
37
+ console.log(chalk.bold('\nPromptCellar CLI Setup\n'));
38
+
39
+ const tools = [
40
+ { name: 'Claude Code', value: 'claude', checked: true },
41
+ { name: 'Cursor (coming soon)', value: 'cursor', disabled: true },
42
+ { name: 'Windsurf (coming soon)', value: 'windsurf', disabled: true }
43
+ ];
44
+
45
+ const { selectedTools } = await inquirer.prompt([{
46
+ type: 'checkbox',
47
+ name: 'selectedTools',
48
+ message: 'Which tools do you want to capture prompts from?',
49
+ choices: tools
50
+ }]);
51
+
52
+ if (selectedTools.length === 0) {
53
+ console.log(chalk.yellow('No tools selected. Setup cancelled.'));
54
+ return;
55
+ }
56
+
57
+ for (const tool of selectedTools) {
58
+ if (tool === 'claude') {
59
+ await setupClaudeCode();
60
+ }
61
+ }
62
+
63
+ console.log(chalk.green('\nSetup complete!'));
64
+ console.log('Your prompts will be automatically captured to PromptCellar.\n');
65
+ }
66
+
67
+ async function setupClaudeCode() {
68
+ console.log(chalk.cyan('\nConfiguring Claude Code...'));
69
+
70
+ const config = getHooksConfig();
71
+
72
+ if (isHookInstalled(config)) {
73
+ console.log(chalk.yellow(' Hook already installed.'));
74
+
75
+ const { reinstall } = await inquirer.prompt([{
76
+ type: 'confirm',
77
+ name: 'reinstall',
78
+ message: 'Reinstall the hook?',
79
+ default: false
80
+ }]);
81
+
82
+ if (!reinstall) {
83
+ return;
84
+ }
85
+
86
+ // Remove existing hook
87
+ config.Stop = (config.Stop || []).filter(hook =>
88
+ !hook.command || !hook.command.includes(HOOK_SCRIPT_NAME)
89
+ );
90
+ }
91
+
92
+ // Add the hook
93
+ if (!config.Stop) {
94
+ config.Stop = [];
95
+ }
96
+
97
+ config.Stop.push({
98
+ command: `pc-capture "$CLAUDE_TRANSCRIPT_FILE"`,
99
+ description: 'Capture prompts to PromptCellar'
100
+ });
101
+
102
+ saveHooksConfig(config);
103
+ console.log(chalk.green(' Hook installed successfully.'));
104
+ }
105
+
106
+ export async function unsetup() {
107
+ console.log(chalk.bold('\nRemoving PromptCellar hooks...\n'));
108
+
109
+ const config = getHooksConfig();
110
+
111
+ if (!isHookInstalled(config)) {
112
+ console.log(chalk.yellow('No hooks installed.'));
113
+ return;
114
+ }
115
+
116
+ config.Stop = (config.Stop || []).filter(hook =>
117
+ !hook.command || !hook.command.includes(HOOK_SCRIPT_NAME)
118
+ );
119
+
120
+ saveHooksConfig(config);
121
+ console.log(chalk.green('Hooks removed successfully.'));
122
+ }
123
+
124
+ export default setup;
@@ -0,0 +1,41 @@
1
+ import chalk from 'chalk';
2
+ import { isLoggedIn, getApiUrl, getCaptureLevel, getSessionId } from '../lib/config.js';
3
+ import { testConnection } from '../lib/api.js';
4
+ import { getGitContext } from '../lib/context.js';
5
+
6
+ export async function status() {
7
+ console.log(chalk.bold('\nPromptCellar CLI Status\n'));
8
+
9
+ // Auth status
10
+ if (isLoggedIn()) {
11
+ const result = await testConnection();
12
+ if (result.success) {
13
+ console.log(chalk.green(' Logged in') + ` to ${getApiUrl()}`);
14
+ } else {
15
+ console.log(chalk.yellow(' Logged in') + ` but connection failed: ${result.error}`);
16
+ }
17
+ } else {
18
+ console.log(chalk.red(' Not logged in') + ' - Run: pc login');
19
+ }
20
+
21
+ // Capture level
22
+ console.log(` Capture level: ${chalk.cyan(getCaptureLevel())}`);
23
+
24
+ // Session ID
25
+ console.log(` Session ID: ${chalk.dim(getSessionId().slice(0, 8))}...`);
26
+
27
+ // Git context
28
+ const git = getGitContext();
29
+ if (git.git_repo) {
30
+ console.log(`\n Git repo: ${chalk.cyan(git.git_repo)}`);
31
+ if (git.git_branch) {
32
+ console.log(` Branch: ${chalk.cyan(git.git_branch)}`);
33
+ }
34
+ } else {
35
+ console.log(chalk.dim('\n Not in a git repository'));
36
+ }
37
+
38
+ console.log('');
39
+ }
40
+
41
+ export default status;
@@ -0,0 +1,45 @@
1
+ import { execFileSync } from 'child_process';
2
+ import ora from 'ora';
3
+ import chalk from 'chalk';
4
+
5
+ export async function update() {
6
+ const spinner = ora('Checking for updates...').start();
7
+
8
+ try {
9
+ // Check current version
10
+ const currentVersion = JSON.parse(
11
+ execFileSync('npm', ['pkg', 'get', 'version'], { encoding: 'utf8' })
12
+ ).replace(/"/g, '');
13
+
14
+ // Check latest version from npm
15
+ let latestVersion;
16
+ try {
17
+ const output = execFileSync('npm', ['view', '@promptcellar/pc', 'version'], {
18
+ encoding: 'utf8',
19
+ stdio: ['pipe', 'pipe', 'pipe']
20
+ }).trim();
21
+ latestVersion = output;
22
+ } catch {
23
+ spinner.fail(chalk.red('Failed to check for updates.'));
24
+ return;
25
+ }
26
+
27
+ if (currentVersion === latestVersion) {
28
+ spinner.succeed(chalk.green(`Already on latest version (${currentVersion})`));
29
+ return;
30
+ }
31
+
32
+ spinner.text = `Updating from ${currentVersion} to ${latestVersion}...`;
33
+
34
+ execFileSync('npm', ['install', '-g', '@promptcellar/pc@latest'], {
35
+ stdio: 'inherit'
36
+ });
37
+
38
+ spinner.succeed(chalk.green(`Updated to version ${latestVersion}`));
39
+ } catch (error) {
40
+ spinner.fail(chalk.red('Update failed: ' + error.message));
41
+ console.log('Try running: npm install -g @promptcellar/pc@latest');
42
+ }
43
+ }
44
+
45
+ export default update;
package/src/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // PromptCellar CLI - Main exports
2
+
3
+ export { login } from './commands/login.js';
4
+ export { logout } from './commands/logout.js';
5
+ export { status } from './commands/status.js';
6
+ export { setup, unsetup } from './commands/setup.js';
7
+ export { save } from './commands/save.js';
8
+ export { push } from './commands/push.js';
9
+ export { config } from './commands/config.js';
10
+ export { update } from './commands/update.js';
11
+
12
+ export * from './lib/config.js';
13
+ export * from './lib/api.js';
14
+ export * from './lib/context.js';
15
+ export * from './lib/websocket.js';
package/src/lib/api.js ADDED
@@ -0,0 +1,72 @@
1
+ import fetch from 'node-fetch';
2
+ import { getApiKey, getApiUrl } from './config.js';
3
+
4
+ async function request(endpoint, options = {}) {
5
+ const apiKey = getApiKey();
6
+ const apiUrl = getApiUrl();
7
+
8
+ if (!apiKey) {
9
+ throw new Error('Not logged in. Run: pc login');
10
+ }
11
+
12
+ const url = `${apiUrl}${endpoint}`;
13
+ const headers = {
14
+ 'Authorization': `Bearer ${apiKey}`,
15
+ 'Content-Type': 'application/json',
16
+ ...options.headers
17
+ };
18
+
19
+ const response = await fetch(url, {
20
+ ...options,
21
+ headers
22
+ });
23
+
24
+ if (!response.ok) {
25
+ const data = await response.json().catch(() => ({}));
26
+ throw new Error(data.error || `HTTP ${response.status}`);
27
+ }
28
+
29
+ return response.json();
30
+ }
31
+
32
+ export async function testConnection() {
33
+ const apiKey = getApiKey();
34
+ const apiUrl = getApiUrl();
35
+
36
+ if (!apiKey) {
37
+ return { success: false, error: 'Not logged in' };
38
+ }
39
+
40
+ try {
41
+ const response = await fetch(`${apiUrl}/health`);
42
+ if (response.ok) {
43
+ return { success: true };
44
+ }
45
+ return { success: false, error: 'Server not responding' };
46
+ } catch (error) {
47
+ return { success: false, error: error.message };
48
+ }
49
+ }
50
+
51
+ export async function capturePrompt(data) {
52
+ return request('/api/v1/capture', {
53
+ method: 'POST',
54
+ body: JSON.stringify(data)
55
+ });
56
+ }
57
+
58
+ export async function listCaptured(options = {}) {
59
+ const params = new URLSearchParams();
60
+ if (options.page) params.set('page', options.page);
61
+ if (options.tool) params.set('tool', options.tool);
62
+ if (options.starred) params.set('starred', 'true');
63
+
64
+ const query = params.toString();
65
+ return request(`/api/v1/captured${query ? '?' + query : ''}`);
66
+ }
67
+
68
+ export async function getPrompt(slug) {
69
+ return request(`/api/v1/prompts/${slug}`);
70
+ }
71
+
72
+ export default { testConnection, capturePrompt, listCaptured, getPrompt };
@@ -0,0 +1,70 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({
4
+ projectName: 'promptcellar',
5
+ schema: {
6
+ apiKey: {
7
+ type: 'string',
8
+ default: ''
9
+ },
10
+ apiUrl: {
11
+ type: 'string',
12
+ default: 'https://prompts.weldedanvil.com'
13
+ },
14
+ captureLevel: {
15
+ type: 'string',
16
+ enum: ['minimal', 'standard', 'rich'],
17
+ default: 'rich'
18
+ },
19
+ sessionId: {
20
+ type: 'string',
21
+ default: ''
22
+ }
23
+ }
24
+ });
25
+
26
+ export function getApiKey() {
27
+ return config.get('apiKey');
28
+ }
29
+
30
+ export function setApiKey(key) {
31
+ config.set('apiKey', key);
32
+ }
33
+
34
+ export function clearApiKey() {
35
+ config.delete('apiKey');
36
+ }
37
+
38
+ export function getApiUrl() {
39
+ return config.get('apiUrl');
40
+ }
41
+
42
+ export function setApiUrl(url) {
43
+ config.set('apiUrl', url);
44
+ }
45
+
46
+ export function getCaptureLevel() {
47
+ return config.get('captureLevel');
48
+ }
49
+
50
+ export function setCaptureLevel(level) {
51
+ if (!['minimal', 'standard', 'rich'].includes(level)) {
52
+ throw new Error('Invalid capture level. Must be: minimal, standard, or rich');
53
+ }
54
+ config.set('captureLevel', level);
55
+ }
56
+
57
+ export function getSessionId() {
58
+ let sessionId = config.get('sessionId');
59
+ if (!sessionId) {
60
+ sessionId = crypto.randomUUID();
61
+ config.set('sessionId', sessionId);
62
+ }
63
+ return sessionId;
64
+ }
65
+
66
+ export function isLoggedIn() {
67
+ return !!config.get('apiKey');
68
+ }
69
+
70
+ export default config;
@@ -0,0 +1,69 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { getCaptureLevel, getSessionId } from './config.js';
3
+
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 getGitContext() {
13
+ const level = getCaptureLevel();
14
+
15
+ // Minimal: no git context
16
+ if (level === 'minimal') {
17
+ return {};
18
+ }
19
+
20
+ const context = {};
21
+
22
+ // Standard: repo name and branch
23
+ const topLevel = execGit('rev-parse', '--show-toplevel');
24
+ if (topLevel) {
25
+ context.git_repo = topLevel.split('/').pop();
26
+ context.git_branch = execGit('rev-parse', '--abbrev-ref', 'HEAD');
27
+ }
28
+
29
+ // Rich: add remote URL and commit hash
30
+ if (level === 'rich' && topLevel) {
31
+ context.git_remote_url = execGit('remote', 'get-url', 'origin');
32
+ context.git_commit = execGit('rev-parse', 'HEAD');
33
+ }
34
+
35
+ return context;
36
+ }
37
+
38
+ export function getFullContext(tool, model) {
39
+ const level = getCaptureLevel();
40
+ const sessionId = getSessionId();
41
+
42
+ const context = {
43
+ tool,
44
+ captured_at: new Date().toISOString()
45
+ };
46
+
47
+ // Minimal: just tool and timestamp
48
+ if (level === 'minimal') {
49
+ return context;
50
+ }
51
+
52
+ // Standard: add working directory and model
53
+ context.working_directory = process.cwd();
54
+ if (model) {
55
+ context.model = model;
56
+ }
57
+
58
+ // Add git context
59
+ Object.assign(context, getGitContext());
60
+
61
+ // Rich: add session ID
62
+ if (level === 'rich') {
63
+ context.session_id = sessionId;
64
+ }
65
+
66
+ return context;
67
+ }
68
+
69
+ export default { getGitContext, getFullContext };
@@ -0,0 +1,78 @@
1
+ import { io } from 'socket.io-client';
2
+ import { getApiKey, getApiUrl, getSessionId } from './config.js';
3
+ import { getFullContext } from './context.js';
4
+
5
+ let socket = null;
6
+ let reconnectTimer = null;
7
+
8
+ export function connect(tool, onPromptReceived) {
9
+ const apiKey = getApiKey();
10
+ const apiUrl = getApiUrl();
11
+
12
+ if (!apiKey) {
13
+ throw new Error('Not logged in. Run: pc login');
14
+ }
15
+
16
+ if (socket?.connected) {
17
+ return socket;
18
+ }
19
+
20
+ socket = io(apiUrl, {
21
+ transports: ['websocket'],
22
+ reconnection: true,
23
+ reconnectionDelay: 1000,
24
+ reconnectionDelayMax: 5000
25
+ });
26
+
27
+ socket.on('connect', () => {
28
+ const context = getFullContext(tool);
29
+ socket.emit('register_cli', {
30
+ api_key: apiKey,
31
+ session_id: getSessionId(),
32
+ tool,
33
+ working_directory: context.working_directory,
34
+ git_repo: context.git_repo,
35
+ git_branch: context.git_branch
36
+ });
37
+ });
38
+
39
+ socket.on('registered', (data) => {
40
+ console.log(`Connected to PromptCellar (session: ${data.session_id})`);
41
+ });
42
+
43
+ socket.on('prompt_pushed', (data) => {
44
+ if (onPromptReceived) {
45
+ onPromptReceived(data);
46
+ }
47
+ });
48
+
49
+ socket.on('error', (data) => {
50
+ console.error('WebSocket error:', data.message);
51
+ });
52
+
53
+ socket.on('disconnect', (reason) => {
54
+ if (reason === 'io server disconnect') {
55
+ // Server disconnected, try to reconnect
56
+ socket.connect();
57
+ }
58
+ });
59
+
60
+ return socket;
61
+ }
62
+
63
+ export function disconnect() {
64
+ if (socket) {
65
+ socket.disconnect();
66
+ socket = null;
67
+ }
68
+ if (reconnectTimer) {
69
+ clearTimeout(reconnectTimer);
70
+ reconnectTimer = null;
71
+ }
72
+ }
73
+
74
+ export function isConnected() {
75
+ return socket?.connected || false;
76
+ }
77
+
78
+ export default { connect, disconnect, isConnected };