@cloudcli-ai/cloudcli 1.29.2 → 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 (140) hide show
  1. package/dist/assets/{index-ByHZ8-LL.css → index-BBAE1OJ_.css} +1 -1
  2. package/dist/assets/{index-Ile1bTCU.js → index-Cyya3LhN.js} +229 -229
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/claude-sdk.js +702 -0
  5. package/dist-server/server/claude-sdk.js.map +1 -0
  6. package/dist-server/server/cli.js +642 -0
  7. package/dist-server/server/cli.js.map +1 -0
  8. package/dist-server/server/constants/config.js +6 -0
  9. package/dist-server/server/constants/config.js.map +1 -0
  10. package/dist-server/server/cursor-cli.js +277 -0
  11. package/dist-server/server/cursor-cli.js.map +1 -0
  12. package/dist-server/server/database/db.js +535 -0
  13. package/dist-server/server/database/db.js.map +1 -0
  14. package/dist-server/server/database/schema.js +97 -0
  15. package/dist-server/server/database/schema.js.map +1 -0
  16. package/dist-server/server/gemini-cli.js +408 -0
  17. package/dist-server/server/gemini-cli.js.map +1 -0
  18. package/dist-server/server/gemini-response-handler.js +72 -0
  19. package/dist-server/server/gemini-response-handler.js.map +1 -0
  20. package/dist-server/server/index.js +2175 -0
  21. package/dist-server/server/index.js.map +1 -0
  22. package/dist-server/server/load-env.js +32 -0
  23. package/dist-server/server/load-env.js.map +1 -0
  24. package/dist-server/server/middleware/auth.js +111 -0
  25. package/dist-server/server/middleware/auth.js.map +1 -0
  26. package/dist-server/server/openai-codex.js +373 -0
  27. package/dist-server/server/openai-codex.js.map +1 -0
  28. package/dist-server/server/projects.js +2325 -0
  29. package/dist-server/server/projects.js.map +1 -0
  30. package/dist-server/server/providers/claude/adapter.js +266 -0
  31. package/dist-server/server/providers/claude/adapter.js.map +1 -0
  32. package/dist-server/server/providers/claude/status.js +121 -0
  33. package/dist-server/server/providers/claude/status.js.map +1 -0
  34. package/dist-server/server/providers/codex/adapter.js +232 -0
  35. package/dist-server/server/providers/codex/adapter.js.map +1 -0
  36. package/dist-server/server/providers/codex/status.js +70 -0
  37. package/dist-server/server/providers/codex/status.js.map +1 -0
  38. package/dist-server/server/providers/cursor/adapter.js +342 -0
  39. package/dist-server/server/providers/cursor/adapter.js.map +1 -0
  40. package/dist-server/server/providers/cursor/status.js +118 -0
  41. package/dist-server/server/providers/cursor/status.js.map +1 -0
  42. package/dist-server/server/providers/gemini/adapter.js +174 -0
  43. package/dist-server/server/providers/gemini/adapter.js.map +1 -0
  44. package/dist-server/server/providers/gemini/status.js +104 -0
  45. package/dist-server/server/providers/gemini/status.js.map +1 -0
  46. package/dist-server/server/providers/registry.js +58 -0
  47. package/dist-server/server/providers/registry.js.map +1 -0
  48. package/dist-server/server/providers/types.js +117 -0
  49. package/dist-server/server/providers/types.js.map +1 -0
  50. package/dist-server/server/providers/utils.js +28 -0
  51. package/dist-server/server/providers/utils.js.map +1 -0
  52. package/dist-server/server/routes/agent.js +1147 -0
  53. package/dist-server/server/routes/agent.js.map +1 -0
  54. package/dist-server/server/routes/auth.js +117 -0
  55. package/dist-server/server/routes/auth.js.map +1 -0
  56. package/dist-server/server/routes/cli-auth.js +25 -0
  57. package/dist-server/server/routes/cli-auth.js.map +1 -0
  58. package/dist-server/server/routes/codex.js +294 -0
  59. package/dist-server/server/routes/codex.js.map +1 -0
  60. package/dist-server/server/routes/commands.js +531 -0
  61. package/dist-server/server/routes/commands.js.map +1 -0
  62. package/dist-server/server/routes/cursor.js +529 -0
  63. package/dist-server/server/routes/cursor.js.map +1 -0
  64. package/dist-server/server/routes/gemini.js +21 -0
  65. package/dist-server/server/routes/gemini.js.map +1 -0
  66. package/dist-server/server/routes/git.js +1259 -0
  67. package/dist-server/server/routes/git.js.map +1 -0
  68. package/dist-server/server/routes/mcp-utils.js +46 -0
  69. package/dist-server/server/routes/mcp-utils.js.map +1 -0
  70. package/dist-server/server/routes/mcp.js +480 -0
  71. package/dist-server/server/routes/mcp.js.map +1 -0
  72. package/dist-server/server/routes/messages.js +56 -0
  73. package/dist-server/server/routes/messages.js.map +1 -0
  74. package/dist-server/server/routes/plugins.js +266 -0
  75. package/dist-server/server/routes/plugins.js.map +1 -0
  76. package/dist-server/server/routes/projects.js +505 -0
  77. package/dist-server/server/routes/projects.js.map +1 -0
  78. package/dist-server/server/routes/settings.js +249 -0
  79. package/dist-server/server/routes/settings.js.map +1 -0
  80. package/dist-server/server/routes/taskmaster.js +1832 -0
  81. package/dist-server/server/routes/taskmaster.js.map +1 -0
  82. package/dist-server/server/routes/user.js +115 -0
  83. package/dist-server/server/routes/user.js.map +1 -0
  84. package/dist-server/server/services/notification-orchestrator.js +177 -0
  85. package/dist-server/server/services/notification-orchestrator.js.map +1 -0
  86. package/dist-server/server/services/vapid-keys.js +26 -0
  87. package/dist-server/server/services/vapid-keys.js.map +1 -0
  88. package/dist-server/server/sessionManager.js +194 -0
  89. package/dist-server/server/sessionManager.js.map +1 -0
  90. package/dist-server/server/utils/colors.js +20 -0
  91. package/dist-server/server/utils/colors.js.map +1 -0
  92. package/dist-server/server/utils/commandParser.js +255 -0
  93. package/dist-server/server/utils/commandParser.js.map +1 -0
  94. package/dist-server/server/utils/frontmatter.js +16 -0
  95. package/dist-server/server/utils/frontmatter.js.map +1 -0
  96. package/dist-server/server/utils/gitConfig.js +36 -0
  97. package/dist-server/server/utils/gitConfig.js.map +1 -0
  98. package/dist-server/server/utils/mcp-detector.js +183 -0
  99. package/dist-server/server/utils/mcp-detector.js.map +1 -0
  100. package/dist-server/server/utils/plugin-loader.js +413 -0
  101. package/dist-server/server/utils/plugin-loader.js.map +1 -0
  102. package/dist-server/server/utils/plugin-process-manager.js +163 -0
  103. package/dist-server/server/utils/plugin-process-manager.js.map +1 -0
  104. package/dist-server/server/utils/runtime-paths.js +30 -0
  105. package/dist-server/server/utils/runtime-paths.js.map +1 -0
  106. package/dist-server/server/utils/taskmaster-websocket.js +118 -0
  107. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -0
  108. package/dist-server/server/utils/url-detection.js +58 -0
  109. package/dist-server/server/utils/url-detection.js.map +1 -0
  110. package/dist-server/shared/modelConstants.js +87 -0
  111. package/dist-server/shared/modelConstants.js.map +1 -0
  112. package/dist-server/shared/networkHosts.js +20 -0
  113. package/dist-server/shared/networkHosts.js.map +1 -0
  114. package/package.json +23 -14
  115. package/server/claude-sdk.js +13 -4
  116. package/server/cli.js +13 -9
  117. package/server/cursor-cli.js +8 -1
  118. package/server/database/db.js +24 -61
  119. package/server/database/schema.js +102 -0
  120. package/server/gemini-cli.js +17 -1
  121. package/server/index.js +20 -96
  122. package/server/load-env.js +11 -6
  123. package/server/openai-codex.js +9 -1
  124. package/server/projects.js +38 -44
  125. package/server/providers/claude/status.js +136 -0
  126. package/server/providers/codex/status.js +78 -0
  127. package/server/providers/cursor/adapter.js +5 -10
  128. package/server/providers/cursor/status.js +128 -0
  129. package/server/providers/gemini/status.js +111 -0
  130. package/server/providers/registry.js +25 -2
  131. package/server/providers/types.js +13 -0
  132. package/server/routes/cli-auth.js +14 -421
  133. package/server/routes/commands.js +6 -4
  134. package/server/routes/cursor.js +6 -240
  135. package/server/tsconfig.json +33 -0
  136. package/server/utils/colors.js +21 -0
  137. package/server/utils/runtime-paths.js +37 -0
  138. package/server/utils/url-detection.js +71 -0
  139. package/shared/modelConstants.js +4 -2
  140. package/server/database/init.sql +0 -99
@@ -0,0 +1,102 @@
1
+ export const APP_CONFIG_TABLE_SQL = `CREATE TABLE IF NOT EXISTS app_config (
2
+ key TEXT PRIMARY KEY,
3
+ value TEXT NOT NULL,
4
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
5
+ );`;
6
+
7
+ export const USER_NOTIFICATION_PREFERENCES_TABLE_SQL = `CREATE TABLE IF NOT EXISTS user_notification_preferences (
8
+ user_id INTEGER PRIMARY KEY,
9
+ preferences_json TEXT NOT NULL,
10
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
11
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
12
+ );`;
13
+
14
+ export const VAPID_KEYS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS vapid_keys (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ public_key TEXT NOT NULL,
17
+ private_key TEXT NOT NULL,
18
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
19
+ );`;
20
+
21
+ export const PUSH_SUBSCRIPTIONS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS push_subscriptions (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ user_id INTEGER NOT NULL,
24
+ endpoint TEXT NOT NULL UNIQUE,
25
+ keys_p256dh TEXT NOT NULL,
26
+ keys_auth TEXT NOT NULL,
27
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
28
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
29
+ );`;
30
+
31
+ export const SESSION_NAMES_TABLE_SQL = `CREATE TABLE IF NOT EXISTS session_names (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ session_id TEXT NOT NULL,
34
+ provider TEXT NOT NULL DEFAULT 'claude',
35
+ custom_name TEXT NOT NULL,
36
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
37
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
38
+ UNIQUE(session_id, provider)
39
+ );`;
40
+
41
+ export const SESSION_NAMES_LOOKUP_INDEX_SQL = `CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider);`;
42
+
43
+ export const DATABASE_SCHEMA_SQL = `PRAGMA foreign_keys = ON;
44
+
45
+ CREATE TABLE IF NOT EXISTS users (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ username TEXT UNIQUE NOT NULL,
48
+ password_hash TEXT NOT NULL,
49
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
50
+ last_login DATETIME,
51
+ is_active BOOLEAN DEFAULT 1,
52
+ git_name TEXT,
53
+ git_email TEXT,
54
+ has_completed_onboarding BOOLEAN DEFAULT 0
55
+ );
56
+
57
+ CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
58
+ CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
59
+
60
+ CREATE TABLE IF NOT EXISTS api_keys (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ user_id INTEGER NOT NULL,
63
+ key_name TEXT NOT NULL,
64
+ api_key TEXT UNIQUE NOT NULL,
65
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
66
+ last_used DATETIME,
67
+ is_active BOOLEAN DEFAULT 1,
68
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
69
+ );
70
+
71
+ CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(api_key);
72
+ CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
73
+ CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active);
74
+
75
+ CREATE TABLE IF NOT EXISTS user_credentials (
76
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
77
+ user_id INTEGER NOT NULL,
78
+ credential_name TEXT NOT NULL,
79
+ credential_type TEXT NOT NULL,
80
+ credential_value TEXT NOT NULL,
81
+ description TEXT,
82
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
83
+ is_active BOOLEAN DEFAULT 1,
84
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
85
+ );
86
+
87
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
88
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
89
+ CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
90
+
91
+ ${USER_NOTIFICATION_PREFERENCES_TABLE_SQL}
92
+
93
+ ${VAPID_KEYS_TABLE_SQL}
94
+
95
+ ${PUSH_SUBSCRIPTIONS_TABLE_SQL}
96
+
97
+ ${SESSION_NAMES_TABLE_SQL}
98
+
99
+ ${SESSION_NAMES_LOOKUP_INDEX_SQL}
100
+
101
+ ${APP_CONFIG_TABLE_SQL}
102
+ `;
@@ -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
@@ -3,33 +3,15 @@
3
3
  import './load-env.js';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
- import { fileURLToPath } from 'url';
7
- import { dirname } from 'path';
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
-
12
- const installMode = fs.existsSync(path.join(__dirname, '..', '.git')) ? 'git' : 'npm';
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
- };
6
+ import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
24
7
 
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
- };
8
+ const __dirname = getModuleDir(import.meta.url);
9
+ // The server source runs from /server, while the compiled output runs from /dist-server/server.
10
+ // Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
11
+ const APP_ROOT = findAppRoot(__dirname);
12
+ const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
13
+
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({
@@ -405,11 +326,11 @@ app.use('/api/sessions', authenticateToken, messagesRoutes);
405
326
  app.use('/api/agent', agentRoutes);
406
327
 
407
328
  // Serve public files (like api-docs.html)
408
- app.use(express.static(path.join(__dirname, '../public')));
329
+ app.use(express.static(path.join(APP_ROOT, 'public')));
409
330
 
410
331
  // Static files served after API routes
411
332
  // Add cache control: HTML files should not be cached, but assets can be cached
412
- app.use(express.static(path.join(__dirname, '../dist'), {
333
+ app.use(express.static(path.join(APP_ROOT, 'dist'), {
413
334
  setHeaders: (res, filePath) => {
414
335
  if (filePath.endsWith('.html')) {
415
336
  // Prevent HTML caching to avoid service worker issues after builds
@@ -431,7 +352,7 @@ app.use(express.static(path.join(__dirname, '../dist'), {
431
352
  app.post('/api/system/update', authenticateToken, async (req, res) => {
432
353
  try {
433
354
  // Get the project root directory (parent of server directory)
434
- const projectRoot = path.join(__dirname, '..');
355
+ const projectRoot = APP_ROOT;
435
356
 
436
357
  console.log('Starting system update from directory:', projectRoot);
437
358
 
@@ -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 });
@@ -2273,7 +2197,7 @@ app.get('*', (req, res) => {
2273
2197
 
2274
2198
  // Only serve index.html for HTML routes, not for static assets
2275
2199
  // Static assets should already be handled by express.static middleware above
2276
- const indexPath = path.join(__dirname, '../dist/index.html');
2200
+ const indexPath = path.join(APP_ROOT, 'dist', 'index.html');
2277
2201
 
2278
2202
  // Check if dist/index.html exists (production build available)
2279
2203
  if (fs.existsSync(indexPath)) {
@@ -2388,7 +2312,7 @@ async function startServer() {
2388
2312
  configureWebPush();
2389
2313
 
2390
2314
  // Check if running in production mode (dist folder exists)
2391
- const distIndexPath = path.join(__dirname, '../dist/index.html');
2315
+ const distIndexPath = path.join(APP_ROOT, 'dist', 'index.html');
2392
2316
  const isProduction = fs.existsSync(distIndexPath);
2393
2317
 
2394
2318
  // Log Claude implementation mode
@@ -2402,7 +2326,7 @@ async function startServer() {
2402
2326
  console.log(`${c.info('[INFO]')} To run in development mode with hot-module replacement, go to http://${DISPLAY_HOST}:${VITE_PORT}`);
2403
2327
 
2404
2328
  server.listen(SERVER_PORT, HOST, async () => {
2405
- const appInstallPath = path.join(__dirname, '..');
2329
+ const appInstallPath = APP_ROOT;
2406
2330
 
2407
2331
  console.log('');
2408
2332
  console.log(c.dim('═'.repeat(63)));
@@ -2,14 +2,15 @@
2
2
  import fs from 'fs';
3
3
  import os from 'os';
4
4
  import path from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { dirname } from 'path';
5
+ import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
7
6
 
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
7
+ const __dirname = getModuleDir(import.meta.url);
8
+ // Resolve the repo/app root via the nearest /server folder so this file keeps finding the
9
+ // same top-level .env file from both /server/load-env.js and /dist-server/server/load-env.js.
10
+ const APP_ROOT = findAppRoot(__dirname);
10
11
 
11
12
  try {
12
- const envPath = path.join(__dirname, '../.env');
13
+ const envPath = path.join(APP_ROOT, '.env');
13
14
  const envFile = fs.readFileSync(envPath, 'utf8');
14
15
  envFile.split('\n').forEach(line => {
15
16
  const trimmedLine = line.trim();
@@ -24,6 +25,10 @@ try {
24
25
  console.log('No .env file found or error reading it:', e.message);
25
26
  }
26
27
 
28
+ // Keep the default database in a stable user-level location so rebuilding dist-server
29
+ // never changes where the backend stores auth.db when DATABASE_PATH is not set explicitly.
30
+ const DEFAULT_DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db');
31
+
27
32
  if (!process.env.DATABASE_PATH) {
28
- process.env.DATABASE_PATH = path.join(os.homedir(), '.cloudcli', 'auth.db');
33
+ process.env.DATABASE_PATH = DEFAULT_DATABASE_PATH;
29
34
  }
@@ -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
+ }