@cloudcli-ai/cloudcli 1.30.0 → 1.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/assets/index-B37ufcoi.js +1262 -0
- package/dist/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +115 -955
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +1 -1
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +125 -0
- package/dist-server/server/modules/database/connection.js.map +1 -0
- package/dist-server/server/modules/database/index.js +13 -0
- package/dist-server/server/modules/database/index.js.map +1 -0
- package/dist-server/server/modules/database/init-db.js +18 -0
- package/dist-server/server/modules/database/init-db.js.map +1 -0
- package/dist-server/server/modules/database/migrations.js +370 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -0
- package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
- package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
- package/dist-server/server/modules/database/repositories/app-config.js +47 -0
- package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
- package/dist-server/server/modules/database/repositories/credentials.js +68 -0
- package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
- package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
- package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
- package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
- package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
- package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/users.js +88 -0
- package/dist-server/server/modules/database/repositories/users.js.map +1 -0
- package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
- package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +143 -0
- package/dist-server/server/modules/database/schema.js.map +1 -0
- package/dist-server/server/modules/projects/index.js +4 -0
- package/dist-server/server/modules/projects/index.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +188 -0
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
- package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
- package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
- package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
- package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/index.js +4 -0
- package/dist-server/server/modules/providers/index.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -10
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +172 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +3 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
- package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
- package/dist-server/server/routes/agent.js +10 -17
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/auth.js +3 -1
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/commands.js +1 -1
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/gemini.js +2 -2
- package/dist-server/server/routes/gemini.js.map +1 -1
- package/dist-server/server/routes/git.js +33 -29
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/settings.js +1 -1
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +97 -110
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/routes/user.js +1 -1
- package/dist-server/server/routes/user.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +2 -2
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/vapid-keys.js +2 -1
- package/dist-server/server/services/vapid-keys.js.map +1 -1
- package/dist-server/server/shared/types.js +0 -1
- package/dist-server/server/shared/types.js.map +1 -1
- package/dist-server/server/shared/utils.js +408 -7
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/taskmaster-websocket.js +18 -12
- package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
- package/dist-server/shared/modelConstants.js +2 -1
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +3 -1
- package/server/index.js +146 -1012
- package/server/middleware/auth.js +1 -1
- package/server/modules/database/connection.ts +143 -0
- package/server/modules/database/index.ts +12 -0
- package/server/modules/database/init-db.ts +17 -0
- package/server/modules/database/migrations.ts +442 -0
- package/server/modules/database/repositories/api-keys.ts +119 -0
- package/server/modules/database/repositories/app-config.ts +53 -0
- package/server/modules/database/repositories/credentials.ts +106 -0
- package/server/modules/database/repositories/github-tokens.ts +100 -0
- package/server/modules/database/repositories/notification-preferences.ts +103 -0
- package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
- package/server/modules/database/repositories/projects.db.ts +183 -0
- package/server/modules/database/repositories/push-subscriptions.ts +80 -0
- package/server/modules/database/repositories/scan-state.db.ts +42 -0
- package/server/modules/database/repositories/sessions.db.ts +174 -0
- package/server/modules/database/repositories/users.ts +140 -0
- package/server/modules/database/repositories/vapid-keys.ts +57 -0
- package/server/modules/database/schema.ts +152 -0
- package/server/modules/projects/index.ts +6 -0
- package/server/modules/projects/projects.routes.ts +247 -0
- package/server/modules/projects/services/project-clone.service.ts +321 -0
- package/server/modules/projects/services/project-delete.service.ts +75 -0
- package/server/modules/projects/services/project-management.service.ts +150 -0
- package/server/modules/projects/services/project-star.service.ts +78 -0
- package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
- package/server/modules/projects/tests/project-management.service.test.ts +117 -0
- package/server/modules/projects/tests/project-star.service.test.ts +123 -0
- package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
- package/server/modules/providers/list/claude/claude.provider.ts +3 -1
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
- package/server/modules/providers/list/codex/codex.provider.ts +3 -1
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
- package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
- package/server/modules/providers/provider.routes.ts +220 -12
- package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
- package/server/modules/providers/services/sessions.service.ts +89 -4
- package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
- package/server/modules/websocket/README.md +267 -0
- package/server/modules/websocket/index.ts +2 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
- package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
- package/server/modules/websocket/services/websocket-server.service.ts +58 -0
- package/server/modules/websocket/services/websocket-state.service.ts +16 -0
- package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
- package/server/routes/agent.js +11 -16
- package/server/routes/auth.js +4 -2
- package/server/routes/commands.js +1 -1
- package/server/routes/gemini.js +3 -2
- package/server/routes/git.js +33 -28
- package/server/routes/settings.js +1 -1
- package/server/routes/taskmaster.js +108 -111
- package/server/routes/user.js +1 -1
- package/server/services/notification-orchestrator.js +3 -2
- package/server/services/vapid-keys.js +2 -1
- package/server/shared/interfaces.ts +39 -1
- package/server/shared/types.ts +185 -30
- package/server/shared/utils.ts +485 -9
- package/server/utils/taskmaster-websocket.js +19 -13
- package/shared/modelConstants.js +2 -1
- package/dist/assets/index-RX-W_g_Q.js +0 -1262
- package/dist-server/server/database/db.js +0 -535
- package/dist-server/server/database/db.js.map +0 -1
- package/dist-server/server/database/schema.js +0 -97
- package/dist-server/server/database/schema.js.map +0 -1
- package/dist-server/server/projects.js +0 -2325
- package/dist-server/server/projects.js.map +0 -1
- package/dist-server/server/routes/codex.js +0 -18
- package/dist-server/server/routes/codex.js.map +0 -1
- package/dist-server/server/routes/messages.js +0 -56
- package/dist-server/server/routes/messages.js.map +0 -1
- package/dist-server/server/routes/projects.js +0 -505
- package/dist-server/server/routes/projects.js.map +0 -1
- package/server/database/db.js +0 -593
- package/server/database/schema.js +0 -102
- package/server/projects.js +0 -2555
- package/server/routes/codex.js +0 -19
- package/server/routes/messages.js +0 -61
- package/server/routes/projects.js +0 -548
|
@@ -1,32 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Load environment variables before other imports execute
|
|
3
3
|
import './load-env.js';
|
|
4
|
-
import fs from 'fs';
|
|
4
|
+
import fs, { promises as fsPromises } from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
|
|
7
|
-
import { AppError, createNormalizedMessage } from './shared/utils.js';
|
|
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
|
-
import { c } from './utils/colors.js';
|
|
14
|
-
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
|
15
|
-
import express from 'express';
|
|
16
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
17
6
|
import os from 'os';
|
|
18
7
|
import http from 'http';
|
|
19
|
-
import cors from 'cors';
|
|
20
|
-
import { promises as fsPromises } from 'fs';
|
|
21
8
|
import { spawn } from 'child_process';
|
|
22
|
-
import
|
|
9
|
+
import express from 'express';
|
|
10
|
+
import cors from 'cors';
|
|
23
11
|
import mime from 'mime-types';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
12
|
+
import { AppError, WORKSPACES_ROOT, validateWorkspacePath } from './shared/utils.js';
|
|
13
|
+
import { closeSessionsWatcher, initializeSessionsWatcher } from './modules/providers/index.js';
|
|
14
|
+
import { createWebSocketServer } from './modules/websocket/index.js';
|
|
15
|
+
import { getConnectableHost } from '../shared/networkHosts.js';
|
|
16
|
+
import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
|
|
17
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter, } from './claude-sdk.js';
|
|
18
|
+
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions, } from './cursor-cli.js';
|
|
19
|
+
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions, } from './openai-codex.js';
|
|
20
|
+
import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions, } from './gemini-cli.js';
|
|
29
21
|
import sessionManager from './sessionManager.js';
|
|
22
|
+
import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput, } from './utils/url-detection.js';
|
|
30
23
|
import gitRoutes from './routes/git.js';
|
|
31
24
|
import authRoutes from './routes/auth.js';
|
|
32
25
|
import cursorRoutes from './routes/cursor.js';
|
|
@@ -35,185 +28,60 @@ import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
|
35
28
|
import commandsRoutes from './routes/commands.js';
|
|
36
29
|
import settingsRoutes from './routes/settings.js';
|
|
37
30
|
import agentRoutes from './routes/agent.js';
|
|
38
|
-
import
|
|
31
|
+
import projectModuleRoutes from './modules/projects/projects.routes.js';
|
|
39
32
|
import userRoutes from './routes/user.js';
|
|
40
|
-
import codexRoutes from './routes/codex.js';
|
|
41
33
|
import geminiRoutes from './routes/gemini.js';
|
|
42
34
|
import pluginsRoutes from './routes/plugins.js';
|
|
43
|
-
import messagesRoutes from './routes/messages.js';
|
|
44
35
|
import providerRoutes from './modules/providers/provider.routes.js';
|
|
45
36
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
46
|
-
import { initializeDatabase,
|
|
37
|
+
import { initializeDatabase, projectsDb } from './modules/database/index.js';
|
|
47
38
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
48
39
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
49
40
|
import { IS_PLATFORM } from './constants/config.js';
|
|
50
|
-
import {
|
|
51
|
-
const
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'projects') },
|
|
58
|
-
{ provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', 'sessions') }
|
|
59
|
-
];
|
|
60
|
-
const WATCHER_IGNORED_PATTERNS = [
|
|
61
|
-
'**/node_modules/**',
|
|
62
|
-
'**/.git/**',
|
|
63
|
-
'**/dist/**',
|
|
64
|
-
'**/build/**',
|
|
65
|
-
'**/*.tmp',
|
|
66
|
-
'**/*.swp',
|
|
67
|
-
'**/.DS_Store'
|
|
68
|
-
];
|
|
69
|
-
const WATCHER_DEBOUNCE_MS = 300;
|
|
70
|
-
let projectsWatchers = [];
|
|
71
|
-
let projectsWatcherDebounceTimer = null;
|
|
72
|
-
const connectedClients = new Set();
|
|
73
|
-
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
74
|
-
// Broadcast progress to all connected WebSocket clients
|
|
75
|
-
function broadcastProgress(progress) {
|
|
76
|
-
const message = JSON.stringify({
|
|
77
|
-
type: 'loading_progress',
|
|
78
|
-
...progress
|
|
79
|
-
});
|
|
80
|
-
connectedClients.forEach(client => {
|
|
81
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
82
|
-
client.send(message);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
// Setup file system watchers for Claude, Cursor, and Codex project/session folders
|
|
87
|
-
async function setupProjectsWatcher() {
|
|
88
|
-
const chokidar = (await import('chokidar')).default;
|
|
89
|
-
if (projectsWatcherDebounceTimer) {
|
|
90
|
-
clearTimeout(projectsWatcherDebounceTimer);
|
|
91
|
-
projectsWatcherDebounceTimer = null;
|
|
92
|
-
}
|
|
93
|
-
await Promise.all(projectsWatchers.map(async (watcher) => {
|
|
94
|
-
try {
|
|
95
|
-
await watcher.close();
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
console.error('[WARN] Failed to close watcher:', error);
|
|
99
|
-
}
|
|
100
|
-
}));
|
|
101
|
-
projectsWatchers = [];
|
|
102
|
-
const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
|
|
103
|
-
if (projectsWatcherDebounceTimer) {
|
|
104
|
-
clearTimeout(projectsWatcherDebounceTimer);
|
|
105
|
-
}
|
|
106
|
-
projectsWatcherDebounceTimer = setTimeout(async () => {
|
|
107
|
-
// Prevent reentrant calls
|
|
108
|
-
if (isGetProjectsRunning) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
isGetProjectsRunning = true;
|
|
113
|
-
// Clear project directory cache when files change
|
|
114
|
-
clearProjectDirectoryCache();
|
|
115
|
-
// Get updated projects list
|
|
116
|
-
const updatedProjects = await getProjects(broadcastProgress);
|
|
117
|
-
// Notify all connected clients about the project changes
|
|
118
|
-
const updateMessage = JSON.stringify({
|
|
119
|
-
type: 'projects_updated',
|
|
120
|
-
projects: updatedProjects,
|
|
121
|
-
timestamp: new Date().toISOString(),
|
|
122
|
-
changeType: eventType,
|
|
123
|
-
changedFile: path.relative(rootPath, filePath),
|
|
124
|
-
watchProvider: provider
|
|
125
|
-
});
|
|
126
|
-
connectedClients.forEach(client => {
|
|
127
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
128
|
-
client.send(updateMessage);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
console.error('[ERROR] Error handling project changes:', error);
|
|
134
|
-
}
|
|
135
|
-
finally {
|
|
136
|
-
isGetProjectsRunning = false;
|
|
137
|
-
}
|
|
138
|
-
}, WATCHER_DEBOUNCE_MS);
|
|
139
|
-
};
|
|
140
|
-
for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
|
|
141
|
-
try {
|
|
142
|
-
// chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
|
|
143
|
-
// Ensure provider folders exist before creating the watcher so watching stays active.
|
|
144
|
-
await fsPromises.mkdir(rootPath, { recursive: true });
|
|
145
|
-
// Initialize chokidar watcher with optimized settings
|
|
146
|
-
const watcher = chokidar.watch(rootPath, {
|
|
147
|
-
ignored: WATCHER_IGNORED_PATTERNS,
|
|
148
|
-
persistent: true,
|
|
149
|
-
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
150
|
-
followSymlinks: false,
|
|
151
|
-
depth: 10, // Reasonable depth limit
|
|
152
|
-
awaitWriteFinish: {
|
|
153
|
-
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
|
154
|
-
pollInterval: 50
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
// Set up event listeners
|
|
158
|
-
watcher
|
|
159
|
-
.on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
|
|
160
|
-
.on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
|
|
161
|
-
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
|
|
162
|
-
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
|
|
163
|
-
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
|
|
164
|
-
.on('error', (error) => {
|
|
165
|
-
console.error(`[ERROR] ${provider} watcher error:`, error);
|
|
166
|
-
})
|
|
167
|
-
.on('ready', () => {
|
|
168
|
-
});
|
|
169
|
-
projectsWatchers.push(watcher);
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (projectsWatchers.length === 0) {
|
|
176
|
-
console.error('[ERROR] Failed to setup any provider watchers');
|
|
177
|
-
}
|
|
178
|
-
}
|
|
41
|
+
import { c } from './utils/colors.js';
|
|
42
|
+
const __dirname = getModuleDir(import.meta.url);
|
|
43
|
+
// The server source runs from /server, while the compiled output runs from /dist-server/server.
|
|
44
|
+
// Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
|
|
45
|
+
const APP_ROOT = findAppRoot(__dirname);
|
|
46
|
+
const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
|
47
|
+
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
|
179
48
|
const app = express();
|
|
180
49
|
const server = http.createServer(app);
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
50
|
+
// Single WebSocket server that handles chat, shell, and plugin proxy paths.
|
|
51
|
+
const wss = createWebSocketServer(server, {
|
|
52
|
+
verifyClient: {
|
|
53
|
+
isPlatform: IS_PLATFORM,
|
|
54
|
+
authenticateWebSocket,
|
|
55
|
+
},
|
|
56
|
+
chat: {
|
|
57
|
+
queryClaudeSDK,
|
|
58
|
+
spawnCursor,
|
|
59
|
+
queryCodex,
|
|
60
|
+
spawnGemini,
|
|
61
|
+
abortClaudeSDKSession,
|
|
62
|
+
abortCursorSession,
|
|
63
|
+
abortCodexSession,
|
|
64
|
+
abortGeminiSession,
|
|
65
|
+
resolveToolApproval,
|
|
66
|
+
isClaudeSDKSessionActive,
|
|
67
|
+
isCursorSessionActive,
|
|
68
|
+
isCodexSessionActive,
|
|
69
|
+
isGeminiSessionActive,
|
|
70
|
+
reconnectSessionWriter,
|
|
71
|
+
getPendingApprovalsForSession,
|
|
72
|
+
getActiveClaudeSDKSessions,
|
|
73
|
+
getActiveCursorSessions,
|
|
74
|
+
getActiveCodexSessions,
|
|
75
|
+
getActiveGeminiSessions,
|
|
76
|
+
},
|
|
77
|
+
shell: {
|
|
78
|
+
getSessionById: (sessionId) => sessionManager.getSession(sessionId),
|
|
79
|
+
stripAnsiSequences,
|
|
80
|
+
normalizeDetectedUrl,
|
|
81
|
+
extractUrlsFromText,
|
|
82
|
+
shouldAutoOpenUrlFromOutput,
|
|
83
|
+
},
|
|
84
|
+
getPluginPort,
|
|
217
85
|
});
|
|
218
86
|
// Make WebSocket server available to routes
|
|
219
87
|
app.locals.wss = wss;
|
|
@@ -243,7 +111,7 @@ app.use('/api', validateApiKey);
|
|
|
243
111
|
// Authentication routes (public)
|
|
244
112
|
app.use('/api/auth', authRoutes);
|
|
245
113
|
// Projects API Routes (protected)
|
|
246
|
-
app.use('/api/projects', authenticateToken,
|
|
114
|
+
app.use('/api/projects', authenticateToken, projectModuleRoutes);
|
|
247
115
|
// Git API Routes (protected)
|
|
248
116
|
app.use('/api/git', authenticateToken, gitRoutes);
|
|
249
117
|
// Cursor API Routes (protected)
|
|
@@ -258,14 +126,10 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
|
258
126
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
|
259
127
|
// User API Routes (protected)
|
|
260
128
|
app.use('/api/user', authenticateToken, userRoutes);
|
|
261
|
-
// Codex API Routes (protected)
|
|
262
|
-
app.use('/api/codex', authenticateToken, codexRoutes);
|
|
263
129
|
// Gemini API Routes (protected)
|
|
264
130
|
app.use('/api/gemini', authenticateToken, geminiRoutes);
|
|
265
131
|
// Plugins API Routes (protected)
|
|
266
132
|
app.use('/api/plugins', authenticateToken, pluginsRoutes);
|
|
267
|
-
// Unified session messages route (protected)
|
|
268
|
-
app.use('/api/sessions', authenticateToken, messagesRoutes);
|
|
269
133
|
// Unified provider MCP routes (protected)
|
|
270
134
|
app.use('/api/providers', authenticateToken, providerRoutes);
|
|
271
135
|
// Agent API Routes (uses API key authentication)
|
|
@@ -356,137 +220,6 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
|
356
220
|
});
|
|
357
221
|
}
|
|
358
222
|
});
|
|
359
|
-
app.get('/api/projects', authenticateToken, async (req, res) => {
|
|
360
|
-
try {
|
|
361
|
-
const projects = await getProjects(broadcastProgress);
|
|
362
|
-
res.json(projects);
|
|
363
|
-
}
|
|
364
|
-
catch (error) {
|
|
365
|
-
res.status(500).json({ error: error.message });
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
|
|
369
|
-
try {
|
|
370
|
-
const { limit = 5, offset = 0 } = req.query;
|
|
371
|
-
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
|
|
372
|
-
applyCustomSessionNames(result.sessions, 'claude');
|
|
373
|
-
res.json(result);
|
|
374
|
-
}
|
|
375
|
-
catch (error) {
|
|
376
|
-
res.status(500).json({ error: error.message });
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
// Rename project endpoint
|
|
380
|
-
app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
|
|
381
|
-
try {
|
|
382
|
-
const { displayName } = req.body;
|
|
383
|
-
await renameProject(req.params.projectName, displayName);
|
|
384
|
-
res.json({ success: true });
|
|
385
|
-
}
|
|
386
|
-
catch (error) {
|
|
387
|
-
res.status(500).json({ error: error.message });
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
// Delete session endpoint
|
|
391
|
-
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
|
|
392
|
-
try {
|
|
393
|
-
const { projectName, sessionId } = req.params;
|
|
394
|
-
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
|
395
|
-
await deleteSession(projectName, sessionId);
|
|
396
|
-
sessionNamesDb.deleteName(sessionId, 'claude');
|
|
397
|
-
console.log(`[API] Session ${sessionId} deleted successfully`);
|
|
398
|
-
res.json({ success: true });
|
|
399
|
-
}
|
|
400
|
-
catch (error) {
|
|
401
|
-
console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
|
|
402
|
-
res.status(500).json({ error: error.message });
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
// Rename session endpoint
|
|
406
|
-
app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) => {
|
|
407
|
-
try {
|
|
408
|
-
const { sessionId } = req.params;
|
|
409
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
410
|
-
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
|
411
|
-
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
412
|
-
}
|
|
413
|
-
const { summary, provider } = req.body;
|
|
414
|
-
if (!summary || typeof summary !== 'string' || summary.trim() === '') {
|
|
415
|
-
return res.status(400).json({ error: 'Summary is required' });
|
|
416
|
-
}
|
|
417
|
-
if (summary.trim().length > 500) {
|
|
418
|
-
return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
|
|
419
|
-
}
|
|
420
|
-
if (!provider || !VALID_PROVIDERS.includes(provider)) {
|
|
421
|
-
return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
|
|
422
|
-
}
|
|
423
|
-
sessionNamesDb.setName(safeSessionId, provider, summary.trim());
|
|
424
|
-
res.json({ success: true });
|
|
425
|
-
}
|
|
426
|
-
catch (error) {
|
|
427
|
-
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
|
|
428
|
-
res.status(500).json({ error: error.message });
|
|
429
|
-
}
|
|
430
|
-
});
|
|
431
|
-
// Delete project endpoint
|
|
432
|
-
// force=true to allow removal even when sessions exist
|
|
433
|
-
// deleteData=true to also delete session/memory files on disk (destructive)
|
|
434
|
-
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
435
|
-
try {
|
|
436
|
-
const { projectName } = req.params;
|
|
437
|
-
const force = req.query.force === 'true';
|
|
438
|
-
const deleteData = req.query.deleteData === 'true';
|
|
439
|
-
await deleteProject(projectName, force, deleteData);
|
|
440
|
-
res.json({ success: true });
|
|
441
|
-
}
|
|
442
|
-
catch (error) {
|
|
443
|
-
res.status(500).json({ error: error.message });
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
// Search conversations content (SSE streaming)
|
|
447
|
-
app.get('/api/search/conversations', authenticateToken, async (req, res) => {
|
|
448
|
-
const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
449
|
-
const parsedLimit = Number.parseInt(String(req.query.limit), 10);
|
|
450
|
-
const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 100));
|
|
451
|
-
if (query.length < 2) {
|
|
452
|
-
return res.status(400).json({ error: 'Query must be at least 2 characters' });
|
|
453
|
-
}
|
|
454
|
-
res.writeHead(200, {
|
|
455
|
-
'Content-Type': 'text/event-stream',
|
|
456
|
-
'Cache-Control': 'no-cache',
|
|
457
|
-
'Connection': 'keep-alive',
|
|
458
|
-
'X-Accel-Buffering': 'no',
|
|
459
|
-
});
|
|
460
|
-
let closed = false;
|
|
461
|
-
const abortController = new AbortController();
|
|
462
|
-
req.on('close', () => { closed = true; abortController.abort(); });
|
|
463
|
-
try {
|
|
464
|
-
await searchConversations(query, limit, ({ projectResult, totalMatches, scannedProjects, totalProjects }) => {
|
|
465
|
-
if (closed)
|
|
466
|
-
return;
|
|
467
|
-
if (projectResult) {
|
|
468
|
-
res.write(`event: result\ndata: ${JSON.stringify({ projectResult, totalMatches, scannedProjects, totalProjects })}\n\n`);
|
|
469
|
-
}
|
|
470
|
-
else {
|
|
471
|
-
res.write(`event: progress\ndata: ${JSON.stringify({ totalMatches, scannedProjects, totalProjects })}\n\n`);
|
|
472
|
-
}
|
|
473
|
-
}, abortController.signal);
|
|
474
|
-
if (!closed) {
|
|
475
|
-
res.write(`event: done\ndata: {}\n\n`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
catch (error) {
|
|
479
|
-
console.error('Error searching conversations:', error);
|
|
480
|
-
if (!closed) {
|
|
481
|
-
res.write(`event: error\ndata: ${JSON.stringify({ error: 'Search failed' })}\n\n`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
finally {
|
|
485
|
-
if (!closed) {
|
|
486
|
-
res.end();
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
223
|
const expandWorkspacePath = (inputPath) => {
|
|
491
224
|
if (!inputPath)
|
|
492
225
|
return inputPath;
|
|
@@ -617,15 +350,17 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
|
|
|
617
350
|
}
|
|
618
351
|
});
|
|
619
352
|
// Read file content endpoint
|
|
620
|
-
app.get('/api/projects/:
|
|
353
|
+
app.get('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
|
|
621
354
|
try {
|
|
622
|
-
const {
|
|
355
|
+
const { projectId } = req.params;
|
|
623
356
|
const { filePath } = req.query;
|
|
624
357
|
// Security: ensure the requested path is inside the project root
|
|
625
358
|
if (!filePath) {
|
|
626
359
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
627
360
|
}
|
|
628
|
-
|
|
361
|
+
// Resolve the absolute project root via the DB-backed helper; the
|
|
362
|
+
// caller passes the DB-assigned `projectId`, not a folder name.
|
|
363
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
629
364
|
if (!projectRoot) {
|
|
630
365
|
return res.status(404).json({ error: 'Project not found' });
|
|
631
366
|
}
|
|
@@ -654,15 +389,16 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
654
389
|
}
|
|
655
390
|
});
|
|
656
391
|
// Serve raw file bytes for previews and downloads.
|
|
657
|
-
app.get('/api/projects/:
|
|
392
|
+
app.get('/api/projects/:projectId/files/content', authenticateToken, async (req, res) => {
|
|
658
393
|
try {
|
|
659
|
-
const {
|
|
394
|
+
const { projectId } = req.params;
|
|
660
395
|
const { path: filePath } = req.query;
|
|
661
396
|
// Security: ensure the requested path is inside the project root
|
|
662
397
|
if (!filePath) {
|
|
663
398
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
664
399
|
}
|
|
665
|
-
|
|
400
|
+
// Projects are now addressed by DB `projectId`, resolved to their path here.
|
|
401
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
666
402
|
if (!projectRoot) {
|
|
667
403
|
return res.status(404).json({ error: 'Project not found' });
|
|
668
404
|
}
|
|
@@ -703,9 +439,9 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
703
439
|
}
|
|
704
440
|
});
|
|
705
441
|
// Save file content endpoint
|
|
706
|
-
app.put('/api/projects/:
|
|
442
|
+
app.put('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
|
|
707
443
|
try {
|
|
708
|
-
const {
|
|
444
|
+
const { projectId } = req.params;
|
|
709
445
|
const { filePath, content } = req.body;
|
|
710
446
|
// Security: ensure the requested path is inside the project root
|
|
711
447
|
if (!filePath) {
|
|
@@ -714,7 +450,8 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
714
450
|
if (content === undefined) {
|
|
715
451
|
return res.status(400).json({ error: 'Content is required' });
|
|
716
452
|
}
|
|
717
|
-
|
|
453
|
+
// Projects are now addressed by DB `projectId`, resolved to their path here.
|
|
454
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
718
455
|
if (!projectRoot) {
|
|
719
456
|
return res.status(404).json({ error: 'Project not found' });
|
|
720
457
|
}
|
|
@@ -747,18 +484,14 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
747
484
|
}
|
|
748
485
|
}
|
|
749
486
|
});
|
|
750
|
-
app.get('/api/projects/:
|
|
487
|
+
app.get('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
|
|
751
488
|
try {
|
|
752
489
|
// Using fsPromises from import
|
|
753
|
-
//
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
catch (error) {
|
|
759
|
-
console.error('Error extracting project directory:', error);
|
|
760
|
-
// Fallback to simple dash replacement
|
|
761
|
-
actualPath = req.params.projectName.replace(/-/g, '/');
|
|
490
|
+
// Resolve the project's absolute path through the DB (projectId is the
|
|
491
|
+
// primary key of the `projects` table after the identifier migration).
|
|
492
|
+
const actualPath = await projectsDb.getProjectPathById(req.params.projectId);
|
|
493
|
+
if (!actualPath) {
|
|
494
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
762
495
|
}
|
|
763
496
|
// Check if path exists
|
|
764
497
|
try {
|
|
@@ -819,10 +552,10 @@ function validateFilename(name) {
|
|
|
819
552
|
}
|
|
820
553
|
return { valid: true };
|
|
821
554
|
}
|
|
822
|
-
// POST /api/projects/:
|
|
823
|
-
app.post('/api/projects/:
|
|
555
|
+
// POST /api/projects/:projectId/files/create - Create new file or directory
|
|
556
|
+
app.post('/api/projects/:projectId/files/create', authenticateToken, async (req, res) => {
|
|
824
557
|
try {
|
|
825
|
-
const {
|
|
558
|
+
const { projectId } = req.params;
|
|
826
559
|
const { path: parentPath, type, name } = req.body;
|
|
827
560
|
// Validate input
|
|
828
561
|
if (!name || !type) {
|
|
@@ -835,8 +568,8 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
|
|
|
835
568
|
if (!nameValidation.valid) {
|
|
836
569
|
return res.status(400).json({ error: nameValidation.error });
|
|
837
570
|
}
|
|
838
|
-
//
|
|
839
|
-
const projectRoot = await
|
|
571
|
+
// Resolve the project directory through the DB using the new projectId.
|
|
572
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
840
573
|
if (!projectRoot) {
|
|
841
574
|
return res.status(404).json({ error: 'Project not found' });
|
|
842
575
|
}
|
|
@@ -892,10 +625,10 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
|
|
|
892
625
|
}
|
|
893
626
|
}
|
|
894
627
|
});
|
|
895
|
-
// PUT /api/projects/:
|
|
896
|
-
app.put('/api/projects/:
|
|
628
|
+
// PUT /api/projects/:projectId/files/rename - Rename file or directory
|
|
629
|
+
app.put('/api/projects/:projectId/files/rename', authenticateToken, async (req, res) => {
|
|
897
630
|
try {
|
|
898
|
-
const {
|
|
631
|
+
const { projectId } = req.params;
|
|
899
632
|
const { oldPath, newName } = req.body;
|
|
900
633
|
// Validate input
|
|
901
634
|
if (!oldPath || !newName) {
|
|
@@ -905,8 +638,8 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
|
|
|
905
638
|
if (!nameValidation.valid) {
|
|
906
639
|
return res.status(400).json({ error: nameValidation.error });
|
|
907
640
|
}
|
|
908
|
-
//
|
|
909
|
-
const projectRoot = await
|
|
641
|
+
// Resolve the project directory through the DB using the new projectId.
|
|
642
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
910
643
|
if (!projectRoot) {
|
|
911
644
|
return res.status(404).json({ error: 'Project not found' });
|
|
912
645
|
}
|
|
@@ -964,17 +697,17 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
|
|
|
964
697
|
}
|
|
965
698
|
}
|
|
966
699
|
});
|
|
967
|
-
// DELETE /api/projects/:
|
|
968
|
-
app.delete('/api/projects/:
|
|
700
|
+
// DELETE /api/projects/:projectId/files - Delete file or directory
|
|
701
|
+
app.delete('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
|
|
969
702
|
try {
|
|
970
|
-
const {
|
|
703
|
+
const { projectId } = req.params;
|
|
971
704
|
const { path: targetPath, type } = req.body;
|
|
972
705
|
// Validate input
|
|
973
706
|
if (!targetPath) {
|
|
974
707
|
return res.status(400).json({ error: 'Path is required' });
|
|
975
708
|
}
|
|
976
|
-
//
|
|
977
|
-
const projectRoot = await
|
|
709
|
+
// Resolve the project directory through the DB using the new projectId.
|
|
710
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
978
711
|
if (!projectRoot) {
|
|
979
712
|
return res.status(404).json({ error: 'Project not found' });
|
|
980
713
|
}
|
|
@@ -1026,7 +759,7 @@ app.delete('/api/projects/:projectName/files', authenticateToken, async (req, re
|
|
|
1026
759
|
}
|
|
1027
760
|
}
|
|
1028
761
|
});
|
|
1029
|
-
// POST /api/projects/:
|
|
762
|
+
// POST /api/projects/:projectId/files/upload - Upload files
|
|
1030
763
|
// Dynamic import of multer for file uploads
|
|
1031
764
|
const uploadFilesHandler = async (req, res) => {
|
|
1032
765
|
// Dynamic import of multer
|
|
@@ -1062,7 +795,7 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1062
795
|
return res.status(500).json({ error: err.message });
|
|
1063
796
|
}
|
|
1064
797
|
try {
|
|
1065
|
-
const {
|
|
798
|
+
const { projectId } = req.params;
|
|
1066
799
|
const { targetPath, relativePaths } = req.body;
|
|
1067
800
|
// Parse relative paths if provided (for folder uploads)
|
|
1068
801
|
let filePaths = [];
|
|
@@ -1075,7 +808,7 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1075
808
|
}
|
|
1076
809
|
}
|
|
1077
810
|
console.log('[DEBUG] File upload request:', {
|
|
1078
|
-
|
|
811
|
+
projectId,
|
|
1079
812
|
targetPath: JSON.stringify(targetPath),
|
|
1080
813
|
targetPathType: typeof targetPath,
|
|
1081
814
|
filesCount: req.files?.length,
|
|
@@ -1084,8 +817,8 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1084
817
|
if (!req.files || req.files.length === 0) {
|
|
1085
818
|
return res.status(400).json({ error: 'No files provided' });
|
|
1086
819
|
}
|
|
1087
|
-
//
|
|
1088
|
-
const projectRoot = await
|
|
820
|
+
// Resolve the project directory through the DB using the new projectId.
|
|
821
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
1089
822
|
if (!projectRoot) {
|
|
1090
823
|
return res.status(404).json({ error: 'Project not found' });
|
|
1091
824
|
}
|
|
@@ -1175,585 +908,11 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1175
908
|
}
|
|
1176
909
|
});
|
|
1177
910
|
};
|
|
1178
|
-
app.post('/api/projects/:
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
function handlePluginWsProxy(clientWs, pathname) {
|
|
1184
|
-
const pluginName = pathname.replace('/plugin-ws/', '');
|
|
1185
|
-
if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) {
|
|
1186
|
-
clientWs.close(4400, 'Invalid plugin name');
|
|
1187
|
-
return;
|
|
1188
|
-
}
|
|
1189
|
-
const port = getPluginPort(pluginName);
|
|
1190
|
-
if (!port) {
|
|
1191
|
-
clientWs.close(4404, 'Plugin not running');
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
const upstream = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
|
1195
|
-
upstream.on('open', () => {
|
|
1196
|
-
console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`);
|
|
1197
|
-
});
|
|
1198
|
-
// Relay messages bidirectionally
|
|
1199
|
-
upstream.on('message', (data) => {
|
|
1200
|
-
if (clientWs.readyState === WebSocket.OPEN)
|
|
1201
|
-
clientWs.send(data);
|
|
1202
|
-
});
|
|
1203
|
-
clientWs.on('message', (data) => {
|
|
1204
|
-
if (upstream.readyState === WebSocket.OPEN)
|
|
1205
|
-
upstream.send(data);
|
|
1206
|
-
});
|
|
1207
|
-
// Propagate close in both directions
|
|
1208
|
-
upstream.on('close', () => { if (clientWs.readyState === WebSocket.OPEN)
|
|
1209
|
-
clientWs.close(); });
|
|
1210
|
-
clientWs.on('close', () => { if (upstream.readyState === WebSocket.OPEN)
|
|
1211
|
-
upstream.close(); });
|
|
1212
|
-
upstream.on('error', (err) => {
|
|
1213
|
-
console.error(`[Plugins] WS proxy error for "${pluginName}":`, err.message);
|
|
1214
|
-
if (clientWs.readyState === WebSocket.OPEN)
|
|
1215
|
-
clientWs.close(4502, 'Upstream error');
|
|
1216
|
-
});
|
|
1217
|
-
clientWs.on('error', () => {
|
|
1218
|
-
if (upstream.readyState === WebSocket.OPEN)
|
|
1219
|
-
upstream.close();
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
// WebSocket connection handler that routes based on URL path
|
|
1223
|
-
wss.on('connection', (ws, request) => {
|
|
1224
|
-
const url = request.url;
|
|
1225
|
-
console.log('[INFO] Client connected to:', url);
|
|
1226
|
-
// Parse URL to get pathname without query parameters
|
|
1227
|
-
const urlObj = new URL(url, 'http://localhost');
|
|
1228
|
-
const pathname = urlObj.pathname;
|
|
1229
|
-
if (pathname === '/shell') {
|
|
1230
|
-
handleShellConnection(ws);
|
|
1231
|
-
}
|
|
1232
|
-
else if (pathname === '/ws') {
|
|
1233
|
-
handleChatConnection(ws, request);
|
|
1234
|
-
}
|
|
1235
|
-
else if (pathname.startsWith('/plugin-ws/')) {
|
|
1236
|
-
handlePluginWsProxy(ws, pathname);
|
|
1237
|
-
}
|
|
1238
|
-
else {
|
|
1239
|
-
console.log('[WARN] Unknown WebSocket path:', pathname);
|
|
1240
|
-
ws.close();
|
|
1241
|
-
}
|
|
1242
|
-
});
|
|
1243
|
-
/**
|
|
1244
|
-
* WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
|
|
1245
|
-
*
|
|
1246
|
-
* Provider files use `createNormalizedMessage()` from `shared/utils.js` and
|
|
1247
|
-
* adapter `normalizeMessage()` to produce unified NormalizedMessage events.
|
|
1248
|
-
* The writer simply serialises and sends.
|
|
1249
|
-
*/
|
|
1250
|
-
class WebSocketWriter {
|
|
1251
|
-
constructor(ws, userId = null) {
|
|
1252
|
-
this.ws = ws;
|
|
1253
|
-
this.sessionId = null;
|
|
1254
|
-
this.userId = userId;
|
|
1255
|
-
this.isWebSocketWriter = true; // Marker for transport detection
|
|
1256
|
-
}
|
|
1257
|
-
send(data) {
|
|
1258
|
-
if (this.ws.readyState === 1) { // WebSocket.OPEN
|
|
1259
|
-
this.ws.send(JSON.stringify(data));
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
updateWebSocket(newRawWs) {
|
|
1263
|
-
this.ws = newRawWs;
|
|
1264
|
-
}
|
|
1265
|
-
setSessionId(sessionId) {
|
|
1266
|
-
this.sessionId = sessionId;
|
|
1267
|
-
}
|
|
1268
|
-
getSessionId() {
|
|
1269
|
-
return this.sessionId;
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
// Handle chat WebSocket connections
|
|
1273
|
-
function handleChatConnection(ws, request) {
|
|
1274
|
-
console.log('[INFO] Chat WebSocket connected');
|
|
1275
|
-
// Add to connected clients for project updates
|
|
1276
|
-
connectedClients.add(ws);
|
|
1277
|
-
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
|
|
1278
|
-
const writer = new WebSocketWriter(ws, request?.user?.id ?? request?.user?.userId ?? null);
|
|
1279
|
-
ws.on('message', async (message) => {
|
|
1280
|
-
try {
|
|
1281
|
-
const data = JSON.parse(message);
|
|
1282
|
-
if (data.type === 'claude-command') {
|
|
1283
|
-
console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
|
|
1284
|
-
console.log('📁 Project:', data.options?.projectPath || 'Unknown');
|
|
1285
|
-
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1286
|
-
// Use Claude Agents SDK
|
|
1287
|
-
await queryClaudeSDK(data.command, data.options, writer);
|
|
1288
|
-
}
|
|
1289
|
-
else if (data.type === 'cursor-command') {
|
|
1290
|
-
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
|
|
1291
|
-
console.log('📁 Project:', data.options?.cwd || 'Unknown');
|
|
1292
|
-
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1293
|
-
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1294
|
-
await spawnCursor(data.command, data.options, writer);
|
|
1295
|
-
}
|
|
1296
|
-
else if (data.type === 'codex-command') {
|
|
1297
|
-
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
|
|
1298
|
-
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
1299
|
-
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1300
|
-
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1301
|
-
await queryCodex(data.command, data.options, writer);
|
|
1302
|
-
}
|
|
1303
|
-
else if (data.type === 'gemini-command') {
|
|
1304
|
-
console.log('[DEBUG] Gemini message:', data.command || '[Continue/Resume]');
|
|
1305
|
-
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
1306
|
-
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1307
|
-
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1308
|
-
await spawnGemini(data.command, data.options, writer);
|
|
1309
|
-
}
|
|
1310
|
-
else if (data.type === 'cursor-resume') {
|
|
1311
|
-
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
1312
|
-
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
|
1313
|
-
await spawnCursor('', {
|
|
1314
|
-
sessionId: data.sessionId,
|
|
1315
|
-
resume: true,
|
|
1316
|
-
cwd: data.options?.cwd
|
|
1317
|
-
}, writer);
|
|
1318
|
-
}
|
|
1319
|
-
else if (data.type === 'abort-session') {
|
|
1320
|
-
console.log('[DEBUG] Abort session request:', data.sessionId);
|
|
1321
|
-
const provider = data.provider || 'claude';
|
|
1322
|
-
let success;
|
|
1323
|
-
if (provider === 'cursor') {
|
|
1324
|
-
success = abortCursorSession(data.sessionId);
|
|
1325
|
-
}
|
|
1326
|
-
else if (provider === 'codex') {
|
|
1327
|
-
success = abortCodexSession(data.sessionId);
|
|
1328
|
-
}
|
|
1329
|
-
else if (provider === 'gemini') {
|
|
1330
|
-
success = abortGeminiSession(data.sessionId);
|
|
1331
|
-
}
|
|
1332
|
-
else {
|
|
1333
|
-
// Use Claude Agents SDK
|
|
1334
|
-
success = await abortClaudeSDKSession(data.sessionId);
|
|
1335
|
-
}
|
|
1336
|
-
writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider }));
|
|
1337
|
-
}
|
|
1338
|
-
else if (data.type === 'claude-permission-response') {
|
|
1339
|
-
// Relay UI approval decisions back into the SDK control flow.
|
|
1340
|
-
// This does not persist permissions; it only resolves the in-flight request,
|
|
1341
|
-
// introduced so the SDK can resume once the user clicks Allow/Deny.
|
|
1342
|
-
if (data.requestId) {
|
|
1343
|
-
resolveToolApproval(data.requestId, {
|
|
1344
|
-
allow: Boolean(data.allow),
|
|
1345
|
-
updatedInput: data.updatedInput,
|
|
1346
|
-
message: data.message,
|
|
1347
|
-
rememberEntry: data.rememberEntry
|
|
1348
|
-
});
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
else if (data.type === 'cursor-abort') {
|
|
1352
|
-
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
|
|
1353
|
-
const success = abortCursorSession(data.sessionId);
|
|
1354
|
-
writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider: 'cursor' }));
|
|
1355
|
-
}
|
|
1356
|
-
else if (data.type === 'check-session-status') {
|
|
1357
|
-
// Check if a specific session is currently processing
|
|
1358
|
-
const provider = data.provider || 'claude';
|
|
1359
|
-
const sessionId = data.sessionId;
|
|
1360
|
-
let isActive;
|
|
1361
|
-
if (provider === 'cursor') {
|
|
1362
|
-
isActive = isCursorSessionActive(sessionId);
|
|
1363
|
-
}
|
|
1364
|
-
else if (provider === 'codex') {
|
|
1365
|
-
isActive = isCodexSessionActive(sessionId);
|
|
1366
|
-
}
|
|
1367
|
-
else if (provider === 'gemini') {
|
|
1368
|
-
isActive = isGeminiSessionActive(sessionId);
|
|
1369
|
-
}
|
|
1370
|
-
else {
|
|
1371
|
-
// Use Claude Agents SDK
|
|
1372
|
-
isActive = isClaudeSDKSessionActive(sessionId);
|
|
1373
|
-
if (isActive) {
|
|
1374
|
-
// Reconnect the session's writer to the new WebSocket so
|
|
1375
|
-
// subsequent SDK output flows to the refreshed client.
|
|
1376
|
-
reconnectSessionWriter(sessionId, ws);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
writer.send({
|
|
1380
|
-
type: 'session-status',
|
|
1381
|
-
sessionId,
|
|
1382
|
-
provider,
|
|
1383
|
-
isProcessing: isActive
|
|
1384
|
-
});
|
|
1385
|
-
}
|
|
1386
|
-
else if (data.type === 'get-pending-permissions') {
|
|
1387
|
-
// Return pending permission requests for a session
|
|
1388
|
-
const sessionId = data.sessionId;
|
|
1389
|
-
if (sessionId && isClaudeSDKSessionActive(sessionId)) {
|
|
1390
|
-
const pending = getPendingApprovalsForSession(sessionId);
|
|
1391
|
-
writer.send({
|
|
1392
|
-
type: 'pending-permissions-response',
|
|
1393
|
-
sessionId,
|
|
1394
|
-
data: pending
|
|
1395
|
-
});
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
else if (data.type === 'get-active-sessions') {
|
|
1399
|
-
// Get all currently active sessions
|
|
1400
|
-
const activeSessions = {
|
|
1401
|
-
claude: getActiveClaudeSDKSessions(),
|
|
1402
|
-
cursor: getActiveCursorSessions(),
|
|
1403
|
-
codex: getActiveCodexSessions(),
|
|
1404
|
-
gemini: getActiveGeminiSessions()
|
|
1405
|
-
};
|
|
1406
|
-
writer.send({
|
|
1407
|
-
type: 'active-sessions',
|
|
1408
|
-
sessions: activeSessions
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
catch (error) {
|
|
1413
|
-
console.error('[ERROR] Chat WebSocket error:', error.message);
|
|
1414
|
-
writer.send({
|
|
1415
|
-
type: 'error',
|
|
1416
|
-
error: error.message
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
});
|
|
1420
|
-
ws.on('close', () => {
|
|
1421
|
-
console.log('🔌 Chat client disconnected');
|
|
1422
|
-
// Remove from connected clients
|
|
1423
|
-
connectedClients.delete(ws);
|
|
1424
|
-
});
|
|
1425
|
-
}
|
|
1426
|
-
// Handle shell WebSocket connections
|
|
1427
|
-
function handleShellConnection(ws) {
|
|
1428
|
-
console.log('🐚 Shell client connected');
|
|
1429
|
-
let shellProcess = null;
|
|
1430
|
-
let ptySessionKey = null;
|
|
1431
|
-
let urlDetectionBuffer = '';
|
|
1432
|
-
const announcedAuthUrls = new Set();
|
|
1433
|
-
ws.on('message', async (message) => {
|
|
1434
|
-
try {
|
|
1435
|
-
const data = JSON.parse(message);
|
|
1436
|
-
console.log('📨 Shell message received:', data.type);
|
|
1437
|
-
if (data.type === 'init') {
|
|
1438
|
-
const projectPath = data.projectPath || process.cwd();
|
|
1439
|
-
const sessionId = data.sessionId;
|
|
1440
|
-
const hasSession = data.hasSession;
|
|
1441
|
-
const provider = data.provider || 'claude';
|
|
1442
|
-
const initialCommand = data.initialCommand;
|
|
1443
|
-
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
1444
|
-
urlDetectionBuffer = '';
|
|
1445
|
-
announcedAuthUrls.clear();
|
|
1446
|
-
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
|
1447
|
-
const isLoginCommand = initialCommand && (initialCommand.includes('setup-token') ||
|
|
1448
|
-
initialCommand.includes('cursor-agent login') ||
|
|
1449
|
-
initialCommand.includes('auth login'));
|
|
1450
|
-
// Include command hash in session key so different commands get separate sessions
|
|
1451
|
-
const commandSuffix = isPlainShell && initialCommand
|
|
1452
|
-
? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
|
|
1453
|
-
: '';
|
|
1454
|
-
ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
|
|
1455
|
-
// Kill any existing login session before starting fresh
|
|
1456
|
-
if (isLoginCommand) {
|
|
1457
|
-
const oldSession = ptySessionsMap.get(ptySessionKey);
|
|
1458
|
-
if (oldSession) {
|
|
1459
|
-
console.log('🧹 Cleaning up existing login session:', ptySessionKey);
|
|
1460
|
-
if (oldSession.timeoutId)
|
|
1461
|
-
clearTimeout(oldSession.timeoutId);
|
|
1462
|
-
if (oldSession.pty && oldSession.pty.kill)
|
|
1463
|
-
oldSession.pty.kill();
|
|
1464
|
-
ptySessionsMap.delete(ptySessionKey);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
|
|
1468
|
-
if (existingSession) {
|
|
1469
|
-
console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
|
|
1470
|
-
shellProcess = existingSession.pty;
|
|
1471
|
-
clearTimeout(existingSession.timeoutId);
|
|
1472
|
-
ws.send(JSON.stringify({
|
|
1473
|
-
type: 'output',
|
|
1474
|
-
data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
|
|
1475
|
-
}));
|
|
1476
|
-
if (existingSession.buffer && existingSession.buffer.length > 0) {
|
|
1477
|
-
console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
|
|
1478
|
-
existingSession.buffer.forEach(bufferedData => {
|
|
1479
|
-
ws.send(JSON.stringify({
|
|
1480
|
-
type: 'output',
|
|
1481
|
-
data: bufferedData
|
|
1482
|
-
}));
|
|
1483
|
-
});
|
|
1484
|
-
}
|
|
1485
|
-
existingSession.ws = ws;
|
|
1486
|
-
return;
|
|
1487
|
-
}
|
|
1488
|
-
console.log('[INFO] Starting shell in:', projectPath);
|
|
1489
|
-
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
|
|
1490
|
-
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
|
|
1491
|
-
if (initialCommand) {
|
|
1492
|
-
console.log('⚡ Initial command:', initialCommand);
|
|
1493
|
-
}
|
|
1494
|
-
// First send a welcome message
|
|
1495
|
-
let welcomeMsg;
|
|
1496
|
-
if (isPlainShell) {
|
|
1497
|
-
welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
|
|
1498
|
-
}
|
|
1499
|
-
else {
|
|
1500
|
-
const providerName = provider === 'cursor' ? 'Cursor' : (provider === 'codex' ? 'Codex' : (provider === 'gemini' ? 'Gemini' : 'Claude'));
|
|
1501
|
-
welcomeMsg = hasSession ?
|
|
1502
|
-
`\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
|
|
1503
|
-
`\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
|
|
1504
|
-
}
|
|
1505
|
-
ws.send(JSON.stringify({
|
|
1506
|
-
type: 'output',
|
|
1507
|
-
data: welcomeMsg
|
|
1508
|
-
}));
|
|
1509
|
-
try {
|
|
1510
|
-
// Validate projectPath — resolve to absolute and verify it exists
|
|
1511
|
-
const resolvedProjectPath = path.resolve(projectPath);
|
|
1512
|
-
try {
|
|
1513
|
-
const stats = fs.statSync(resolvedProjectPath);
|
|
1514
|
-
if (!stats.isDirectory()) {
|
|
1515
|
-
throw new Error('Not a directory');
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
catch (pathErr) {
|
|
1519
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Invalid project path' }));
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
// Validate sessionId — only allow safe characters
|
|
1523
|
-
const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
|
|
1524
|
-
if (sessionId && !safeSessionIdPattern.test(sessionId)) {
|
|
1525
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Invalid session ID' }));
|
|
1526
|
-
return;
|
|
1527
|
-
}
|
|
1528
|
-
// Build shell command — use cwd for project path (never interpolate into shell string)
|
|
1529
|
-
let shellCommand;
|
|
1530
|
-
if (isPlainShell) {
|
|
1531
|
-
// Plain shell mode - run the initial command in the project directory
|
|
1532
|
-
shellCommand = initialCommand;
|
|
1533
|
-
}
|
|
1534
|
-
else if (provider === 'cursor') {
|
|
1535
|
-
if (hasSession && sessionId) {
|
|
1536
|
-
shellCommand = `cursor-agent --resume="${sessionId}"`;
|
|
1537
|
-
}
|
|
1538
|
-
else {
|
|
1539
|
-
shellCommand = 'cursor-agent';
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
else if (provider === 'codex') {
|
|
1543
|
-
// Use codex command; attempt to resume and fall back to a new session when the resume fails.
|
|
1544
|
-
if (hasSession && sessionId) {
|
|
1545
|
-
if (os.platform() === 'win32') {
|
|
1546
|
-
// PowerShell syntax for fallback
|
|
1547
|
-
shellCommand = `codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
|
|
1548
|
-
}
|
|
1549
|
-
else {
|
|
1550
|
-
shellCommand = `codex resume "${sessionId}" || codex`;
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
else {
|
|
1554
|
-
shellCommand = 'codex';
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
else if (provider === 'gemini') {
|
|
1558
|
-
const command = initialCommand || 'gemini';
|
|
1559
|
-
let resumeId = sessionId;
|
|
1560
|
-
if (hasSession && sessionId) {
|
|
1561
|
-
try {
|
|
1562
|
-
// Gemini CLI enforces its own native session IDs, unlike other agents that accept arbitrary string names.
|
|
1563
|
-
// The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
|
|
1564
|
-
// We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell.
|
|
1565
|
-
const sess = sessionManager.getSession(sessionId);
|
|
1566
|
-
if (sess && sess.cliSessionId) {
|
|
1567
|
-
resumeId = sess.cliSessionId;
|
|
1568
|
-
// Validate the looked-up CLI session ID too
|
|
1569
|
-
if (!safeSessionIdPattern.test(resumeId)) {
|
|
1570
|
-
resumeId = null;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
catch (err) {
|
|
1575
|
-
console.error('Failed to get Gemini CLI session ID:', err);
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
if (hasSession && resumeId) {
|
|
1579
|
-
shellCommand = `${command} --resume "${resumeId}"`;
|
|
1580
|
-
}
|
|
1581
|
-
else {
|
|
1582
|
-
shellCommand = command;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
else {
|
|
1586
|
-
// Claude (default provider)
|
|
1587
|
-
const command = initialCommand || 'claude';
|
|
1588
|
-
if (hasSession && sessionId) {
|
|
1589
|
-
if (os.platform() === 'win32') {
|
|
1590
|
-
shellCommand = `claude --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
|
|
1591
|
-
}
|
|
1592
|
-
else {
|
|
1593
|
-
shellCommand = `claude --resume "${sessionId}" || claude`;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
else {
|
|
1597
|
-
shellCommand = command;
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
console.log('🔧 Executing shell command:', shellCommand);
|
|
1601
|
-
// Use appropriate shell based on platform
|
|
1602
|
-
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
1603
|
-
const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
|
1604
|
-
// Use terminal dimensions from client if provided, otherwise use defaults
|
|
1605
|
-
const termCols = data.cols || 80;
|
|
1606
|
-
const termRows = data.rows || 24;
|
|
1607
|
-
console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
|
|
1608
|
-
shellProcess = pty.spawn(shell, shellArgs, {
|
|
1609
|
-
name: 'xterm-256color',
|
|
1610
|
-
cols: termCols,
|
|
1611
|
-
rows: termRows,
|
|
1612
|
-
cwd: resolvedProjectPath,
|
|
1613
|
-
env: {
|
|
1614
|
-
...process.env,
|
|
1615
|
-
TERM: 'xterm-256color',
|
|
1616
|
-
COLORTERM: 'truecolor',
|
|
1617
|
-
FORCE_COLOR: '3'
|
|
1618
|
-
}
|
|
1619
|
-
});
|
|
1620
|
-
console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
|
|
1621
|
-
ptySessionsMap.set(ptySessionKey, {
|
|
1622
|
-
pty: shellProcess,
|
|
1623
|
-
ws: ws,
|
|
1624
|
-
buffer: [],
|
|
1625
|
-
timeoutId: null,
|
|
1626
|
-
projectPath,
|
|
1627
|
-
sessionId
|
|
1628
|
-
});
|
|
1629
|
-
// Handle data output
|
|
1630
|
-
shellProcess.onData((data) => {
|
|
1631
|
-
const session = ptySessionsMap.get(ptySessionKey);
|
|
1632
|
-
if (!session)
|
|
1633
|
-
return;
|
|
1634
|
-
if (session.buffer.length < 5000) {
|
|
1635
|
-
session.buffer.push(data);
|
|
1636
|
-
}
|
|
1637
|
-
else {
|
|
1638
|
-
session.buffer.shift();
|
|
1639
|
-
session.buffer.push(data);
|
|
1640
|
-
}
|
|
1641
|
-
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1642
|
-
let outputData = data;
|
|
1643
|
-
const cleanChunk = stripAnsiSequences(data);
|
|
1644
|
-
urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
|
|
1645
|
-
outputData = outputData.replace(/OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g, '[INFO] Opening in browser: $1');
|
|
1646
|
-
const emitAuthUrl = (detectedUrl, autoOpen = false) => {
|
|
1647
|
-
const normalizedUrl = normalizeDetectedUrl(detectedUrl);
|
|
1648
|
-
if (!normalizedUrl)
|
|
1649
|
-
return;
|
|
1650
|
-
const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
|
|
1651
|
-
if (isNewUrl) {
|
|
1652
|
-
announcedAuthUrls.add(normalizedUrl);
|
|
1653
|
-
session.ws.send(JSON.stringify({
|
|
1654
|
-
type: 'auth_url',
|
|
1655
|
-
url: normalizedUrl,
|
|
1656
|
-
autoOpen
|
|
1657
|
-
}));
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
|
|
1661
|
-
.map((url) => normalizeDetectedUrl(url))
|
|
1662
|
-
.filter(Boolean);
|
|
1663
|
-
// Prefer the most complete URL if shorter prefix variants are also present.
|
|
1664
|
-
const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter((url, _, urls) => !urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url)));
|
|
1665
|
-
dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
|
|
1666
|
-
if (shouldAutoOpenUrlFromOutput(cleanChunk) && dedupedDetectedUrls.length > 0) {
|
|
1667
|
-
const bestUrl = dedupedDetectedUrls.reduce((longest, current) => current.length > longest.length ? current : longest);
|
|
1668
|
-
emitAuthUrl(bestUrl, true);
|
|
1669
|
-
}
|
|
1670
|
-
// Send regular output
|
|
1671
|
-
session.ws.send(JSON.stringify({
|
|
1672
|
-
type: 'output',
|
|
1673
|
-
data: outputData
|
|
1674
|
-
}));
|
|
1675
|
-
}
|
|
1676
|
-
});
|
|
1677
|
-
// Handle process exit
|
|
1678
|
-
shellProcess.onExit((exitCode) => {
|
|
1679
|
-
console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
|
|
1680
|
-
const session = ptySessionsMap.get(ptySessionKey);
|
|
1681
|
-
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1682
|
-
session.ws.send(JSON.stringify({
|
|
1683
|
-
type: 'output',
|
|
1684
|
-
data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
|
|
1685
|
-
}));
|
|
1686
|
-
}
|
|
1687
|
-
if (session && session.timeoutId) {
|
|
1688
|
-
clearTimeout(session.timeoutId);
|
|
1689
|
-
}
|
|
1690
|
-
ptySessionsMap.delete(ptySessionKey);
|
|
1691
|
-
shellProcess = null;
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
catch (spawnError) {
|
|
1695
|
-
console.error('[ERROR] Error spawning process:', spawnError);
|
|
1696
|
-
ws.send(JSON.stringify({
|
|
1697
|
-
type: 'output',
|
|
1698
|
-
data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n`
|
|
1699
|
-
}));
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
else if (data.type === 'input') {
|
|
1703
|
-
// Send input to shell process
|
|
1704
|
-
if (shellProcess && shellProcess.write) {
|
|
1705
|
-
try {
|
|
1706
|
-
shellProcess.write(data.data);
|
|
1707
|
-
}
|
|
1708
|
-
catch (error) {
|
|
1709
|
-
console.error('Error writing to shell:', error);
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
else {
|
|
1713
|
-
console.warn('No active shell process to send input to');
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
else if (data.type === 'resize') {
|
|
1717
|
-
// Handle terminal resize
|
|
1718
|
-
if (shellProcess && shellProcess.resize) {
|
|
1719
|
-
console.log('Terminal resize requested:', data.cols, 'x', data.rows);
|
|
1720
|
-
shellProcess.resize(data.cols, data.rows);
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
catch (error) {
|
|
1725
|
-
console.error('[ERROR] Shell WebSocket error:', error.message);
|
|
1726
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1727
|
-
ws.send(JSON.stringify({
|
|
1728
|
-
type: 'output',
|
|
1729
|
-
data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
|
|
1730
|
-
}));
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
});
|
|
1734
|
-
ws.on('close', () => {
|
|
1735
|
-
console.log('🔌 Shell client disconnected');
|
|
1736
|
-
if (ptySessionKey) {
|
|
1737
|
-
const session = ptySessionsMap.get(ptySessionKey);
|
|
1738
|
-
if (session) {
|
|
1739
|
-
console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
|
|
1740
|
-
session.ws = null;
|
|
1741
|
-
session.timeoutId = setTimeout(() => {
|
|
1742
|
-
console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
|
|
1743
|
-
if (session.pty && session.pty.kill) {
|
|
1744
|
-
session.pty.kill();
|
|
1745
|
-
}
|
|
1746
|
-
ptySessionsMap.delete(ptySessionKey);
|
|
1747
|
-
}, PTY_SESSION_TIMEOUT);
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
});
|
|
1751
|
-
ws.on('error', (error) => {
|
|
1752
|
-
console.error('[ERROR] Shell WebSocket error:', error);
|
|
1753
|
-
});
|
|
1754
|
-
}
|
|
1755
|
-
// Image upload endpoint
|
|
1756
|
-
app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => {
|
|
911
|
+
app.post('/api/projects/:projectId/files/upload', authenticateToken, uploadFilesHandler);
|
|
912
|
+
// Image upload endpoint. Accepts the DB-assigned `projectId` (not a folder name)
|
|
913
|
+
// but the current implementation doesn't need to touch the project directory,
|
|
914
|
+
// so we just leave the param rename for consistency with the rest of the API.
|
|
915
|
+
app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req, res) => {
|
|
1757
916
|
try {
|
|
1758
917
|
const multer = (await import('multer')).default;
|
|
1759
918
|
const path = (await import('path')).default;
|
|
@@ -1828,10 +987,11 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|
|
1828
987
|
res.status(500).json({ error: 'Internal server error' });
|
|
1829
988
|
}
|
|
1830
989
|
});
|
|
1831
|
-
// Get token usage for a specific session
|
|
1832
|
-
|
|
990
|
+
// Get token usage for a specific session. `projectId` is the DB primary key;
|
|
991
|
+
// the Claude branch below resolves it to an absolute path via the DB.
|
|
992
|
+
app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
|
1833
993
|
try {
|
|
1834
|
-
const {
|
|
994
|
+
const { projectId, sessionId } = req.params;
|
|
1835
995
|
const { provider = 'claude' } = req.query;
|
|
1836
996
|
const homeDir = os.homedir();
|
|
1837
997
|
// Allow only safe characters in sessionId
|
|
@@ -1928,14 +1088,13 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
1928
1088
|
});
|
|
1929
1089
|
}
|
|
1930
1090
|
// Handle Claude sessions (default)
|
|
1931
|
-
//
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
return res.status(500).json({ error: 'Failed to determine project path' });
|
|
1091
|
+
// Resolve the project path through the DB using the caller-supplied
|
|
1092
|
+
// `projectId`. Legacy code here called extractProjectDirectory with a
|
|
1093
|
+
// folder-encoded project name; the migration centralizes that lookup
|
|
1094
|
+
// in the projects table.
|
|
1095
|
+
const projectPath = await projectsDb.getProjectPathById(projectId);
|
|
1096
|
+
if (!projectPath) {
|
|
1097
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
1939
1098
|
}
|
|
1940
1099
|
// Construct the JSONL file path
|
|
1941
1100
|
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
|
@@ -2154,12 +1313,13 @@ async function startServer() {
|
|
|
2154
1313
|
console.log(`${c.tip('[TIP]')} Run "cloudcli status" for full configuration details`);
|
|
2155
1314
|
console.log('');
|
|
2156
1315
|
// Start watching the projects folder for changes
|
|
2157
|
-
await
|
|
1316
|
+
await initializeSessionsWatcher();
|
|
2158
1317
|
// Start server-side plugin processes for enabled plugins
|
|
2159
1318
|
startEnabledPluginServers().catch(err => {
|
|
2160
1319
|
console.error('[Plugins] Error during startup:', err.message);
|
|
2161
1320
|
});
|
|
2162
1321
|
});
|
|
1322
|
+
await closeSessionsWatcher();
|
|
2163
1323
|
// Clean up plugin processes on shutdown
|
|
2164
1324
|
const shutdownPlugins = async () => {
|
|
2165
1325
|
await stopAllPlugins();
|