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