promptcase 1.0.4

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Stop command - Stop daemon for this OS session
3
+ *
4
+ * Behavior:
5
+ * - Stops the daemon process if running
6
+ * - Keeps auto-start configuration intact so daemon restarts on next boot
7
+ */
8
+ import { Command } from 'commander';
9
+ import { DaemonService } from '../services/daemon.js';
10
+ import { DEFAULT_API_URL } from '../lib/constants.js';
11
+ export function createStopCommand(apiUrl = DEFAULT_API_URL) {
12
+ const command = new Command('stop');
13
+ command
14
+ .description('Stop the daemon for this OS session (auto-start on next boot preserved)')
15
+ .action(async () => {
16
+ const daemon = new DaemonService(apiUrl);
17
+ console.log('\nšŸ›‘ Stopping PromptCase Daemon\n');
18
+ const isRunning = await daemon.isRunning();
19
+ if (!isRunning) {
20
+ console.log(' Daemon is not running.\n');
21
+ return;
22
+ }
23
+ const success = await daemon.stop();
24
+ if (success) {
25
+ console.log(' āœ… Daemon stopped successfully');
26
+ console.log(' ā„¹ļø Auto-start is still configured. Daemon will restart on next boot.');
27
+ }
28
+ else {
29
+ console.log(' āš ļø Could not stop daemon gracefully. Kill the process manually:');
30
+ if (process.platform === 'win32') {
31
+ console.log(' taskkill /F /IM node.exe');
32
+ }
33
+ else {
34
+ console.log(' pkill -f "promptcase start"');
35
+ }
36
+ }
37
+ console.log('\n Auto-start preserved. To completely remove auto-start, run:');
38
+ console.log(' promptcase logout');
39
+ console.log(' Or manually remove the LaunchAgent / systemd service / .bat file.\n');
40
+ });
41
+ return command;
42
+ }
43
+ //# sourceMappingURL=stop.js.map
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Sync command - Force a sync without invalidating login
3
+ *
4
+ * Used when daemon gets stuck or sync isn't working.
5
+ * Performs diagnostics and forces a refresh of the sync process.
6
+ */
7
+ import { Command } from 'commander';
8
+ import { spawn } from 'child_process';
9
+ import { APIService } from '../services/api.js';
10
+ import { DaemonService } from '../services/daemon.js';
11
+ import { ClaudeCaptureService } from '../services/claude-capture.js';
12
+ import { getConfig } from '../lib/config.js';
13
+ import { scriptPath } from '../lib/path.js';
14
+ import { DEFAULT_API_URL } from '../lib/constants.js';
15
+ export function createSyncCommand(apiUrl = DEFAULT_API_URL) {
16
+ const command = new Command('sync');
17
+ command
18
+ .description('Force a sync and run diagnostics (does not invalidate login)')
19
+ .action(async () => {
20
+ console.log('\nšŸ”„ Force Sync & Diagnostics\n');
21
+ const config = getConfig();
22
+ const api = new APIService(apiUrl);
23
+ const daemon = new DaemonService(apiUrl);
24
+ // 1. Authentication check
25
+ const isAuthenticated = await config.isAuthenticated();
26
+ if (!isAuthenticated) {
27
+ console.log(' āŒ Not authenticated. Run "promptcase init" first.');
28
+ return;
29
+ }
30
+ const credentials = await config.getCredentials();
31
+ if (!credentials) {
32
+ console.log(' āŒ No credentials found. Run "promptcase init" first.');
33
+ return;
34
+ }
35
+ api.setTokens(credentials.accessToken, credentials.refreshToken);
36
+ // 2. Verify token is still valid
37
+ console.log(' šŸ“” Checking API connection...');
38
+ const userInfo = await api.verifyToken();
39
+ if (!userInfo) {
40
+ console.log(' āŒ Token is invalid or expired. Run "promptcase init" to re-authenticate.');
41
+ return;
42
+ }
43
+ console.log(` āœ… API connection successful (User: ${userInfo.userId})`);
44
+ // 3. Check daemon status
45
+ console.log('\n šŸ“Š Daemon status:');
46
+ const isRunning = await daemon.isRunning();
47
+ if (isRunning) {
48
+ const daemonStatus = await config.getDaemonStatus();
49
+ console.log(` Status: Running (PID ${daemonStatus.pid})`);
50
+ console.log(` Last sync: ${daemonStatus.lastSyncAt ? new Date(daemonStatus.lastSyncAt).toLocaleString() : 'Never'}`);
51
+ }
52
+ else {
53
+ console.log(' Status: Not running');
54
+ console.log(' āš ļø Daemon is not running - starting a sync anyway');
55
+ }
56
+ // 4. Check Claude installation
57
+ console.log('\n šŸ“¦ Claude Code check:');
58
+ const capture = new ClaudeCaptureService();
59
+ const sessionCount = await capture.countSessionFiles();
60
+ if (sessionCount > 0) {
61
+ console.log(` āœ… Claude installed with ${sessionCount} session file(s)`);
62
+ }
63
+ else {
64
+ console.log(' āš ļø Claude Code not installed (~/.claude not found)');
65
+ console.log(' Nothing to capture until Claude Code is installed and used.');
66
+ }
67
+ // 5. Perform sync
68
+ console.log('\n šŸ”„ Triggering sync...');
69
+ const initialized = await daemon.initialize();
70
+ if (!initialized) {
71
+ console.log(' āŒ Failed to initialize daemon');
72
+ return;
73
+ }
74
+ const status = await daemon.sync();
75
+ console.log('\n šŸ“ˆ Results:');
76
+ if (status.lastSyncAt) {
77
+ console.log(` Last sync: ${status.lastSyncAt.toLocaleString()}`);
78
+ console.log(` Total synced (lifetime): ${status.promptsSynced} prompts`);
79
+ if (status.errors.length > 0) {
80
+ console.log(` Errors (${status.errors.length}):`);
81
+ status.errors.slice(-3).forEach((err) => console.log(` - ${err}`));
82
+ }
83
+ }
84
+ // 6. If daemon is not running, start it
85
+ if (!isRunning) {
86
+ console.log('\n šŸš€ Daemon was not running. Starting now in background...');
87
+ const child = spawn(process.execPath, [scriptPath(), 'start'], {
88
+ detached: true,
89
+ stdio: 'ignore',
90
+ windowsHide: true,
91
+ });
92
+ child.unref();
93
+ // Wait then verify
94
+ await new Promise((resolve) => setTimeout(resolve, 1500));
95
+ const nowRunning = await daemon.isRunning();
96
+ if (nowRunning) {
97
+ console.log(' āœ… Daemon started successfully');
98
+ console.log(' ā„¹ļø Auto-start is configured - daemon will resume on next boot');
99
+ }
100
+ else {
101
+ console.log(' āš ļø Daemon may not have started. Check with "promptcase status"');
102
+ }
103
+ }
104
+ // 7. Refresh token if needed
105
+ const expiresAt = new Date(credentials.expiresAt);
106
+ const daysLeft = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
107
+ if (daysLeft < 7) {
108
+ console.log(`\n āš ļø Token expires in ${daysLeft} days. Consider running "promptcase init" to refresh.`);
109
+ }
110
+ else {
111
+ console.log(`\n āœ… Login still valid for ${daysLeft} more days`);
112
+ }
113
+ console.log('');
114
+ });
115
+ return command;
116
+ }
117
+ //# sourceMappingURL=sync.js.map
package/dist/index.js ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PromptCase CLI - Main Entry Point
4
+ *
5
+ * A daemon to capture and sync AI prompts from Claude Code
6
+ * to the PromptCase web application.
7
+ */
8
+ import { Command } from 'commander';
9
+ import { createRequire } from 'module';
10
+ import { createInitCommand, handleNonInteractiveToken } from './commands/init.js';
11
+ import { createStartCommand } from './commands/start.js';
12
+ import { createStopCommand } from './commands/stop.js';
13
+ import { createSyncCommand } from './commands/sync.js';
14
+ import { createShowCommand } from './commands/show.js';
15
+ import { createStatusCommand } from './commands/status.js';
16
+ import { createLogoutCommand } from './commands/logout.js';
17
+ import { DEFAULT_API_URL } from './lib/constants.js';
18
+ // Default API URL (same as deployed web app)
19
+ // Read version from package.json so it always matches the published version
20
+ const require = createRequire(import.meta.url);
21
+ const pkg = require('../package.json');
22
+ const program = new Command();
23
+ program
24
+ .name('promptcase')
25
+ .description('CLI daemon to capture and sync AI prompts from Claude Code to PromptCase')
26
+ .version(pkg.version)
27
+ .option('-a, --api-url <url>', 'API URL', DEFAULT_API_URL);
28
+ // Check if init command is called with piped input (no TTY and no options/flags)
29
+ const isPipedInit = process.argv[2] === 'init' &&
30
+ process.argv.length === 3 &&
31
+ !process.stdin.isTTY;
32
+ if (isPipedInit) {
33
+ // Read piped JSON from stdin
34
+ let pipedData = '';
35
+ let handled = false;
36
+ process.stdin.on('data', (chunk) => {
37
+ pipedData += chunk;
38
+ });
39
+ process.stdin.on('end', () => {
40
+ if (!handled && pipedData) {
41
+ handled = true;
42
+ handleNonInteractiveToken(pipedData.trim(), DEFAULT_API_URL);
43
+ }
44
+ });
45
+ // Failsafe timeout in case stdin never ends
46
+ setTimeout(() => {
47
+ if (!handled) {
48
+ handled = true;
49
+ // No piped data within 200ms - use commander
50
+ runCommander();
51
+ }
52
+ }, 200);
53
+ }
54
+ else {
55
+ runCommander();
56
+ }
57
+ function runCommander() {
58
+ // Register commands
59
+ program.addCommand(createInitCommand());
60
+ // `start` is an internal entry point used by auto-start services; hide it from help
61
+ program.addCommand(createStartCommand(DEFAULT_API_URL), { hidden: true });
62
+ program.addCommand(createStopCommand(DEFAULT_API_URL));
63
+ program.addCommand(createSyncCommand(DEFAULT_API_URL));
64
+ program.addCommand(createShowCommand(DEFAULT_API_URL));
65
+ program.addCommand(createStatusCommand(DEFAULT_API_URL));
66
+ program.addCommand(createLogoutCommand(DEFAULT_API_URL));
67
+ // Parse and execute
68
+ program.parse(process.argv);
69
+ // Show help if no command provided
70
+ if (process.argv.length === 2) {
71
+ console.log('');
72
+ program.outputHelp();
73
+ console.log('\nExamples:');
74
+ console.log(' promptcase init Authenticate (and auto-start daemon)');
75
+ console.log(' promptcase status Show auth + daemon + sync status');
76
+ console.log(' promptcase sync Force sync + run diagnostics');
77
+ console.log(' promptcase show Show last 15 synced prompts');
78
+ console.log(' promptcase stop Stop daemon (auto-start kept)');
79
+ console.log(' promptcase logout Logout and invalidate token\n');
80
+ }
81
+ }
82
+ // Handle uncaught errors
83
+ process.on('uncaughtException', (error) => {
84
+ console.error('\nāŒ Unexpected error:', error.message);
85
+ process.exit(1);
86
+ });
87
+ // Log unhandled rejections but don't exit. The daemon and other commands
88
+ // handle their own promise errors; a transient network failure shouldn't
89
+ // crash a long-running background process.
90
+ process.on('unhandledRejection', (reason) => {
91
+ const message = reason instanceof Error ? reason.message : String(reason);
92
+ console.error('\nāš ļø Unhandled promise rejection:', message);
93
+ });
94
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Configuration store for CLI credentials and settings
3
+ */
4
+ import Conf from 'conf';
5
+ import { createRequire } from 'module';
6
+ import os from 'os';
7
+ import path from 'path';
8
+ import { DEFAULT_API_URL, SYNC_INTERVAL_MS } from './constants.js';
9
+ const require = createRequire(import.meta.url);
10
+ // From compiled file at `dist/lib/config.js`, package.json lives at
11
+ // `dist/../package.json` (two levels up). Reading via createRequire with
12
+ // an absolute path avoids any path-resolution drift between dev and prod.
13
+ const pkg = require('../../package.json');
14
+ const CLI_VERSION = pkg.version;
15
+ export class ConfigService {
16
+ store;
17
+ configDir;
18
+ constructor() {
19
+ // Use OS-appropriate config directory
20
+ this.configDir = path.join(os.homedir(), '.promptcase');
21
+ this.store = new Conf({
22
+ projectName: 'promptcase',
23
+ projectVersion: CLI_VERSION,
24
+ defaults: {
25
+ credentials: null,
26
+ daemon: {
27
+ status: {
28
+ isRunning: false,
29
+ pid: undefined,
30
+ startedAt: null,
31
+ lastSyncAt: null,
32
+ nextSyncAt: null,
33
+ promptsSynced: 0,
34
+ },
35
+ },
36
+ settings: {
37
+ syncInterval: SYNC_INTERVAL_MS,
38
+ apiUrl: DEFAULT_API_URL,
39
+ lastKnownHistoryFile: null,
40
+ lastSyncCursor: null,
41
+ },
42
+ },
43
+ });
44
+ }
45
+ // Credentials
46
+ async getCredentials() {
47
+ return this.store.get('credentials');
48
+ }
49
+ async setCredentials(credentials) {
50
+ this.store.set('credentials', credentials);
51
+ }
52
+ async clearCredentials() {
53
+ this.store.delete('credentials');
54
+ }
55
+ async hasCredentials() {
56
+ const creds = await this.getCredentials();
57
+ return creds !== null;
58
+ }
59
+ // Daemon status
60
+ async getDaemonStatus() {
61
+ return this.store.get('daemon.status');
62
+ }
63
+ async setDaemonStatus(status) {
64
+ this.store.set('daemon.status', status);
65
+ }
66
+ // Settings
67
+ async getSyncInterval() {
68
+ return this.store.get('settings.syncInterval');
69
+ }
70
+ async setSyncInterval(interval) {
71
+ this.store.set('settings.syncInterval', interval);
72
+ }
73
+ async getApiUrl() {
74
+ return this.store.get('settings.apiUrl');
75
+ }
76
+ async setApiUrl(url) {
77
+ this.store.set('settings.apiUrl', url);
78
+ }
79
+ async getLastKnownHistoryFile() {
80
+ return this.store.get('settings.lastKnownHistoryFile');
81
+ }
82
+ async setLastKnownHistoryFile(file) {
83
+ this.store.set('settings.lastKnownHistoryFile', file);
84
+ }
85
+ /**
86
+ * ISO timestamp string of the most recent prompt successfully synced.
87
+ * Used as a cursor so subsequent syncs only fetch newer prompts.
88
+ * `null` means "no cursor yet, do a full backfill".
89
+ */
90
+ async getLastSyncCursor() {
91
+ return this.store.get('settings.lastSyncCursor');
92
+ }
93
+ async setLastSyncCursor(cursor) {
94
+ this.store.set('settings.lastSyncCursor', cursor);
95
+ }
96
+ // Check if token is expired
97
+ async isTokenExpired() {
98
+ const creds = await this.getCredentials();
99
+ if (!creds)
100
+ return true;
101
+ const expiresAt = new Date(creds.expiresAt);
102
+ return expiresAt.getTime() < Date.now();
103
+ }
104
+ // Get config directory path
105
+ getConfigDir() {
106
+ return this.configDir;
107
+ }
108
+ // Get credentials file path
109
+ getCredentialsFilePath() {
110
+ return path.join(this.configDir, 'credentials');
111
+ }
112
+ // Clear all data (for logout)
113
+ async clearAll() {
114
+ this.store.clear();
115
+ }
116
+ // Check if authenticated (has valid credentials)
117
+ async isAuthenticated() {
118
+ const hasCreds = await this.hasCredentials();
119
+ if (!hasCreds)
120
+ return false;
121
+ return !(await this.isTokenExpired());
122
+ }
123
+ }
124
+ // Singleton instance
125
+ let configInstance = null;
126
+ export function getConfig() {
127
+ if (!configInstance) {
128
+ configInstance = new ConfigService();
129
+ }
130
+ return configInstance;
131
+ }
132
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Centralized constants for the CLI
3
+ *
4
+ * Previously these were duplicated across 7+ files. Keeping them in one
5
+ * place makes it obvious what to change when the production URL changes.
6
+ */
7
+ export const DEFAULT_API_URL = 'https://web-six-iota-a0lchij54v.vercel.app';
8
+ /** Where users generate a CLI token from the web app. */
9
+ export const CLI_TOKEN_URL = `${DEFAULT_API_URL}/dashboard/devices/cli-token`;
10
+ /** Sync interval (1 minute) — daemon loop ticks this often. */
11
+ export const SYNC_INTERVAL_MS = 60_000;
12
+ /** Max prompts fetched per sync (avoid huge payloads). */
13
+ export const MAX_PROMPTS_PER_SYNC = 100;
14
+ /** Prompts shorter than this are filtered out as noise. */
15
+ export const PROMPT_MIN_LENGTH = 10;
16
+ /** Friendly process name shown in `ps`/Activity Monitor when daemon is running. */
17
+ export const DAEMON_PROCESS_TITLE = 'PromptCase Daemon';
18
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared ESM path helpers
3
+ *
4
+ * Node.js ESM does not provide `__filename` / `__dirname`. This module
5
+ * computes them once (per module evaluation) and exposes:
6
+ * - `scriptPath()` — absolute path to dist/index.js (used to spawn
7
+ * detached daemon processes)
8
+ * - `here()` — directory of the current source file (for any other
9
+ * call site that needs `__dirname`)
10
+ */
11
+ import { fileURLToPath } from 'url';
12
+ import { dirname, resolve } from 'path';
13
+ const here_filename = fileURLToPath(import.meta.url);
14
+ const here_dirname = dirname(here_filename);
15
+ /**
16
+ * Absolute path to this CLI's compiled entry point. Used to spawn the
17
+ * daemon process detached from the parent's terminal.
18
+ */
19
+ export function scriptPath() {
20
+ return resolve(here_dirname, '..', 'index.js');
21
+ }
22
+ /** Absolute path to the directory containing the compiled file. */
23
+ export function here() {
24
+ return here_dirname;
25
+ }
26
+ //# sourceMappingURL=path.js.map
@@ -0,0 +1,211 @@
1
+ /**
2
+ * API service for communicating with PromptCase backend
3
+ */
4
+ import got from 'got';
5
+ import { createHash } from 'crypto';
6
+ export class APIService {
7
+ apiUrl;
8
+ accessToken = null;
9
+ refreshToken = null;
10
+ constructor(apiUrl) {
11
+ this.apiUrl = apiUrl.replace(/\/$/, ''); // Remove trailing slash
12
+ }
13
+ /**
14
+ * Set authentication tokens
15
+ */
16
+ setTokens(accessToken, refreshToken) {
17
+ this.accessToken = accessToken;
18
+ this.refreshToken = refreshToken;
19
+ }
20
+ /**
21
+ * Clear authentication tokens
22
+ */
23
+ clearTokens() {
24
+ this.accessToken = null;
25
+ this.refreshToken = null;
26
+ }
27
+ /**
28
+ * Check if authenticated
29
+ */
30
+ isAuthenticated() {
31
+ return this.accessToken !== null;
32
+ }
33
+ /**
34
+ * Read the current access token. Exposed for callers that need to persist
35
+ * refreshed tokens (e.g. the daemon's hourly refresh-write task).
36
+ */
37
+ getAccessToken() {
38
+ return this.accessToken;
39
+ }
40
+ /**
41
+ * Read the current refresh token.
42
+ */
43
+ getRefreshToken() {
44
+ return this.refreshToken;
45
+ }
46
+ /**
47
+ * Refresh the access token using refresh token
48
+ */
49
+ async refreshAccessToken() {
50
+ if (!this.refreshToken) {
51
+ throw new Error('No refresh token available');
52
+ }
53
+ try {
54
+ const response = await got.post(`${this.apiUrl}/api/auth/refresh`, {
55
+ json: {
56
+ refresh_token: this.refreshToken,
57
+ },
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ responseType: 'json',
62
+ });
63
+ const data = response.body;
64
+ this.accessToken = data.access_token;
65
+ if (data.refresh_token) {
66
+ this.refreshToken = data.refresh_token;
67
+ }
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ console.error('Failed to refresh token:', error.message);
72
+ return false;
73
+ }
74
+ }
75
+ /**
76
+ * Make an authenticated API request
77
+ */
78
+ async request(endpoint, options = {}) {
79
+ const { method = 'GET', body, retry = true } = options;
80
+ if (!this.accessToken) {
81
+ throw new Error('Not authenticated');
82
+ }
83
+ const headers = {
84
+ 'Authorization': `Bearer ${this.accessToken}`,
85
+ 'Content-Type': 'application/json',
86
+ };
87
+ try {
88
+ const response = await got(`${this.apiUrl}${endpoint}`, {
89
+ method,
90
+ json: body,
91
+ headers,
92
+ responseType: 'json',
93
+ });
94
+ return response.body;
95
+ }
96
+ catch (error) {
97
+ // Handle 401 - try to refresh token
98
+ if (error.response?.statusCode === 401 && retry) {
99
+ const refreshed = await this.refreshAccessToken();
100
+ if (refreshed) {
101
+ // Retry the request with new token
102
+ return this.request(endpoint, { method, body, retry: false });
103
+ }
104
+ }
105
+ // Re-throw with more info
106
+ if (error.response?.body) {
107
+ throw new Error(error.response.body.error || error.message);
108
+ }
109
+ throw error;
110
+ }
111
+ }
112
+ /**
113
+ * Sync prompts to the backend via /api/prompts/batch
114
+ *
115
+ * The batch endpoint accepts an array of prompt objects and dedupes by
116
+ * content_hash (falls back to SHA-256 of content if not provided). Returns
117
+ * `{synced, failed, total}` so the daemon can update its counters.
118
+ */
119
+ async syncPrompts(prompts) {
120
+ if (!this.accessToken) {
121
+ throw new Error('Not authenticated');
122
+ }
123
+ // Compute SHA-256 for any prompt missing a content_hash, so the server
124
+ // can dedup reliably. Prompts that already have a hash pass through as-is.
125
+ const payload = prompts.map((p) => ({
126
+ ...p,
127
+ content_hash: p.content_hash ?? generatePromptHash(p.content),
128
+ }));
129
+ const response = await got.post(`${this.apiUrl}/api/prompts/batch`, {
130
+ json: { prompts: payload },
131
+ headers: {
132
+ 'Authorization': `Bearer ${this.accessToken}`,
133
+ 'Content-Type': 'application/json',
134
+ },
135
+ responseType: 'json',
136
+ // Server processes up to 100 prompts sequentially with dedup; observed
137
+ // ~56s for a full batch on slow networks. Use 90s headroom.
138
+ timeout: { request: 90_000 },
139
+ });
140
+ const body = response.body;
141
+ return {
142
+ synced: body.synced ?? 0,
143
+ failed: body.failed ?? 0,
144
+ total: body.total ?? payload.length,
145
+ };
146
+ }
147
+ /**
148
+ * Get recent prompts from backend
149
+ */
150
+ async getRecentPrompts(limit = 15) {
151
+ const response = await this.request(`/api/prompts?limit=${limit}`);
152
+ return response.prompts;
153
+ }
154
+ /**
155
+ * Verify token is valid by calling /api/auth/device
156
+ * Returns user info if valid
157
+ */
158
+ async verifyToken() {
159
+ if (!this.accessToken)
160
+ return null;
161
+ try {
162
+ const response = await got(`${this.apiUrl}/api/auth/device`, {
163
+ method: 'GET',
164
+ headers: {
165
+ 'Authorization': `Bearer ${this.accessToken}`,
166
+ 'Content-Type': 'application/json',
167
+ },
168
+ responseType: 'json',
169
+ });
170
+ const data = response.body;
171
+ return {
172
+ userId: data.device?.client_id || 'unknown',
173
+ deviceId: data.device?.id,
174
+ deviceName: data.device?.name || 'CLI',
175
+ deviceType: data.device?.type || 'cli',
176
+ };
177
+ }
178
+ catch (error) {
179
+ return null;
180
+ }
181
+ }
182
+ /**
183
+ * Revoke device token (logout)
184
+ */
185
+ async revokeDevice() {
186
+ if (!this.accessToken)
187
+ return false;
188
+ try {
189
+ await got(`${this.apiUrl}/api/auth/device`, {
190
+ method: 'DELETE',
191
+ headers: {
192
+ 'Authorization': `Bearer ${this.accessToken}`,
193
+ 'Content-Type': 'application/json',
194
+ },
195
+ responseType: 'json',
196
+ });
197
+ return true;
198
+ }
199
+ catch (error) {
200
+ console.error('Failed to revoke device:', error.message);
201
+ return false;
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * Generate hash for deduplication
207
+ */
208
+ export function generatePromptHash(content) {
209
+ return createHash('sha256').update(content).digest('hex');
210
+ }
211
+ //# sourceMappingURL=api.js.map