@pixelbyte-software/pixcode 1.51.2 → 1.51.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +41 -41
- package/CONTRIBUTING.md +155 -155
- package/LICENSE +718 -718
- package/README.de.md +169 -169
- package/README.ja.md +167 -167
- package/README.ko.md +167 -167
- package/README.md +419 -419
- package/README.ru.md +169 -169
- package/README.tr.md +298 -298
- package/README.zh-CN.md +167 -167
- package/SECURITY.md +46 -46
- package/dist/api-automation.html +110 -110
- package/dist/api-docs.html +548 -548
- package/dist/assets/index-B9N-gfOQ.css +32 -0
- package/dist/assets/{index-EN9ngyxf.js → index-HfGHXhD6.js} +175 -175
- package/dist/clear-cache.html +85 -85
- package/dist/convert-icons.md +52 -52
- package/dist/docs.html +308 -308
- package/dist/favicon.svg +8 -8
- package/dist/features.html +133 -133
- package/dist/generate-icons.js +48 -48
- package/dist/humans.txt +15 -15
- package/dist/icons/codex-white.svg +3 -3
- package/dist/icons/codex.svg +3 -3
- package/dist/icons/cursor-white.svg +11 -11
- package/dist/icons/icon-128x128.svg +9 -9
- package/dist/icons/icon-144x144.svg +9 -9
- package/dist/icons/icon-152x152.svg +9 -9
- package/dist/icons/icon-192x192.svg +9 -9
- package/dist/icons/icon-384x384.svg +9 -9
- package/dist/icons/icon-512x512.svg +9 -9
- package/dist/icons/icon-72x72.svg +9 -9
- package/dist/icons/icon-96x96.svg +9 -9
- package/dist/icons/icon-template.svg +9 -9
- package/dist/icons/qwen-logo.svg +14 -14
- package/dist/index.html +59 -59
- package/dist/landing.html +268 -268
- package/dist/llms-full.txt +119 -119
- package/dist/llms.txt +53 -53
- package/dist/logo.svg +12 -12
- package/dist/manifest.json +60 -60
- package/dist/openapi.yaml +1696 -1696
- package/dist/orchestration.html +125 -125
- package/dist/robots.txt +4 -4
- package/dist/site.css +692 -692
- package/dist/sitemap.xml +51 -51
- package/dist/sw.js +132 -132
- package/dist-server/server/cli.js +96 -96
- package/dist-server/server/daemon/manager.js +33 -33
- package/dist-server/server/daemon-manager.js +64 -64
- package/dist-server/server/database/db.js +14 -2
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +191 -31
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +16 -5
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js +84 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.js.map +1 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js +43 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/json-event.adapter.test.js.map +1 -0
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +55 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
- package/dist-server/server/modules/orchestration/index.js +1 -0
- package/dist-server/server/modules/orchestration/index.js.map +1 -1
- package/dist-server/server/routes/auth.js +12 -5
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/commands.js +25 -25
- package/dist-server/server/routes/git.js +29 -17
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/live-view.js +46 -46
- package/dist-server/server/routes/platformization.js +7 -6
- package/dist-server/server/routes/platformization.js.map +1 -1
- package/dist-server/server/services/hermes-gateway.js +310 -0
- package/dist-server/server/services/hermes-gateway.js.map +1 -1
- package/dist-server/server/services/platformization.js +58 -2
- package/dist-server/server/services/platformization.js.map +1 -1
- package/dist-server/server/services/public-api-manifest.js +59 -51
- package/dist-server/server/services/public-api-manifest.js.map +1 -1
- package/package.json +222 -222
- package/scripts/fix-node-pty.js +67 -67
- package/scripts/github/create-v1.38-issues.mjs +351 -351
- package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
- package/scripts/hermes/configure-pixcode-mcp.mjs +165 -163
- package/scripts/hermes/pixcode-mcp-server.mjs +1009 -958
- package/scripts/smoke/changes-panel-layout.mjs +48 -48
- package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
- package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
- package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
- package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
- package/scripts/smoke/chat-session-state.mjs +19 -19
- package/scripts/smoke/code-editor-theme.mjs +55 -55
- package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
- package/scripts/smoke/command-center-agent-writes.mjs +79 -79
- package/scripts/smoke/command-center-non-git.mjs +46 -46
- package/scripts/smoke/context-packet.mjs +43 -43
- package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
- package/scripts/smoke/daemon-entrypoint.mjs +20 -20
- package/scripts/smoke/default-landing-routing.mjs +33 -33
- package/scripts/smoke/desktop-native-notifications.mjs +30 -30
- package/scripts/smoke/desktop-tray-icon.mjs +33 -33
- package/scripts/smoke/discord-release-workflow.mjs +24 -24
- package/scripts/smoke/git-install-update.mjs +255 -255
- package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
- package/scripts/smoke/hermes-api-install.mjs +56 -56
- package/scripts/smoke/hermes-gateway-persistence.mjs +104 -104
- package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +426 -367
- package/scripts/smoke/hermes-rest-chat-api.mjs +162 -162
- package/scripts/smoke/hermes-rest-chat-live.mjs +45 -45
- package/scripts/smoke/hermes-rest-codex-launch.mjs +209 -209
- package/scripts/smoke/hermes-rest-gateway.mjs +79 -70
- package/scripts/smoke/hermes-rest-live.mjs +42 -42
- package/scripts/smoke/hermes-roundtrip.mjs +167 -167
- package/scripts/smoke/hermes-settings-commands.mjs +349 -346
- package/scripts/smoke/hermes-smoke-launcher-guard.mjs +34 -34
- package/scripts/smoke/live-view-diagnostics.mjs +53 -53
- package/scripts/smoke/live-view-environment.mjs +92 -92
- package/scripts/smoke/live-view-integration.mjs +450 -450
- package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
- package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
- package/scripts/smoke/model-registry.mjs +36 -36
- package/scripts/smoke/multi-project-ui.mjs +45 -45
- package/scripts/smoke/multi-worker-slots.mjs +42 -42
- package/scripts/smoke/notification-center.mjs +87 -87
- package/scripts/smoke/notification-inapp-preference.mjs +23 -23
- package/scripts/smoke/notification-taxonomy.mjs +58 -58
- package/scripts/smoke/orchestration-api.mjs +172 -172
- package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
- package/scripts/smoke/orchestration-live-run.mjs +176 -176
- package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
- package/scripts/smoke/orchestration-model-sync.mjs +30 -30
- package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
- package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
- package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
- package/scripts/smoke/permission-policy.mjs +50 -50
- package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -167
- package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
- package/scripts/smoke/provider-rest-api.mjs +124 -124
- package/scripts/smoke/provider-selection-status.mjs +52 -52
- package/scripts/smoke/run-state-refresh.mjs +52 -52
- package/scripts/smoke/runtime-manager.mjs +99 -99
- package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
- package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
- package/scripts/smoke/static-root-routing.mjs +21 -21
- package/scripts/smoke/strict-handoff-compact.mjs +60 -60
- package/scripts/smoke/taskmaster-config.mjs +24 -24
- package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
- package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
- package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
- package/scripts/smoke/telegram-control.mjs +242 -242
- package/scripts/smoke/tunnel-persistence.mjs +56 -56
- package/scripts/smoke/update-issue-progress.mjs +69 -69
- package/scripts/smoke/update-ux.mjs +55 -55
- package/scripts/smoke/v138-completion.mjs +132 -132
- package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
- package/scripts/smoke/v138-diagnostics.mjs +63 -63
- package/scripts/smoke/v138-issue-planner.mjs +33 -33
- package/scripts/smoke/v143-remote-control.mjs +76 -76
- package/scripts/smoke/v144-production-loop.mjs +47 -47
- package/scripts/smoke/v145-platformization.mjs +46 -46
- package/scripts/smoke/v146-control-room-ui.mjs +150 -150
- package/scripts/smoke/version-modal-autoshow.mjs +29 -29
- package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
- package/scripts/smoke/vscode-workbench-polish.mjs +461 -436
- package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
- package/scripts/smoke/workflow-templates.mjs +43 -43
- package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
- package/scripts/update-git-install.mjs +293 -293
- package/server/claude-sdk.js +920 -920
- package/server/cli.js +1039 -1039
- package/server/constants/config.js +4 -4
- package/server/cursor-cli.js +344 -344
- package/server/daemon/manager.js +563 -563
- package/server/daemon-manager.js +964 -964
- package/server/database/db.js +908 -895
- package/server/database/json-store.js +197 -197
- package/server/gemini-cli.js +550 -550
- package/server/gemini-response-handler.js +79 -79
- package/server/index.js +201 -30
- package/server/load-env.js +35 -35
- package/server/middleware/auth.js +171 -156
- package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
- package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
- package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
- package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
- package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
- package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/adapters/json-event.adapter.test.ts +60 -0
- package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +101 -0
- package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
- package/server/modules/orchestration/a2a/agent-card.ts +55 -55
- package/server/modules/orchestration/a2a/routes.ts +590 -590
- package/server/modules/orchestration/a2a/task-store.ts +178 -178
- package/server/modules/orchestration/a2a/types.ts +126 -126
- package/server/modules/orchestration/a2a/validator.ts +113 -113
- package/server/modules/orchestration/hermes/hermes.routes.ts +642 -583
- package/server/modules/orchestration/index.ts +101 -100
- package/server/modules/orchestration/preview/port-watcher.ts +112 -112
- package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
- package/server/modules/orchestration/preview/types.ts +19 -19
- package/server/modules/orchestration/security/permission-policy.ts +401 -401
- package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
- package/server/modules/orchestration/tasks/orchestration-task.routes.ts +64 -64
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +209 -209
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
- package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
- package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
- package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
- package/server/modules/orchestration/workflows/context-packet.ts +186 -186
- package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
- package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
- package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
- package/server/modules/orchestration/workflows/workflow-runner.ts +2070 -2070
- package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
- package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
- package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
- package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
- package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
- package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
- package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
- package/server/modules/orchestration/workspace/path-safety.ts +55 -55
- package/server/modules/orchestration/workspace/types.ts +52 -52
- package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
- package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
- package/server/modules/providers/index.ts +2 -2
- package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
- package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
- package/server/modules/providers/list/claude/claude.provider.ts +15 -15
- package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
- package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
- package/server/modules/providers/list/codex/codex.provider.ts +15 -15
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
- package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
- package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
- package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
- package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
- package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
- package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
- package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
- package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
- package/server/modules/providers/provider.registry.ts +40 -40
- package/server/modules/providers/provider.routes.ts +944 -944
- package/server/modules/providers/services/mcp.service.ts +86 -86
- package/server/modules/providers/services/provider-auth.service.ts +26 -26
- package/server/modules/providers/services/sessions.service.ts +45 -45
- package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
- package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
- package/server/modules/providers/shared/provider-configs.ts +142 -142
- package/server/modules/providers/tests/mcp.test.ts +293 -293
- package/server/openai-codex.js +462 -462
- package/server/opencode-cli.js +491 -491
- package/server/opencode-response-handler.js +111 -111
- package/server/projects.js +3008 -3008
- package/server/qwen-code-cli.js +410 -410
- package/server/qwen-response-handler.js +73 -73
- package/server/routes/agent.js +1435 -1435
- package/server/routes/auth.js +154 -146
- package/server/routes/codex.js +20 -20
- package/server/routes/commands.js +570 -570
- package/server/routes/cursor.js +61 -61
- package/server/routes/diagnostics.js +41 -41
- package/server/routes/gemini.js +25 -25
- package/server/routes/git.js +1650 -1635
- package/server/routes/live-view.js +411 -411
- package/server/routes/mcp-utils.js +13 -13
- package/server/routes/messages.js +62 -62
- package/server/routes/network.js +125 -125
- package/server/routes/platformization.js +198 -197
- package/server/routes/plugins.js +320 -320
- package/server/routes/production-agent-loop.js +90 -90
- package/server/routes/projects.js +917 -917
- package/server/routes/public-api.js +34 -34
- package/server/routes/qwen.js +27 -27
- package/server/routes/remote.js +55 -55
- package/server/routes/settings.js +321 -321
- package/server/routes/telegram.js +140 -140
- package/server/routes/user.js +125 -125
- package/server/routes/webhooks.js +63 -63
- package/server/services/control-room.js +102 -102
- package/server/services/diagnostics.js +165 -165
- package/server/services/external-access.js +375 -375
- package/server/services/hermes-gateway.js +1562 -1247
- package/server/services/hermes-install-jobs.js +729 -729
- package/server/services/install-jobs.js +715 -715
- package/server/services/live-view.js +956 -956
- package/server/services/managed-runtimes.js +493 -493
- package/server/services/model-registry.js +144 -144
- package/server/services/notification-orchestrator.js +365 -365
- package/server/services/notification-taxonomy.js +204 -204
- package/server/services/platformization.js +844 -779
- package/server/services/production-agent-loop.js +248 -248
- package/server/services/provider-cli-versions.js +149 -149
- package/server/services/provider-credentials.js +189 -189
- package/server/services/provider-models.js +396 -396
- package/server/services/public-api-manifest.js +190 -182
- package/server/services/remote-connection.js +127 -127
- package/server/services/runtime-manager.js +323 -323
- package/server/services/startup-update.js +234 -234
- package/server/services/telegram/bot.js +331 -331
- package/server/services/telegram/control-center.js +979 -979
- package/server/services/telegram/telegram-http-client.js +151 -151
- package/server/services/telegram/translations.js +340 -340
- package/server/services/vapid-keys.js +36 -36
- package/server/services/webhooks.js +216 -216
- package/server/sessionManager.js +225 -225
- package/server/shared/interfaces.ts +54 -54
- package/server/shared/types.ts +172 -172
- package/server/shared/utils.ts +193 -193
- package/server/tsconfig.json +36 -36
- package/server/utils/colors.js +21 -21
- package/server/utils/commandParser.js +305 -305
- package/server/utils/frontmatter.js +18 -18
- package/server/utils/gitConfig.js +34 -34
- package/server/utils/plugin-loader.js +457 -457
- package/server/utils/plugin-process-manager.js +185 -185
- package/server/utils/port-access.js +209 -209
- package/server/utils/runtime-paths.js +37 -37
- package/server/utils/url-detection.js +71 -71
- package/server/vite-daemon.js +79 -79
- package/shared/modelConstants.js +161 -161
- package/shared/networkHosts.js +22 -22
- package/dist/assets/index-DMz0zv6T.css +0 -32
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
// Gemini Response Handler - JSON Stream processing
|
|
2
|
-
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
3
|
-
|
|
4
|
-
class GeminiResponseHandler {
|
|
5
|
-
constructor(ws, options = {}) {
|
|
6
|
-
this.ws = ws;
|
|
7
|
-
this.buffer = '';
|
|
8
|
-
this.onContentFragment = options.onContentFragment || null;
|
|
9
|
-
this.onInit = options.onInit || null;
|
|
10
|
-
this.onToolUse = options.onToolUse || null;
|
|
11
|
-
this.onToolResult = options.onToolResult || null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Process incoming raw data from Gemini stream-json
|
|
15
|
-
processData(data) {
|
|
16
|
-
this.buffer += data;
|
|
17
|
-
|
|
18
|
-
// Split by newline
|
|
19
|
-
const lines = this.buffer.split('\n');
|
|
20
|
-
|
|
21
|
-
// Keep the last incomplete line in the buffer
|
|
22
|
-
this.buffer = lines.pop() || '';
|
|
23
|
-
|
|
24
|
-
for (const line of lines) {
|
|
25
|
-
if (!line.trim()) continue;
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const event = JSON.parse(line);
|
|
29
|
-
this.handleEvent(event);
|
|
30
|
-
} catch (err) {
|
|
31
|
-
// Not a JSON line, probably debug output or CLI warnings
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
handleEvent(event) {
|
|
37
|
-
const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
|
|
38
|
-
|
|
39
|
-
if (event.type === 'init') {
|
|
40
|
-
if (this.onInit) {
|
|
41
|
-
this.onInit(event);
|
|
42
|
-
}
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Invoke per-type callbacks for session tracking
|
|
47
|
-
if (event.type === 'message' && event.role === 'assistant') {
|
|
48
|
-
const content = event.content || '';
|
|
49
|
-
if (this.onContentFragment && content) {
|
|
50
|
-
this.onContentFragment(content);
|
|
51
|
-
}
|
|
52
|
-
} else if (event.type === 'tool_use' && this.onToolUse) {
|
|
53
|
-
this.onToolUse(event);
|
|
54
|
-
} else if (event.type === 'tool_result' && this.onToolResult) {
|
|
55
|
-
this.onToolResult(event);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Normalize via adapter and send all resulting messages
|
|
59
|
-
const normalized = sessionsService.normalizeMessage('gemini', event, sid);
|
|
60
|
-
for (const msg of normalized) {
|
|
61
|
-
this.ws.send(msg);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
forceFlush() {
|
|
66
|
-
if (this.buffer.trim()) {
|
|
67
|
-
try {
|
|
68
|
-
const event = JSON.parse(this.buffer);
|
|
69
|
-
this.handleEvent(event);
|
|
70
|
-
} catch (err) { }
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
destroy() {
|
|
75
|
-
this.buffer = '';
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export default GeminiResponseHandler;
|
|
1
|
+
// Gemini Response Handler - JSON Stream processing
|
|
2
|
+
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
3
|
+
|
|
4
|
+
class GeminiResponseHandler {
|
|
5
|
+
constructor(ws, options = {}) {
|
|
6
|
+
this.ws = ws;
|
|
7
|
+
this.buffer = '';
|
|
8
|
+
this.onContentFragment = options.onContentFragment || null;
|
|
9
|
+
this.onInit = options.onInit || null;
|
|
10
|
+
this.onToolUse = options.onToolUse || null;
|
|
11
|
+
this.onToolResult = options.onToolResult || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Process incoming raw data from Gemini stream-json
|
|
15
|
+
processData(data) {
|
|
16
|
+
this.buffer += data;
|
|
17
|
+
|
|
18
|
+
// Split by newline
|
|
19
|
+
const lines = this.buffer.split('\n');
|
|
20
|
+
|
|
21
|
+
// Keep the last incomplete line in the buffer
|
|
22
|
+
this.buffer = lines.pop() || '';
|
|
23
|
+
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
if (!line.trim()) continue;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const event = JSON.parse(line);
|
|
29
|
+
this.handleEvent(event);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
// Not a JSON line, probably debug output or CLI warnings
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
handleEvent(event) {
|
|
37
|
+
const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
|
|
38
|
+
|
|
39
|
+
if (event.type === 'init') {
|
|
40
|
+
if (this.onInit) {
|
|
41
|
+
this.onInit(event);
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Invoke per-type callbacks for session tracking
|
|
47
|
+
if (event.type === 'message' && event.role === 'assistant') {
|
|
48
|
+
const content = event.content || '';
|
|
49
|
+
if (this.onContentFragment && content) {
|
|
50
|
+
this.onContentFragment(content);
|
|
51
|
+
}
|
|
52
|
+
} else if (event.type === 'tool_use' && this.onToolUse) {
|
|
53
|
+
this.onToolUse(event);
|
|
54
|
+
} else if (event.type === 'tool_result' && this.onToolResult) {
|
|
55
|
+
this.onToolResult(event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Normalize via adapter and send all resulting messages
|
|
59
|
+
const normalized = sessionsService.normalizeMessage('gemini', event, sid);
|
|
60
|
+
for (const msg of normalized) {
|
|
61
|
+
this.ws.send(msg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
forceFlush() {
|
|
66
|
+
if (this.buffer.trim()) {
|
|
67
|
+
try {
|
|
68
|
+
const event = JSON.parse(this.buffer);
|
|
69
|
+
this.handleEvent(event);
|
|
70
|
+
} catch (err) { }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
destroy() {
|
|
75
|
+
this.buffer = '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export default GeminiResponseHandler;
|
package/server/index.js
CHANGED
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
GeminiA2AAdapter,
|
|
112
112
|
OpenCodeA2AAdapter,
|
|
113
113
|
QwenA2AAdapter,
|
|
114
|
+
JsonEventA2AAdapter,
|
|
114
115
|
createPreviewProxyRouter,
|
|
115
116
|
createOrchestrationTaskRouter,
|
|
116
117
|
createHermesRouter,
|
|
@@ -130,7 +131,8 @@ import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './util
|
|
|
130
131
|
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, apiKeysDb } from './database/db.js';
|
|
131
132
|
import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
|
|
132
133
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
133
|
-
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
134
|
+
import { validateApiKey, authenticateToken, authenticateWebSocket, requireAdmin } from './middleware/auth.js';
|
|
135
|
+
import { filterProjectsForUser, userHasProjectAccess } from './services/platformization.js';
|
|
134
136
|
import { IS_PLATFORM } from './constants/config.js';
|
|
135
137
|
|
|
136
138
|
import { getConnectableHost } from '../shared/networkHosts.js';
|
|
@@ -139,6 +141,37 @@ import { buildDaemonCliCommand, handleDaemonCommand } from './daemon-manager.js'
|
|
|
139
141
|
|
|
140
142
|
const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode'];
|
|
141
143
|
|
|
144
|
+
function requireProjectAccess(capability = 'viewFiles') {
|
|
145
|
+
return (req, res, next) => {
|
|
146
|
+
const projectName = req.params.projectName || req.query.project || req.body?.project;
|
|
147
|
+
if (!projectName) {
|
|
148
|
+
return next();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!userHasProjectAccess(req.user, { name: String(projectName), projectName: String(projectName) }, capability)) {
|
|
152
|
+
return res.status(403).json({ error: 'Project access denied.' });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
next();
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function requireProjectPathAccess(capability = 'viewFiles') {
|
|
160
|
+
return (req, res, next) => {
|
|
161
|
+
const projectPath = req.body?.projectPath || req.query.projectPath || os.homedir();
|
|
162
|
+
const resolvedProjectPath = path.resolve(String(projectPath));
|
|
163
|
+
if (!userHasProjectAccess(req.user, {
|
|
164
|
+
fullPath: resolvedProjectPath,
|
|
165
|
+
path: resolvedProjectPath,
|
|
166
|
+
projectPath: resolvedProjectPath,
|
|
167
|
+
}, capability)) {
|
|
168
|
+
return res.status(403).json({ error: 'Project access denied.' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
next();
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
// File system watchers for provider project/session folders
|
|
143
176
|
const PROVIDER_WATCH_PATHS = [
|
|
144
177
|
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
@@ -229,19 +262,21 @@ async function setupProjectsWatcher() {
|
|
|
229
262
|
// Get updated projects list
|
|
230
263
|
const updatedProjects = await getProjects(broadcastProgress);
|
|
231
264
|
|
|
232
|
-
|
|
233
|
-
const updateMessage = JSON.stringify({
|
|
265
|
+
const updatePayload = {
|
|
234
266
|
type: 'projects_updated',
|
|
235
|
-
projects: updatedProjects,
|
|
236
267
|
timestamp: new Date().toISOString(),
|
|
237
268
|
changeType: eventType,
|
|
238
269
|
changedFile: path.relative(rootPath, filePath),
|
|
239
270
|
watchProvider: provider
|
|
240
|
-
}
|
|
271
|
+
};
|
|
241
272
|
|
|
273
|
+
// Notify all connected clients about project changes, scoped to their access.
|
|
242
274
|
connectedClients.forEach(client => {
|
|
243
275
|
if (client.readyState === WebSocket.OPEN) {
|
|
244
|
-
client.send(
|
|
276
|
+
client.send(JSON.stringify({
|
|
277
|
+
...updatePayload,
|
|
278
|
+
projects: filterProjectsForUser(updatedProjects, client.user),
|
|
279
|
+
}));
|
|
245
280
|
}
|
|
246
281
|
});
|
|
247
282
|
|
|
@@ -302,6 +337,118 @@ async function setupProjectsWatcher() {
|
|
|
302
337
|
}
|
|
303
338
|
}
|
|
304
339
|
|
|
340
|
+
// ── Per-project workspace watchers (file explorer live refresh) ─────────────
|
|
341
|
+
// setupProjectsWatcher() above only watches provider metadata folders
|
|
342
|
+
// (~/.claude/projects etc.), so the file explorer never learned about changes
|
|
343
|
+
// inside the actual project working directory. These watchers are created on
|
|
344
|
+
// demand when a client sends `watch-project` over /ws and broadcast debounced
|
|
345
|
+
// `project_files_updated` events to subscribed clients only, letting the
|
|
346
|
+
// explorer refresh automatically without HTTP polling.
|
|
347
|
+
const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
|
|
348
|
+
const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath }
|
|
349
|
+
|
|
350
|
+
async function subscribeToWorkspace(ws, projectName) {
|
|
351
|
+
if (!projectName || typeof projectName !== 'string') return;
|
|
352
|
+
if (!userHasProjectAccess(ws.user, { name: projectName, projectName }, 'viewFiles')) return;
|
|
353
|
+
|
|
354
|
+
const existing = workspaceWatchers.get(projectName);
|
|
355
|
+
if (existing) {
|
|
356
|
+
existing.subscribers.add(ws);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let rootPath;
|
|
361
|
+
try {
|
|
362
|
+
rootPath = await extractProjectDirectory(projectName);
|
|
363
|
+
await fsPromises.access(rootPath);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.warn(`[watcher] Cannot watch workspace for ${projectName}:`, error.message);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const entry = {
|
|
370
|
+
watcher: null,
|
|
371
|
+
subscribers: new Set([ws]),
|
|
372
|
+
debounceTimer: null,
|
|
373
|
+
pendingEvent: null,
|
|
374
|
+
rootPath,
|
|
375
|
+
};
|
|
376
|
+
workspaceWatchers.set(projectName, entry);
|
|
377
|
+
|
|
378
|
+
const broadcastFileUpdate = (eventType, filePath) => {
|
|
379
|
+
entry.pendingEvent = { eventType, filePath };
|
|
380
|
+
if (entry.debounceTimer) {
|
|
381
|
+
clearTimeout(entry.debounceTimer);
|
|
382
|
+
}
|
|
383
|
+
entry.debounceTimer = setTimeout(() => {
|
|
384
|
+
entry.debounceTimer = null;
|
|
385
|
+
const pending = entry.pendingEvent || {};
|
|
386
|
+
entry.pendingEvent = null;
|
|
387
|
+
const message = JSON.stringify({
|
|
388
|
+
type: 'project_files_updated',
|
|
389
|
+
projectName,
|
|
390
|
+
changeType: pending.eventType || 'change',
|
|
391
|
+
changedFile: pending.filePath ? path.relative(rootPath, pending.filePath) : null,
|
|
392
|
+
timestamp: new Date().toISOString(),
|
|
393
|
+
});
|
|
394
|
+
entry.subscribers.forEach((client) => {
|
|
395
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
396
|
+
client.send(message);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}, WORKSPACE_WATCHER_DEBOUNCE_MS);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const chokidar = (await import('chokidar')).default;
|
|
404
|
+
const watcher = chokidar.watch(rootPath, {
|
|
405
|
+
ignored: WATCHER_IGNORED_PATTERNS,
|
|
406
|
+
persistent: true,
|
|
407
|
+
ignoreInitial: true,
|
|
408
|
+
followSymlinks: false,
|
|
409
|
+
depth: 10,
|
|
410
|
+
awaitWriteFinish: {
|
|
411
|
+
stabilityThreshold: 500,
|
|
412
|
+
pollInterval: 250
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
watcher
|
|
417
|
+
.on('add', (filePath) => broadcastFileUpdate('add', filePath))
|
|
418
|
+
.on('change', (filePath) => broadcastFileUpdate('change', filePath))
|
|
419
|
+
.on('unlink', (filePath) => broadcastFileUpdate('unlink', filePath))
|
|
420
|
+
.on('addDir', (dirPath) => broadcastFileUpdate('addDir', dirPath))
|
|
421
|
+
.on('unlinkDir', (dirPath) => broadcastFileUpdate('unlinkDir', dirPath))
|
|
422
|
+
.on('error', (error) => {
|
|
423
|
+
console.error(`[ERROR] Workspace watcher error for ${projectName}:`, error);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
entry.watcher = watcher;
|
|
427
|
+
} catch (error) {
|
|
428
|
+
workspaceWatchers.delete(projectName);
|
|
429
|
+
console.error(`[ERROR] Failed to watch workspace for ${projectName}:`, error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function unsubscribeFromWorkspace(ws, projectName = null) {
|
|
434
|
+
const entries = projectName
|
|
435
|
+
? (workspaceWatchers.has(projectName) ? [[projectName, workspaceWatchers.get(projectName)]] : [])
|
|
436
|
+
: Array.from(workspaceWatchers.entries());
|
|
437
|
+
|
|
438
|
+
for (const [name, entry] of entries) {
|
|
439
|
+
entry.subscribers.delete(ws);
|
|
440
|
+
if (entry.subscribers.size === 0) {
|
|
441
|
+
if (entry.debounceTimer) {
|
|
442
|
+
clearTimeout(entry.debounceTimer);
|
|
443
|
+
}
|
|
444
|
+
workspaceWatchers.delete(name);
|
|
445
|
+
entry.watcher?.close().catch((error) => {
|
|
446
|
+
console.warn(`[watcher] Failed to close workspace watcher for ${name}:`, error?.message || error);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
305
452
|
|
|
306
453
|
const app = express();
|
|
307
454
|
const server = http.createServer(app);
|
|
@@ -781,7 +928,7 @@ app.get('/health', (req, res) => {
|
|
|
781
928
|
// Optional API key validation (if configured)
|
|
782
929
|
app.use('/api', validateApiKey);
|
|
783
930
|
|
|
784
|
-
app.post('/api/shell/sessions/terminate', authenticateToken, (req, res) => {
|
|
931
|
+
app.post('/api/shell/sessions/terminate', authenticateToken, requireProjectPathAccess('useShell'), (req, res) => {
|
|
785
932
|
const provider = req.body?.provider || 'claude';
|
|
786
933
|
const projectPath = req.body?.projectPath || os.homedir();
|
|
787
934
|
|
|
@@ -793,7 +940,7 @@ app.post('/api/shell/sessions/terminate', authenticateToken, (req, res) => {
|
|
|
793
940
|
res.json({ success: true, killedSessions });
|
|
794
941
|
});
|
|
795
942
|
|
|
796
|
-
app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) => {
|
|
943
|
+
app.get('/api/shell/sessions/provider-output', authenticateToken, requireProjectPathAccess('useShell'), (req, res) => {
|
|
797
944
|
const provider = String(req.query.provider || 'claude');
|
|
798
945
|
const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
|
|
799
946
|
? req.query.projectPath.trim()
|
|
@@ -850,7 +997,7 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
|
|
|
850
997
|
});
|
|
851
998
|
});
|
|
852
999
|
|
|
853
|
-
app.post('/api/shell/sessions/provider-input', authenticateToken, (req, res) => {
|
|
1000
|
+
app.post('/api/shell/sessions/provider-input', authenticateToken, requireProjectPathAccess('useShell'), (req, res) => {
|
|
854
1001
|
const provider = String(req.body?.provider || 'claude');
|
|
855
1002
|
const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.trim()
|
|
856
1003
|
? req.body.projectPath.trim()
|
|
@@ -970,8 +1117,8 @@ app.use('/api/webhooks', authenticateToken, webhooksRoutes);
|
|
|
970
1117
|
// Production agent loop APIs (protected)
|
|
971
1118
|
app.use('/api/production-agent-loop', authenticateToken, productionAgentLoopRoutes);
|
|
972
1119
|
|
|
973
|
-
// Platform control plane APIs (
|
|
974
|
-
app.use('/api/platformization', authenticateToken, platformizationRoutes);
|
|
1120
|
+
// Platform control plane APIs (admin-only)
|
|
1121
|
+
app.use('/api/platformization', authenticateToken, requireAdmin, platformizationRoutes);
|
|
975
1122
|
|
|
976
1123
|
// Project Live View (protected control API + public share proxy)
|
|
977
1124
|
app.use('/api/live-view', authenticateToken, liveViewRoutes);
|
|
@@ -986,6 +1133,7 @@ adapterRegistry.register(new CursorA2AAdapter());
|
|
|
986
1133
|
adapterRegistry.register(new GeminiA2AAdapter());
|
|
987
1134
|
adapterRegistry.register(new QwenA2AAdapter());
|
|
988
1135
|
adapterRegistry.register(new OpenCodeA2AAdapter());
|
|
1136
|
+
adapterRegistry.register(new JsonEventA2AAdapter());
|
|
989
1137
|
app.use('/hermes', createHermesTaskRouter());
|
|
990
1138
|
app.use('/preview', authenticateToken, createPreviewProxyRouter());
|
|
991
1139
|
app.use('/api/orchestration', authenticateToken, createOrchestrationTaskRouter());
|
|
@@ -1432,13 +1580,13 @@ app.post('/api/system/restart', authenticateToken, (req, res) => {
|
|
|
1432
1580
|
app.get('/api/projects', authenticateToken, async (req, res) => {
|
|
1433
1581
|
try {
|
|
1434
1582
|
const projects = await getProjects(broadcastProgress);
|
|
1435
|
-
res.json(projects);
|
|
1583
|
+
res.json(filterProjectsForUser(projects, req.user));
|
|
1436
1584
|
} catch (error) {
|
|
1437
1585
|
res.status(500).json({ error: error.message });
|
|
1438
1586
|
}
|
|
1439
1587
|
});
|
|
1440
1588
|
|
|
1441
|
-
app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, res) => {
|
|
1589
|
+
app.get('/api/projects/:projectName/sessions', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
|
|
1442
1590
|
try {
|
|
1443
1591
|
const { limit = 5, offset = 0 } = req.query;
|
|
1444
1592
|
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
|
|
@@ -1450,7 +1598,7 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
|
|
|
1450
1598
|
});
|
|
1451
1599
|
|
|
1452
1600
|
// Rename project endpoint
|
|
1453
|
-
app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res) => {
|
|
1601
|
+
app.put('/api/projects/:projectName/rename', authenticateToken, requireProjectAccess('manageProjectSettings'), async (req, res) => {
|
|
1454
1602
|
try {
|
|
1455
1603
|
const { displayName } = req.body;
|
|
1456
1604
|
await renameProject(req.params.projectName, displayName);
|
|
@@ -1461,7 +1609,7 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
|
|
|
1461
1609
|
});
|
|
1462
1610
|
|
|
1463
1611
|
// Delete session endpoint
|
|
1464
|
-
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
|
|
1612
|
+
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
|
|
1465
1613
|
try {
|
|
1466
1614
|
const { projectName, sessionId } = req.params;
|
|
1467
1615
|
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
|
@@ -1504,7 +1652,7 @@ app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) =
|
|
|
1504
1652
|
// Delete project endpoint
|
|
1505
1653
|
// force=true to allow removal even when sessions exist
|
|
1506
1654
|
// deleteData=true to also delete session/memory files on disk (destructive)
|
|
1507
|
-
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
1655
|
+
app.delete('/api/projects/:projectName', authenticateToken, requireProjectAccess('manageProjectSettings'), async (req, res) => {
|
|
1508
1656
|
try {
|
|
1509
1657
|
const { projectName } = req.params;
|
|
1510
1658
|
const force = req.query.force === 'true';
|
|
@@ -1522,7 +1670,7 @@ app.delete('/api/projects/:projectName', authenticateToken, async (req, res) =>
|
|
|
1522
1670
|
});
|
|
1523
1671
|
|
|
1524
1672
|
// Search conversations content (SSE streaming)
|
|
1525
|
-
app.get('/api/search/conversations', authenticateToken, async (req, res) => {
|
|
1673
|
+
app.get('/api/search/conversations', authenticateToken, requireAdmin, async (req, res) => {
|
|
1526
1674
|
const query = typeof req.query.q === 'string' ? req.query.q.trim() : '';
|
|
1527
1675
|
const parsedLimit = Number.parseInt(String(req.query.limit), 10);
|
|
1528
1676
|
const limit = Number.isNaN(parsedLimit) ? 50 : Math.max(1, Math.min(parsedLimit, 100));
|
|
@@ -1717,7 +1865,7 @@ app.post('/api/create-folder', authenticateToken, async (req, res) => {
|
|
|
1717
1865
|
});
|
|
1718
1866
|
|
|
1719
1867
|
// Read file content endpoint
|
|
1720
|
-
app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
|
1868
|
+
app.get('/api/projects/:projectName/file', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
|
|
1721
1869
|
try {
|
|
1722
1870
|
const { projectName } = req.params;
|
|
1723
1871
|
const { filePath } = req.query;
|
|
@@ -1757,7 +1905,7 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
1757
1905
|
});
|
|
1758
1906
|
|
|
1759
1907
|
// Serve raw file bytes for previews and downloads.
|
|
1760
|
-
app.get('/api/projects/:projectName/files/content', authenticateToken, async (req, res) => {
|
|
1908
|
+
app.get('/api/projects/:projectName/files/content', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
|
|
1761
1909
|
try {
|
|
1762
1910
|
const { projectName } = req.params;
|
|
1763
1911
|
const { path: filePath } = req.query;
|
|
@@ -1814,7 +1962,7 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
1814
1962
|
});
|
|
1815
1963
|
|
|
1816
1964
|
// Save file content endpoint
|
|
1817
|
-
app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) => {
|
|
1965
|
+
app.put('/api/projects/:projectName/file', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
|
|
1818
1966
|
try {
|
|
1819
1967
|
const { projectName } = req.params;
|
|
1820
1968
|
const { filePath, content } = req.body;
|
|
@@ -1863,7 +2011,7 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
1863
2011
|
}
|
|
1864
2012
|
});
|
|
1865
2013
|
|
|
1866
|
-
app.get('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
|
2014
|
+
app.get('/api/projects/:projectName/files', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
|
|
1867
2015
|
try {
|
|
1868
2016
|
|
|
1869
2017
|
// Using fsPromises from import
|
|
@@ -1874,8 +2022,12 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
|
|
1874
2022
|
actualPath = await extractProjectDirectory(req.params.projectName);
|
|
1875
2023
|
} catch (error) {
|
|
1876
2024
|
console.error('Error extracting project directory:', error);
|
|
1877
|
-
//
|
|
1878
|
-
|
|
2025
|
+
// Do NOT fall back to projectName.replace(/-/g, '/') here: on Windows the
|
|
2026
|
+
// dash-encoded name ("C--Users-...") decodes to a garbage path ("C//Users/...")
|
|
2027
|
+
// that can never exist, so the old fallback just produced a confusing 404
|
|
2028
|
+
// with a fabricated path. extractProjectDirectory already handles the
|
|
2029
|
+
// POSIX-style decode internally; if it throws, the project is unknown.
|
|
2030
|
+
return res.status(404).json({ error: `Project not found: ${req.params.projectName}` });
|
|
1879
2031
|
}
|
|
1880
2032
|
|
|
1881
2033
|
// Check if path exists
|
|
@@ -1941,7 +2093,7 @@ function validateFilename(name) {
|
|
|
1941
2093
|
}
|
|
1942
2094
|
|
|
1943
2095
|
// POST /api/projects/:projectName/files/create - Create new file or directory
|
|
1944
|
-
app.post('/api/projects/:projectName/files/create', authenticateToken, async (req, res) => {
|
|
2096
|
+
app.post('/api/projects/:projectName/files/create', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
|
|
1945
2097
|
try {
|
|
1946
2098
|
const { projectName } = req.params;
|
|
1947
2099
|
const { path: parentPath, type, name } = req.body;
|
|
@@ -2018,7 +2170,7 @@ app.post('/api/projects/:projectName/files/create', authenticateToken, async (re
|
|
|
2018
2170
|
});
|
|
2019
2171
|
|
|
2020
2172
|
// PUT /api/projects/:projectName/files/rename - Rename file or directory
|
|
2021
|
-
app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req, res) => {
|
|
2173
|
+
app.put('/api/projects/:projectName/files/rename', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
|
|
2022
2174
|
try {
|
|
2023
2175
|
const { projectName } = req.params;
|
|
2024
2176
|
const { oldPath, newName } = req.body;
|
|
@@ -2095,7 +2247,7 @@ app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req
|
|
|
2095
2247
|
});
|
|
2096
2248
|
|
|
2097
2249
|
// DELETE /api/projects/:projectName/files - Delete file or directory
|
|
2098
|
-
app.delete('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
|
2250
|
+
app.delete('/api/projects/:projectName/files', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
|
|
2099
2251
|
try {
|
|
2100
2252
|
const { projectName } = req.params;
|
|
2101
2253
|
const { path: targetPath, type } = req.body;
|
|
@@ -2321,7 +2473,7 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
2321
2473
|
});
|
|
2322
2474
|
};
|
|
2323
2475
|
|
|
2324
|
-
app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
|
|
2476
|
+
app.post('/api/projects/:projectName/files/upload', authenticateToken, requireProjectAccess('editFiles'), uploadFilesHandler);
|
|
2325
2477
|
|
|
2326
2478
|
/**
|
|
2327
2479
|
* Proxy an authenticated client WebSocket to a plugin's internal WS server.
|
|
@@ -2428,6 +2580,7 @@ function handleChatConnection(ws, request) {
|
|
|
2428
2580
|
|
|
2429
2581
|
// Add to connected clients for project updates
|
|
2430
2582
|
ws.userId = request?.user?.id ?? request?.user?.userId ?? null;
|
|
2583
|
+
ws.user = request?.user ?? null;
|
|
2431
2584
|
connectedClients.add(ws);
|
|
2432
2585
|
|
|
2433
2586
|
// Wrap WebSocket with writer for consistent interface with SSEStreamWriter
|
|
@@ -2554,6 +2707,13 @@ function handleChatConnection(ws, request) {
|
|
|
2554
2707
|
data: pending
|
|
2555
2708
|
});
|
|
2556
2709
|
}
|
|
2710
|
+
} else if (data.type === 'watch-project') {
|
|
2711
|
+
// Subscribe this client to live file-tree updates for a project
|
|
2712
|
+
// workspace. The server pushes debounced `project_files_updated`
|
|
2713
|
+
// events so the explorer refreshes without HTTP polling.
|
|
2714
|
+
await subscribeToWorkspace(ws, data.projectName);
|
|
2715
|
+
} else if (data.type === 'unwatch-project') {
|
|
2716
|
+
unsubscribeFromWorkspace(ws, data.projectName || null);
|
|
2557
2717
|
} else if (data.type === 'get-active-sessions') {
|
|
2558
2718
|
// Get all currently active sessions
|
|
2559
2719
|
const activeSessions = {
|
|
@@ -2582,6 +2742,8 @@ function handleChatConnection(ws, request) {
|
|
|
2582
2742
|
console.log('🔌 Chat client disconnected');
|
|
2583
2743
|
// Remove from connected clients
|
|
2584
2744
|
connectedClients.delete(ws);
|
|
2745
|
+
// Drop any workspace watcher subscriptions held by this socket
|
|
2746
|
+
unsubscribeFromWorkspace(ws);
|
|
2585
2747
|
});
|
|
2586
2748
|
}
|
|
2587
2749
|
|
|
@@ -2613,6 +2775,15 @@ function handleShellConnection(ws, request) {
|
|
|
2613
2775
|
// is writable, has a git-friendly cwd, and matches where
|
|
2614
2776
|
// every provider already stores its config (~/.codex etc.).
|
|
2615
2777
|
const projectPath = data.projectPath || os.homedir();
|
|
2778
|
+
const requestedProjectPath = path.resolve(projectPath);
|
|
2779
|
+
if (!userHasProjectAccess(request.user, {
|
|
2780
|
+
fullPath: requestedProjectPath,
|
|
2781
|
+
path: requestedProjectPath,
|
|
2782
|
+
projectPath: requestedProjectPath,
|
|
2783
|
+
}, 'useShell')) {
|
|
2784
|
+
ws.send(JSON.stringify({ type: 'error', message: 'Shell access denied for this project' }));
|
|
2785
|
+
return;
|
|
2786
|
+
}
|
|
2616
2787
|
const sessionId = data.sessionId;
|
|
2617
2788
|
const hasSession = data.hasSession;
|
|
2618
2789
|
const provider = data.provider || 'claude';
|
|
@@ -2750,7 +2921,7 @@ function handleShellConnection(ws, request) {
|
|
|
2750
2921
|
|
|
2751
2922
|
try {
|
|
2752
2923
|
// Validate projectPath — resolve to absolute and verify it exists
|
|
2753
|
-
const resolvedProjectPath =
|
|
2924
|
+
const resolvedProjectPath = requestedProjectPath;
|
|
2754
2925
|
try {
|
|
2755
2926
|
const stats = fs.statSync(resolvedProjectPath);
|
|
2756
2927
|
if (!stats.isDirectory()) {
|
|
@@ -3108,7 +3279,7 @@ function handleShellConnection(ws, request) {
|
|
|
3108
3279
|
});
|
|
3109
3280
|
}
|
|
3110
3281
|
// Image upload endpoint
|
|
3111
|
-
app.post('/api/projects/:projectName/upload-images', authenticateToken, async (req, res) => {
|
|
3282
|
+
app.post('/api/projects/:projectName/upload-images', authenticateToken, requireProjectAccess('editFiles'), async (req, res) => {
|
|
3112
3283
|
try {
|
|
3113
3284
|
const multer = (await import('multer')).default;
|
|
3114
3285
|
const path = (await import('path')).default;
|
|
@@ -3193,7 +3364,7 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|
|
3193
3364
|
});
|
|
3194
3365
|
|
|
3195
3366
|
// Get token usage for a specific session
|
|
3196
|
-
app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
|
3367
|
+
app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, requireProjectAccess('viewFiles'), async (req, res) => {
|
|
3197
3368
|
try {
|
|
3198
3369
|
const { projectName, sessionId } = req.params;
|
|
3199
3370
|
const { provider = 'claude' } = req.query;
|