@cloudcli-ai/cloudcli 1.29.3 → 1.29.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.
Files changed (55) hide show
  1. package/dist/assets/{index-D-9h5kNf.css → index-BBAE1OJ_.css} +1 -1
  2. package/dist/assets/{index-y8yqvoLv.js → index-Cyya3LhN.js} +200 -200
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/claude-sdk.js +11 -3
  5. package/dist-server/server/claude-sdk.js.map +1 -1
  6. package/dist-server/server/cursor-cli.js +7 -1
  7. package/dist-server/server/cursor-cli.js.map +1 -1
  8. package/dist-server/server/gemini-cli.js +15 -1
  9. package/dist-server/server/gemini-cli.js.map +1 -1
  10. package/dist-server/server/index.js +7 -76
  11. package/dist-server/server/index.js.map +1 -1
  12. package/dist-server/server/openai-codex.js +7 -1
  13. package/dist-server/server/openai-codex.js.map +1 -1
  14. package/dist-server/server/projects.js +41 -47
  15. package/dist-server/server/projects.js.map +1 -1
  16. package/dist-server/server/providers/claude/status.js +121 -0
  17. package/dist-server/server/providers/claude/status.js.map +1 -0
  18. package/dist-server/server/providers/codex/status.js +70 -0
  19. package/dist-server/server/providers/codex/status.js.map +1 -0
  20. package/dist-server/server/providers/cursor/adapter.js +5 -10
  21. package/dist-server/server/providers/cursor/adapter.js.map +1 -1
  22. package/dist-server/server/providers/cursor/status.js +118 -0
  23. package/dist-server/server/providers/cursor/status.js.map +1 -0
  24. package/dist-server/server/providers/gemini/status.js +104 -0
  25. package/dist-server/server/providers/gemini/status.js.map +1 -0
  26. package/dist-server/server/providers/registry.js +21 -2
  27. package/dist-server/server/providers/registry.js.map +1 -1
  28. package/dist-server/server/providers/types.js +11 -0
  29. package/dist-server/server/providers/types.js.map +1 -1
  30. package/dist-server/server/routes/cli-auth.js +14 -395
  31. package/dist-server/server/routes/cli-auth.js.map +1 -1
  32. package/dist-server/server/routes/cursor.js +6 -22
  33. package/dist-server/server/routes/cursor.js.map +1 -1
  34. package/dist-server/server/utils/colors.js +20 -0
  35. package/dist-server/server/utils/colors.js.map +1 -0
  36. package/dist-server/server/utils/url-detection.js +58 -0
  37. package/dist-server/server/utils/url-detection.js.map +1 -0
  38. package/package.json +3 -5
  39. package/server/claude-sdk.js +13 -4
  40. package/server/cursor-cli.js +8 -1
  41. package/server/gemini-cli.js +17 -1
  42. package/server/index.js +7 -83
  43. package/server/openai-codex.js +9 -1
  44. package/server/projects.js +38 -44
  45. package/server/providers/claude/status.js +136 -0
  46. package/server/providers/codex/status.js +78 -0
  47. package/server/providers/cursor/adapter.js +5 -10
  48. package/server/providers/cursor/status.js +128 -0
  49. package/server/providers/gemini/status.js +111 -0
  50. package/server/providers/registry.js +25 -2
  51. package/server/providers/types.js +13 -0
  52. package/server/routes/cli-auth.js +14 -421
  53. package/server/routes/cursor.js +6 -22
  54. package/server/utils/colors.js +21 -0
  55. package/server/utils/url-detection.js +71 -0
@@ -0,0 +1,58 @@
1
+ const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
2
+ const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
3
+ function stripAnsiSequences(value = '') {
4
+ return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
5
+ }
6
+ function normalizeDetectedUrl(url) {
7
+ if (!url || typeof url !== 'string')
8
+ return null;
9
+ const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
10
+ if (!cleaned)
11
+ return null;
12
+ try {
13
+ const parsed = new URL(cleaned);
14
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
15
+ return null;
16
+ }
17
+ return parsed.toString();
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ function extractUrlsFromText(value = '') {
24
+ const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || [];
25
+ // Handle wrapped terminal URLs split across lines by terminal width.
26
+ const wrappedMatches = [];
27
+ const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
28
+ const lines = value.split(/\r?\n/);
29
+ for (let i = 0; i < lines.length; i++) {
30
+ const line = lines[i].trim();
31
+ const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
32
+ if (!startMatch)
33
+ continue;
34
+ let combined = startMatch[0];
35
+ let j = i + 1;
36
+ while (j < lines.length) {
37
+ const continuation = lines[j].trim();
38
+ if (!continuation)
39
+ break;
40
+ if (!continuationRegex.test(continuation))
41
+ break;
42
+ combined += continuation;
43
+ j++;
44
+ }
45
+ wrappedMatches.push(combined.replace(/\r?\n\s*/g, ''));
46
+ }
47
+ return Array.from(new Set([...directMatches, ...wrappedMatches]));
48
+ }
49
+ function shouldAutoOpenUrlFromOutput(value = '') {
50
+ const normalized = value.toLowerCase();
51
+ return (normalized.includes('browser didn\'t open') ||
52
+ normalized.includes('open this url') ||
53
+ normalized.includes('continue in your browser') ||
54
+ normalized.includes('press enter to open') ||
55
+ normalized.includes('open_url:'));
56
+ }
57
+ export { ANSI_ESCAPE_SEQUENCE_REGEX, TRAILING_URL_PUNCTUATION_REGEX, stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput };
58
+ //# sourceMappingURL=url-detection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-detection.js","sourceRoot":"","sources":["../../../server/utils/url-detection.js"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcli-ai/cloudcli",
3
- "version": "1.29.3",
3
+ "version": "1.29.4",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "dist-server/server/index.js",
@@ -117,15 +117,13 @@
117
117
  "rehype-raw": "^7.0.0",
118
118
  "remark-gfm": "^4.0.0",
119
119
  "remark-math": "^6.0.0",
120
- "sqlite": "^5.1.1",
121
- "sqlite3": "^5.1.7",
122
120
  "tailwind-merge": "^3.3.1",
123
121
  "web-push": "^3.6.7",
124
122
  "ws": "^8.14.2"
125
123
  },
126
124
  "devDependencies": {
127
- "@commitlint/cli": "^20.4.3",
128
- "@commitlint/config-conventional": "^20.4.3",
125
+ "@commitlint/cli": "^20.5.0",
126
+ "@commitlint/config-conventional": "^20.5.0",
129
127
  "@eslint/js": "^9.39.3",
130
128
  "@release-it/conventional-changelog": "^10.0.5",
131
129
  "@types/node": "^22.19.7",
@@ -26,13 +26,14 @@ import {
26
26
  } from './services/notification-orchestrator.js';
27
27
  import { claudeAdapter } from './providers/claude/adapter.js';
28
28
  import { createNormalizedMessage } from './providers/types.js';
29
+ import { getStatusChecker } from './providers/registry.js';
29
30
 
30
31
  const activeSessions = new Map();
31
32
  const pendingToolApprovals = new Map();
32
33
 
33
34
  const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
34
35
 
35
- const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion']);
36
+ const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion', 'ExitPlanMode']);
36
37
 
37
38
  function createRequestId() {
38
39
  if (typeof crypto.randomUUID === 'function') {
@@ -148,6 +149,10 @@ function mapCliOptionsToSDK(options = {}) {
148
149
 
149
150
  const sdkOptions = {};
150
151
 
152
+ if (process.env.CLAUDE_CLI_PATH) {
153
+ sdkOptions.pathToClaudeCodeExecutable = process.env.CLAUDE_CLI_PATH;
154
+ }
155
+
151
156
  // Map working directory
152
157
  if (cwd) {
153
158
  sdkOptions.cwd = cwd;
@@ -701,8 +706,14 @@ async function queryClaudeSDK(command, options = {}, ws) {
701
706
  // Clean up temporary image files on error
702
707
  await cleanupTempFiles(tempImagePaths, tempDir);
703
708
 
709
+ // Check if Claude CLI is installed for a clearer error message
710
+ const installed = getStatusChecker('claude')?.checkInstalled() ?? true;
711
+ const errorContent = !installed
712
+ ? 'Claude Code is not installed. Please install it first: https://docs.anthropic.com/en/docs/claude-code'
713
+ : error.message;
714
+
704
715
  // Send error to WebSocket
705
- ws.send(createNormalizedMessage({ kind: 'error', content: error.message, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
716
+ ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
706
717
  notifyRunFailed({
707
718
  userId: ws?.userId || null,
708
719
  provider: 'claude',
@@ -710,8 +721,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
710
721
  sessionName: sessionSummary,
711
722
  error
712
723
  });
713
-
714
- throw error;
715
724
  }
716
725
  }
717
726
 
@@ -3,6 +3,7 @@ import crossSpawn from 'cross-spawn';
3
3
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
4
4
  import { cursorAdapter } from './providers/cursor/adapter.js';
5
5
  import { createNormalizedMessage } from './providers/types.js';
6
+ import { getStatusChecker } from './providers/registry.js';
6
7
 
7
8
  // Use cross-spawn on Windows for better command execution
8
9
  const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
@@ -294,7 +295,13 @@ async function spawnCursor(command, options = {}, ws) {
294
295
  const finalSessionId = capturedSessionId || sessionId || processKey;
295
296
  activeCursorProcesses.delete(finalSessionId);
296
297
 
297
- ws.send(createNormalizedMessage({ kind: 'error', content: error.message, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
298
+ // Check if Cursor CLI is installed for a clearer error message
299
+ const installed = getStatusChecker('cursor')?.checkInstalled() ?? true;
300
+ const errorContent = !installed
301
+ ? 'Cursor CLI is not installed. Please install it from https://cursor.com'
302
+ : error.message;
303
+
304
+ ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
298
305
  notifyTerminalState({ error });
299
306
 
300
307
  settleOnce(() => reject(error));
@@ -10,6 +10,7 @@ import sessionManager from './sessionManager.js';
10
10
  import GeminiResponseHandler from './gemini-response-handler.js';
11
11
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
12
12
  import { createNormalizedMessage } from './providers/types.js';
13
+ import { getStatusChecker } from './providers/registry.js';
13
14
 
14
15
  let activeGeminiProcesses = new Map(); // Track active processes by session ID
15
16
 
@@ -380,6 +381,15 @@ async function spawnGemini(command, options = {}, ws) {
380
381
  notifyTerminalState({ code });
381
382
  resolve();
382
383
  } else {
384
+ // code 127 = shell "command not found" — check installation
385
+ if (code === 127) {
386
+ const installed = getStatusChecker('gemini')?.checkInstalled() ?? true;
387
+ if (!installed) {
388
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
389
+ ws.send(createNormalizedMessage({ kind: 'error', content: 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli', sessionId: socketSessionId, provider: 'gemini' }));
390
+ }
391
+ }
392
+
383
393
  notifyTerminalState({
384
394
  code,
385
395
  error: code === null ? 'Gemini CLI process was terminated or timed out' : null
@@ -394,8 +404,14 @@ async function spawnGemini(command, options = {}, ws) {
394
404
  const finalSessionId = capturedSessionId || sessionId || processKey;
395
405
  activeGeminiProcesses.delete(finalSessionId);
396
406
 
407
+ // Check if Gemini CLI is installed for a clearer error message
408
+ const installed = getStatusChecker('gemini')?.checkInstalled() ?? true;
409
+ const errorContent = !installed
410
+ ? 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli'
411
+ : error.message;
412
+
397
413
  const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
398
- ws.send(createNormalizedMessage({ kind: 'error', content: error.message, sessionId: errorSessionId, provider: 'gemini' }));
414
+ ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'gemini' }));
399
415
  notifyTerminalState({ error });
400
416
 
401
417
  reject(error);
package/server/index.js CHANGED
@@ -11,25 +11,7 @@ const __dirname = getModuleDir(import.meta.url);
11
11
  const APP_ROOT = findAppRoot(__dirname);
12
12
  const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
13
13
 
14
- // ANSI color codes for terminal output
15
- const colors = {
16
- reset: '\x1b[0m',
17
- bright: '\x1b[1m',
18
- cyan: '\x1b[36m',
19
- green: '\x1b[32m',
20
- yellow: '\x1b[33m',
21
- blue: '\x1b[34m',
22
- dim: '\x1b[2m',
23
- };
24
-
25
- const c = {
26
- info: (text) => `${colors.cyan}${text}${colors.reset}`,
27
- ok: (text) => `${colors.green}${text}${colors.reset}`,
28
- warn: (text) => `${colors.yellow}${text}${colors.reset}`,
29
- tip: (text) => `${colors.blue}${text}${colors.reset}`,
30
- bright: (text) => `${colors.bright}${text}${colors.reset}`,
31
- dim: (text) => `${colors.dim}${text}${colors.reset}`,
32
- };
14
+ import { c } from './utils/colors.js';
33
15
 
34
16
  console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
35
17
 
@@ -226,68 +208,7 @@ const server = http.createServer(app);
226
208
  const ptySessionsMap = new Map();
227
209
  const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
228
210
  const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
229
- const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
230
- const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
231
-
232
- function stripAnsiSequences(value = '') {
233
- return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
234
- }
235
-
236
- function normalizeDetectedUrl(url) {
237
- if (!url || typeof url !== 'string') return null;
238
-
239
- const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
240
- if (!cleaned) return null;
241
-
242
- try {
243
- const parsed = new URL(cleaned);
244
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
245
- return null;
246
- }
247
- return parsed.toString();
248
- } catch {
249
- return null;
250
- }
251
- }
252
-
253
- function extractUrlsFromText(value = '') {
254
- const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || [];
255
-
256
- // Handle wrapped terminal URLs split across lines by terminal width.
257
- const wrappedMatches = [];
258
- const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
259
- const lines = value.split(/\r?\n/);
260
- for (let i = 0; i < lines.length; i++) {
261
- const line = lines[i].trim();
262
- const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
263
- if (!startMatch) continue;
264
-
265
- let combined = startMatch[0];
266
- let j = i + 1;
267
- while (j < lines.length) {
268
- const continuation = lines[j].trim();
269
- if (!continuation) break;
270
- if (!continuationRegex.test(continuation)) break;
271
- combined += continuation;
272
- j++;
273
- }
274
-
275
- wrappedMatches.push(combined.replace(/\r?\n\s*/g, ''));
276
- }
277
-
278
- return Array.from(new Set([...directMatches, ...wrappedMatches]));
279
- }
280
-
281
- function shouldAutoOpenUrlFromOutput(value = '') {
282
- const normalized = value.toLowerCase();
283
- return (
284
- normalized.includes('browser didn\'t open') ||
285
- normalized.includes('open this url') ||
286
- normalized.includes('continue in your browser') ||
287
- normalized.includes('press enter to open') ||
288
- normalized.includes('open_url:')
289
- );
290
- }
211
+ import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
291
212
 
292
213
  // Single WebSocket server that handles both paths
293
214
  const wss = new WebSocketServer({
@@ -573,12 +494,15 @@ app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) =
573
494
  }
574
495
  });
575
496
 
576
- // Delete project endpoint (force=true to delete with sessions)
497
+ // Delete project endpoint
498
+ // force=true to allow removal even when sessions exist
499
+ // deleteData=true to also delete session/memory files on disk (destructive)
577
500
  app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
578
501
  try {
579
502
  const { projectName } = req.params;
580
503
  const force = req.query.force === 'true';
581
- await deleteProject(projectName, force);
504
+ const deleteData = req.query.deleteData === 'true';
505
+ await deleteProject(projectName, force, deleteData);
582
506
  res.json({ success: true });
583
507
  } catch (error) {
584
508
  res.status(500).json({ error: error.message });
@@ -17,6 +17,7 @@ import { Codex } from '@openai/codex-sdk';
17
17
  import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
18
18
  import { codexAdapter } from './providers/codex/adapter.js';
19
19
  import { createNormalizedMessage } from './providers/types.js';
20
+ import { getStatusChecker } from './providers/registry.js';
20
21
 
21
22
  // Track active sessions
22
23
  const activeCodexSessions = new Map();
@@ -308,7 +309,14 @@ export async function queryCodex(command, options = {}, ws) {
308
309
 
309
310
  if (!wasAborted) {
310
311
  console.error('[Codex] Error:', error);
311
- sendMessage(ws, createNormalizedMessage({ kind: 'error', content: error.message, sessionId: currentSessionId, provider: 'codex' }));
312
+
313
+ // Check if Codex SDK is available for a clearer error message
314
+ const installed = getStatusChecker('codex')?.checkInstalled() ?? true;
315
+ const errorContent = !installed
316
+ ? 'Codex CLI is not configured. Please set up authentication first.'
317
+ : error.message;
318
+
319
+ sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: currentSessionId, provider: 'codex' }));
312
320
  if (!terminalFailure) {
313
321
  notifyRunFailed({
314
322
  userId: ws?.userId || null,
@@ -62,8 +62,7 @@ import fsSync from 'fs';
62
62
  import path from 'path';
63
63
  import readline from 'readline';
64
64
  import crypto from 'crypto';
65
- import sqlite3 from 'sqlite3';
66
- import { open } from 'sqlite';
65
+ import Database from 'better-sqlite3';
67
66
  import os from 'os';
68
67
  import sessionManager from './sessionManager.js';
69
68
  import { applyCustomSessionNames } from './database/db.js';
@@ -1164,8 +1163,9 @@ async function isProjectEmpty(projectName) {
1164
1163
  }
1165
1164
  }
1166
1165
 
1167
- // Delete a project (force=true to delete even with sessions)
1168
- async function deleteProject(projectName, force = false) {
1166
+ // Remove a project from the UI.
1167
+ // When deleteData=true, also delete session/memory files on disk (destructive).
1168
+ async function deleteProject(projectName, force = false, deleteData = false) {
1169
1169
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1170
1170
 
1171
1171
  try {
@@ -1175,48 +1175,50 @@ async function deleteProject(projectName, force = false) {
1175
1175
  }
1176
1176
 
1177
1177
  const config = await loadProjectConfig();
1178
- let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
1179
1178
 
1180
- // Fallback to extractProjectDirectory if projectPath is not in config
1181
- if (!projectPath) {
1182
- projectPath = await extractProjectDirectory(projectName);
1183
- }
1179
+ // Destructive path: delete underlying data when explicitly requested
1180
+ if (deleteData) {
1181
+ let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
1182
+ if (!projectPath) {
1183
+ projectPath = await extractProjectDirectory(projectName);
1184
+ }
1184
1185
 
1185
- // Remove the project directory (includes all Claude sessions)
1186
- await fs.rm(projectDir, { recursive: true, force: true });
1186
+ // Remove the Claude project directory (session logs, memory, subagent data)
1187
+ await fs.rm(projectDir, { recursive: true, force: true });
1187
1188
 
1188
- // Delete all Codex sessions associated with this project
1189
- if (projectPath) {
1190
- try {
1191
- const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
1192
- for (const session of codexSessions) {
1193
- try {
1194
- await deleteCodexSession(session.id);
1195
- } catch (err) {
1196
- console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
1189
+ // Delete Codex sessions associated with this project
1190
+ if (projectPath) {
1191
+ try {
1192
+ const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
1193
+ for (const session of codexSessions) {
1194
+ try {
1195
+ await deleteCodexSession(session.id);
1196
+ } catch (err) {
1197
+ console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
1198
+ }
1197
1199
  }
1200
+ } catch (err) {
1201
+ console.warn('Failed to delete Codex sessions:', err.message);
1198
1202
  }
1199
- } catch (err) {
1200
- console.warn('Failed to delete Codex sessions:', err.message);
1201
- }
1202
1203
 
1203
- // Delete Cursor sessions directory if it exists
1204
- try {
1205
- const hash = crypto.createHash('md5').update(projectPath).digest('hex');
1206
- const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
1207
- await fs.rm(cursorProjectDir, { recursive: true, force: true });
1208
- } catch (err) {
1209
- // Cursor dir may not exist, ignore
1204
+ // Delete Cursor sessions directory if it exists
1205
+ try {
1206
+ const hash = crypto.createHash('md5').update(projectPath).digest('hex');
1207
+ const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
1208
+ await fs.rm(cursorProjectDir, { recursive: true, force: true });
1209
+ } catch (err) {
1210
+ // Cursor dir may not exist, ignore
1211
+ }
1210
1212
  }
1211
1213
  }
1212
1214
 
1213
- // Remove from project config
1215
+ // Always remove from project config
1214
1216
  delete config[projectName];
1215
1217
  await saveProjectConfig(config);
1216
1218
 
1217
1219
  return true;
1218
1220
  } catch (error) {
1219
- console.error(`Error deleting project ${projectName}:`, error);
1221
+ console.error(`Error removing project ${projectName}:`, error);
1220
1222
  throw error;
1221
1223
  }
1222
1224
  }
@@ -1305,16 +1307,10 @@ async function getCursorSessions(projectPath) {
1305
1307
  } catch (_) { }
1306
1308
 
1307
1309
  // Open SQLite database
1308
- const db = await open({
1309
- filename: storeDbPath,
1310
- driver: sqlite3.Database,
1311
- mode: sqlite3.OPEN_READONLY
1312
- });
1310
+ const db = new Database(storeDbPath, { readonly: true, fileMustExist: true });
1313
1311
 
1314
1312
  // Get metadata from meta table
1315
- const metaRows = await db.all(`
1316
- SELECT key, value FROM meta
1317
- `);
1313
+ const metaRows = db.prepare('SELECT key, value FROM meta').all();
1318
1314
 
1319
1315
  // Parse metadata
1320
1316
  let metadata = {};
@@ -1336,11 +1332,9 @@ async function getCursorSessions(projectPath) {
1336
1332
  }
1337
1333
 
1338
1334
  // Get message count
1339
- const messageCountResult = await db.get(`
1340
- SELECT COUNT(*) as count FROM blobs
1341
- `);
1335
+ const messageCountResult = db.prepare('SELECT COUNT(*) as count FROM blobs').get();
1342
1336
 
1343
- await db.close();
1337
+ db.close();
1344
1338
 
1345
1339
  // Extract session info
1346
1340
  const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Claude Provider Status
3
+ *
4
+ * Checks whether Claude Code CLI is installed and whether the user
5
+ * has valid authentication credentials.
6
+ *
7
+ * @module providers/claude/status
8
+ */
9
+
10
+ import { execFileSync } from 'child_process';
11
+ import { promises as fs } from 'fs';
12
+ import path from 'path';
13
+ import os from 'os';
14
+
15
+ /**
16
+ * Check if Claude Code CLI is installed and available.
17
+ * Uses CLAUDE_CLI_PATH env var if set, otherwise looks for 'claude' in PATH.
18
+ * @returns {boolean}
19
+ */
20
+ export function checkInstalled() {
21
+ const cliPath = process.env.CLAUDE_CLI_PATH || 'claude';
22
+ try {
23
+ execFileSync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Full status check: installation + authentication.
32
+ * @returns {Promise<import('../types.js').ProviderStatus>}
33
+ */
34
+ export async function checkStatus() {
35
+ const installed = checkInstalled();
36
+
37
+ if (!installed) {
38
+ return {
39
+ installed,
40
+ authenticated: false,
41
+ email: null,
42
+ method: null,
43
+ error: 'Claude Code CLI is not installed'
44
+ };
45
+ }
46
+
47
+ const credentialsResult = await checkCredentials();
48
+
49
+ if (credentialsResult.authenticated) {
50
+ return {
51
+ installed,
52
+ authenticated: true,
53
+ email: credentialsResult.email || 'Authenticated',
54
+ method: credentialsResult.method || null,
55
+ error: null
56
+ };
57
+ }
58
+
59
+ return {
60
+ installed,
61
+ authenticated: false,
62
+ email: credentialsResult.email || null,
63
+ method: credentialsResult.method || null,
64
+ error: credentialsResult.error || 'Not authenticated'
65
+ };
66
+ }
67
+
68
+ // ─── Internal helpers ───────────────────────────────────────────────────────
69
+
70
+ async function loadSettingsEnv() {
71
+ try {
72
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
73
+ const content = await fs.readFile(settingsPath, 'utf8');
74
+ const settings = JSON.parse(content);
75
+
76
+ if (settings?.env && typeof settings.env === 'object') {
77
+ return settings.env;
78
+ }
79
+ } catch {
80
+ // Ignore missing or malformed settings.
81
+ }
82
+
83
+ return {};
84
+ }
85
+
86
+ /**
87
+ * Checks Claude authentication credentials.
88
+ *
89
+ * Priority 1: ANTHROPIC_API_KEY environment variable
90
+ * Priority 1b: ~/.claude/settings.json env values
91
+ * Priority 2: ~/.claude/.credentials.json OAuth tokens
92
+ */
93
+ async function checkCredentials() {
94
+ if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim()) {
95
+ return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
96
+ }
97
+
98
+ const settingsEnv = await loadSettingsEnv();
99
+
100
+ if (typeof settingsEnv.ANTHROPIC_API_KEY === 'string' && settingsEnv.ANTHROPIC_API_KEY.trim()) {
101
+ return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
102
+ }
103
+
104
+ if (typeof settingsEnv.ANTHROPIC_AUTH_TOKEN === 'string' && settingsEnv.ANTHROPIC_AUTH_TOKEN.trim()) {
105
+ return { authenticated: true, email: 'Configured via settings.json', method: 'api_key' };
106
+ }
107
+
108
+ try {
109
+ const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
110
+ const content = await fs.readFile(credPath, 'utf8');
111
+ const creds = JSON.parse(content);
112
+
113
+ const oauth = creds.claudeAiOauth;
114
+ if (oauth && oauth.accessToken) {
115
+ const isExpired = oauth.expiresAt && Date.now() >= oauth.expiresAt;
116
+ if (!isExpired) {
117
+ return {
118
+ authenticated: true,
119
+ email: creds.email || creds.user || null,
120
+ method: 'credentials_file'
121
+ };
122
+ }
123
+
124
+ return {
125
+ authenticated: false,
126
+ email: creds.email || creds.user || null,
127
+ method: 'credentials_file',
128
+ error: 'OAuth token has expired. Please re-authenticate with claude login'
129
+ };
130
+ }
131
+
132
+ return { authenticated: false, email: null, method: null };
133
+ } catch {
134
+ return { authenticated: false, email: null, method: null };
135
+ }
136
+ }