@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
package/server/index.js
CHANGED
|
@@ -1,251 +1,123 @@
|
|
|
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
|
-
|
|
8
|
-
const __dirname = getModuleDir(import.meta.url);
|
|
9
|
-
// The server source runs from /server, while the compiled output runs from /dist-server/server.
|
|
10
|
-
// Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
|
|
11
|
-
const APP_ROOT = findAppRoot(__dirname);
|
|
12
|
-
const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
|
13
|
-
|
|
14
|
-
import { c } from './utils/colors.js';
|
|
15
|
-
|
|
16
|
-
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
|
17
|
-
|
|
18
|
-
import express from 'express';
|
|
19
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
20
6
|
import os from 'os';
|
|
21
7
|
import http from 'http';
|
|
22
|
-
import cors from 'cors';
|
|
23
|
-
import { promises as fsPromises } from 'fs';
|
|
24
8
|
import { spawn } from 'child_process';
|
|
25
|
-
|
|
26
|
-
import
|
|
9
|
+
|
|
10
|
+
import express from 'express';
|
|
11
|
+
import cors from 'cors';
|
|
27
12
|
import mime from 'mime-types';
|
|
28
13
|
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
import {
|
|
14
|
+
import { AppError, WORKSPACES_ROOT, validateWorkspacePath } from '@/shared/utils.js';
|
|
15
|
+
import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
|
|
16
|
+
import { createWebSocketServer } from '@/modules/websocket/index.js';
|
|
17
|
+
|
|
18
|
+
import { getConnectableHost } from '../shared/networkHosts.js';
|
|
19
|
+
|
|
20
|
+
import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
|
|
21
|
+
import {
|
|
22
|
+
queryClaudeSDK,
|
|
23
|
+
abortClaudeSDKSession,
|
|
24
|
+
isClaudeSDKSessionActive,
|
|
25
|
+
getActiveClaudeSDKSessions,
|
|
26
|
+
resolveToolApproval,
|
|
27
|
+
getPendingApprovalsForSession,
|
|
28
|
+
reconnectSessionWriter,
|
|
29
|
+
} from './claude-sdk.js';
|
|
30
|
+
import {
|
|
31
|
+
spawnCursor,
|
|
32
|
+
abortCursorSession,
|
|
33
|
+
isCursorSessionActive,
|
|
34
|
+
getActiveCursorSessions,
|
|
35
|
+
} from './cursor-cli.js';
|
|
36
|
+
import {
|
|
37
|
+
queryCodex,
|
|
38
|
+
abortCodexSession,
|
|
39
|
+
isCodexSessionActive,
|
|
40
|
+
getActiveCodexSessions,
|
|
41
|
+
} from './openai-codex.js';
|
|
42
|
+
import {
|
|
43
|
+
spawnGemini,
|
|
44
|
+
abortGeminiSession,
|
|
45
|
+
isGeminiSessionActive,
|
|
46
|
+
getActiveGeminiSessions,
|
|
47
|
+
} from './gemini-cli.js';
|
|
34
48
|
import sessionManager from './sessionManager.js';
|
|
49
|
+
import {
|
|
50
|
+
stripAnsiSequences,
|
|
51
|
+
normalizeDetectedUrl,
|
|
52
|
+
extractUrlsFromText,
|
|
53
|
+
shouldAutoOpenUrlFromOutput,
|
|
54
|
+
} from './utils/url-detection.js';
|
|
35
55
|
import gitRoutes from './routes/git.js';
|
|
36
56
|
import authRoutes from './routes/auth.js';
|
|
37
|
-
import mcpRoutes from './routes/mcp.js';
|
|
38
57
|
import cursorRoutes from './routes/cursor.js';
|
|
39
58
|
import taskmasterRoutes from './routes/taskmaster.js';
|
|
40
59
|
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
41
60
|
import commandsRoutes from './routes/commands.js';
|
|
42
61
|
import settingsRoutes from './routes/settings.js';
|
|
43
62
|
import agentRoutes from './routes/agent.js';
|
|
44
|
-
import
|
|
45
|
-
import cliAuthRoutes from './routes/cli-auth.js';
|
|
63
|
+
import projectModuleRoutes from './modules/projects/projects.routes.js';
|
|
46
64
|
import userRoutes from './routes/user.js';
|
|
47
|
-
import codexRoutes from './routes/codex.js';
|
|
48
65
|
import geminiRoutes from './routes/gemini.js';
|
|
49
66
|
import pluginsRoutes from './routes/plugins.js';
|
|
50
|
-
import
|
|
51
|
-
import { createNormalizedMessage } from './providers/types.js';
|
|
67
|
+
import providerRoutes from './modules/providers/provider.routes.js';
|
|
52
68
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
53
|
-
import { initializeDatabase,
|
|
69
|
+
import { initializeDatabase, projectsDb } from './modules/database/index.js';
|
|
54
70
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
55
71
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
56
72
|
import { IS_PLATFORM } from './constants/config.js';
|
|
57
|
-
import {
|
|
58
|
-
|
|
59
|
-
const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
|
|
60
|
-
|
|
61
|
-
// File system watchers for provider project/session folders
|
|
62
|
-
const PROVIDER_WATCH_PATHS = [
|
|
63
|
-
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
64
|
-
{ provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
|
|
65
|
-
{ provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') },
|
|
66
|
-
{ provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'projects') },
|
|
67
|
-
{ provider: 'gemini_sessions', rootPath: path.join(os.homedir(), '.gemini', 'sessions') }
|
|
68
|
-
];
|
|
69
|
-
const WATCHER_IGNORED_PATTERNS = [
|
|
70
|
-
'**/node_modules/**',
|
|
71
|
-
'**/.git/**',
|
|
72
|
-
'**/dist/**',
|
|
73
|
-
'**/build/**',
|
|
74
|
-
'**/*.tmp',
|
|
75
|
-
'**/*.swp',
|
|
76
|
-
'**/.DS_Store'
|
|
77
|
-
];
|
|
78
|
-
const WATCHER_DEBOUNCE_MS = 300;
|
|
79
|
-
let projectsWatchers = [];
|
|
80
|
-
let projectsWatcherDebounceTimer = null;
|
|
81
|
-
const connectedClients = new Set();
|
|
82
|
-
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
83
|
-
|
|
84
|
-
// Broadcast progress to all connected WebSocket clients
|
|
85
|
-
function broadcastProgress(progress) {
|
|
86
|
-
const message = JSON.stringify({
|
|
87
|
-
type: 'loading_progress',
|
|
88
|
-
...progress
|
|
89
|
-
});
|
|
90
|
-
connectedClients.forEach(client => {
|
|
91
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
92
|
-
client.send(message);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Setup file system watchers for Claude, Cursor, and Codex project/session folders
|
|
98
|
-
async function setupProjectsWatcher() {
|
|
99
|
-
const chokidar = (await import('chokidar')).default;
|
|
100
|
-
|
|
101
|
-
if (projectsWatcherDebounceTimer) {
|
|
102
|
-
clearTimeout(projectsWatcherDebounceTimer);
|
|
103
|
-
projectsWatcherDebounceTimer = null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
await Promise.all(
|
|
107
|
-
projectsWatchers.map(async (watcher) => {
|
|
108
|
-
try {
|
|
109
|
-
await watcher.close();
|
|
110
|
-
} catch (error) {
|
|
111
|
-
console.error('[WARN] Failed to close watcher:', error);
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
);
|
|
115
|
-
projectsWatchers = [];
|
|
116
|
-
|
|
117
|
-
const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
|
|
118
|
-
if (projectsWatcherDebounceTimer) {
|
|
119
|
-
clearTimeout(projectsWatcherDebounceTimer);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
projectsWatcherDebounceTimer = setTimeout(async () => {
|
|
123
|
-
// Prevent reentrant calls
|
|
124
|
-
if (isGetProjectsRunning) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
isGetProjectsRunning = true;
|
|
130
|
-
|
|
131
|
-
// Clear project directory cache when files change
|
|
132
|
-
clearProjectDirectoryCache();
|
|
133
|
-
|
|
134
|
-
// Get updated projects list
|
|
135
|
-
const updatedProjects = await getProjects(broadcastProgress);
|
|
136
|
-
|
|
137
|
-
// Notify all connected clients about the project changes
|
|
138
|
-
const updateMessage = JSON.stringify({
|
|
139
|
-
type: 'projects_updated',
|
|
140
|
-
projects: updatedProjects,
|
|
141
|
-
timestamp: new Date().toISOString(),
|
|
142
|
-
changeType: eventType,
|
|
143
|
-
changedFile: path.relative(rootPath, filePath),
|
|
144
|
-
watchProvider: provider
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
connectedClients.forEach(client => {
|
|
148
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
149
|
-
client.send(updateMessage);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
} catch (error) {
|
|
154
|
-
console.error('[ERROR] Error handling project changes:', error);
|
|
155
|
-
} finally {
|
|
156
|
-
isGetProjectsRunning = false;
|
|
157
|
-
}
|
|
158
|
-
}, WATCHER_DEBOUNCE_MS);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
|
|
162
|
-
try {
|
|
163
|
-
// chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
|
|
164
|
-
// Ensure provider folders exist before creating the watcher so watching stays active.
|
|
165
|
-
await fsPromises.mkdir(rootPath, { recursive: true });
|
|
166
|
-
|
|
167
|
-
// Initialize chokidar watcher with optimized settings
|
|
168
|
-
const watcher = chokidar.watch(rootPath, {
|
|
169
|
-
ignored: WATCHER_IGNORED_PATTERNS,
|
|
170
|
-
persistent: true,
|
|
171
|
-
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
172
|
-
followSymlinks: false,
|
|
173
|
-
depth: 10, // Reasonable depth limit
|
|
174
|
-
awaitWriteFinish: {
|
|
175
|
-
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
|
176
|
-
pollInterval: 50
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Set up event listeners
|
|
181
|
-
watcher
|
|
182
|
-
.on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
|
|
183
|
-
.on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
|
|
184
|
-
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
|
|
185
|
-
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
|
|
186
|
-
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
|
|
187
|
-
.on('error', (error) => {
|
|
188
|
-
console.error(`[ERROR] ${provider} watcher error:`, error);
|
|
189
|
-
})
|
|
190
|
-
.on('ready', () => {
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
projectsWatchers.push(watcher);
|
|
194
|
-
} catch (error) {
|
|
195
|
-
console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
73
|
+
import { c } from './utils/colors.js';
|
|
198
74
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
75
|
+
const __dirname = getModuleDir(import.meta.url);
|
|
76
|
+
// The server source runs from /server, while the compiled output runs from /dist-server/server.
|
|
77
|
+
// Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
|
|
78
|
+
const APP_ROOT = findAppRoot(__dirname);
|
|
79
|
+
const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
|
203
80
|
|
|
81
|
+
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
|
204
82
|
|
|
205
83
|
const app = express();
|
|
206
84
|
const server = http.createServer(app);
|
|
207
85
|
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// Store user info in the request for later use
|
|
245
|
-
info.req.user = user;
|
|
246
|
-
console.log('[OK] WebSocket authenticated for user:', user.username);
|
|
247
|
-
return true;
|
|
248
|
-
}
|
|
86
|
+
// Single WebSocket server that handles chat, shell, and plugin proxy paths.
|
|
87
|
+
const wss = createWebSocketServer(server, {
|
|
88
|
+
verifyClient: {
|
|
89
|
+
isPlatform: IS_PLATFORM,
|
|
90
|
+
authenticateWebSocket,
|
|
91
|
+
},
|
|
92
|
+
chat: {
|
|
93
|
+
queryClaudeSDK,
|
|
94
|
+
spawnCursor,
|
|
95
|
+
queryCodex,
|
|
96
|
+
spawnGemini,
|
|
97
|
+
abortClaudeSDKSession,
|
|
98
|
+
abortCursorSession,
|
|
99
|
+
abortCodexSession,
|
|
100
|
+
abortGeminiSession,
|
|
101
|
+
resolveToolApproval,
|
|
102
|
+
isClaudeSDKSessionActive,
|
|
103
|
+
isCursorSessionActive,
|
|
104
|
+
isCodexSessionActive,
|
|
105
|
+
isGeminiSessionActive,
|
|
106
|
+
reconnectSessionWriter,
|
|
107
|
+
getPendingApprovalsForSession,
|
|
108
|
+
getActiveClaudeSDKSessions,
|
|
109
|
+
getActiveCursorSessions,
|
|
110
|
+
getActiveCodexSessions,
|
|
111
|
+
getActiveGeminiSessions,
|
|
112
|
+
},
|
|
113
|
+
shell: {
|
|
114
|
+
getSessionById: (sessionId) => sessionManager.getSession(sessionId),
|
|
115
|
+
stripAnsiSequences,
|
|
116
|
+
normalizeDetectedUrl,
|
|
117
|
+
extractUrlsFromText,
|
|
118
|
+
shouldAutoOpenUrlFromOutput,
|
|
119
|
+
},
|
|
120
|
+
getPluginPort,
|
|
249
121
|
});
|
|
250
122
|
|
|
251
123
|
// Make WebSocket server available to routes
|
|
@@ -281,14 +153,11 @@ app.use('/api', validateApiKey);
|
|
|
281
153
|
app.use('/api/auth', authRoutes);
|
|
282
154
|
|
|
283
155
|
// Projects API Routes (protected)
|
|
284
|
-
app.use('/api/projects', authenticateToken,
|
|
156
|
+
app.use('/api/projects', authenticateToken, projectModuleRoutes);
|
|
285
157
|
|
|
286
158
|
// Git API Routes (protected)
|
|
287
159
|
app.use('/api/git', authenticateToken, gitRoutes);
|
|
288
160
|
|
|
289
|
-
// MCP API Routes (protected)
|
|
290
|
-
app.use('/api/mcp', authenticateToken, mcpRoutes);
|
|
291
|
-
|
|
292
161
|
// Cursor API Routes (protected)
|
|
293
162
|
app.use('/api/cursor', authenticateToken, cursorRoutes);
|
|
294
163
|
|
|
@@ -304,23 +173,17 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
|
304
173
|
// Settings API Routes (protected)
|
|
305
174
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
|
306
175
|
|
|
307
|
-
// CLI Authentication API Routes (protected)
|
|
308
|
-
app.use('/api/cli', authenticateToken, cliAuthRoutes);
|
|
309
|
-
|
|
310
176
|
// User API Routes (protected)
|
|
311
177
|
app.use('/api/user', authenticateToken, userRoutes);
|
|
312
178
|
|
|
313
|
-
// Codex API Routes (protected)
|
|
314
|
-
app.use('/api/codex', authenticateToken, codexRoutes);
|
|
315
|
-
|
|
316
179
|
// Gemini API Routes (protected)
|
|
317
180
|
app.use('/api/gemini', authenticateToken, geminiRoutes);
|
|
318
181
|
|
|
319
182
|
// Plugins API Routes (protected)
|
|
320
183
|
app.use('/api/plugins', authenticateToken, pluginsRoutes);
|
|
321
184
|
|
|
322
|
-
// Unified
|
|
323
|
-
app.use('/api/
|
|
185
|
+
// Unified provider MCP routes (protected)
|
|
186
|
+
app.use('/api/providers', authenticateToken, providerRoutes);
|
|
324
187
|
|
|
325
188
|
// Agent API Routes (uses API key authentication)
|
|
326
189
|
app.use('/api/agent', agentRoutes);
|
|
@@ -422,155 +285,6 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
|
|
422
285
|
}
|
|
423
286
|
});
|
|
424
287
|
|
|
425
|
-
app.get('/api/projects', authenticateToken, async (req, res) => {
|
|
426
|
-
try {
|
|
427
|
-
const projects = await getProjects(broadcastProgress);
|
|
428
|
-
res.json(projects);
|
|
429
|
-
} catch (error) {
|
|
430
|
-
res.status(500).json({ error: error.message });
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
|
|
435
|
-
try {
|
|
436
|
-
const { limit = 5, offset = 0 } = req.query;
|
|
437
|
-
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
|
|
438
|
-
applyCustomSessionNames(result.sessions, 'claude');
|
|
439
|
-
res.json(result);
|
|
440
|
-
} catch (error) {
|
|
441
|
-
res.status(500).json({ error: error.message });
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
// Rename project endpoint
|
|
446
|
-
app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
|
|
447
|
-
try {
|
|
448
|
-
const { displayName } = req.body;
|
|
449
|
-
await renameProject(req.params.projectName, displayName);
|
|
450
|
-
res.json({ success: true });
|
|
451
|
-
} catch (error) {
|
|
452
|
-
res.status(500).json({ error: error.message });
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
// Delete session endpoint
|
|
457
|
-
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
|
|
458
|
-
try {
|
|
459
|
-
const { projectName, sessionId } = req.params;
|
|
460
|
-
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
|
461
|
-
await deleteSession(projectName, sessionId);
|
|
462
|
-
sessionNamesDb.deleteName(sessionId, 'claude');
|
|
463
|
-
console.log(`[API] Session ${sessionId} deleted successfully`);
|
|
464
|
-
res.json({ success: true });
|
|
465
|
-
} catch (error) {
|
|
466
|
-
console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
|
|
467
|
-
res.status(500).json({ error: error.message });
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// Rename session endpoint
|
|
472
|
-
app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) => {
|
|
473
|
-
try {
|
|
474
|
-
const { sessionId } = req.params;
|
|
475
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
476
|
-
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
|
477
|
-
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
478
|
-
}
|
|
479
|
-
const { summary, provider } = req.body;
|
|
480
|
-
if (!summary || typeof summary !== 'string' || summary.trim() === '') {
|
|
481
|
-
return res.status(400).json({ error: 'Summary is required' });
|
|
482
|
-
}
|
|
483
|
-
if (summary.trim().length > 500) {
|
|
484
|
-
return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
|
|
485
|
-
}
|
|
486
|
-
if (!provider || !VALID_PROVIDERS.includes(provider)) {
|
|
487
|
-
return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
|
|
488
|
-
}
|
|
489
|
-
sessionNamesDb.setName(safeSessionId, provider, summary.trim());
|
|
490
|
-
res.json({ success: true });
|
|
491
|
-
} catch (error) {
|
|
492
|
-
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
|
|
493
|
-
res.status(500).json({ error: error.message });
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// Delete project endpoint
|
|
498
|
-
// force=true to allow removal even when sessions exist
|
|
499
|
-
// deleteData=true to also delete session/memory files on disk (destructive)
|
|
500
|
-
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
501
|
-
try {
|
|
502
|
-
const { projectName } = req.params;
|
|
503
|
-
const force = req.query.force === 'true';
|
|
504
|
-
const deleteData = req.query.deleteData === 'true';
|
|
505
|
-
await deleteProject(projectName, force, deleteData);
|
|
506
|
-
res.json({ success: true });
|
|
507
|
-
} catch (error) {
|
|
508
|
-
res.status(500).json({ error: error.message });
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// Create project endpoint
|
|
513
|
-
app.post('/api/projects/create', authenticateToken, async (req, res) => {
|
|
514
|
-
try {
|
|
515
|
-
const { path: projectPath } = req.body;
|
|
516
|
-
|
|
517
|
-
if (!projectPath || !projectPath.trim()) {
|
|
518
|
-
return res.status(400).json({ error: 'Project path is required' });
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const project = await addProjectManually(projectPath.trim());
|
|
522
|
-
res.json({ success: true, project });
|
|
523
|
-
} catch (error) {
|
|
524
|
-
console.error('Error creating project:', error);
|
|
525
|
-
res.status(500).json({ error: error.message });
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// Search conversations content (SSE streaming)
|
|
530
|
-
app.get('/api/search/conversations', authenticateToken, async (req, res) => {
|
|
531
|
-
const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
532
|
-
const parsedLimit = Number.parseInt(String(req.query.limit), 10);
|
|
533
|
-
const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 100));
|
|
534
|
-
|
|
535
|
-
if (query.length < 2) {
|
|
536
|
-
return res.status(400).json({ error: 'Query must be at least 2 characters' });
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
res.writeHead(200, {
|
|
540
|
-
'Content-Type': 'text/event-stream',
|
|
541
|
-
'Cache-Control': 'no-cache',
|
|
542
|
-
'Connection': 'keep-alive',
|
|
543
|
-
'X-Accel-Buffering': 'no',
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
let closed = false;
|
|
547
|
-
const abortController = new AbortController();
|
|
548
|
-
req.on('close', () => { closed = true; abortController.abort(); });
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
await searchConversations(query, limit, ({ projectResult, totalMatches, scannedProjects, totalProjects }) => {
|
|
552
|
-
if (closed) return;
|
|
553
|
-
if (projectResult) {
|
|
554
|
-
res.write(`event: result\ndata: ${JSON.stringify({ projectResult, totalMatches, scannedProjects, totalProjects })}\n\n`);
|
|
555
|
-
} else {
|
|
556
|
-
res.write(`event: progress\ndata: ${JSON.stringify({ totalMatches, scannedProjects, totalProjects })}\n\n`);
|
|
557
|
-
}
|
|
558
|
-
}, abortController.signal);
|
|
559
|
-
if (!closed) {
|
|
560
|
-
res.write(`event: done\ndata: {}\n\n`);
|
|
561
|
-
}
|
|
562
|
-
} catch (error) {
|
|
563
|
-
console.error('Error searching conversations:', error);
|
|
564
|
-
if (!closed) {
|
|
565
|
-
res.write(`event: error\ndata: ${JSON.stringify({ error: 'Search failed' })}\n\n`);
|
|
566
|
-
}
|
|
567
|
-
} finally {
|
|
568
|
-
if (!closed) {
|
|
569
|
-
res.end();
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
});
|
|
573
|
-
|
|
574
288
|
const expandWorkspacePath = (inputPath) => {
|
|
575
289
|
if (!inputPath) return inputPath;
|
|
576
290
|
if (inputPath === '~') {
|
|
@@ -704,9 +418,9 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
|
|
|
704
418
|
});
|
|
705
419
|
|
|
706
420
|
// Read file content endpoint
|
|
707
|
-
app.get('/api/projects/:
|
|
421
|
+
app.get('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
|
|
708
422
|
try {
|
|
709
|
-
const {
|
|
423
|
+
const { projectId } = req.params;
|
|
710
424
|
const { filePath } = req.query;
|
|
711
425
|
|
|
712
426
|
|
|
@@ -715,7 +429,9 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
715
429
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
716
430
|
}
|
|
717
431
|
|
|
718
|
-
|
|
432
|
+
// Resolve the absolute project root via the DB-backed helper; the
|
|
433
|
+
// caller passes the DB-assigned `projectId`, not a folder name.
|
|
434
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
719
435
|
if (!projectRoot) {
|
|
720
436
|
return res.status(404).json({ error: 'Project not found' });
|
|
721
437
|
}
|
|
@@ -744,9 +460,9 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
744
460
|
});
|
|
745
461
|
|
|
746
462
|
// Serve raw file bytes for previews and downloads.
|
|
747
|
-
app.get('/api/projects/:
|
|
463
|
+
app.get('/api/projects/:projectId/files/content', authenticateToken, async (req, res) => {
|
|
748
464
|
try {
|
|
749
|
-
const {
|
|
465
|
+
const { projectId } = req.params;
|
|
750
466
|
const { path: filePath } = req.query;
|
|
751
467
|
|
|
752
468
|
|
|
@@ -755,7 +471,8 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
755
471
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
756
472
|
}
|
|
757
473
|
|
|
758
|
-
|
|
474
|
+
// Projects are now addressed by DB `projectId`, resolved to their path here.
|
|
475
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
759
476
|
if (!projectRoot) {
|
|
760
477
|
return res.status(404).json({ error: 'Project not found' });
|
|
761
478
|
}
|
|
@@ -801,9 +518,9 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
801
518
|
});
|
|
802
519
|
|
|
803
520
|
// Save file content endpoint
|
|
804
|
-
app.put('/api/projects/:
|
|
521
|
+
app.put('/api/projects/:projectId/file', authenticateToken, async (req, res) => {
|
|
805
522
|
try {
|
|
806
|
-
const {
|
|
523
|
+
const { projectId } = req.params;
|
|
807
524
|
const { filePath, content } = req.body;
|
|
808
525
|
|
|
809
526
|
|
|
@@ -816,7 +533,8 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
816
533
|
return res.status(400).json({ error: 'Content is required' });
|
|
817
534
|
}
|
|
818
535
|
|
|
819
|
-
|
|
536
|
+
// Projects are now addressed by DB `projectId`, resolved to their path here.
|
|
537
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
820
538
|
if (!projectRoot) {
|
|
821
539
|
return res.status(404).json({ error: 'Project not found' });
|
|
822
540
|
}
|
|
@@ -850,19 +568,16 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
850
568
|
}
|
|
851
569
|
});
|
|
852
570
|
|
|
853
|
-
app.get('/api/projects/:
|
|
571
|
+
app.get('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
|
|
854
572
|
try {
|
|
855
573
|
|
|
856
574
|
// Using fsPromises from import
|
|
857
575
|
|
|
858
|
-
//
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
console.error('Error extracting project directory:', error);
|
|
864
|
-
// Fallback to simple dash replacement
|
|
865
|
-
actualPath = req.params.projectName.replace(/-/g, '/');
|
|
576
|
+
// Resolve the project's absolute path through the DB (projectId is the
|
|
577
|
+
// primary key of the `projects` table after the identifier migration).
|
|
578
|
+
const actualPath = await projectsDb.getProjectPathById(req.params.projectId);
|
|
579
|
+
if (!actualPath) {
|
|
580
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
866
581
|
}
|
|
867
582
|
|
|
868
583
|
// Check if path exists
|
|
@@ -927,10 +642,10 @@ function validateFilename(name) {
|
|
|
927
642
|
return { valid: true };
|
|
928
643
|
}
|
|
929
644
|
|
|
930
|
-
// POST /api/projects/:
|
|
931
|
-
app.post('/api/projects/:
|
|
645
|
+
// POST /api/projects/:projectId/files/create - Create new file or directory
|
|
646
|
+
app.post('/api/projects/:projectId/files/create', authenticateToken, async (req, res) => {
|
|
932
647
|
try {
|
|
933
|
-
const {
|
|
648
|
+
const { projectId } = req.params;
|
|
934
649
|
const { path: parentPath, type, name } = req.body;
|
|
935
650
|
|
|
936
651
|
// Validate input
|
|
@@ -947,8 +662,8 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
|
|
|
947
662
|
return res.status(400).json({ error: nameValidation.error });
|
|
948
663
|
}
|
|
949
664
|
|
|
950
|
-
//
|
|
951
|
-
const projectRoot = await
|
|
665
|
+
// Resolve the project directory through the DB using the new projectId.
|
|
666
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
952
667
|
if (!projectRoot) {
|
|
953
668
|
return res.status(404).json({ error: 'Project not found' });
|
|
954
669
|
}
|
|
@@ -1004,10 +719,10 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
|
|
|
1004
719
|
}
|
|
1005
720
|
});
|
|
1006
721
|
|
|
1007
|
-
// PUT /api/projects/:
|
|
1008
|
-
app.put('/api/projects/:
|
|
722
|
+
// PUT /api/projects/:projectId/files/rename - Rename file or directory
|
|
723
|
+
app.put('/api/projects/:projectId/files/rename', authenticateToken, async (req, res) => {
|
|
1009
724
|
try {
|
|
1010
|
-
const {
|
|
725
|
+
const { projectId } = req.params;
|
|
1011
726
|
const { oldPath, newName } = req.body;
|
|
1012
727
|
|
|
1013
728
|
// Validate input
|
|
@@ -1020,8 +735,8 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
|
|
|
1020
735
|
return res.status(400).json({ error: nameValidation.error });
|
|
1021
736
|
}
|
|
1022
737
|
|
|
1023
|
-
//
|
|
1024
|
-
const projectRoot = await
|
|
738
|
+
// Resolve the project directory through the DB using the new projectId.
|
|
739
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
1025
740
|
if (!projectRoot) {
|
|
1026
741
|
return res.status(404).json({ error: 'Project not found' });
|
|
1027
742
|
}
|
|
@@ -1081,10 +796,10 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
|
|
|
1081
796
|
}
|
|
1082
797
|
});
|
|
1083
798
|
|
|
1084
|
-
// DELETE /api/projects/:
|
|
1085
|
-
app.delete('/api/projects/:
|
|
799
|
+
// DELETE /api/projects/:projectId/files - Delete file or directory
|
|
800
|
+
app.delete('/api/projects/:projectId/files', authenticateToken, async (req, res) => {
|
|
1086
801
|
try {
|
|
1087
|
-
const {
|
|
802
|
+
const { projectId } = req.params;
|
|
1088
803
|
const { path: targetPath, type } = req.body;
|
|
1089
804
|
|
|
1090
805
|
// Validate input
|
|
@@ -1092,8 +807,8 @@ app.delete('/api/projects/:projectName/files', authenticateToken, async (req, re
|
|
|
1092
807
|
return res.status(400).json({ error: 'Path is required' });
|
|
1093
808
|
}
|
|
1094
809
|
|
|
1095
|
-
//
|
|
1096
|
-
const projectRoot = await
|
|
810
|
+
// Resolve the project directory through the DB using the new projectId.
|
|
811
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
1097
812
|
if (!projectRoot) {
|
|
1098
813
|
return res.status(404).json({ error: 'Project not found' });
|
|
1099
814
|
}
|
|
@@ -1146,7 +861,7 @@ app.delete('/api/projects/:projectName/files', authenticateToken, async (req, re
|
|
|
1146
861
|
}
|
|
1147
862
|
});
|
|
1148
863
|
|
|
1149
|
-
// POST /api/projects/:
|
|
864
|
+
// POST /api/projects/:projectId/files/upload - Upload files
|
|
1150
865
|
// Dynamic import of multer for file uploads
|
|
1151
866
|
const uploadFilesHandler = async (req, res) => {
|
|
1152
867
|
// Dynamic import of multer
|
|
@@ -1185,7 +900,7 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1185
900
|
}
|
|
1186
901
|
|
|
1187
902
|
try {
|
|
1188
|
-
const {
|
|
903
|
+
const { projectId } = req.params;
|
|
1189
904
|
const { targetPath, relativePaths } = req.body;
|
|
1190
905
|
|
|
1191
906
|
// Parse relative paths if provided (for folder uploads)
|
|
@@ -1199,7 +914,7 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1199
914
|
}
|
|
1200
915
|
|
|
1201
916
|
console.log('[DEBUG] File upload request:', {
|
|
1202
|
-
|
|
917
|
+
projectId,
|
|
1203
918
|
targetPath: JSON.stringify(targetPath),
|
|
1204
919
|
targetPathType: typeof targetPath,
|
|
1205
920
|
filesCount: req.files?.length,
|
|
@@ -1210,8 +925,8 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1210
925
|
return res.status(400).json({ error: 'No files provided' });
|
|
1211
926
|
}
|
|
1212
927
|
|
|
1213
|
-
//
|
|
1214
|
-
const projectRoot = await
|
|
928
|
+
// Resolve the project directory through the DB using the new projectId.
|
|
929
|
+
const projectRoot = await projectsDb.getProjectPathById(projectId);
|
|
1215
930
|
if (!projectRoot) {
|
|
1216
931
|
return res.status(404).json({ error: 'Project not found' });
|
|
1217
932
|
}
|
|
@@ -1308,615 +1023,12 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1308
1023
|
});
|
|
1309
1024
|
};
|
|
1310
1025
|
|
|
1311
|
-
app.post('/api/projects/:
|
|
1312
|
-
|
|
1313
|
-
/**
|
|
1314
|
-
* Proxy an authenticated client WebSocket to a plugin's internal WS server.
|
|
1315
|
-
* Auth is enforced by verifyClient before this function is reached.
|
|
1316
|
-
*/
|
|
1317
|
-
function handlePluginWsProxy(clientWs, pathname) {
|
|
1318
|
-
const pluginName = pathname.replace('/plugin-ws/', '');
|
|
1319
|
-
if (!pluginName || /[^a-zA-Z0-9_-]/.test(pluginName)) {
|
|
1320
|
-
clientWs.close(4400, 'Invalid plugin name');
|
|
1321
|
-
return;
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
const port = getPluginPort(pluginName);
|
|
1325
|
-
if (!port) {
|
|
1326
|
-
clientWs.close(4404, 'Plugin not running');
|
|
1327
|
-
return;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
const upstream = new WebSocket(`ws://127.0.0.1:${port}/ws`);
|
|
1331
|
-
|
|
1332
|
-
upstream.on('open', () => {
|
|
1333
|
-
console.log(`[Plugins] WS proxy connected to "${pluginName}" on port ${port}`);
|
|
1334
|
-
});
|
|
1335
|
-
|
|
1336
|
-
// Relay messages bidirectionally
|
|
1337
|
-
upstream.on('message', (data) => {
|
|
1338
|
-
if (clientWs.readyState === WebSocket.OPEN) clientWs.send(data);
|
|
1339
|
-
});
|
|
1340
|
-
clientWs.on('message', (data) => {
|
|
1341
|
-
if (upstream.readyState === WebSocket.OPEN) upstream.send(data);
|
|
1342
|
-
});
|
|
1343
|
-
|
|
1344
|
-
// Propagate close in both directions
|
|
1345
|
-
upstream.on('close', () => { if (clientWs.readyState === WebSocket.OPEN) clientWs.close(); });
|
|
1346
|
-
clientWs.on('close', () => { if (upstream.readyState === WebSocket.OPEN) upstream.close(); });
|
|
1347
|
-
|
|
1348
|
-
upstream.on('error', (err) => {
|
|
1349
|
-
console.error(`[Plugins] WS proxy error for "${pluginName}":`, err.message);
|
|
1350
|
-
if (clientWs.readyState === WebSocket.OPEN) clientWs.close(4502, 'Upstream error');
|
|
1351
|
-
});
|
|
1352
|
-
clientWs.on('error', () => {
|
|
1353
|
-
if (upstream.readyState === WebSocket.OPEN) upstream.close();
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// WebSocket connection handler that routes based on URL path
|
|
1358
|
-
wss.on('connection', (ws, request) => {
|
|
1359
|
-
const url = request.url;
|
|
1360
|
-
console.log('[INFO] Client connected to:', url);
|
|
1361
|
-
|
|
1362
|
-
// Parse URL to get pathname without query parameters
|
|
1363
|
-
const urlObj = new URL(url, 'http://localhost');
|
|
1364
|
-
const pathname = urlObj.pathname;
|
|
1365
|
-
|
|
1366
|
-
if (pathname === '/shell') {
|
|
1367
|
-
handleShellConnection(ws);
|
|
1368
|
-
} else if (pathname === '/ws') {
|
|
1369
|
-
handleChatConnection(ws, request);
|
|
1370
|
-
} else if (pathname.startsWith('/plugin-ws/')) {
|
|
1371
|
-
handlePluginWsProxy(ws, pathname);
|
|
1372
|
-
} else {
|
|
1373
|
-
console.log('[WARN] Unknown WebSocket path:', pathname);
|
|
1374
|
-
ws.close();
|
|
1375
|
-
}
|
|
1376
|
-
});
|
|
1377
|
-
|
|
1378
|
-
/**
|
|
1379
|
-
* WebSocket Writer - Wrapper for WebSocket to match SSEStreamWriter interface
|
|
1380
|
-
*
|
|
1381
|
-
* Provider files use `createNormalizedMessage()` from `providers/types.js` and
|
|
1382
|
-
* adapter `normalizeMessage()` to produce unified NormalizedMessage events.
|
|
1383
|
-
* The writer simply serialises and sends.
|
|
1384
|
-
*/
|
|
1385
|
-
class WebSocketWriter {
|
|
1386
|
-
constructor(ws, userId = null) {
|
|
1387
|
-
this.ws = ws;
|
|
1388
|
-
this.sessionId = null;
|
|
1389
|
-
this.userId = userId;
|
|
1390
|
-
this.isWebSocketWriter = true; // Marker for transport detection
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
send(data) {
|
|
1394
|
-
if (this.ws.readyState === 1) { // WebSocket.OPEN
|
|
1395
|
-
this.ws.send(JSON.stringify(data));
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
updateWebSocket(newRawWs) {
|
|
1400
|
-
this.ws = newRawWs;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
setSessionId(sessionId) {
|
|
1404
|
-
this.sessionId = sessionId;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
getSessionId() {
|
|
1408
|
-
return this.sessionId;
|
|
1409
|
-
}
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// Handle chat WebSocket connections
|
|
1413
|
-
function handleChatConnection(ws, request) {
|
|
1414
|
-
console.log('[INFO] Chat WebSocket connected');
|
|
1415
|
-
|
|
1416
|
-
// Add to connected clients for project updates
|
|
1417
|
-
connectedClients.add(ws);
|
|
1418
|
-
|
|
1419
|
-
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
|
|
1420
|
-
const writer = new WebSocketWriter(ws, request?.user?.id ?? request?.user?.userId ?? null);
|
|
1421
|
-
|
|
1422
|
-
ws.on('message', async (message) => {
|
|
1423
|
-
try {
|
|
1424
|
-
const data = JSON.parse(message);
|
|
1425
|
-
|
|
1426
|
-
if (data.type === 'claude-command') {
|
|
1427
|
-
console.log('[DEBUG] User message:', data.command || '[Continue/Resume]');
|
|
1428
|
-
console.log('📁 Project:', data.options?.projectPath || 'Unknown');
|
|
1429
|
-
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1430
|
-
|
|
1431
|
-
// Use Claude Agents SDK
|
|
1432
|
-
await queryClaudeSDK(data.command, data.options, writer);
|
|
1433
|
-
} else if (data.type === 'cursor-command') {
|
|
1434
|
-
console.log('[DEBUG] Cursor message:', data.command || '[Continue/Resume]');
|
|
1435
|
-
console.log('📁 Project:', data.options?.cwd || 'Unknown');
|
|
1436
|
-
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1437
|
-
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1438
|
-
await spawnCursor(data.command, data.options, writer);
|
|
1439
|
-
} else if (data.type === 'codex-command') {
|
|
1440
|
-
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
|
|
1441
|
-
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
1442
|
-
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1443
|
-
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1444
|
-
await queryCodex(data.command, data.options, writer);
|
|
1445
|
-
} else if (data.type === 'gemini-command') {
|
|
1446
|
-
console.log('[DEBUG] Gemini message:', data.command || '[Continue/Resume]');
|
|
1447
|
-
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
1448
|
-
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
1449
|
-
console.log('🤖 Model:', data.options?.model || 'default');
|
|
1450
|
-
await spawnGemini(data.command, data.options, writer);
|
|
1451
|
-
} else if (data.type === 'cursor-resume') {
|
|
1452
|
-
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
1453
|
-
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
|
1454
|
-
await spawnCursor('', {
|
|
1455
|
-
sessionId: data.sessionId,
|
|
1456
|
-
resume: true,
|
|
1457
|
-
cwd: data.options?.cwd
|
|
1458
|
-
}, writer);
|
|
1459
|
-
} else if (data.type === 'abort-session') {
|
|
1460
|
-
console.log('[DEBUG] Abort session request:', data.sessionId);
|
|
1461
|
-
const provider = data.provider || 'claude';
|
|
1462
|
-
let success;
|
|
1463
|
-
|
|
1464
|
-
if (provider === 'cursor') {
|
|
1465
|
-
success = abortCursorSession(data.sessionId);
|
|
1466
|
-
} else if (provider === 'codex') {
|
|
1467
|
-
success = abortCodexSession(data.sessionId);
|
|
1468
|
-
} else if (provider === 'gemini') {
|
|
1469
|
-
success = abortGeminiSession(data.sessionId);
|
|
1470
|
-
} else {
|
|
1471
|
-
// Use Claude Agents SDK
|
|
1472
|
-
success = await abortClaudeSDKSession(data.sessionId);
|
|
1473
|
-
}
|
|
1026
|
+
app.post('/api/projects/:projectId/files/upload', authenticateToken, uploadFilesHandler);
|
|
1474
1027
|
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
// introduced so the SDK can resume once the user clicks Allow/Deny.
|
|
1480
|
-
if (data.requestId) {
|
|
1481
|
-
resolveToolApproval(data.requestId, {
|
|
1482
|
-
allow: Boolean(data.allow),
|
|
1483
|
-
updatedInput: data.updatedInput,
|
|
1484
|
-
message: data.message,
|
|
1485
|
-
rememberEntry: data.rememberEntry
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
} else if (data.type === 'cursor-abort') {
|
|
1489
|
-
console.log('[DEBUG] Abort Cursor session:', data.sessionId);
|
|
1490
|
-
const success = abortCursorSession(data.sessionId);
|
|
1491
|
-
writer.send(createNormalizedMessage({ kind: 'complete', exitCode: success ? 0 : 1, aborted: true, success, sessionId: data.sessionId, provider: 'cursor' }));
|
|
1492
|
-
} else if (data.type === 'check-session-status') {
|
|
1493
|
-
// Check if a specific session is currently processing
|
|
1494
|
-
const provider = data.provider || 'claude';
|
|
1495
|
-
const sessionId = data.sessionId;
|
|
1496
|
-
let isActive;
|
|
1497
|
-
|
|
1498
|
-
if (provider === 'cursor') {
|
|
1499
|
-
isActive = isCursorSessionActive(sessionId);
|
|
1500
|
-
} else if (provider === 'codex') {
|
|
1501
|
-
isActive = isCodexSessionActive(sessionId);
|
|
1502
|
-
} else if (provider === 'gemini') {
|
|
1503
|
-
isActive = isGeminiSessionActive(sessionId);
|
|
1504
|
-
} else {
|
|
1505
|
-
// Use Claude Agents SDK
|
|
1506
|
-
isActive = isClaudeSDKSessionActive(sessionId);
|
|
1507
|
-
if (isActive) {
|
|
1508
|
-
// Reconnect the session's writer to the new WebSocket so
|
|
1509
|
-
// subsequent SDK output flows to the refreshed client.
|
|
1510
|
-
reconnectSessionWriter(sessionId, ws);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
writer.send({
|
|
1515
|
-
type: 'session-status',
|
|
1516
|
-
sessionId,
|
|
1517
|
-
provider,
|
|
1518
|
-
isProcessing: isActive
|
|
1519
|
-
});
|
|
1520
|
-
} else if (data.type === 'get-pending-permissions') {
|
|
1521
|
-
// Return pending permission requests for a session
|
|
1522
|
-
const sessionId = data.sessionId;
|
|
1523
|
-
if (sessionId && isClaudeSDKSessionActive(sessionId)) {
|
|
1524
|
-
const pending = getPendingApprovalsForSession(sessionId);
|
|
1525
|
-
writer.send({
|
|
1526
|
-
type: 'pending-permissions-response',
|
|
1527
|
-
sessionId,
|
|
1528
|
-
data: pending
|
|
1529
|
-
});
|
|
1530
|
-
}
|
|
1531
|
-
} else if (data.type === 'get-active-sessions') {
|
|
1532
|
-
// Get all currently active sessions
|
|
1533
|
-
const activeSessions = {
|
|
1534
|
-
claude: getActiveClaudeSDKSessions(),
|
|
1535
|
-
cursor: getActiveCursorSessions(),
|
|
1536
|
-
codex: getActiveCodexSessions(),
|
|
1537
|
-
gemini: getActiveGeminiSessions()
|
|
1538
|
-
};
|
|
1539
|
-
writer.send({
|
|
1540
|
-
type: 'active-sessions',
|
|
1541
|
-
sessions: activeSessions
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
} catch (error) {
|
|
1545
|
-
console.error('[ERROR] Chat WebSocket error:', error.message);
|
|
1546
|
-
writer.send({
|
|
1547
|
-
type: 'error',
|
|
1548
|
-
error: error.message
|
|
1549
|
-
});
|
|
1550
|
-
}
|
|
1551
|
-
});
|
|
1552
|
-
|
|
1553
|
-
ws.on('close', () => {
|
|
1554
|
-
console.log('🔌 Chat client disconnected');
|
|
1555
|
-
// Remove from connected clients
|
|
1556
|
-
connectedClients.delete(ws);
|
|
1557
|
-
});
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
// Handle shell WebSocket connections
|
|
1561
|
-
function handleShellConnection(ws) {
|
|
1562
|
-
console.log('🐚 Shell client connected');
|
|
1563
|
-
let shellProcess = null;
|
|
1564
|
-
let ptySessionKey = null;
|
|
1565
|
-
let urlDetectionBuffer = '';
|
|
1566
|
-
const announcedAuthUrls = new Set();
|
|
1567
|
-
|
|
1568
|
-
ws.on('message', async (message) => {
|
|
1569
|
-
try {
|
|
1570
|
-
const data = JSON.parse(message);
|
|
1571
|
-
console.log('📨 Shell message received:', data.type);
|
|
1572
|
-
|
|
1573
|
-
if (data.type === 'init') {
|
|
1574
|
-
const projectPath = data.projectPath || process.cwd();
|
|
1575
|
-
const sessionId = data.sessionId;
|
|
1576
|
-
const hasSession = data.hasSession;
|
|
1577
|
-
const provider = data.provider || 'claude';
|
|
1578
|
-
const initialCommand = data.initialCommand;
|
|
1579
|
-
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
1580
|
-
urlDetectionBuffer = '';
|
|
1581
|
-
announcedAuthUrls.clear();
|
|
1582
|
-
|
|
1583
|
-
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
|
1584
|
-
const isLoginCommand = initialCommand && (
|
|
1585
|
-
initialCommand.includes('setup-token') ||
|
|
1586
|
-
initialCommand.includes('cursor-agent login') ||
|
|
1587
|
-
initialCommand.includes('auth login')
|
|
1588
|
-
);
|
|
1589
|
-
|
|
1590
|
-
// Include command hash in session key so different commands get separate sessions
|
|
1591
|
-
const commandSuffix = isPlainShell && initialCommand
|
|
1592
|
-
? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
|
|
1593
|
-
: '';
|
|
1594
|
-
ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
|
|
1595
|
-
|
|
1596
|
-
// Kill any existing login session before starting fresh
|
|
1597
|
-
if (isLoginCommand) {
|
|
1598
|
-
const oldSession = ptySessionsMap.get(ptySessionKey);
|
|
1599
|
-
if (oldSession) {
|
|
1600
|
-
console.log('🧹 Cleaning up existing login session:', ptySessionKey);
|
|
1601
|
-
if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
|
|
1602
|
-
if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
|
|
1603
|
-
ptySessionsMap.delete(ptySessionKey);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
|
|
1608
|
-
if (existingSession) {
|
|
1609
|
-
console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
|
|
1610
|
-
shellProcess = existingSession.pty;
|
|
1611
|
-
|
|
1612
|
-
clearTimeout(existingSession.timeoutId);
|
|
1613
|
-
|
|
1614
|
-
ws.send(JSON.stringify({
|
|
1615
|
-
type: 'output',
|
|
1616
|
-
data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
|
|
1617
|
-
}));
|
|
1618
|
-
|
|
1619
|
-
if (existingSession.buffer && existingSession.buffer.length > 0) {
|
|
1620
|
-
console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
|
|
1621
|
-
existingSession.buffer.forEach(bufferedData => {
|
|
1622
|
-
ws.send(JSON.stringify({
|
|
1623
|
-
type: 'output',
|
|
1624
|
-
data: bufferedData
|
|
1625
|
-
}));
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
existingSession.ws = ws;
|
|
1630
|
-
|
|
1631
|
-
return;
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
console.log('[INFO] Starting shell in:', projectPath);
|
|
1635
|
-
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
|
|
1636
|
-
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
|
|
1637
|
-
if (initialCommand) {
|
|
1638
|
-
console.log('⚡ Initial command:', initialCommand);
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// First send a welcome message
|
|
1642
|
-
let welcomeMsg;
|
|
1643
|
-
if (isPlainShell) {
|
|
1644
|
-
welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
|
|
1645
|
-
} else {
|
|
1646
|
-
const providerName = provider === 'cursor' ? 'Cursor' : (provider === 'codex' ? 'Codex' : (provider === 'gemini' ? 'Gemini' : 'Claude'));
|
|
1647
|
-
welcomeMsg = hasSession ?
|
|
1648
|
-
`\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
|
|
1649
|
-
`\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
ws.send(JSON.stringify({
|
|
1653
|
-
type: 'output',
|
|
1654
|
-
data: welcomeMsg
|
|
1655
|
-
}));
|
|
1656
|
-
|
|
1657
|
-
try {
|
|
1658
|
-
// Validate projectPath — resolve to absolute and verify it exists
|
|
1659
|
-
const resolvedProjectPath = path.resolve(projectPath);
|
|
1660
|
-
try {
|
|
1661
|
-
const stats = fs.statSync(resolvedProjectPath);
|
|
1662
|
-
if (!stats.isDirectory()) {
|
|
1663
|
-
throw new Error('Not a directory');
|
|
1664
|
-
}
|
|
1665
|
-
} catch (pathErr) {
|
|
1666
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Invalid project path' }));
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
// Validate sessionId — only allow safe characters
|
|
1671
|
-
const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
|
|
1672
|
-
if (sessionId && !safeSessionIdPattern.test(sessionId)) {
|
|
1673
|
-
ws.send(JSON.stringify({ type: 'error', message: 'Invalid session ID' }));
|
|
1674
|
-
return;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// Build shell command — use cwd for project path (never interpolate into shell string)
|
|
1678
|
-
let shellCommand;
|
|
1679
|
-
if (isPlainShell) {
|
|
1680
|
-
// Plain shell mode - run the initial command in the project directory
|
|
1681
|
-
shellCommand = initialCommand;
|
|
1682
|
-
} else if (provider === 'cursor') {
|
|
1683
|
-
if (hasSession && sessionId) {
|
|
1684
|
-
shellCommand = `cursor-agent --resume="${sessionId}"`;
|
|
1685
|
-
} else {
|
|
1686
|
-
shellCommand = 'cursor-agent';
|
|
1687
|
-
}
|
|
1688
|
-
} else if (provider === 'codex') {
|
|
1689
|
-
// Use codex command; attempt to resume and fall back to a new session when the resume fails.
|
|
1690
|
-
if (hasSession && sessionId) {
|
|
1691
|
-
if (os.platform() === 'win32') {
|
|
1692
|
-
// PowerShell syntax for fallback
|
|
1693
|
-
shellCommand = `codex resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { codex }`;
|
|
1694
|
-
} else {
|
|
1695
|
-
shellCommand = `codex resume "${sessionId}" || codex`;
|
|
1696
|
-
}
|
|
1697
|
-
} else {
|
|
1698
|
-
shellCommand = 'codex';
|
|
1699
|
-
}
|
|
1700
|
-
} else if (provider === 'gemini') {
|
|
1701
|
-
const command = initialCommand || 'gemini';
|
|
1702
|
-
let resumeId = sessionId;
|
|
1703
|
-
if (hasSession && sessionId) {
|
|
1704
|
-
try {
|
|
1705
|
-
// Gemini CLI enforces its own native session IDs, unlike other agents that accept arbitrary string names.
|
|
1706
|
-
// The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
|
|
1707
|
-
// We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell.
|
|
1708
|
-
const sess = sessionManager.getSession(sessionId);
|
|
1709
|
-
if (sess && sess.cliSessionId) {
|
|
1710
|
-
resumeId = sess.cliSessionId;
|
|
1711
|
-
// Validate the looked-up CLI session ID too
|
|
1712
|
-
if (!safeSessionIdPattern.test(resumeId)) {
|
|
1713
|
-
resumeId = null;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
} catch (err) {
|
|
1717
|
-
console.error('Failed to get Gemini CLI session ID:', err);
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
if (hasSession && resumeId) {
|
|
1722
|
-
shellCommand = `${command} --resume "${resumeId}"`;
|
|
1723
|
-
} else {
|
|
1724
|
-
shellCommand = command;
|
|
1725
|
-
}
|
|
1726
|
-
} else {
|
|
1727
|
-
// Claude (default provider)
|
|
1728
|
-
const command = initialCommand || 'claude';
|
|
1729
|
-
if (hasSession && sessionId) {
|
|
1730
|
-
if (os.platform() === 'win32') {
|
|
1731
|
-
shellCommand = `claude --resume "${sessionId}"; if ($LASTEXITCODE -ne 0) { claude }`;
|
|
1732
|
-
} else {
|
|
1733
|
-
shellCommand = `claude --resume "${sessionId}" || claude`;
|
|
1734
|
-
}
|
|
1735
|
-
} else {
|
|
1736
|
-
shellCommand = command;
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
console.log('🔧 Executing shell command:', shellCommand);
|
|
1741
|
-
|
|
1742
|
-
// Use appropriate shell based on platform
|
|
1743
|
-
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
1744
|
-
const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
|
1745
|
-
|
|
1746
|
-
// Use terminal dimensions from client if provided, otherwise use defaults
|
|
1747
|
-
const termCols = data.cols || 80;
|
|
1748
|
-
const termRows = data.rows || 24;
|
|
1749
|
-
console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
|
|
1750
|
-
|
|
1751
|
-
shellProcess = pty.spawn(shell, shellArgs, {
|
|
1752
|
-
name: 'xterm-256color',
|
|
1753
|
-
cols: termCols,
|
|
1754
|
-
rows: termRows,
|
|
1755
|
-
cwd: resolvedProjectPath,
|
|
1756
|
-
env: {
|
|
1757
|
-
...process.env,
|
|
1758
|
-
TERM: 'xterm-256color',
|
|
1759
|
-
COLORTERM: 'truecolor',
|
|
1760
|
-
FORCE_COLOR: '3'
|
|
1761
|
-
}
|
|
1762
|
-
});
|
|
1763
|
-
|
|
1764
|
-
console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
|
|
1765
|
-
|
|
1766
|
-
ptySessionsMap.set(ptySessionKey, {
|
|
1767
|
-
pty: shellProcess,
|
|
1768
|
-
ws: ws,
|
|
1769
|
-
buffer: [],
|
|
1770
|
-
timeoutId: null,
|
|
1771
|
-
projectPath,
|
|
1772
|
-
sessionId
|
|
1773
|
-
});
|
|
1774
|
-
|
|
1775
|
-
// Handle data output
|
|
1776
|
-
shellProcess.onData((data) => {
|
|
1777
|
-
const session = ptySessionsMap.get(ptySessionKey);
|
|
1778
|
-
if (!session) return;
|
|
1779
|
-
|
|
1780
|
-
if (session.buffer.length < 5000) {
|
|
1781
|
-
session.buffer.push(data);
|
|
1782
|
-
} else {
|
|
1783
|
-
session.buffer.shift();
|
|
1784
|
-
session.buffer.push(data);
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1788
|
-
let outputData = data;
|
|
1789
|
-
|
|
1790
|
-
const cleanChunk = stripAnsiSequences(data);
|
|
1791
|
-
urlDetectionBuffer = `${urlDetectionBuffer}${cleanChunk}`.slice(-SHELL_URL_PARSE_BUFFER_LIMIT);
|
|
1792
|
-
|
|
1793
|
-
outputData = outputData.replace(
|
|
1794
|
-
/OPEN_URL:\s*(https?:\/\/[^\s\x1b\x07]+)/g,
|
|
1795
|
-
'[INFO] Opening in browser: $1'
|
|
1796
|
-
);
|
|
1797
|
-
|
|
1798
|
-
const emitAuthUrl = (detectedUrl, autoOpen = false) => {
|
|
1799
|
-
const normalizedUrl = normalizeDetectedUrl(detectedUrl);
|
|
1800
|
-
if (!normalizedUrl) return;
|
|
1801
|
-
|
|
1802
|
-
const isNewUrl = !announcedAuthUrls.has(normalizedUrl);
|
|
1803
|
-
if (isNewUrl) {
|
|
1804
|
-
announcedAuthUrls.add(normalizedUrl);
|
|
1805
|
-
session.ws.send(JSON.stringify({
|
|
1806
|
-
type: 'auth_url',
|
|
1807
|
-
url: normalizedUrl,
|
|
1808
|
-
autoOpen
|
|
1809
|
-
}));
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
};
|
|
1813
|
-
|
|
1814
|
-
const normalizedDetectedUrls = extractUrlsFromText(urlDetectionBuffer)
|
|
1815
|
-
.map((url) => normalizeDetectedUrl(url))
|
|
1816
|
-
.filter(Boolean);
|
|
1817
|
-
|
|
1818
|
-
// Prefer the most complete URL if shorter prefix variants are also present.
|
|
1819
|
-
const dedupedDetectedUrls = Array.from(new Set(normalizedDetectedUrls)).filter((url, _, urls) =>
|
|
1820
|
-
!urls.some((otherUrl) => otherUrl !== url && otherUrl.startsWith(url))
|
|
1821
|
-
);
|
|
1822
|
-
|
|
1823
|
-
dedupedDetectedUrls.forEach((url) => emitAuthUrl(url, false));
|
|
1824
|
-
|
|
1825
|
-
if (shouldAutoOpenUrlFromOutput(cleanChunk) && dedupedDetectedUrls.length > 0) {
|
|
1826
|
-
const bestUrl = dedupedDetectedUrls.reduce((longest, current) =>
|
|
1827
|
-
current.length > longest.length ? current : longest
|
|
1828
|
-
);
|
|
1829
|
-
emitAuthUrl(bestUrl, true);
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
// Send regular output
|
|
1833
|
-
session.ws.send(JSON.stringify({
|
|
1834
|
-
type: 'output',
|
|
1835
|
-
data: outputData
|
|
1836
|
-
}));
|
|
1837
|
-
}
|
|
1838
|
-
});
|
|
1839
|
-
|
|
1840
|
-
// Handle process exit
|
|
1841
|
-
shellProcess.onExit((exitCode) => {
|
|
1842
|
-
console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
|
|
1843
|
-
const session = ptySessionsMap.get(ptySessionKey);
|
|
1844
|
-
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1845
|
-
session.ws.send(JSON.stringify({
|
|
1846
|
-
type: 'output',
|
|
1847
|
-
data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
|
|
1848
|
-
}));
|
|
1849
|
-
}
|
|
1850
|
-
if (session && session.timeoutId) {
|
|
1851
|
-
clearTimeout(session.timeoutId);
|
|
1852
|
-
}
|
|
1853
|
-
ptySessionsMap.delete(ptySessionKey);
|
|
1854
|
-
shellProcess = null;
|
|
1855
|
-
});
|
|
1856
|
-
|
|
1857
|
-
} catch (spawnError) {
|
|
1858
|
-
console.error('[ERROR] Error spawning process:', spawnError);
|
|
1859
|
-
ws.send(JSON.stringify({
|
|
1860
|
-
type: 'output',
|
|
1861
|
-
data: `\r\n\x1b[31mError: ${spawnError.message}\x1b[0m\r\n`
|
|
1862
|
-
}));
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1865
|
-
} else if (data.type === 'input') {
|
|
1866
|
-
// Send input to shell process
|
|
1867
|
-
if (shellProcess && shellProcess.write) {
|
|
1868
|
-
try {
|
|
1869
|
-
shellProcess.write(data.data);
|
|
1870
|
-
} catch (error) {
|
|
1871
|
-
console.error('Error writing to shell:', error);
|
|
1872
|
-
}
|
|
1873
|
-
} else {
|
|
1874
|
-
console.warn('No active shell process to send input to');
|
|
1875
|
-
}
|
|
1876
|
-
} else if (data.type === 'resize') {
|
|
1877
|
-
// Handle terminal resize
|
|
1878
|
-
if (shellProcess && shellProcess.resize) {
|
|
1879
|
-
console.log('Terminal resize requested:', data.cols, 'x', data.rows);
|
|
1880
|
-
shellProcess.resize(data.cols, data.rows);
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
} catch (error) {
|
|
1884
|
-
console.error('[ERROR] Shell WebSocket error:', error.message);
|
|
1885
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1886
|
-
ws.send(JSON.stringify({
|
|
1887
|
-
type: 'output',
|
|
1888
|
-
data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
|
|
1889
|
-
}));
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
});
|
|
1893
|
-
|
|
1894
|
-
ws.on('close', () => {
|
|
1895
|
-
console.log('🔌 Shell client disconnected');
|
|
1896
|
-
|
|
1897
|
-
if (ptySessionKey) {
|
|
1898
|
-
const session = ptySessionsMap.get(ptySessionKey);
|
|
1899
|
-
if (session) {
|
|
1900
|
-
console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
|
|
1901
|
-
session.ws = null;
|
|
1902
|
-
|
|
1903
|
-
session.timeoutId = setTimeout(() => {
|
|
1904
|
-
console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
|
|
1905
|
-
if (session.pty && session.pty.kill) {
|
|
1906
|
-
session.pty.kill();
|
|
1907
|
-
}
|
|
1908
|
-
ptySessionsMap.delete(ptySessionKey);
|
|
1909
|
-
}, PTY_SESSION_TIMEOUT);
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
});
|
|
1913
|
-
|
|
1914
|
-
ws.on('error', (error) => {
|
|
1915
|
-
console.error('[ERROR] Shell WebSocket error:', error);
|
|
1916
|
-
});
|
|
1917
|
-
}
|
|
1918
|
-
// Image upload endpoint
|
|
1919
|
-
app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => {
|
|
1028
|
+
// Image upload endpoint. Accepts the DB-assigned `projectId` (not a folder name)
|
|
1029
|
+
// but the current implementation doesn't need to touch the project directory,
|
|
1030
|
+
// so we just leave the param rename for consistency with the rest of the API.
|
|
1031
|
+
app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req, res) => {
|
|
1920
1032
|
try {
|
|
1921
1033
|
const multer = (await import('multer')).default;
|
|
1922
1034
|
const path = (await import('path')).default;
|
|
@@ -2000,10 +1112,11 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|
|
2000
1112
|
}
|
|
2001
1113
|
});
|
|
2002
1114
|
|
|
2003
|
-
// Get token usage for a specific session
|
|
2004
|
-
|
|
1115
|
+
// Get token usage for a specific session. `projectId` is the DB primary key;
|
|
1116
|
+
// the Claude branch below resolves it to an absolute path via the DB.
|
|
1117
|
+
app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
|
2005
1118
|
try {
|
|
2006
|
-
const {
|
|
1119
|
+
const { projectId, sessionId } = req.params;
|
|
2007
1120
|
const { provider = 'claude' } = req.query;
|
|
2008
1121
|
const homeDir = os.homedir();
|
|
2009
1122
|
|
|
@@ -2107,13 +1220,13 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
2107
1220
|
}
|
|
2108
1221
|
|
|
2109
1222
|
// Handle Claude sessions (default)
|
|
2110
|
-
//
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
return res.status(
|
|
1223
|
+
// Resolve the project path through the DB using the caller-supplied
|
|
1224
|
+
// `projectId`. Legacy code here called extractProjectDirectory with a
|
|
1225
|
+
// folder-encoded project name; the migration centralizes that lookup
|
|
1226
|
+
// in the projects table.
|
|
1227
|
+
const projectPath = await projectsDb.getProjectPathById(projectId);
|
|
1228
|
+
if (!projectPath) {
|
|
1229
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
2117
1230
|
}
|
|
2118
1231
|
|
|
2119
1232
|
// Construct the JSONL file path
|
|
@@ -2213,6 +1326,30 @@ app.get('*', (req, res) => {
|
|
|
2213
1326
|
}
|
|
2214
1327
|
});
|
|
2215
1328
|
|
|
1329
|
+
// global error middleware must be last
|
|
1330
|
+
app.use((err, req, res, next) => {
|
|
1331
|
+
if (err instanceof AppError) {
|
|
1332
|
+
return res.status(err.statusCode).json({
|
|
1333
|
+
success: false,
|
|
1334
|
+
error: {
|
|
1335
|
+
code: err.code,
|
|
1336
|
+
message: err.message,
|
|
1337
|
+
details: err.details,
|
|
1338
|
+
},
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
console.error(err);
|
|
1343
|
+
|
|
1344
|
+
return res.status(500).json({
|
|
1345
|
+
success: false,
|
|
1346
|
+
error: {
|
|
1347
|
+
code: 'INTERNAL_ERROR',
|
|
1348
|
+
message: 'Internal server error',
|
|
1349
|
+
},
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
|
|
2216
1353
|
// Helper function to convert permissions to rwx format
|
|
2217
1354
|
function permToRwx(perm) {
|
|
2218
1355
|
const r = perm & 4 ? 'r' : '-';
|
|
@@ -2339,7 +1476,7 @@ async function startServer() {
|
|
|
2339
1476
|
console.log('');
|
|
2340
1477
|
|
|
2341
1478
|
// Start watching the projects folder for changes
|
|
2342
|
-
await
|
|
1479
|
+
await initializeSessionsWatcher();
|
|
2343
1480
|
|
|
2344
1481
|
// Start server-side plugin processes for enabled plugins
|
|
2345
1482
|
startEnabledPluginServers().catch(err => {
|
|
@@ -2347,6 +1484,7 @@ async function startServer() {
|
|
|
2347
1484
|
});
|
|
2348
1485
|
});
|
|
2349
1486
|
|
|
1487
|
+
await closeSessionsWatcher();
|
|
2350
1488
|
// Clean up plugin processes on shutdown
|
|
2351
1489
|
const shutdownPlugins = async () => {
|
|
2352
1490
|
await stopAllPlugins();
|