@cloudcli-ai/cloudcli 1.34.0 → 1.35.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 +9 -1
- package/README.ja.md +8 -0
- package/README.ko.md +9 -1
- package/README.md +16 -3
- package/README.ru.md +9 -1
- package/README.tr.md +8 -1
- package/README.zh-CN.md +9 -1
- package/README.zh-TW.md +9 -1
- package/dist/api-docs.html +36 -18
- package/dist/assets/index-CtwxqzGG.js +1407 -0
- package/dist/assets/index-DlvtxFyt.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/browser-use-mcp.js +350 -0
- package/dist-server/server/browser-use-mcp.js.map +1 -0
- package/dist-server/server/claude-sdk.js +25 -5
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cli.js +14 -6
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/computer-use-agent.js +242 -0
- package/dist-server/server/computer-use-agent.js.map +1 -0
- package/dist-server/server/computer-use-mcp.js +519 -0
- package/dist-server/server/computer-use-mcp.js.map +1 -0
- package/dist-server/server/cursor-cli.js +27 -11
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +17 -2
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/index.js +149 -40
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/load-env.js +1 -1
- package/dist-server/server/load-env.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js +107 -0
- package/dist-server/server/modules/browser-use/browser-use-mcp.routes.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.routes.js +93 -0
- package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js +688 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -0
- package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js +8 -0
- package/dist-server/server/modules/browser-use/tests/browser-use.service.test.js.map +1 -0
- package/dist-server/server/modules/computer-use/actions/raw-action-dispatcher.js +55 -0
- package/dist-server/server/modules/computer-use/actions/raw-action-dispatcher.js.map +1 -0
- package/dist-server/server/modules/computer-use/actions/raw-action-types.js +2 -0
- package/dist-server/server/modules/computer-use/actions/raw-action-types.js.map +1 -0
- package/dist-server/server/modules/computer-use/computer-executor.js +212 -0
- package/dist-server/server/modules/computer-use/computer-executor.js.map +1 -0
- package/dist-server/server/modules/computer-use/computer-semantics.service.js +422 -0
- package/dist-server/server/modules/computer-use/computer-semantics.service.js.map +1 -0
- package/dist-server/server/modules/computer-use/computer-use-mcp.routes.js +126 -0
- package/dist-server/server/modules/computer-use/computer-use-mcp.routes.js.map +1 -0
- package/dist-server/server/modules/computer-use/computer-use.routes.js +196 -0
- package/dist-server/server/modules/computer-use/computer-use.routes.js.map +1 -0
- package/dist-server/server/modules/computer-use/computer-use.service.js +759 -0
- package/dist-server/server/modules/computer-use/computer-use.service.js.map +1 -0
- package/dist-server/server/modules/computer-use/desktop-agent-relay.service.js +126 -0
- package/dist-server/server/modules/computer-use/desktop-agent-relay.service.js.map +1 -0
- package/dist-server/server/modules/computer-use/index.js +3 -0
- package/dist-server/server/modules/computer-use/index.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.js +59 -0
- package/dist-server/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.js +5 -0
- package/dist-server/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/adapters/semantic-adapter.js +2 -0
- package/dist-server/server/modules/computer-use/semantics/adapters/semantic-adapter.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.js +5 -0
- package/dist-server/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/bin/darwin-arm64/CloudCLISemantics +0 -0
- package/dist-server/server/modules/computer-use/semantics/bin/darwin-x64/CloudCLISemantics +0 -0
- package/dist-server/server/modules/computer-use/semantics/bin/win32-arm64/CloudCLISemantics.exe +0 -0
- package/dist-server/server/modules/computer-use/semantics/bin/win32-x64/CloudCLISemantics.exe +0 -0
- package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-process.js +106 -0
- package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-process.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.js +78 -0
- package/dist-server/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/semantic-session-store.js +68 -0
- package/dist-server/server/modules/computer-use/semantics/semantic-session-store.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/semantic-tool-dispatcher.js +16 -0
- package/dist-server/server/modules/computer-use/semantics/semantic-tool-dispatcher.js.map +1 -0
- package/dist-server/server/modules/computer-use/semantics/semantic-types.js +2 -0
- package/dist-server/server/modules/computer-use/semantics/semantic-types.js.map +1 -0
- package/dist-server/server/modules/database/index.js +1 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +23 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js +100 -0
- package/dist-server/server/modules/database/repositories/notification-channel-endpoints.js.map +1 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js +8 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -1
- package/dist-server/server/modules/database/repositories/sessions.db.js +173 -17
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +26 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/projects.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +78 -0
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -0
- package/dist-server/server/modules/database/{repositories → tests}/sessions.db.integration.test.js +10 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/notifications/index.js +4 -0
- package/dist-server/server/modules/notifications/index.js.map +1 -0
- package/dist-server/server/modules/notifications/notifications.routes.js +115 -0
- package/dist-server/server/modules/notifications/notifications.routes.js.map +1 -0
- package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js +97 -0
- package/dist-server/server/modules/notifications/services/desktop-notification-clients.service.js.map +1 -0
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +229 -0
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -0
- package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js +79 -0
- package/dist-server/server/modules/notifications/websocket/desktop-notifications-websocket.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js +0 -4
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +10 -38
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +1 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +24 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +9 -12
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +7 -0
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +7 -3
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +3 -9
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +7 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +13 -22
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +7 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +5 -8
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +6 -6
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +7 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +17 -3
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +9 -10
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +123 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/mcp.service.js +23 -0
- package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +65 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/provider-models.service.js +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +62 -23
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +62 -4
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/skills.service.js +11 -0
- package/dist-server/server/modules/providers/services/skills.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js +163 -1
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +45 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/skills.test.js +160 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +1 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +252 -0
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +105 -0
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +218 -156
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/desktop-agent-websocket.service.js +39 -0
- package/dist-server/server/modules/websocket/services/desktop-agent-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +83 -31
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/websocket-server.service.js +10 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +216 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -0
- package/dist-server/server/openai-codex.js +24 -14
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +21 -8
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-cli.test.js +19 -1
- package/dist-server/server/opencode-cli.test.js.map +1 -1
- package/dist-server/server/routes/settings.js +1 -1
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +1 -176
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/tests/notification-orchestrator.test.js +64 -0
- package/dist-server/server/services/tests/notification-orchestrator.test.js.map +1 -0
- package/dist-server/server/shared/{claude-cli-path.test.js → tests/claude-cli-path.test.js} +1 -1
- package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -0
- package/dist-server/server/shared/tests/slice-tail-page.test.js +35 -0
- package/dist-server/server/shared/tests/slice-tail-page.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +73 -1
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/voice-proxy.js +226 -0
- package/dist-server/server/voice-proxy.js.map +1 -0
- package/electron/assets/logo-macos.icns +0 -0
- package/electron/assets/logo-macos.png +0 -0
- package/electron/assets/logo-windows.ico +0 -0
- package/electron/cloud.js +260 -0
- package/electron/computerAgent.js +290 -0
- package/electron/desktopNotifications.js +378 -0
- package/electron/desktopWindow.js +781 -0
- package/electron/launcher/index.html +14 -0
- package/electron/launcher/launcher.css +801 -0
- package/electron/launcher/launcher.js +829 -0
- package/electron/localServer.js +550 -0
- package/electron/main.js +1060 -0
- package/electron/preload.cjs +63 -0
- package/electron/scripts/generate-macos-icon.js +62 -0
- package/electron/serverInstaller.js +277 -0
- package/electron/tabs.js +87 -0
- package/electron/viewHost.js +331 -0
- package/package.json +86 -5
- package/public/api-docs.html +36 -18
- package/scripts/build-computer-semantics.mjs +133 -0
- package/scripts/copy-computer-semantics-bin.mjs +24 -0
- package/scripts/release/build-server-bundle.js +176 -0
- package/scripts/release/prepare-desktop-app.js +158 -0
- package/server/browser-use-mcp.ts +384 -0
- package/server/claude-sdk.js +27 -5
- package/server/cli.js +15 -6
- package/server/computer-use-agent.ts +279 -0
- package/server/computer-use-mcp.ts +574 -0
- package/server/cursor-cli.js +27 -11
- package/server/gemini-cli.js +17 -2
- package/server/index.js +151 -47
- package/server/load-env.js +1 -1
- package/server/modules/browser-use/browser-use-mcp.routes.ts +120 -0
- package/server/modules/browser-use/browser-use.routes.ts +96 -0
- package/server/modules/browser-use/browser-use.service.ts +836 -0
- package/server/modules/browser-use/tests/browser-use.service.test.ts +10 -0
- package/server/modules/computer-use/actions/raw-action-dispatcher.ts +67 -0
- package/server/modules/computer-use/actions/raw-action-types.ts +28 -0
- package/server/modules/computer-use/computer-executor.ts +242 -0
- package/server/modules/computer-use/computer-semantics.service.ts +460 -0
- package/server/modules/computer-use/computer-use-mcp.routes.ts +141 -0
- package/server/modules/computer-use/computer-use.routes.ts +211 -0
- package/server/modules/computer-use/computer-use.service.ts +920 -0
- package/server/modules/computer-use/desktop-agent-relay.service.ts +158 -0
- package/server/modules/computer-use/index.ts +2 -0
- package/server/modules/computer-use/semantics/adapters/helper-semantic-adapter.ts +82 -0
- package/server/modules/computer-use/semantics/adapters/macos/macos-semantic-adapter.ts +5 -0
- package/server/modules/computer-use/semantics/adapters/semantic-adapter.ts +23 -0
- package/server/modules/computer-use/semantics/adapters/windows/windows-semantic-adapter.ts +5 -0
- package/server/modules/computer-use/semantics/bin/darwin-arm64/CloudCLISemantics +0 -0
- package/server/modules/computer-use/semantics/bin/darwin-x64/CloudCLISemantics +0 -0
- package/server/modules/computer-use/semantics/bin/win32-arm64/CloudCLISemantics.exe +0 -0
- package/server/modules/computer-use/semantics/bin/win32-x64/CloudCLISemantics.exe +0 -0
- package/server/modules/computer-use/semantics/helpers/macos/CloudCLISemantics.swift +467 -0
- package/server/modules/computer-use/semantics/helpers/semantic-helper-process.ts +124 -0
- package/server/modules/computer-use/semantics/helpers/semantic-helper-resolver.ts +97 -0
- package/server/modules/computer-use/semantics/helpers/windows/CloudCLISemantics.csproj +11 -0
- package/server/modules/computer-use/semantics/helpers/windows/Program.cs +534 -0
- package/server/modules/computer-use/semantics/semantic-session-store.ts +87 -0
- package/server/modules/computer-use/semantics/semantic-tool-dispatcher.ts +17 -0
- package/server/modules/computer-use/semantics/semantic-types.ts +58 -0
- package/server/modules/database/index.ts +1 -0
- package/server/modules/database/migrations.ts +25 -0
- package/server/modules/database/repositories/notification-channel-endpoints.ts +153 -0
- package/server/modules/database/repositories/notification-preferences.ts +12 -1
- package/server/modules/database/repositories/sessions.db.ts +227 -23
- package/server/modules/database/schema.ts +27 -0
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +108 -0
- package/server/modules/database/{repositories → tests}/sessions.db.integration.test.ts +12 -0
- package/server/modules/notifications/index.ts +13 -0
- package/server/modules/notifications/notifications.routes.ts +127 -0
- package/server/modules/notifications/services/desktop-notification-clients.service.ts +124 -0
- package/server/modules/notifications/services/notification-orchestrator.service.js +288 -0
- package/server/modules/notifications/websocket/desktop-notifications-websocket.service.ts +109 -0
- package/server/modules/projects/services/project-management.service.ts +0 -8
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +12 -53
- package/server/modules/providers/README.md +1 -1
- package/server/modules/providers/index.ts +1 -0
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +26 -1
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +9 -14
- package/server/modules/providers/list/claude/claude-skills.provider.ts +8 -0
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +7 -3
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +3 -12
- package/server/modules/providers/list/codex/codex-skills.provider.ts +8 -0
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +13 -22
- package/server/modules/providers/list/cursor/cursor-skills.provider.ts +8 -0
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +5 -8
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +6 -6
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +8 -0
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +18 -4
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +9 -12
- package/server/modules/providers/provider.routes.ts +167 -1
- package/server/modules/providers/services/mcp.service.ts +26 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +91 -0
- package/server/modules/providers/services/provider-models.service.ts +1 -1
- package/server/modules/providers/services/sessions-watcher.service.ts +71 -25
- package/server/modules/providers/services/sessions.service.ts +79 -4
- package/server/modules/providers/services/skills.service.ts +25 -1
- package/server/modules/providers/shared/skills/skills.provider.ts +223 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +49 -0
- package/server/modules/providers/tests/skills.test.ts +212 -0
- package/server/modules/websocket/README.md +33 -27
- package/server/modules/websocket/index.ts +1 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +327 -0
- package/server/modules/websocket/services/chat-session-writer.service.ts +145 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +292 -207
- package/server/modules/websocket/services/desktop-agent-websocket.service.ts +51 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +108 -32
- package/server/modules/websocket/services/websocket-server.service.ts +12 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +244 -0
- package/server/openai-codex.js +24 -14
- package/server/opencode-cli.js +21 -8
- package/server/opencode-cli.test.js +20 -1
- package/server/routes/settings.js +6 -1
- package/server/services/notification-orchestrator.js +3 -224
- package/server/services/tests/notification-orchestrator.test.js +80 -0
- package/server/shared/interfaces.ts +15 -0
- package/server/shared/tests/slice-tail-page.test.ts +42 -0
- package/server/shared/types.ts +84 -1
- package/server/shared/utils.ts +95 -1
- package/server/voice-proxy.js +224 -0
- package/dist/assets/index-73Xw3i9v.css +0 -32
- package/dist/assets/index-BIHaviaA.js +0 -1361
- package/dist/modelConstants.js +0 -854
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +0 -1
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +0 -1
- package/dist-server/server/shared/claude-cli-path.test.js.map +0 -1
- package/public/modelConstants.js +0 -854
- /package/dist-server/server/modules/database/{repositories → tests}/projects.db.integration.test.js +0 -0
- /package/server/modules/database/{repositories → tests}/projects.db.integration.test.ts +0 -0
- /package/server/shared/{claude-cli-path.test.ts → tests/claude-cli-path.test.ts} +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { closeConnection } from '@/modules/database/connection.js';
|
|
8
|
+
import { initializeDatabase } from '@/modules/database/init-db.js';
|
|
9
|
+
import { sessionsDb } from '@/modules/database/repositories/sessions.db.js';
|
|
10
|
+
|
|
11
|
+
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
12
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
13
|
+
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'sessions-mapping-'));
|
|
14
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
15
|
+
|
|
16
|
+
closeConnection();
|
|
17
|
+
process.env.DATABASE_PATH = databasePath;
|
|
18
|
+
await initializeDatabase();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await runTest();
|
|
22
|
+
} finally {
|
|
23
|
+
closeConnection();
|
|
24
|
+
if (previousDatabasePath === undefined) {
|
|
25
|
+
delete process.env.DATABASE_PATH;
|
|
26
|
+
} else {
|
|
27
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
28
|
+
}
|
|
29
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('disk-discovered sessions are keyed by the provider id for both columns', async () => {
|
|
34
|
+
await withIsolatedDatabase(() => {
|
|
35
|
+
sessionsDb.createSession('provider-abc', 'claude', '/workspace/demo', 'From Disk');
|
|
36
|
+
|
|
37
|
+
const row = sessionsDb.getSessionById('provider-abc');
|
|
38
|
+
assert.equal(row?.session_id, 'provider-abc');
|
|
39
|
+
assert.equal(row?.provider_session_id, 'provider-abc');
|
|
40
|
+
|
|
41
|
+
const byProviderId = sessionsDb.getSessionByProviderSessionId('provider-abc');
|
|
42
|
+
assert.equal(byProviderId?.session_id, 'provider-abc');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('app sessions get the provider id assigned without creating a duplicate row', async () => {
|
|
47
|
+
await withIsolatedDatabase(() => {
|
|
48
|
+
sessionsDb.createAppSession('app-id-1', 'claude', '/workspace/demo');
|
|
49
|
+
sessionsDb.assignProviderSessionId('app-id-1', 'provider-xyz');
|
|
50
|
+
|
|
51
|
+
// A later synchronizer pass that discovers the transcript on disk must
|
|
52
|
+
// update the app row in place instead of inserting a provider-keyed row.
|
|
53
|
+
const returnedId = sessionsDb.createSession(
|
|
54
|
+
'provider-xyz',
|
|
55
|
+
'claude',
|
|
56
|
+
'/workspace/demo',
|
|
57
|
+
'Synced Name',
|
|
58
|
+
undefined,
|
|
59
|
+
undefined,
|
|
60
|
+
'/fake/path/provider-xyz.jsonl',
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
assert.equal(returnedId, 'app-id-1');
|
|
64
|
+
assert.equal(sessionsDb.getAllSessions().length, 1);
|
|
65
|
+
|
|
66
|
+
const row = sessionsDb.getSessionById('app-id-1');
|
|
67
|
+
assert.equal(row?.provider_session_id, 'provider-xyz');
|
|
68
|
+
assert.equal(row?.jsonl_path, '/fake/path/provider-xyz.jsonl');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('assignProviderSessionId merges a watcher-created duplicate into the app row', async () => {
|
|
73
|
+
await withIsolatedDatabase(() => {
|
|
74
|
+
sessionsDb.createAppSession('app-id-2', 'codex', '/workspace/demo');
|
|
75
|
+
|
|
76
|
+
// Simulate the race: the filesystem watcher indexed the provider
|
|
77
|
+
// transcript before the runtime announced its session id to the gateway.
|
|
78
|
+
sessionsDb.createSession(
|
|
79
|
+
'provider-race',
|
|
80
|
+
'codex',
|
|
81
|
+
'/workspace/demo',
|
|
82
|
+
'Watcher Name',
|
|
83
|
+
undefined,
|
|
84
|
+
undefined,
|
|
85
|
+
'/fake/provider-race.jsonl',
|
|
86
|
+
);
|
|
87
|
+
assert.equal(sessionsDb.getAllSessions().length, 2);
|
|
88
|
+
|
|
89
|
+
sessionsDb.assignProviderSessionId('app-id-2', 'provider-race');
|
|
90
|
+
|
|
91
|
+
const rows = sessionsDb.getAllSessions();
|
|
92
|
+
assert.equal(rows.length, 1);
|
|
93
|
+
assert.equal(rows[0]?.session_id, 'app-id-2');
|
|
94
|
+
assert.equal(rows[0]?.provider_session_id, 'provider-race');
|
|
95
|
+
// Transcript path and name from the duplicate are adopted.
|
|
96
|
+
assert.equal(rows[0]?.jsonl_path, '/fake/provider-race.jsonl');
|
|
97
|
+
assert.equal(rows[0]?.custom_name, 'Watcher Name');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('legacy provider-keyed rows stay resolvable through both lookups', async () => {
|
|
102
|
+
await withIsolatedDatabase(() => {
|
|
103
|
+
sessionsDb.createSession('legacy-1', 'gemini', '/workspace/demo');
|
|
104
|
+
|
|
105
|
+
assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, 'gemini');
|
|
106
|
+
assert.equal(sessionsDb.getSessionByProviderSessionId('legacy-1')?.session_id, 'legacy-1');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -70,3 +70,15 @@ test('createSession reactivates archived rows when the session becomes active ag
|
|
|
70
70
|
assert.equal(restoredSession?.isArchived, 0);
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
test('repository reads normalize SQLite UTC timestamps to ISO strings', async () => {
|
|
75
|
+
await withIsolatedDatabase(() => {
|
|
76
|
+
sessionsDb.createAppSession('session-timezone', 'claude', '/workspace/demo-project');
|
|
77
|
+
|
|
78
|
+
const row = sessionsDb.getSessionById('session-timezone');
|
|
79
|
+
assert.ok(row?.created_at.endsWith('Z'));
|
|
80
|
+
assert.ok(row?.updated_at.endsWith('Z'));
|
|
81
|
+
assert.match(row?.created_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
82
|
+
assert.match(row?.updated_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildNotificationPayload,
|
|
3
|
+
createNotificationEvent,
|
|
4
|
+
notifyUserIfEnabled,
|
|
5
|
+
notifyRunFailed,
|
|
6
|
+
notifyRunStopped,
|
|
7
|
+
} from '@/modules/notifications/services/notification-orchestrator.service.js';
|
|
8
|
+
export {
|
|
9
|
+
registerDesktopNotificationClient,
|
|
10
|
+
sendDesktopNotification,
|
|
11
|
+
unregisterDesktopNotificationClient,
|
|
12
|
+
} from '@/modules/notifications/services/desktop-notification-clients.service.js';
|
|
13
|
+
export { handleDesktopNotificationsConnection } from '@/modules/notifications/websocket/desktop-notifications-websocket.service.js';
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
import { notificationChannelEndpointsDb, notificationPreferencesDb } from '@/modules/database/index.js';
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
function readText(value: unknown): string {
|
|
8
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sanitizeEndpoint(endpoint: any) {
|
|
12
|
+
return {
|
|
13
|
+
id: endpoint.id,
|
|
14
|
+
channel: endpoint.channel,
|
|
15
|
+
endpointId: endpoint.endpoint_id,
|
|
16
|
+
label: endpoint.label,
|
|
17
|
+
metadata: notificationChannelEndpointsDb.parseMetadata(endpoint.metadata_json),
|
|
18
|
+
enabled: Boolean(endpoint.enabled),
|
|
19
|
+
lastSeenAt: endpoint.last_seen_at,
|
|
20
|
+
createdAt: endpoint.created_at,
|
|
21
|
+
updatedAt: endpoint.updated_at,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readUserId(req: express.Request): number {
|
|
26
|
+
const userId = Number((req as any).user?.id);
|
|
27
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
28
|
+
throw new Error('Authenticated user is missing');
|
|
29
|
+
}
|
|
30
|
+
return userId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function updateChannelPreference(userId: number, channel: string): unknown {
|
|
34
|
+
const currentPrefs = notificationPreferencesDb.getPreferences(userId);
|
|
35
|
+
const hasEnabledEndpoint = notificationChannelEndpointsDb.getEnabledEndpoints(userId, channel).length > 0;
|
|
36
|
+
return notificationPreferencesDb.updatePreferences(userId, {
|
|
37
|
+
...currentPrefs,
|
|
38
|
+
channels: { ...currentPrefs.channels, [channel]: hasEnabledEndpoint },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
router.get('/endpoints', (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
const channel = readText(req.query.channel);
|
|
45
|
+
if (!channel) {
|
|
46
|
+
return res.status(400).json({ error: 'channel is required' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const userId = readUserId(req);
|
|
50
|
+
const endpoints = notificationChannelEndpointsDb
|
|
51
|
+
.getEndpoints(userId, channel)
|
|
52
|
+
.map(sanitizeEndpoint);
|
|
53
|
+
return res.json({ success: true, endpoints });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Error fetching notification endpoints:', error);
|
|
56
|
+
return res.status(500).json({ error: 'Failed to fetch notification endpoints' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
router.post('/endpoints/current', (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const { channel, endpointId, label, metadata = {}, enabled = true } = req.body || {};
|
|
63
|
+
const normalizedChannel = readText(channel);
|
|
64
|
+
const normalizedEndpointId = readText(endpointId);
|
|
65
|
+
if (!normalizedChannel || !normalizedEndpointId) {
|
|
66
|
+
return res.status(400).json({ error: 'channel and endpointId are required' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const userId = readUserId(req);
|
|
70
|
+
const endpoint = notificationChannelEndpointsDb.upsertEndpoint({
|
|
71
|
+
userId,
|
|
72
|
+
channel: normalizedChannel,
|
|
73
|
+
endpointId: normalizedEndpointId,
|
|
74
|
+
label,
|
|
75
|
+
metadata: metadata && typeof metadata === 'object' ? metadata : {},
|
|
76
|
+
enabled: enabled !== false,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const preferences = updateChannelPreference(userId, normalizedChannel);
|
|
80
|
+
return res.json({ success: true, endpoint: sanitizeEndpoint(endpoint), preferences });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Error registering notification endpoint:', error);
|
|
83
|
+
return res.status(500).json({ error: 'Failed to register notification endpoint' });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
router.patch('/endpoints/:channel/:endpointId', (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const { channel, endpointId } = req.params;
|
|
90
|
+
const { enabled } = req.body || {};
|
|
91
|
+
if (typeof enabled !== 'boolean') {
|
|
92
|
+
return res.status(400).json({ error: 'enabled must be a boolean' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const userId = readUserId(req);
|
|
96
|
+
const updated = notificationChannelEndpointsDb.setEndpointEnabled(userId, channel, endpointId, enabled);
|
|
97
|
+
if (!updated) {
|
|
98
|
+
return res.status(404).json({ error: 'Notification endpoint not found' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const endpoint = notificationChannelEndpointsDb.getEndpoint(userId, channel, endpointId);
|
|
102
|
+
const preferences = updateChannelPreference(userId, channel);
|
|
103
|
+
return res.json({ success: true, endpoint: endpoint ? sanitizeEndpoint(endpoint) : null, preferences });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Error updating notification endpoint:', error);
|
|
106
|
+
return res.status(500).json({ error: 'Failed to update notification endpoint' });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
router.delete('/endpoints/:channel/:endpointId', (req, res) => {
|
|
111
|
+
try {
|
|
112
|
+
const { channel, endpointId } = req.params;
|
|
113
|
+
const userId = readUserId(req);
|
|
114
|
+
const removed = notificationChannelEndpointsDb.removeEndpoint(userId, channel, endpointId);
|
|
115
|
+
if (!removed) {
|
|
116
|
+
return res.status(404).json({ error: 'Notification endpoint not found' });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const preferences = updateChannelPreference(userId, channel);
|
|
120
|
+
return res.json({ success: true, preferences });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Error removing notification endpoint:', error);
|
|
123
|
+
return res.status(500).json({ error: 'Failed to remove notification endpoint' });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export default router;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { WebSocket } from 'ws';
|
|
2
|
+
|
|
3
|
+
import { notificationChannelEndpointsDb } from '@/modules/database/index.js';
|
|
4
|
+
|
|
5
|
+
const DESKTOP_CHANNEL = 'desktop';
|
|
6
|
+
|
|
7
|
+
const clientsByUserId = new Map<number, Map<string, WebSocket>>();
|
|
8
|
+
const clientBySocket = new WeakMap<WebSocket, { userId: number; endpointId: string }>();
|
|
9
|
+
|
|
10
|
+
function normalizeUserId(userId: unknown): number | null {
|
|
11
|
+
const numeric = Number(userId);
|
|
12
|
+
return Number.isInteger(numeric) && numeric > 0 ? numeric : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeEndpointId(endpointId: unknown): string {
|
|
16
|
+
if (typeof endpointId !== 'string') return '';
|
|
17
|
+
return endpointId.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getUserClients(userId: unknown, create = false): Map<string, WebSocket> | null {
|
|
21
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
22
|
+
if (!normalizedUserId) return null;
|
|
23
|
+
let clients = clientsByUserId.get(normalizedUserId);
|
|
24
|
+
if (!clients && create) {
|
|
25
|
+
clients = new Map();
|
|
26
|
+
clientsByUserId.set(normalizedUserId, clients);
|
|
27
|
+
}
|
|
28
|
+
return clients || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function registerDesktopNotificationClient({
|
|
32
|
+
userId,
|
|
33
|
+
deviceId,
|
|
34
|
+
label = null,
|
|
35
|
+
platform = null,
|
|
36
|
+
appVersion = null,
|
|
37
|
+
ws,
|
|
38
|
+
}: {
|
|
39
|
+
userId: number;
|
|
40
|
+
deviceId: string;
|
|
41
|
+
label?: string | null;
|
|
42
|
+
platform?: string | null;
|
|
43
|
+
appVersion?: string | null;
|
|
44
|
+
ws: WebSocket;
|
|
45
|
+
}) {
|
|
46
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
47
|
+
const endpointId = normalizeEndpointId(deviceId);
|
|
48
|
+
if (!normalizedUserId || !endpointId) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const endpoint = notificationChannelEndpointsDb.upsertEndpoint({
|
|
53
|
+
userId: normalizedUserId,
|
|
54
|
+
channel: DESKTOP_CHANNEL,
|
|
55
|
+
endpointId,
|
|
56
|
+
label,
|
|
57
|
+
metadata: { platform, appVersion },
|
|
58
|
+
enabled: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const clients = getUserClients(normalizedUserId, true)!;
|
|
62
|
+
const previous = clients.get(endpointId);
|
|
63
|
+
if (previous && previous !== ws && previous.readyState === previous.OPEN) {
|
|
64
|
+
previous.close(4000, 'Device reconnected');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
clients.set(endpointId, ws);
|
|
68
|
+
clientBySocket.set(ws, { userId: normalizedUserId, endpointId });
|
|
69
|
+
return endpoint;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function unregisterDesktopNotificationClient(ws: WebSocket): void {
|
|
73
|
+
const registration = clientBySocket.get(ws);
|
|
74
|
+
if (!registration) return;
|
|
75
|
+
|
|
76
|
+
const clients = getUserClients(registration.userId);
|
|
77
|
+
if (clients?.get(registration.endpointId) === ws) {
|
|
78
|
+
clients.delete(registration.endpointId);
|
|
79
|
+
if (clients.size === 0) {
|
|
80
|
+
clientsByUserId.delete(registration.userId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
clientBySocket.delete(ws);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function sendDesktopNotification(userId: unknown, payload: unknown): { attempted: number; sent: number } {
|
|
87
|
+
const normalizedUserId = normalizeUserId(userId);
|
|
88
|
+
if (!normalizedUserId) return { attempted: 0, sent: 0 };
|
|
89
|
+
|
|
90
|
+
const clients = getUserClients(normalizedUserId);
|
|
91
|
+
if (!clients?.size) return { attempted: 0, sent: 0 };
|
|
92
|
+
|
|
93
|
+
const enabledEndpointIds = new Set(
|
|
94
|
+
notificationChannelEndpointsDb
|
|
95
|
+
.getEnabledEndpoints(normalizedUserId, DESKTOP_CHANNEL)
|
|
96
|
+
.map((endpoint) => endpoint.endpoint_id)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const message = JSON.stringify({
|
|
100
|
+
type: 'notification',
|
|
101
|
+
id: typeof (payload as any)?.data?.tag === 'string' ? (payload as any).data.tag : `${Date.now()}`,
|
|
102
|
+
payload,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let attempted = 0;
|
|
106
|
+
let sent = 0;
|
|
107
|
+
for (const [endpointId, ws] of clients.entries()) {
|
|
108
|
+
if (!enabledEndpointIds.has(endpointId)) continue;
|
|
109
|
+
attempted += 1;
|
|
110
|
+
if (ws.readyState !== ws.OPEN) {
|
|
111
|
+
unregisterDesktopNotificationClient(ws);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
ws.send(message);
|
|
116
|
+
notificationChannelEndpointsDb.touchEndpoint(normalizedUserId, DESKTOP_CHANNEL, endpointId);
|
|
117
|
+
sent += 1;
|
|
118
|
+
} catch {
|
|
119
|
+
unregisterDesktopNotificationClient(ws);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { attempted, sent };
|
|
124
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import webPush from 'web-push';
|
|
2
|
+
|
|
3
|
+
import { notificationPreferencesDb, pushSubscriptionsDb, sessionsDb } from '@/modules/database/index.js';
|
|
4
|
+
import { sendDesktopNotification as sendDesktopNotificationToClients } from '@/modules/notifications/services/desktop-notification-clients.service.js';
|
|
5
|
+
|
|
6
|
+
const KIND_TO_PREF_KEY = {
|
|
7
|
+
action_required: 'actionRequired',
|
|
8
|
+
stop: 'stop',
|
|
9
|
+
error: 'error'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const PROVIDER_LABELS = {
|
|
13
|
+
claude: 'Claude',
|
|
14
|
+
cursor: 'Cursor',
|
|
15
|
+
codex: 'Codex',
|
|
16
|
+
gemini: 'Gemini',
|
|
17
|
+
system: 'System'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const recentEventKeys = new Map();
|
|
21
|
+
const DEDUPE_WINDOW_MS = 20000;
|
|
22
|
+
|
|
23
|
+
const cleanupOldEventKeys = () => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const [key, timestamp] of recentEventKeys.entries()) {
|
|
26
|
+
if (now - timestamp > DEDUPE_WINDOW_MS) {
|
|
27
|
+
recentEventKeys.delete(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function isNotificationEventEnabled(preferences, event) {
|
|
33
|
+
const prefEventKey = KIND_TO_PREF_KEY[event.kind];
|
|
34
|
+
const eventEnabled = prefEventKey ? Boolean(preferences?.events?.[prefEventKey]) : true;
|
|
35
|
+
|
|
36
|
+
return eventEnabled;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isDuplicate(event) {
|
|
40
|
+
cleanupOldEventKeys();
|
|
41
|
+
const key = event.dedupeKey || `${event.provider}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
|
|
42
|
+
if (recentEventKeys.has(key)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
recentEventKeys.set(key, Date.now());
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createNotificationEvent({
|
|
50
|
+
provider,
|
|
51
|
+
sessionId = null,
|
|
52
|
+
kind = 'info',
|
|
53
|
+
code = 'generic.info',
|
|
54
|
+
meta = {},
|
|
55
|
+
severity = 'info',
|
|
56
|
+
dedupeKey = null,
|
|
57
|
+
requiresUserAction = false
|
|
58
|
+
}) {
|
|
59
|
+
return {
|
|
60
|
+
provider,
|
|
61
|
+
sessionId,
|
|
62
|
+
kind,
|
|
63
|
+
code,
|
|
64
|
+
meta,
|
|
65
|
+
severity,
|
|
66
|
+
requiresUserAction,
|
|
67
|
+
dedupeKey,
|
|
68
|
+
createdAt: new Date().toISOString()
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeErrorMessage(error) {
|
|
73
|
+
if (typeof error === 'string') {
|
|
74
|
+
return error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (error && typeof error.message === 'string') {
|
|
78
|
+
return error.message;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error == null) {
|
|
82
|
+
return 'Unknown error';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return String(error);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeSessionName(sessionName) {
|
|
89
|
+
if (typeof sessionName !== 'string') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const normalized = sessionName.replace(/\s+/g, ' ').trim();
|
|
94
|
+
if (!normalized) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return normalized.length > 80 ? `${normalized.slice(0, 77)}...` : normalized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rowMatchesProvider(row, provider) {
|
|
102
|
+
return row && (!provider || row.provider === provider);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveSessionRow(sessionId, provider) {
|
|
106
|
+
if (!sessionId) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const appSessionRow = sessionsDb.getSessionById(sessionId);
|
|
111
|
+
if (rowMatchesProvider(appSessionRow, provider)) {
|
|
112
|
+
return appSessionRow;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const providerSessionRow = sessionsDb.getSessionByProviderSessionId(sessionId);
|
|
116
|
+
if (rowMatchesProvider(providerSessionRow, provider)) {
|
|
117
|
+
return providerSessionRow;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeNotificationSession(event) {
|
|
124
|
+
if (!event?.sessionId || !event.provider || event.provider === 'system') {
|
|
125
|
+
return event;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const row = resolveSessionRow(event.sessionId, event.provider);
|
|
129
|
+
if (!row || row.session_id === event.sessionId) {
|
|
130
|
+
return event;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
...event,
|
|
135
|
+
sessionId: row.session_id
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveSessionName(event) {
|
|
140
|
+
const explicitSessionName = normalizeSessionName(event.meta?.sessionName);
|
|
141
|
+
if (explicitSessionName) {
|
|
142
|
+
return explicitSessionName;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!event.sessionId || !event.provider) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return normalizeSessionName(sessionsDb.getSessionName(event.sessionId, event.provider));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildNotificationPayload(event) {
|
|
153
|
+
const normalizedEvent = normalizeNotificationSession(event);
|
|
154
|
+
const CODE_MAP = {
|
|
155
|
+
'permission.required': normalizedEvent.meta?.toolName
|
|
156
|
+
? `Action Required: Tool "${normalizedEvent.meta.toolName}" needs approval`
|
|
157
|
+
: 'Action Required: A tool needs your approval',
|
|
158
|
+
'run.stopped': normalizedEvent.meta?.stopReason || 'Run Stopped: The run has stopped',
|
|
159
|
+
'run.failed': normalizedEvent.meta?.error ? `Run Failed: ${normalizedEvent.meta.error}` : 'Run Failed: The run encountered an error',
|
|
160
|
+
'agent.notification': normalizedEvent.meta?.message ? String(normalizedEvent.meta.message) : 'You have a new notification',
|
|
161
|
+
'push.enabled': 'Push notifications are now enabled!'
|
|
162
|
+
};
|
|
163
|
+
const providerLabel = PROVIDER_LABELS[normalizedEvent.provider] || 'Assistant';
|
|
164
|
+
const sessionName = resolveSessionName(normalizedEvent);
|
|
165
|
+
const message = CODE_MAP[normalizedEvent.code] || 'You have a new notification';
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
title: sessionName || 'CloudCLI',
|
|
169
|
+
body: `${providerLabel}: ${message}`,
|
|
170
|
+
data: {
|
|
171
|
+
sessionId: normalizedEvent.sessionId || null,
|
|
172
|
+
code: normalizedEvent.code,
|
|
173
|
+
provider: normalizedEvent.provider || null,
|
|
174
|
+
sessionName,
|
|
175
|
+
tag: `${normalizedEvent.provider || 'assistant'}:${normalizedEvent.sessionId || 'none'}:${normalizedEvent.code}`
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sendWebPushPayload(userId, payload) {
|
|
181
|
+
const subscriptions = pushSubscriptionsDb.getSubscriptions(userId);
|
|
182
|
+
if (!subscriptions.length) return Promise.resolve();
|
|
183
|
+
|
|
184
|
+
const serializedPayload = JSON.stringify(payload);
|
|
185
|
+
return Promise.allSettled(
|
|
186
|
+
subscriptions.map((sub) =>
|
|
187
|
+
webPush.sendNotification(
|
|
188
|
+
{
|
|
189
|
+
endpoint: sub.endpoint,
|
|
190
|
+
keys: {
|
|
191
|
+
p256dh: sub.keys_p256dh,
|
|
192
|
+
auth: sub.keys_auth
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
serializedPayload
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
).then((results) => {
|
|
199
|
+
results.forEach((result, index) => {
|
|
200
|
+
if (result.status === 'rejected') {
|
|
201
|
+
const statusCode = result.reason?.statusCode;
|
|
202
|
+
if (statusCode === 410 || statusCode === 404) {
|
|
203
|
+
pushSubscriptionsDb.removeSubscription(subscriptions[index].endpoint);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const notificationChannels = [
|
|
211
|
+
{
|
|
212
|
+
id: 'webPush',
|
|
213
|
+
// TODO: Web push still uses push_subscriptions. Do not remove that table until
|
|
214
|
+
// browser push subscriptions are migrated into notification_channel_endpoints.
|
|
215
|
+
isEnabled: (preferences) => Boolean(preferences?.channels?.webPush),
|
|
216
|
+
send: ({ userId, payload }) => sendWebPushPayload(userId, payload)
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: 'desktop',
|
|
220
|
+
isEnabled: (preferences) => Boolean(preferences?.channels?.desktop),
|
|
221
|
+
send: ({ userId, payload }) => sendDesktopNotificationToClients(userId, payload)
|
|
222
|
+
}
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
function notifyUserIfEnabled({ userId, event }) {
|
|
226
|
+
if (!userId || !event) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const normalizedEvent = normalizeNotificationSession(event);
|
|
231
|
+
const preferences = notificationPreferencesDb.getPreferences(userId);
|
|
232
|
+
if (!isNotificationEventEnabled(preferences, normalizedEvent)) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (isDuplicate(normalizedEvent)) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const payload = buildNotificationPayload(normalizedEvent);
|
|
240
|
+
for (const channel of notificationChannels) {
|
|
241
|
+
if (!channel.isEnabled(preferences)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
Promise.resolve(channel.send({ userId, event: normalizedEvent, payload })).catch((err) => {
|
|
245
|
+
console.error(`Notification channel "${channel.id}" send error:`, err);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
|
|
251
|
+
notifyUserIfEnabled({
|
|
252
|
+
userId,
|
|
253
|
+
event: createNotificationEvent({
|
|
254
|
+
provider,
|
|
255
|
+
sessionId,
|
|
256
|
+
kind: 'stop',
|
|
257
|
+
code: 'run.stopped',
|
|
258
|
+
meta: { stopReason, sessionName },
|
|
259
|
+
severity: 'info',
|
|
260
|
+
dedupeKey: `${provider}:run:stop:${sessionId || 'none'}:${stopReason}`
|
|
261
|
+
})
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function notifyRunFailed({ userId, provider, sessionId = null, error, sessionName = null }) {
|
|
266
|
+
const errorMessage = normalizeErrorMessage(error);
|
|
267
|
+
|
|
268
|
+
notifyUserIfEnabled({
|
|
269
|
+
userId,
|
|
270
|
+
event: createNotificationEvent({
|
|
271
|
+
provider,
|
|
272
|
+
sessionId,
|
|
273
|
+
kind: 'error',
|
|
274
|
+
code: 'run.failed',
|
|
275
|
+
meta: { error: errorMessage, sessionName },
|
|
276
|
+
severity: 'error',
|
|
277
|
+
dedupeKey: `${provider}:run:error:${sessionId || 'none'}:${errorMessage}`
|
|
278
|
+
})
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export {
|
|
283
|
+
buildNotificationPayload,
|
|
284
|
+
createNotificationEvent,
|
|
285
|
+
notifyUserIfEnabled,
|
|
286
|
+
notifyRunStopped,
|
|
287
|
+
notifyRunFailed
|
|
288
|
+
};
|