cowork-os 0.3.21
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/LICENSE +21 -0
- package/README.md +1638 -0
- package/bin/cowork.js +42 -0
- package/build/entitlements.mac.plist +16 -0
- package/build/icon.icns +0 -0
- package/build/icon.png +0 -0
- package/dist/electron/electron/activity/ActivityRepository.js +190 -0
- package/dist/electron/electron/agent/browser/browser-service.js +639 -0
- package/dist/electron/electron/agent/context-manager.js +225 -0
- package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
- package/dist/electron/electron/agent/daemon.js +975 -0
- package/dist/electron/electron/agent/executor.js +3561 -0
- package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
- package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
- package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
- package/dist/electron/electron/agent/llm/index.js +34 -0
- package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
- package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
- package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
- package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
- package/dist/electron/electron/agent/llm/pricing.js +160 -0
- package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
- package/dist/electron/electron/agent/llm/types.js +178 -0
- package/dist/electron/electron/agent/queue-manager.js +378 -0
- package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
- package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
- package/dist/electron/electron/agent/sandbox/runner.js +410 -0
- package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
- package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
- package/dist/electron/electron/agent/search/brave-provider.js +119 -0
- package/dist/electron/electron/agent/search/google-provider.js +100 -0
- package/dist/electron/electron/agent/search/index.js +28 -0
- package/dist/electron/electron/agent/search/provider-factory.js +395 -0
- package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
- package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
- package/dist/electron/electron/agent/search/types.js +40 -0
- package/dist/electron/electron/agent/security/index.js +12 -0
- package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
- package/dist/electron/electron/agent/security/output-filter.js +217 -0
- package/dist/electron/electron/agent/skill-eligibility.js +281 -0
- package/dist/electron/electron/agent/skill-registry.js +396 -0
- package/dist/electron/electron/agent/skills/document.js +878 -0
- package/dist/electron/electron/agent/skills/image-generator.js +225 -0
- package/dist/electron/electron/agent/skills/organizer.js +141 -0
- package/dist/electron/electron/agent/skills/presentation.js +367 -0
- package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
- package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
- package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
- package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
- package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
- package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
- package/dist/electron/electron/agent/tools/file-tools.js +719 -0
- package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
- package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
- package/dist/electron/electron/agent/tools/image-tools.js +111 -0
- package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
- package/dist/electron/electron/agent/tools/node-tools.js +476 -0
- package/dist/electron/electron/agent/tools/registry.js +2719 -0
- package/dist/electron/electron/agent/tools/search-tools.js +91 -0
- package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
- package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
- package/dist/electron/electron/agent/tools/system-tools.js +578 -0
- package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
- package/dist/electron/electron/agent/tools/x-tools.js +264 -0
- package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
- package/dist/electron/electron/agents/HeartbeatService.js +356 -0
- package/dist/electron/electron/agents/MentionRepository.js +197 -0
- package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
- package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
- package/dist/electron/electron/canvas/canvas-manager.js +714 -0
- package/dist/electron/electron/canvas/canvas-preload.js +53 -0
- package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
- package/dist/electron/electron/canvas/canvas-store.js +174 -0
- package/dist/electron/electron/canvas/index.js +13 -0
- package/dist/electron/electron/control-plane/client.js +364 -0
- package/dist/electron/electron/control-plane/handlers.js +572 -0
- package/dist/electron/electron/control-plane/index.js +41 -0
- package/dist/electron/electron/control-plane/node-manager.js +264 -0
- package/dist/electron/electron/control-plane/protocol.js +194 -0
- package/dist/electron/electron/control-plane/remote-client.js +437 -0
- package/dist/electron/electron/control-plane/server.js +640 -0
- package/dist/electron/electron/control-plane/settings.js +369 -0
- package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
- package/dist/electron/electron/cron/index.js +30 -0
- package/dist/electron/electron/cron/schedule.js +190 -0
- package/dist/electron/electron/cron/service.js +614 -0
- package/dist/electron/electron/cron/store.js +155 -0
- package/dist/electron/electron/cron/types.js +82 -0
- package/dist/electron/electron/cron/webhook.js +258 -0
- package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
- package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
- package/dist/electron/electron/database/repositories.js +1781 -0
- package/dist/electron/electron/database/schema.js +978 -0
- package/dist/electron/electron/extensions/index.js +33 -0
- package/dist/electron/electron/extensions/loader.js +313 -0
- package/dist/electron/electron/extensions/registry.js +485 -0
- package/dist/electron/electron/extensions/types.js +11 -0
- package/dist/electron/electron/gateway/channel-registry.js +1102 -0
- package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
- package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
- package/dist/electron/electron/gateway/channels/discord.js +975 -0
- package/dist/electron/electron/gateway/channels/email-client.js +593 -0
- package/dist/electron/electron/gateway/channels/email.js +443 -0
- package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
- package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
- package/dist/electron/electron/gateway/channels/imessage.js +465 -0
- package/dist/electron/electron/gateway/channels/index.js +36 -0
- package/dist/electron/electron/gateway/channels/line-client.js +470 -0
- package/dist/electron/electron/gateway/channels/line.js +479 -0
- package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
- package/dist/electron/electron/gateway/channels/matrix.js +592 -0
- package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
- package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
- package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
- package/dist/electron/electron/gateway/channels/signal.js +582 -0
- package/dist/electron/electron/gateway/channels/slack.js +415 -0
- package/dist/electron/electron/gateway/channels/teams.js +596 -0
- package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
- package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
- package/dist/electron/electron/gateway/channels/twitch.js +396 -0
- package/dist/electron/electron/gateway/channels/types.js +8 -0
- package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
- package/dist/electron/electron/gateway/context-policy.js +268 -0
- package/dist/electron/electron/gateway/index.js +1063 -0
- package/dist/electron/electron/gateway/infrastructure.js +496 -0
- package/dist/electron/electron/gateway/router.js +2700 -0
- package/dist/electron/electron/gateway/security.js +375 -0
- package/dist/electron/electron/gateway/session.js +115 -0
- package/dist/electron/electron/gateway/tunnel.js +503 -0
- package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
- package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
- package/dist/electron/electron/hooks/index.js +46 -0
- package/dist/electron/electron/hooks/mappings.js +381 -0
- package/dist/electron/electron/hooks/server.js +480 -0
- package/dist/electron/electron/hooks/settings.js +447 -0
- package/dist/electron/electron/hooks/types.js +41 -0
- package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
- package/dist/electron/electron/ipc/handlers.js +3138 -0
- package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
- package/dist/electron/electron/main.js +448 -0
- package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
- package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
- package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
- package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
- package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
- package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
- package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
- package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
- package/dist/electron/electron/mcp/settings.js +446 -0
- package/dist/electron/electron/mcp/types.js +59 -0
- package/dist/electron/electron/memory/MemoryService.js +435 -0
- package/dist/electron/electron/notifications/index.js +17 -0
- package/dist/electron/electron/notifications/service.js +118 -0
- package/dist/electron/electron/notifications/store.js +144 -0
- package/dist/electron/electron/preload.js +842 -0
- package/dist/electron/electron/reports/StandupReportService.js +272 -0
- package/dist/electron/electron/security/concurrency.js +293 -0
- package/dist/electron/electron/security/index.js +15 -0
- package/dist/electron/electron/security/policy-manager.js +435 -0
- package/dist/electron/electron/settings/appearance-manager.js +193 -0
- package/dist/electron/electron/settings/personality-manager.js +724 -0
- package/dist/electron/electron/settings/x-manager.js +58 -0
- package/dist/electron/electron/tailscale/exposure.js +188 -0
- package/dist/electron/electron/tailscale/index.js +28 -0
- package/dist/electron/electron/tailscale/settings.js +205 -0
- package/dist/electron/electron/tailscale/tailscale.js +355 -0
- package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
- package/dist/electron/electron/tray/TrayManager.js +895 -0
- package/dist/electron/electron/tray/index.js +9 -0
- package/dist/electron/electron/updater/index.js +6 -0
- package/dist/electron/electron/updater/update-manager.js +418 -0
- package/dist/electron/electron/utils/env-migration.js +209 -0
- package/dist/electron/electron/utils/process.js +102 -0
- package/dist/electron/electron/utils/rate-limiter.js +104 -0
- package/dist/electron/electron/utils/validation.js +419 -0
- package/dist/electron/electron/utils/x-cli.js +177 -0
- package/dist/electron/electron/voice/VoiceService.js +507 -0
- package/dist/electron/electron/voice/index.js +14 -0
- package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
- package/dist/electron/shared/channelMessages.js +170 -0
- package/dist/electron/shared/types.js +1185 -0
- package/package.json +159 -0
- package/resources/skills/1password.json +10 -0
- package/resources/skills/add-documentation.json +31 -0
- package/resources/skills/analyze-csv.json +17 -0
- package/resources/skills/apple-notes.json +10 -0
- package/resources/skills/apple-reminders.json +10 -0
- package/resources/skills/auto-commenter.json +10 -0
- package/resources/skills/bear-notes.json +10 -0
- package/resources/skills/bird.json +35 -0
- package/resources/skills/blogwatcher.json +10 -0
- package/resources/skills/blucli.json +10 -0
- package/resources/skills/bluebubbles.json +10 -0
- package/resources/skills/camsnap.json +10 -0
- package/resources/skills/clean-imports.json +18 -0
- package/resources/skills/code-review.json +18 -0
- package/resources/skills/coding-agent.json +10 -0
- package/resources/skills/compare-files.json +23 -0
- package/resources/skills/convert-code.json +34 -0
- package/resources/skills/create-changelog.json +24 -0
- package/resources/skills/debug-error.json +17 -0
- package/resources/skills/dependency-check.json +10 -0
- package/resources/skills/discord.json +10 -0
- package/resources/skills/eightctl.json +10 -0
- package/resources/skills/explain-code.json +29 -0
- package/resources/skills/extract-todos.json +18 -0
- package/resources/skills/food-order.json +10 -0
- package/resources/skills/gemini.json +10 -0
- package/resources/skills/generate-readme.json +10 -0
- package/resources/skills/gifgrep.json +10 -0
- package/resources/skills/git-commit.json +10 -0
- package/resources/skills/github.json +10 -0
- package/resources/skills/gog.json +10 -0
- package/resources/skills/goplaces.json +10 -0
- package/resources/skills/himalaya.json +10 -0
- package/resources/skills/imsg.json +10 -0
- package/resources/skills/karpathy-guidelines.json +12 -0
- package/resources/skills/last30days.json +26 -0
- package/resources/skills/local-places.json +10 -0
- package/resources/skills/mcporter.json +10 -0
- package/resources/skills/model-usage.json +10 -0
- package/resources/skills/nano-banana-pro.json +10 -0
- package/resources/skills/nano-pdf.json +10 -0
- package/resources/skills/notion.json +10 -0
- package/resources/skills/obsidian.json +10 -0
- package/resources/skills/openai-image-gen.json +10 -0
- package/resources/skills/openai-whisper-api.json +10 -0
- package/resources/skills/openai-whisper.json +10 -0
- package/resources/skills/openhue.json +10 -0
- package/resources/skills/oracle.json +10 -0
- package/resources/skills/ordercli.json +10 -0
- package/resources/skills/peekaboo.json +10 -0
- package/resources/skills/project-structure.json +10 -0
- package/resources/skills/proofread.json +17 -0
- package/resources/skills/refactor-code.json +31 -0
- package/resources/skills/rename-symbol.json +23 -0
- package/resources/skills/sag.json +10 -0
- package/resources/skills/security-audit.json +18 -0
- package/resources/skills/session-logs.json +10 -0
- package/resources/skills/sherpa-onnx-tts.json +10 -0
- package/resources/skills/skill-creator.json +15 -0
- package/resources/skills/skill-hub.json +29 -0
- package/resources/skills/slack.json +10 -0
- package/resources/skills/songsee.json +10 -0
- package/resources/skills/sonoscli.json +10 -0
- package/resources/skills/spotify-player.json +10 -0
- package/resources/skills/startup-cfo.json +55 -0
- package/resources/skills/summarize-folder.json +18 -0
- package/resources/skills/summarize.json +10 -0
- package/resources/skills/things-mac.json +10 -0
- package/resources/skills/tmux.json +10 -0
- package/resources/skills/translate.json +36 -0
- package/resources/skills/trello.json +10 -0
- package/resources/skills/video-frames.json +10 -0
- package/resources/skills/voice-call.json +10 -0
- package/resources/skills/wacli.json +10 -0
- package/resources/skills/weather.json +10 -0
- package/resources/skills/write-tests.json +31 -0
- package/src/electron/activity/ActivityRepository.ts +238 -0
- package/src/electron/agent/browser/browser-service.ts +721 -0
- package/src/electron/agent/context-manager.ts +257 -0
- package/src/electron/agent/custom-skill-loader.ts +634 -0
- package/src/electron/agent/daemon.ts +1097 -0
- package/src/electron/agent/executor.ts +4017 -0
- package/src/electron/agent/llm/anthropic-provider.ts +175 -0
- package/src/electron/agent/llm/bedrock-provider.ts +236 -0
- package/src/electron/agent/llm/gemini-provider.ts +422 -0
- package/src/electron/agent/llm/index.ts +9 -0
- package/src/electron/agent/llm/ollama-provider.ts +347 -0
- package/src/electron/agent/llm/openai-oauth.ts +127 -0
- package/src/electron/agent/llm/openai-provider.ts +686 -0
- package/src/electron/agent/llm/openrouter-provider.ts +273 -0
- package/src/electron/agent/llm/pricing.ts +180 -0
- package/src/electron/agent/llm/provider-factory.ts +971 -0
- package/src/electron/agent/llm/types.ts +291 -0
- package/src/electron/agent/queue-manager.ts +408 -0
- package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
- package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
- package/src/electron/agent/sandbox/runner.ts +453 -0
- package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
- package/src/electron/agent/sandbox/security-utils.ts +251 -0
- package/src/electron/agent/search/brave-provider.ts +141 -0
- package/src/electron/agent/search/google-provider.ts +131 -0
- package/src/electron/agent/search/index.ts +6 -0
- package/src/electron/agent/search/provider-factory.ts +450 -0
- package/src/electron/agent/search/serpapi-provider.ts +138 -0
- package/src/electron/agent/search/tavily-provider.ts +108 -0
- package/src/electron/agent/search/types.ts +118 -0
- package/src/electron/agent/security/index.ts +20 -0
- package/src/electron/agent/security/input-sanitizer.ts +380 -0
- package/src/electron/agent/security/output-filter.ts +259 -0
- package/src/electron/agent/skill-eligibility.ts +334 -0
- package/src/electron/agent/skill-registry.ts +457 -0
- package/src/electron/agent/skills/document.ts +1070 -0
- package/src/electron/agent/skills/image-generator.ts +272 -0
- package/src/electron/agent/skills/organizer.ts +131 -0
- package/src/electron/agent/skills/presentation.ts +418 -0
- package/src/electron/agent/skills/spreadsheet.ts +166 -0
- package/src/electron/agent/tools/browser-tools.ts +546 -0
- package/src/electron/agent/tools/builtin-settings.ts +422 -0
- package/src/electron/agent/tools/canvas-tools.ts +572 -0
- package/src/electron/agent/tools/cron-tools.ts +723 -0
- package/src/electron/agent/tools/edit-tools.ts +196 -0
- package/src/electron/agent/tools/file-tools.ts +811 -0
- package/src/electron/agent/tools/glob-tools.ts +303 -0
- package/src/electron/agent/tools/grep-tools.ts +432 -0
- package/src/electron/agent/tools/image-tools.ts +126 -0
- package/src/electron/agent/tools/mention-tools.ts +371 -0
- package/src/electron/agent/tools/node-tools.ts +550 -0
- package/src/electron/agent/tools/registry.ts +3052 -0
- package/src/electron/agent/tools/search-tools.ts +111 -0
- package/src/electron/agent/tools/shell-tools.ts +651 -0
- package/src/electron/agent/tools/skill-tools.ts +340 -0
- package/src/electron/agent/tools/system-tools.ts +665 -0
- package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
- package/src/electron/agent/tools/x-tools.ts +267 -0
- package/src/electron/agents/AgentRoleRepository.ts +557 -0
- package/src/electron/agents/HeartbeatService.ts +469 -0
- package/src/electron/agents/MentionRepository.ts +242 -0
- package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
- package/src/electron/agents/WorkingStateRepository.ts +278 -0
- package/src/electron/canvas/canvas-manager.ts +818 -0
- package/src/electron/canvas/canvas-preload.ts +102 -0
- package/src/electron/canvas/canvas-protocol.ts +174 -0
- package/src/electron/canvas/canvas-store.ts +200 -0
- package/src/electron/canvas/index.ts +8 -0
- package/src/electron/control-plane/client.ts +527 -0
- package/src/electron/control-plane/handlers.ts +723 -0
- package/src/electron/control-plane/index.ts +51 -0
- package/src/electron/control-plane/node-manager.ts +322 -0
- package/src/electron/control-plane/protocol.ts +269 -0
- package/src/electron/control-plane/remote-client.ts +517 -0
- package/src/electron/control-plane/server.ts +853 -0
- package/src/electron/control-plane/settings.ts +401 -0
- package/src/electron/control-plane/ssh-tunnel.ts +624 -0
- package/src/electron/cron/index.ts +9 -0
- package/src/electron/cron/schedule.ts +217 -0
- package/src/electron/cron/service.ts +743 -0
- package/src/electron/cron/store.ts +165 -0
- package/src/electron/cron/types.ts +291 -0
- package/src/electron/cron/webhook.ts +303 -0
- package/src/electron/database/SecureSettingsRepository.ts +514 -0
- package/src/electron/database/TaskLabelRepository.ts +148 -0
- package/src/electron/database/repositories.ts +2397 -0
- package/src/electron/database/schema.ts +1017 -0
- package/src/electron/extensions/index.ts +18 -0
- package/src/electron/extensions/loader.ts +336 -0
- package/src/electron/extensions/registry.ts +546 -0
- package/src/electron/extensions/types.ts +372 -0
- package/src/electron/gateway/channel-registry.ts +1267 -0
- package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
- package/src/electron/gateway/channels/bluebubbles.ts +509 -0
- package/src/electron/gateway/channels/discord.ts +1150 -0
- package/src/electron/gateway/channels/email-client.ts +708 -0
- package/src/electron/gateway/channels/email.ts +516 -0
- package/src/electron/gateway/channels/google-chat.ts +760 -0
- package/src/electron/gateway/channels/imessage-client.ts +473 -0
- package/src/electron/gateway/channels/imessage.ts +520 -0
- package/src/electron/gateway/channels/index.ts +21 -0
- package/src/electron/gateway/channels/line-client.ts +598 -0
- package/src/electron/gateway/channels/line.ts +559 -0
- package/src/electron/gateway/channels/matrix-client.ts +632 -0
- package/src/electron/gateway/channels/matrix.ts +655 -0
- package/src/electron/gateway/channels/mattermost-client.ts +526 -0
- package/src/electron/gateway/channels/mattermost.ts +550 -0
- package/src/electron/gateway/channels/signal-client.ts +722 -0
- package/src/electron/gateway/channels/signal.ts +666 -0
- package/src/electron/gateway/channels/slack.ts +458 -0
- package/src/electron/gateway/channels/teams.ts +681 -0
- package/src/electron/gateway/channels/telegram.ts +1727 -0
- package/src/electron/gateway/channels/twitch-client.ts +665 -0
- package/src/electron/gateway/channels/twitch.ts +468 -0
- package/src/electron/gateway/channels/types.ts +1002 -0
- package/src/electron/gateway/channels/whatsapp.ts +1101 -0
- package/src/electron/gateway/context-policy.ts +382 -0
- package/src/electron/gateway/index.ts +1274 -0
- package/src/electron/gateway/infrastructure.ts +645 -0
- package/src/electron/gateway/router.ts +3206 -0
- package/src/electron/gateway/security.ts +422 -0
- package/src/electron/gateway/session.ts +144 -0
- package/src/electron/gateway/tunnel.ts +626 -0
- package/src/electron/guardrails/guardrail-manager.ts +380 -0
- package/src/electron/hooks/gmail-watcher.ts +355 -0
- package/src/electron/hooks/index.ts +30 -0
- package/src/electron/hooks/mappings.ts +404 -0
- package/src/electron/hooks/server.ts +574 -0
- package/src/electron/hooks/settings.ts +466 -0
- package/src/electron/hooks/types.ts +245 -0
- package/src/electron/ipc/canvas-handlers.ts +223 -0
- package/src/electron/ipc/handlers.ts +3661 -0
- package/src/electron/ipc/mission-control-handlers.ts +182 -0
- package/src/electron/main.ts +496 -0
- package/src/electron/mcp/client/MCPClientManager.ts +406 -0
- package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
- package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
- package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
- package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
- package/src/electron/mcp/host/MCPHostServer.ts +388 -0
- package/src/electron/mcp/host/ToolAdapter.ts +140 -0
- package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
- package/src/electron/mcp/settings.ts +468 -0
- package/src/electron/mcp/types.ts +371 -0
- package/src/electron/memory/MemoryService.ts +523 -0
- package/src/electron/notifications/index.ts +16 -0
- package/src/electron/notifications/service.ts +161 -0
- package/src/electron/notifications/store.ts +163 -0
- package/src/electron/preload.ts +2845 -0
- package/src/electron/reports/StandupReportService.ts +356 -0
- package/src/electron/security/concurrency.ts +333 -0
- package/src/electron/security/index.ts +17 -0
- package/src/electron/security/policy-manager.ts +539 -0
- package/src/electron/settings/appearance-manager.ts +182 -0
- package/src/electron/settings/personality-manager.ts +800 -0
- package/src/electron/settings/x-manager.ts +62 -0
- package/src/electron/tailscale/exposure.ts +262 -0
- package/src/electron/tailscale/index.ts +34 -0
- package/src/electron/tailscale/settings.ts +218 -0
- package/src/electron/tailscale/tailscale.ts +379 -0
- package/src/electron/tray/QuickInputWindow.ts +609 -0
- package/src/electron/tray/TrayManager.ts +1005 -0
- package/src/electron/tray/index.ts +6 -0
- package/src/electron/updater/index.ts +1 -0
- package/src/electron/updater/update-manager.ts +447 -0
- package/src/electron/utils/env-migration.ts +203 -0
- package/src/electron/utils/process.ts +124 -0
- package/src/electron/utils/rate-limiter.ts +130 -0
- package/src/electron/utils/validation.ts +493 -0
- package/src/electron/utils/x-cli.ts +198 -0
- package/src/electron/voice/VoiceService.ts +583 -0
- package/src/electron/voice/index.ts +9 -0
- package/src/electron/voice/voice-settings-manager.ts +403 -0
- package/src/renderer/App.tsx +775 -0
- package/src/renderer/components/ActivityFeed.tsx +407 -0
- package/src/renderer/components/ActivityFeedItem.tsx +285 -0
- package/src/renderer/components/AgentRoleCard.tsx +343 -0
- package/src/renderer/components/AgentRoleEditor.tsx +805 -0
- package/src/renderer/components/AgentSquadSettings.tsx +295 -0
- package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
- package/src/renderer/components/AppearanceSettings.tsx +122 -0
- package/src/renderer/components/ApprovalDialog.tsx +100 -0
- package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
- package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
- package/src/renderer/components/CanvasPreview.tsx +1189 -0
- package/src/renderer/components/CommandOutput.tsx +202 -0
- package/src/renderer/components/ContextPolicySettings.tsx +523 -0
- package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
- package/src/renderer/components/DisclaimerModal.tsx +124 -0
- package/src/renderer/components/DiscordSettings.tsx +436 -0
- package/src/renderer/components/EmailSettings.tsx +606 -0
- package/src/renderer/components/ExtensionsSettings.tsx +542 -0
- package/src/renderer/components/FileViewer.tsx +224 -0
- package/src/renderer/components/GoogleChatSettings.tsx +535 -0
- package/src/renderer/components/GuardrailSettings.tsx +487 -0
- package/src/renderer/components/HooksSettings.tsx +581 -0
- package/src/renderer/components/ImessageSettings.tsx +484 -0
- package/src/renderer/components/LineSettings.tsx +483 -0
- package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
- package/src/renderer/components/MCPSettings.tsx +943 -0
- package/src/renderer/components/MainContent.tsx +2433 -0
- package/src/renderer/components/MatrixSettings.tsx +510 -0
- package/src/renderer/components/MattermostSettings.tsx +473 -0
- package/src/renderer/components/MemorySettings.tsx +247 -0
- package/src/renderer/components/MentionBadge.tsx +87 -0
- package/src/renderer/components/MentionInput.tsx +409 -0
- package/src/renderer/components/MentionList.tsx +476 -0
- package/src/renderer/components/MissionControlPanel.tsx +1995 -0
- package/src/renderer/components/NodesSettings.tsx +316 -0
- package/src/renderer/components/NotificationPanel.tsx +481 -0
- package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
- package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
- package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
- package/src/renderer/components/Onboarding/index.ts +3 -0
- package/src/renderer/components/OnboardingModal.tsx +698 -0
- package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
- package/src/renderer/components/PersonalitySettings.tsx +597 -0
- package/src/renderer/components/QueueSettings.tsx +119 -0
- package/src/renderer/components/QuickTaskFAB.tsx +71 -0
- package/src/renderer/components/RightPanel.tsx +413 -0
- package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
- package/src/renderer/components/SearchSettings.tsx +328 -0
- package/src/renderer/components/Settings.tsx +1504 -0
- package/src/renderer/components/Sidebar.tsx +344 -0
- package/src/renderer/components/SignalSettings.tsx +673 -0
- package/src/renderer/components/SkillHubBrowser.tsx +458 -0
- package/src/renderer/components/SkillParameterModal.tsx +185 -0
- package/src/renderer/components/SkillsSettings.tsx +451 -0
- package/src/renderer/components/SlackSettings.tsx +442 -0
- package/src/renderer/components/StandupReportViewer.tsx +614 -0
- package/src/renderer/components/TaskBoard.tsx +498 -0
- package/src/renderer/components/TaskBoardCard.tsx +357 -0
- package/src/renderer/components/TaskBoardColumn.tsx +211 -0
- package/src/renderer/components/TaskLabelManager.tsx +472 -0
- package/src/renderer/components/TaskQueuePanel.tsx +144 -0
- package/src/renderer/components/TaskQuickActions.tsx +492 -0
- package/src/renderer/components/TaskTimeline.tsx +216 -0
- package/src/renderer/components/TaskView.tsx +162 -0
- package/src/renderer/components/TeamsSettings.tsx +518 -0
- package/src/renderer/components/TelegramSettings.tsx +421 -0
- package/src/renderer/components/Toast.tsx +76 -0
- package/src/renderer/components/TraySettings.tsx +189 -0
- package/src/renderer/components/TwitchSettings.tsx +511 -0
- package/src/renderer/components/UpdateSettings.tsx +295 -0
- package/src/renderer/components/VoiceIndicator.tsx +270 -0
- package/src/renderer/components/VoiceSettings.tsx +867 -0
- package/src/renderer/components/WhatsAppSettings.tsx +721 -0
- package/src/renderer/components/WorkingStateEditor.tsx +309 -0
- package/src/renderer/components/WorkingStateHistory.tsx +481 -0
- package/src/renderer/components/WorkspaceSelector.tsx +150 -0
- package/src/renderer/components/XSettings.tsx +311 -0
- package/src/renderer/global.d.ts +9 -0
- package/src/renderer/hooks/useAgentContext.ts +153 -0
- package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
- package/src/renderer/hooks/useVoiceInput.ts +268 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +10 -0
- package/src/renderer/public/cowork-os-logo.png +0 -0
- package/src/renderer/quick-input.html +164 -0
- package/src/renderer/styles/index.css +14504 -0
- package/src/renderer/utils/agentMessages.ts +749 -0
- package/src/renderer/utils/voice-directives.ts +169 -0
- package/src/shared/channelMessages.ts +213 -0
- package/src/shared/types.ts +3608 -0
- package/tsconfig.electron.json +26 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +23 -0
|
@@ -0,0 +1,2397 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import {
|
|
4
|
+
Task,
|
|
5
|
+
TaskEvent,
|
|
6
|
+
Artifact,
|
|
7
|
+
Workspace,
|
|
8
|
+
ApprovalRequest,
|
|
9
|
+
Skill,
|
|
10
|
+
WorkspacePermissions,
|
|
11
|
+
} from '../../shared/types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safely parse JSON with error handling
|
|
15
|
+
* Returns defaultValue if parsing fails
|
|
16
|
+
*/
|
|
17
|
+
function safeJsonParse<T>(jsonString: string, defaultValue: T, context?: string): T {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(jsonString);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`Failed to parse JSON${context ? ` in ${context}` : ''}:`, error, 'Input:', jsonString?.slice(0, 100));
|
|
22
|
+
return defaultValue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class WorkspaceRepository {
|
|
27
|
+
constructor(private db: Database.Database) {}
|
|
28
|
+
|
|
29
|
+
create(name: string, path: string, permissions: WorkspacePermissions): Workspace {
|
|
30
|
+
const workspace: Workspace = {
|
|
31
|
+
id: uuidv4(),
|
|
32
|
+
name,
|
|
33
|
+
path,
|
|
34
|
+
createdAt: Date.now(),
|
|
35
|
+
permissions,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const stmt = this.db.prepare(`
|
|
39
|
+
INSERT INTO workspaces (id, name, path, created_at, permissions)
|
|
40
|
+
VALUES (?, ?, ?, ?, ?)
|
|
41
|
+
`);
|
|
42
|
+
|
|
43
|
+
stmt.run(
|
|
44
|
+
workspace.id,
|
|
45
|
+
workspace.name,
|
|
46
|
+
workspace.path,
|
|
47
|
+
workspace.createdAt,
|
|
48
|
+
JSON.stringify(workspace.permissions)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return workspace;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
findById(id: string): Workspace | undefined {
|
|
55
|
+
const stmt = this.db.prepare('SELECT * FROM workspaces WHERE id = ?');
|
|
56
|
+
const row = stmt.get(id) as any;
|
|
57
|
+
return row ? this.mapRowToWorkspace(row) : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
findAll(): Workspace[] {
|
|
61
|
+
const stmt = this.db.prepare('SELECT * FROM workspaces ORDER BY created_at DESC');
|
|
62
|
+
const rows = stmt.all() as any[];
|
|
63
|
+
return rows.map(row => this.mapRowToWorkspace(row));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a workspace with the given path already exists
|
|
68
|
+
*/
|
|
69
|
+
existsByPath(path: string): boolean {
|
|
70
|
+
const stmt = this.db.prepare('SELECT 1 FROM workspaces WHERE path = ?');
|
|
71
|
+
const row = stmt.get(path);
|
|
72
|
+
return !!row;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Find a workspace by its path
|
|
77
|
+
*/
|
|
78
|
+
findByPath(path: string): Workspace | undefined {
|
|
79
|
+
const stmt = this.db.prepare('SELECT * FROM workspaces WHERE path = ?');
|
|
80
|
+
const row = stmt.get(path) as any;
|
|
81
|
+
return row ? this.mapRowToWorkspace(row) : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Update workspace permissions
|
|
86
|
+
*/
|
|
87
|
+
updatePermissions(id: string, permissions: WorkspacePermissions): void {
|
|
88
|
+
const stmt = this.db.prepare('UPDATE workspaces SET permissions = ? WHERE id = ?');
|
|
89
|
+
stmt.run(JSON.stringify(permissions), id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Delete a workspace by ID
|
|
94
|
+
*/
|
|
95
|
+
delete(id: string): void {
|
|
96
|
+
const stmt = this.db.prepare('DELETE FROM workspaces WHERE id = ?');
|
|
97
|
+
stmt.run(id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private mapRowToWorkspace(row: any): Workspace {
|
|
101
|
+
// Note: network is true by default for browser tools (web access)
|
|
102
|
+
const defaultPermissions: WorkspacePermissions = { read: true, write: true, delete: false, network: true, shell: false };
|
|
103
|
+
const storedPermissions = safeJsonParse(row.permissions, defaultPermissions, 'workspace.permissions');
|
|
104
|
+
|
|
105
|
+
// Merge with defaults to ensure new fields (like network) get proper defaults
|
|
106
|
+
// for workspaces created before those fields existed
|
|
107
|
+
const mergedPermissions: WorkspacePermissions = {
|
|
108
|
+
...defaultPermissions,
|
|
109
|
+
...storedPermissions,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Migration: if network was explicitly false (old default), upgrade it to true
|
|
113
|
+
// This ensures existing workspaces get browser tool access
|
|
114
|
+
if (storedPermissions.network === false) {
|
|
115
|
+
mergedPermissions.network = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id: row.id,
|
|
120
|
+
name: row.name,
|
|
121
|
+
path: row.path,
|
|
122
|
+
createdAt: row.created_at,
|
|
123
|
+
permissions: mergedPermissions,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class TaskRepository {
|
|
129
|
+
constructor(private db: Database.Database) {}
|
|
130
|
+
|
|
131
|
+
create(task: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Task {
|
|
132
|
+
const newTask: Task = {
|
|
133
|
+
...task,
|
|
134
|
+
id: uuidv4(),
|
|
135
|
+
createdAt: Date.now(),
|
|
136
|
+
updatedAt: Date.now(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const stmt = this.db.prepare(`
|
|
140
|
+
INSERT INTO tasks (id, title, prompt, status, workspace_id, created_at, updated_at, budget_tokens, budget_cost, success_criteria, max_attempts, current_attempt, parent_task_id, agent_type, agent_config, depth, result_summary)
|
|
141
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
142
|
+
`);
|
|
143
|
+
|
|
144
|
+
stmt.run(
|
|
145
|
+
newTask.id,
|
|
146
|
+
newTask.title,
|
|
147
|
+
newTask.prompt,
|
|
148
|
+
newTask.status,
|
|
149
|
+
newTask.workspaceId,
|
|
150
|
+
newTask.createdAt,
|
|
151
|
+
newTask.updatedAt,
|
|
152
|
+
newTask.budgetTokens || null,
|
|
153
|
+
newTask.budgetCost || null,
|
|
154
|
+
newTask.successCriteria ? JSON.stringify(newTask.successCriteria) : null,
|
|
155
|
+
newTask.maxAttempts || null,
|
|
156
|
+
newTask.currentAttempt || 1,
|
|
157
|
+
newTask.parentTaskId || null,
|
|
158
|
+
newTask.agentType || 'main',
|
|
159
|
+
newTask.agentConfig ? JSON.stringify(newTask.agentConfig) : null,
|
|
160
|
+
newTask.depth ?? 0,
|
|
161
|
+
newTask.resultSummary || null
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return newTask;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Whitelist of allowed update fields to prevent SQL injection
|
|
168
|
+
private static readonly ALLOWED_UPDATE_FIELDS = new Set([
|
|
169
|
+
'title', 'status', 'error', 'result', 'budgetTokens', 'budgetCost',
|
|
170
|
+
'successCriteria', 'maxAttempts', 'currentAttempt', 'completedAt',
|
|
171
|
+
'parentTaskId', 'agentType', 'agentConfig', 'depth', 'resultSummary',
|
|
172
|
+
// Agent Squad fields
|
|
173
|
+
'assignedAgentRoleId', 'boardColumn', 'priority',
|
|
174
|
+
// Task Board fields
|
|
175
|
+
'labels', 'dueDate', 'estimatedMinutes', 'actualMinutes', 'mentionedAgentRoleIds'
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
update(id: string, updates: Partial<Task>): void {
|
|
179
|
+
const fields: string[] = [];
|
|
180
|
+
const values: any[] = [];
|
|
181
|
+
|
|
182
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
183
|
+
// Validate field name against whitelist
|
|
184
|
+
if (!TaskRepository.ALLOWED_UPDATE_FIELDS.has(key)) {
|
|
185
|
+
console.warn(`Ignoring unknown field in task update: ${key}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
189
|
+
fields.push(`${snakeKey} = ?`);
|
|
190
|
+
// JSON serialize object/array fields
|
|
191
|
+
if ((key === 'successCriteria' || key === 'agentConfig' || key === 'labels' || key === 'mentionedAgentRoleIds') && value != null) {
|
|
192
|
+
values.push(JSON.stringify(value));
|
|
193
|
+
} else {
|
|
194
|
+
values.push(value);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (fields.length === 0) {
|
|
199
|
+
return; // No valid fields to update
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fields.push('updated_at = ?');
|
|
203
|
+
values.push(Date.now());
|
|
204
|
+
values.push(id);
|
|
205
|
+
|
|
206
|
+
const stmt = this.db.prepare(`UPDATE tasks SET ${fields.join(', ')} WHERE id = ?`);
|
|
207
|
+
stmt.run(...values);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
findById(id: string): Task | undefined {
|
|
211
|
+
const stmt = this.db.prepare('SELECT * FROM tasks WHERE id = ?');
|
|
212
|
+
const row = stmt.get(id) as any;
|
|
213
|
+
return row ? this.mapRowToTask(row) : undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
findAll(limit = 100, offset = 0): Task[] {
|
|
217
|
+
const stmt = this.db.prepare(`
|
|
218
|
+
SELECT * FROM tasks
|
|
219
|
+
ORDER BY created_at DESC
|
|
220
|
+
LIMIT ? OFFSET ?
|
|
221
|
+
`);
|
|
222
|
+
const rows = stmt.all(limit, offset) as any[];
|
|
223
|
+
return rows.map(row => this.mapRowToTask(row));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Find tasks by status (single status or array of statuses)
|
|
228
|
+
*/
|
|
229
|
+
findByStatus(status: string | string[]): Task[] {
|
|
230
|
+
const statuses = Array.isArray(status) ? status : [status];
|
|
231
|
+
const placeholders = statuses.map(() => '?').join(', ');
|
|
232
|
+
const stmt = this.db.prepare(`
|
|
233
|
+
SELECT * FROM tasks
|
|
234
|
+
WHERE status IN (${placeholders})
|
|
235
|
+
ORDER BY created_at ASC
|
|
236
|
+
`);
|
|
237
|
+
const rows = stmt.all(...statuses) as any[];
|
|
238
|
+
return rows.map(row => this.mapRowToTask(row));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Find tasks by workspace ID
|
|
243
|
+
*/
|
|
244
|
+
findByWorkspace(workspaceId: string): Task[] {
|
|
245
|
+
const stmt = this.db.prepare(`
|
|
246
|
+
SELECT * FROM tasks
|
|
247
|
+
WHERE workspace_id = ?
|
|
248
|
+
ORDER BY created_at DESC
|
|
249
|
+
`);
|
|
250
|
+
const rows = stmt.all(workspaceId) as any[];
|
|
251
|
+
return rows.map(row => this.mapRowToTask(row));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
delete(id: string): void {
|
|
255
|
+
// Use transaction to ensure atomic deletion
|
|
256
|
+
const deleteTransaction = this.db.transaction((taskId: string) => {
|
|
257
|
+
// Delete related records from all tables with foreign keys to tasks
|
|
258
|
+
const deleteEvents = this.db.prepare('DELETE FROM task_events WHERE task_id = ?');
|
|
259
|
+
deleteEvents.run(taskId);
|
|
260
|
+
|
|
261
|
+
const deleteArtifacts = this.db.prepare('DELETE FROM artifacts WHERE task_id = ?');
|
|
262
|
+
deleteArtifacts.run(taskId);
|
|
263
|
+
|
|
264
|
+
const deleteApprovals = this.db.prepare('DELETE FROM approvals WHERE task_id = ?');
|
|
265
|
+
deleteApprovals.run(taskId);
|
|
266
|
+
|
|
267
|
+
// Delete activity feed entries for this task
|
|
268
|
+
const deleteActivities = this.db.prepare('DELETE FROM activity_feed WHERE task_id = ?');
|
|
269
|
+
deleteActivities.run(taskId);
|
|
270
|
+
|
|
271
|
+
// Delete agent mentions for this task
|
|
272
|
+
const deleteMentions = this.db.prepare('DELETE FROM agent_mentions WHERE task_id = ?');
|
|
273
|
+
deleteMentions.run(taskId);
|
|
274
|
+
|
|
275
|
+
// Delete working state entries for this task
|
|
276
|
+
const deleteWorkingState = this.db.prepare('DELETE FROM agent_working_state WHERE task_id = ?');
|
|
277
|
+
deleteWorkingState.run(taskId);
|
|
278
|
+
|
|
279
|
+
// Nullify task_id in memories rather than deleting them
|
|
280
|
+
const clearMemoryTaskId = this.db.prepare('UPDATE memories SET task_id = NULL WHERE task_id = ?');
|
|
281
|
+
clearMemoryTaskId.run(taskId);
|
|
282
|
+
|
|
283
|
+
// Nullify task_id in channel_sessions rather than deleting the session
|
|
284
|
+
const clearSessionTaskId = this.db.prepare('UPDATE channel_sessions SET task_id = NULL WHERE task_id = ?');
|
|
285
|
+
clearSessionTaskId.run(taskId);
|
|
286
|
+
|
|
287
|
+
// Finally delete the task
|
|
288
|
+
const deleteTask = this.db.prepare('DELETE FROM tasks WHERE id = ?');
|
|
289
|
+
deleteTask.run(taskId);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
deleteTransaction(id);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private mapRowToTask(row: any): Task {
|
|
296
|
+
return {
|
|
297
|
+
id: row.id,
|
|
298
|
+
title: row.title,
|
|
299
|
+
prompt: row.prompt,
|
|
300
|
+
status: row.status,
|
|
301
|
+
workspaceId: row.workspace_id,
|
|
302
|
+
createdAt: row.created_at,
|
|
303
|
+
updatedAt: row.updated_at,
|
|
304
|
+
completedAt: row.completed_at || undefined,
|
|
305
|
+
budgetTokens: row.budget_tokens || undefined,
|
|
306
|
+
budgetCost: row.budget_cost || undefined,
|
|
307
|
+
error: row.error || undefined,
|
|
308
|
+
// Goal Mode fields
|
|
309
|
+
successCriteria: row.success_criteria ? safeJsonParse(row.success_criteria, undefined, 'task.successCriteria') : undefined,
|
|
310
|
+
maxAttempts: row.max_attempts || undefined,
|
|
311
|
+
currentAttempt: row.current_attempt || undefined,
|
|
312
|
+
// Sub-Agent / Parallel Agent fields
|
|
313
|
+
parentTaskId: row.parent_task_id || undefined,
|
|
314
|
+
agentType: row.agent_type || undefined,
|
|
315
|
+
agentConfig: row.agent_config ? safeJsonParse(row.agent_config, undefined, 'task.agentConfig') : undefined,
|
|
316
|
+
depth: row.depth ?? undefined,
|
|
317
|
+
resultSummary: row.result_summary || undefined,
|
|
318
|
+
// Agent Squad fields
|
|
319
|
+
assignedAgentRoleId: row.assigned_agent_role_id || undefined,
|
|
320
|
+
boardColumn: row.board_column || undefined,
|
|
321
|
+
priority: row.priority ?? undefined,
|
|
322
|
+
// Task Board fields
|
|
323
|
+
labels: row.labels ? safeJsonParse<string[]>(row.labels, [], 'task.labels') : undefined,
|
|
324
|
+
dueDate: row.due_date || undefined,
|
|
325
|
+
estimatedMinutes: row.estimated_minutes || undefined,
|
|
326
|
+
actualMinutes: row.actual_minutes || undefined,
|
|
327
|
+
mentionedAgentRoleIds: row.mentioned_agent_role_ids ? safeJsonParse<string[]>(row.mentioned_agent_role_ids, [], 'task.mentionedAgentRoleIds') : undefined,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Find tasks by parent task ID
|
|
333
|
+
*/
|
|
334
|
+
findByParent(parentTaskId: string): Task[] {
|
|
335
|
+
const stmt = this.db.prepare(`
|
|
336
|
+
SELECT * FROM tasks
|
|
337
|
+
WHERE parent_task_id = ?
|
|
338
|
+
ORDER BY created_at ASC
|
|
339
|
+
`);
|
|
340
|
+
const rows = stmt.all(parentTaskId) as any[];
|
|
341
|
+
return rows.map(row => this.mapRowToTask(row));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============ Task Board Methods ============
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Find tasks by workspace and board column
|
|
348
|
+
*/
|
|
349
|
+
findByBoardColumn(workspaceId: string, boardColumn: string): Task[] {
|
|
350
|
+
const stmt = this.db.prepare(`
|
|
351
|
+
SELECT * FROM tasks
|
|
352
|
+
WHERE workspace_id = ? AND board_column = ?
|
|
353
|
+
ORDER BY priority DESC, created_at ASC
|
|
354
|
+
`);
|
|
355
|
+
const rows = stmt.all(workspaceId, boardColumn) as any[];
|
|
356
|
+
return rows.map(row => this.mapRowToTask(row));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get tasks grouped by board column for a workspace
|
|
361
|
+
*/
|
|
362
|
+
getTaskBoard(workspaceId: string): Record<string, Task[]> {
|
|
363
|
+
const stmt = this.db.prepare(`
|
|
364
|
+
SELECT * FROM tasks
|
|
365
|
+
WHERE workspace_id = ? AND parent_task_id IS NULL
|
|
366
|
+
ORDER BY board_column, priority DESC, created_at ASC
|
|
367
|
+
`);
|
|
368
|
+
const rows = stmt.all(workspaceId) as any[];
|
|
369
|
+
const tasks = rows.map(row => this.mapRowToTask(row));
|
|
370
|
+
|
|
371
|
+
// Group tasks by board column
|
|
372
|
+
const board: Record<string, Task[]> = {
|
|
373
|
+
backlog: [],
|
|
374
|
+
todo: [],
|
|
375
|
+
in_progress: [],
|
|
376
|
+
review: [],
|
|
377
|
+
done: [],
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
for (const task of tasks) {
|
|
381
|
+
const column = task.boardColumn || 'backlog';
|
|
382
|
+
if (board[column]) {
|
|
383
|
+
board[column].push(task);
|
|
384
|
+
} else {
|
|
385
|
+
board.backlog.push(task);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return board;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Move a task to a different board column
|
|
394
|
+
*/
|
|
395
|
+
moveToColumn(id: string, boardColumn: string): Task | undefined {
|
|
396
|
+
this.update(id, { boardColumn: boardColumn as any });
|
|
397
|
+
return this.findById(id);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Set task priority
|
|
402
|
+
*/
|
|
403
|
+
setPriority(id: string, priority: number): Task | undefined {
|
|
404
|
+
this.update(id, { priority });
|
|
405
|
+
return this.findById(id);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Set task due date
|
|
410
|
+
*/
|
|
411
|
+
setDueDate(id: string, dueDate: number | null): Task | undefined {
|
|
412
|
+
this.update(id, { dueDate: dueDate || undefined } as any);
|
|
413
|
+
return this.findById(id);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Set task time estimate
|
|
418
|
+
*/
|
|
419
|
+
setEstimate(id: string, estimatedMinutes: number | null): Task | undefined {
|
|
420
|
+
this.update(id, { estimatedMinutes: estimatedMinutes || undefined } as any);
|
|
421
|
+
return this.findById(id);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Add a label to a task
|
|
426
|
+
*/
|
|
427
|
+
addLabel(id: string, labelId: string): Task | undefined {
|
|
428
|
+
const task = this.findById(id);
|
|
429
|
+
if (!task) return undefined;
|
|
430
|
+
|
|
431
|
+
const labels = task.labels || [];
|
|
432
|
+
if (!labels.includes(labelId)) {
|
|
433
|
+
labels.push(labelId);
|
|
434
|
+
this.update(id, { labels } as any);
|
|
435
|
+
}
|
|
436
|
+
return this.findById(id);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Remove a label from a task
|
|
441
|
+
*/
|
|
442
|
+
removeLabel(id: string, labelId: string): Task | undefined {
|
|
443
|
+
const task = this.findById(id);
|
|
444
|
+
if (!task) return undefined;
|
|
445
|
+
|
|
446
|
+
const labels = task.labels || [];
|
|
447
|
+
const newLabels = labels.filter(l => l !== labelId);
|
|
448
|
+
this.update(id, { labels: newLabels } as any);
|
|
449
|
+
return this.findById(id);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Assign an agent role to a task
|
|
454
|
+
*/
|
|
455
|
+
assignAgentRole(id: string, agentRoleId: string | null): Task | undefined {
|
|
456
|
+
this.update(id, { assignedAgentRoleId: agentRoleId || undefined } as any);
|
|
457
|
+
return this.findById(id);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export class TaskEventRepository {
|
|
462
|
+
constructor(private db: Database.Database) {}
|
|
463
|
+
|
|
464
|
+
create(event: Omit<TaskEvent, 'id'>): TaskEvent {
|
|
465
|
+
const newEvent: TaskEvent = {
|
|
466
|
+
...event,
|
|
467
|
+
id: uuidv4(),
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const stmt = this.db.prepare(`
|
|
471
|
+
INSERT INTO task_events (id, task_id, timestamp, type, payload)
|
|
472
|
+
VALUES (?, ?, ?, ?, ?)
|
|
473
|
+
`);
|
|
474
|
+
|
|
475
|
+
stmt.run(
|
|
476
|
+
newEvent.id,
|
|
477
|
+
newEvent.taskId,
|
|
478
|
+
newEvent.timestamp,
|
|
479
|
+
newEvent.type,
|
|
480
|
+
JSON.stringify(newEvent.payload)
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
return newEvent;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
findByTaskId(taskId: string): TaskEvent[] {
|
|
487
|
+
const stmt = this.db.prepare(`
|
|
488
|
+
SELECT * FROM task_events
|
|
489
|
+
WHERE task_id = ?
|
|
490
|
+
ORDER BY timestamp ASC
|
|
491
|
+
`);
|
|
492
|
+
const rows = stmt.all(taskId) as any[];
|
|
493
|
+
return rows.map(row => this.mapRowToEvent(row));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private mapRowToEvent(row: any): TaskEvent {
|
|
497
|
+
return {
|
|
498
|
+
id: row.id,
|
|
499
|
+
taskId: row.task_id,
|
|
500
|
+
timestamp: row.timestamp,
|
|
501
|
+
type: row.type,
|
|
502
|
+
payload: safeJsonParse(row.payload, {}, 'taskEvent.payload'),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Prune old conversation snapshots for a task, keeping only the most recent one.
|
|
508
|
+
* This prevents database bloat from accumulating snapshots over time.
|
|
509
|
+
*/
|
|
510
|
+
pruneOldSnapshots(taskId: string): void {
|
|
511
|
+
// Find all conversation_snapshot events for this task, ordered by timestamp descending
|
|
512
|
+
const findStmt = this.db.prepare(`
|
|
513
|
+
SELECT id, timestamp FROM task_events
|
|
514
|
+
WHERE task_id = ? AND type = 'conversation_snapshot'
|
|
515
|
+
ORDER BY timestamp DESC
|
|
516
|
+
`);
|
|
517
|
+
const snapshots = findStmt.all(taskId) as { id: string; timestamp: number }[];
|
|
518
|
+
|
|
519
|
+
// Keep only the most recent one, delete the rest
|
|
520
|
+
if (snapshots.length > 1) {
|
|
521
|
+
const idsToDelete = snapshots.slice(1).map(s => s.id);
|
|
522
|
+
const deleteStmt = this.db.prepare(`
|
|
523
|
+
DELETE FROM task_events WHERE id = ?
|
|
524
|
+
`);
|
|
525
|
+
|
|
526
|
+
for (const id of idsToDelete) {
|
|
527
|
+
deleteStmt.run(id);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
console.log(`[TaskEventRepository] Pruned ${idsToDelete.length} old snapshot(s) for task ${taskId}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export class ArtifactRepository {
|
|
536
|
+
constructor(private db: Database.Database) {}
|
|
537
|
+
|
|
538
|
+
create(artifact: Omit<Artifact, 'id'>): Artifact {
|
|
539
|
+
const newArtifact: Artifact = {
|
|
540
|
+
...artifact,
|
|
541
|
+
id: uuidv4(),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const stmt = this.db.prepare(`
|
|
545
|
+
INSERT INTO artifacts (id, task_id, path, mime_type, sha256, size, created_at)
|
|
546
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
547
|
+
`);
|
|
548
|
+
|
|
549
|
+
stmt.run(
|
|
550
|
+
newArtifact.id,
|
|
551
|
+
newArtifact.taskId,
|
|
552
|
+
newArtifact.path,
|
|
553
|
+
newArtifact.mimeType,
|
|
554
|
+
newArtifact.sha256,
|
|
555
|
+
newArtifact.size,
|
|
556
|
+
newArtifact.createdAt
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return newArtifact;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
findByTaskId(taskId: string): Artifact[] {
|
|
563
|
+
const stmt = this.db.prepare('SELECT * FROM artifacts WHERE task_id = ? ORDER BY created_at DESC');
|
|
564
|
+
const rows = stmt.all(taskId) as any[];
|
|
565
|
+
return rows.map(row => this.mapRowToArtifact(row));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private mapRowToArtifact(row: any): Artifact {
|
|
569
|
+
return {
|
|
570
|
+
id: row.id,
|
|
571
|
+
taskId: row.task_id,
|
|
572
|
+
path: row.path,
|
|
573
|
+
mimeType: row.mime_type,
|
|
574
|
+
sha256: row.sha256,
|
|
575
|
+
size: row.size,
|
|
576
|
+
createdAt: row.created_at,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export class ApprovalRepository {
|
|
582
|
+
constructor(private db: Database.Database) {}
|
|
583
|
+
|
|
584
|
+
create(approval: Omit<ApprovalRequest, 'id'>): ApprovalRequest {
|
|
585
|
+
const newApproval: ApprovalRequest = {
|
|
586
|
+
...approval,
|
|
587
|
+
id: uuidv4(),
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const stmt = this.db.prepare(`
|
|
591
|
+
INSERT INTO approvals (id, task_id, type, description, details, status, requested_at)
|
|
592
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
593
|
+
`);
|
|
594
|
+
|
|
595
|
+
stmt.run(
|
|
596
|
+
newApproval.id,
|
|
597
|
+
newApproval.taskId,
|
|
598
|
+
newApproval.type,
|
|
599
|
+
newApproval.description,
|
|
600
|
+
JSON.stringify(newApproval.details),
|
|
601
|
+
newApproval.status,
|
|
602
|
+
newApproval.requestedAt
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
return newApproval;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
update(id: string, status: 'approved' | 'denied'): void {
|
|
609
|
+
const stmt = this.db.prepare(`
|
|
610
|
+
UPDATE approvals
|
|
611
|
+
SET status = ?, resolved_at = ?
|
|
612
|
+
WHERE id = ?
|
|
613
|
+
`);
|
|
614
|
+
stmt.run(status, Date.now(), id);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
findPendingByTaskId(taskId: string): ApprovalRequest[] {
|
|
618
|
+
const stmt = this.db.prepare(`
|
|
619
|
+
SELECT * FROM approvals
|
|
620
|
+
WHERE task_id = ? AND status = 'pending'
|
|
621
|
+
ORDER BY requested_at ASC
|
|
622
|
+
`);
|
|
623
|
+
const rows = stmt.all(taskId) as any[];
|
|
624
|
+
return rows.map(row => this.mapRowToApproval(row));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private mapRowToApproval(row: any): ApprovalRequest {
|
|
628
|
+
return {
|
|
629
|
+
id: row.id,
|
|
630
|
+
taskId: row.task_id,
|
|
631
|
+
type: row.type,
|
|
632
|
+
description: row.description,
|
|
633
|
+
details: safeJsonParse(row.details, {}, 'approval.details'),
|
|
634
|
+
status: row.status,
|
|
635
|
+
requestedAt: row.requested_at,
|
|
636
|
+
resolvedAt: row.resolved_at || undefined,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
export class SkillRepository {
|
|
642
|
+
constructor(private db: Database.Database) {}
|
|
643
|
+
|
|
644
|
+
create(skill: Omit<Skill, 'id'>): Skill {
|
|
645
|
+
const newSkill: Skill = {
|
|
646
|
+
...skill,
|
|
647
|
+
id: uuidv4(),
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const stmt = this.db.prepare(`
|
|
651
|
+
INSERT INTO skills (id, name, description, category, prompt, script_path, parameters)
|
|
652
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
653
|
+
`);
|
|
654
|
+
|
|
655
|
+
stmt.run(
|
|
656
|
+
newSkill.id,
|
|
657
|
+
newSkill.name,
|
|
658
|
+
newSkill.description,
|
|
659
|
+
newSkill.category,
|
|
660
|
+
newSkill.prompt,
|
|
661
|
+
newSkill.scriptPath || null,
|
|
662
|
+
newSkill.parameters ? JSON.stringify(newSkill.parameters) : null
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
return newSkill;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
findAll(): Skill[] {
|
|
669
|
+
const stmt = this.db.prepare('SELECT * FROM skills ORDER BY name ASC');
|
|
670
|
+
const rows = stmt.all() as any[];
|
|
671
|
+
return rows.map(row => this.mapRowToSkill(row));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
findById(id: string): Skill | undefined {
|
|
675
|
+
const stmt = this.db.prepare('SELECT * FROM skills WHERE id = ?');
|
|
676
|
+
const row = stmt.get(id) as any;
|
|
677
|
+
return row ? this.mapRowToSkill(row) : undefined;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
private mapRowToSkill(row: any): Skill {
|
|
681
|
+
return {
|
|
682
|
+
id: row.id,
|
|
683
|
+
name: row.name,
|
|
684
|
+
description: row.description,
|
|
685
|
+
category: row.category,
|
|
686
|
+
prompt: row.prompt,
|
|
687
|
+
scriptPath: row.script_path || undefined,
|
|
688
|
+
parameters: row.parameters ? safeJsonParse(row.parameters, undefined, 'skill.parameters') : undefined,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export interface LLMModel {
|
|
694
|
+
id: string;
|
|
695
|
+
key: string;
|
|
696
|
+
displayName: string;
|
|
697
|
+
description: string;
|
|
698
|
+
anthropicModelId: string;
|
|
699
|
+
bedrockModelId: string;
|
|
700
|
+
sortOrder: number;
|
|
701
|
+
isActive: boolean;
|
|
702
|
+
createdAt: number;
|
|
703
|
+
updatedAt: number;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
export class LLMModelRepository {
|
|
707
|
+
constructor(private db: Database.Database) {}
|
|
708
|
+
|
|
709
|
+
findAll(): LLMModel[] {
|
|
710
|
+
const stmt = this.db.prepare(`
|
|
711
|
+
SELECT * FROM llm_models
|
|
712
|
+
WHERE is_active = 1
|
|
713
|
+
ORDER BY sort_order ASC
|
|
714
|
+
`);
|
|
715
|
+
const rows = stmt.all() as any[];
|
|
716
|
+
return rows.map(row => this.mapRowToModel(row));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
findByKey(key: string): LLMModel | undefined {
|
|
720
|
+
const stmt = this.db.prepare('SELECT * FROM llm_models WHERE key = ?');
|
|
721
|
+
const row = stmt.get(key) as any;
|
|
722
|
+
return row ? this.mapRowToModel(row) : undefined;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
findById(id: string): LLMModel | undefined {
|
|
726
|
+
const stmt = this.db.prepare('SELECT * FROM llm_models WHERE id = ?');
|
|
727
|
+
const row = stmt.get(id) as any;
|
|
728
|
+
return row ? this.mapRowToModel(row) : undefined;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private mapRowToModel(row: any): LLMModel {
|
|
732
|
+
return {
|
|
733
|
+
id: row.id,
|
|
734
|
+
key: row.key,
|
|
735
|
+
displayName: row.display_name,
|
|
736
|
+
description: row.description,
|
|
737
|
+
anthropicModelId: row.anthropic_model_id,
|
|
738
|
+
bedrockModelId: row.bedrock_model_id,
|
|
739
|
+
sortOrder: row.sort_order,
|
|
740
|
+
isActive: row.is_active === 1,
|
|
741
|
+
createdAt: row.created_at,
|
|
742
|
+
updatedAt: row.updated_at,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ============================================================
|
|
748
|
+
// Channel Gateway Repositories
|
|
749
|
+
// ============================================================
|
|
750
|
+
|
|
751
|
+
export interface Channel {
|
|
752
|
+
id: string;
|
|
753
|
+
type: string;
|
|
754
|
+
name: string;
|
|
755
|
+
enabled: boolean;
|
|
756
|
+
config: Record<string, unknown>;
|
|
757
|
+
securityConfig: {
|
|
758
|
+
mode: 'open' | 'allowlist' | 'pairing';
|
|
759
|
+
allowedUsers?: string[];
|
|
760
|
+
pairingCodeTTL?: number;
|
|
761
|
+
maxPairingAttempts?: number;
|
|
762
|
+
rateLimitPerMinute?: number;
|
|
763
|
+
};
|
|
764
|
+
status: string;
|
|
765
|
+
botUsername?: string;
|
|
766
|
+
createdAt: number;
|
|
767
|
+
updatedAt: number;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export interface ChannelUser {
|
|
771
|
+
id: string;
|
|
772
|
+
channelId: string;
|
|
773
|
+
channelUserId: string;
|
|
774
|
+
displayName: string;
|
|
775
|
+
username?: string;
|
|
776
|
+
allowed: boolean;
|
|
777
|
+
pairingCode?: string;
|
|
778
|
+
pairingAttempts: number;
|
|
779
|
+
pairingExpiresAt?: number;
|
|
780
|
+
/** Separate field for brute-force lockout timestamp (distinct from pairing code expiration) */
|
|
781
|
+
lockoutUntil?: number;
|
|
782
|
+
createdAt: number;
|
|
783
|
+
lastSeenAt: number;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
export interface ChannelSession {
|
|
787
|
+
id: string;
|
|
788
|
+
channelId: string;
|
|
789
|
+
chatId: string;
|
|
790
|
+
userId?: string;
|
|
791
|
+
taskId?: string;
|
|
792
|
+
workspaceId?: string;
|
|
793
|
+
state: 'idle' | 'active' | 'waiting_approval';
|
|
794
|
+
context?: Record<string, unknown>;
|
|
795
|
+
shellEnabled?: boolean;
|
|
796
|
+
debugMode?: boolean;
|
|
797
|
+
createdAt: number;
|
|
798
|
+
lastActivityAt: number;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
export interface ChannelMessage {
|
|
802
|
+
id: string;
|
|
803
|
+
channelId: string;
|
|
804
|
+
sessionId?: string;
|
|
805
|
+
channelMessageId: string;
|
|
806
|
+
chatId: string;
|
|
807
|
+
userId?: string;
|
|
808
|
+
direction: 'incoming' | 'outgoing';
|
|
809
|
+
content: string;
|
|
810
|
+
attachments?: Array<{ type: string; url?: string; fileName?: string }>;
|
|
811
|
+
timestamp: number;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export class ChannelRepository {
|
|
815
|
+
constructor(private db: Database.Database) {}
|
|
816
|
+
|
|
817
|
+
create(channel: Omit<Channel, 'id' | 'createdAt' | 'updatedAt'>): Channel {
|
|
818
|
+
const now = Date.now();
|
|
819
|
+
const newChannel: Channel = {
|
|
820
|
+
...channel,
|
|
821
|
+
id: uuidv4(),
|
|
822
|
+
createdAt: now,
|
|
823
|
+
updatedAt: now,
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const stmt = this.db.prepare(`
|
|
827
|
+
INSERT INTO channels (id, type, name, enabled, config, security_config, status, bot_username, created_at, updated_at)
|
|
828
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
829
|
+
`);
|
|
830
|
+
|
|
831
|
+
stmt.run(
|
|
832
|
+
newChannel.id,
|
|
833
|
+
newChannel.type,
|
|
834
|
+
newChannel.name,
|
|
835
|
+
newChannel.enabled ? 1 : 0,
|
|
836
|
+
JSON.stringify(newChannel.config),
|
|
837
|
+
JSON.stringify(newChannel.securityConfig),
|
|
838
|
+
newChannel.status,
|
|
839
|
+
newChannel.botUsername || null,
|
|
840
|
+
newChannel.createdAt,
|
|
841
|
+
newChannel.updatedAt
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
return newChannel;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
update(id: string, updates: Partial<Channel>): void {
|
|
848
|
+
const fields: string[] = [];
|
|
849
|
+
const values: unknown[] = [];
|
|
850
|
+
|
|
851
|
+
if (updates.name !== undefined) {
|
|
852
|
+
fields.push('name = ?');
|
|
853
|
+
values.push(updates.name);
|
|
854
|
+
}
|
|
855
|
+
if (updates.enabled !== undefined) {
|
|
856
|
+
fields.push('enabled = ?');
|
|
857
|
+
values.push(updates.enabled ? 1 : 0);
|
|
858
|
+
}
|
|
859
|
+
if (updates.config !== undefined) {
|
|
860
|
+
fields.push('config = ?');
|
|
861
|
+
values.push(JSON.stringify(updates.config));
|
|
862
|
+
}
|
|
863
|
+
if (updates.securityConfig !== undefined) {
|
|
864
|
+
fields.push('security_config = ?');
|
|
865
|
+
values.push(JSON.stringify(updates.securityConfig));
|
|
866
|
+
}
|
|
867
|
+
if (updates.status !== undefined) {
|
|
868
|
+
fields.push('status = ?');
|
|
869
|
+
values.push(updates.status);
|
|
870
|
+
}
|
|
871
|
+
if (updates.botUsername !== undefined) {
|
|
872
|
+
fields.push('bot_username = ?');
|
|
873
|
+
values.push(updates.botUsername);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (fields.length === 0) return;
|
|
877
|
+
|
|
878
|
+
fields.push('updated_at = ?');
|
|
879
|
+
values.push(Date.now());
|
|
880
|
+
values.push(id);
|
|
881
|
+
|
|
882
|
+
const stmt = this.db.prepare(`UPDATE channels SET ${fields.join(', ')} WHERE id = ?`);
|
|
883
|
+
stmt.run(...values);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
findById(id: string): Channel | undefined {
|
|
887
|
+
const stmt = this.db.prepare('SELECT * FROM channels WHERE id = ?');
|
|
888
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
889
|
+
return row ? this.mapRowToChannel(row) : undefined;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
findByType(type: string): Channel | undefined {
|
|
893
|
+
const stmt = this.db.prepare('SELECT * FROM channels WHERE type = ?');
|
|
894
|
+
const row = stmt.get(type) as Record<string, unknown> | undefined;
|
|
895
|
+
return row ? this.mapRowToChannel(row) : undefined;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
findAll(): Channel[] {
|
|
899
|
+
const stmt = this.db.prepare('SELECT * FROM channels ORDER BY created_at ASC');
|
|
900
|
+
const rows = stmt.all() as Record<string, unknown>[];
|
|
901
|
+
return rows.map(row => this.mapRowToChannel(row));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
findEnabled(): Channel[] {
|
|
905
|
+
const stmt = this.db.prepare('SELECT * FROM channels WHERE enabled = 1 ORDER BY created_at ASC');
|
|
906
|
+
const rows = stmt.all() as Record<string, unknown>[];
|
|
907
|
+
return rows.map(row => this.mapRowToChannel(row));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
delete(id: string): void {
|
|
911
|
+
const stmt = this.db.prepare('DELETE FROM channels WHERE id = ?');
|
|
912
|
+
stmt.run(id);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private mapRowToChannel(row: Record<string, unknown>): Channel {
|
|
916
|
+
const defaultSecurityConfig = { mode: 'pairing' as const };
|
|
917
|
+
return {
|
|
918
|
+
id: row.id as string,
|
|
919
|
+
type: row.type as string,
|
|
920
|
+
name: row.name as string,
|
|
921
|
+
enabled: row.enabled === 1,
|
|
922
|
+
config: safeJsonParse(row.config as string, {}, 'channel.config'),
|
|
923
|
+
securityConfig: safeJsonParse(row.security_config as string, defaultSecurityConfig, 'channel.securityConfig'),
|
|
924
|
+
status: row.status as string,
|
|
925
|
+
botUsername: (row.bot_username as string) || undefined,
|
|
926
|
+
createdAt: row.created_at as number,
|
|
927
|
+
updatedAt: row.updated_at as number,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export class ChannelUserRepository {
|
|
933
|
+
constructor(private db: Database.Database) {}
|
|
934
|
+
|
|
935
|
+
create(user: Omit<ChannelUser, 'id' | 'createdAt' | 'lastSeenAt' | 'pairingAttempts'>): ChannelUser {
|
|
936
|
+
const now = Date.now();
|
|
937
|
+
const newUser: ChannelUser = {
|
|
938
|
+
...user,
|
|
939
|
+
id: uuidv4(),
|
|
940
|
+
pairingAttempts: 0,
|
|
941
|
+
createdAt: now,
|
|
942
|
+
lastSeenAt: now,
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
const stmt = this.db.prepare(`
|
|
946
|
+
INSERT INTO channel_users (id, channel_id, channel_user_id, display_name, username, allowed, pairing_code, pairing_attempts, pairing_expires_at, created_at, last_seen_at)
|
|
947
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
948
|
+
`);
|
|
949
|
+
|
|
950
|
+
stmt.run(
|
|
951
|
+
newUser.id,
|
|
952
|
+
newUser.channelId,
|
|
953
|
+
newUser.channelUserId,
|
|
954
|
+
newUser.displayName,
|
|
955
|
+
newUser.username || null,
|
|
956
|
+
newUser.allowed ? 1 : 0,
|
|
957
|
+
newUser.pairingCode || null,
|
|
958
|
+
newUser.pairingAttempts,
|
|
959
|
+
newUser.pairingExpiresAt || null,
|
|
960
|
+
newUser.createdAt,
|
|
961
|
+
newUser.lastSeenAt
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
return newUser;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
update(id: string, updates: Partial<ChannelUser>): void {
|
|
968
|
+
const fields: string[] = [];
|
|
969
|
+
const values: unknown[] = [];
|
|
970
|
+
|
|
971
|
+
if (updates.displayName !== undefined) {
|
|
972
|
+
fields.push('display_name = ?');
|
|
973
|
+
values.push(updates.displayName);
|
|
974
|
+
}
|
|
975
|
+
if (updates.username !== undefined) {
|
|
976
|
+
fields.push('username = ?');
|
|
977
|
+
values.push(updates.username);
|
|
978
|
+
}
|
|
979
|
+
if (updates.allowed !== undefined) {
|
|
980
|
+
fields.push('allowed = ?');
|
|
981
|
+
values.push(updates.allowed ? 1 : 0);
|
|
982
|
+
}
|
|
983
|
+
if (updates.pairingCode !== undefined) {
|
|
984
|
+
fields.push('pairing_code = ?');
|
|
985
|
+
values.push(updates.pairingCode);
|
|
986
|
+
}
|
|
987
|
+
if (updates.pairingAttempts !== undefined) {
|
|
988
|
+
fields.push('pairing_attempts = ?');
|
|
989
|
+
values.push(updates.pairingAttempts);
|
|
990
|
+
}
|
|
991
|
+
if (updates.pairingExpiresAt !== undefined) {
|
|
992
|
+
fields.push('pairing_expires_at = ?');
|
|
993
|
+
values.push(updates.pairingExpiresAt);
|
|
994
|
+
}
|
|
995
|
+
if (updates.lockoutUntil !== undefined) {
|
|
996
|
+
fields.push('lockout_until = ?');
|
|
997
|
+
values.push(updates.lockoutUntil);
|
|
998
|
+
}
|
|
999
|
+
if (updates.lastSeenAt !== undefined) {
|
|
1000
|
+
fields.push('last_seen_at = ?');
|
|
1001
|
+
values.push(updates.lastSeenAt);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
if (fields.length === 0) return;
|
|
1005
|
+
|
|
1006
|
+
values.push(id);
|
|
1007
|
+
const stmt = this.db.prepare(`UPDATE channel_users SET ${fields.join(', ')} WHERE id = ?`);
|
|
1008
|
+
stmt.run(...values);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
findById(id: string): ChannelUser | undefined {
|
|
1012
|
+
const stmt = this.db.prepare('SELECT * FROM channel_users WHERE id = ?');
|
|
1013
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
1014
|
+
return row ? this.mapRowToUser(row) : undefined;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
findByChannelUserId(channelId: string, channelUserId: string): ChannelUser | undefined {
|
|
1018
|
+
const stmt = this.db.prepare('SELECT * FROM channel_users WHERE channel_id = ? AND channel_user_id = ?');
|
|
1019
|
+
const row = stmt.get(channelId, channelUserId) as Record<string, unknown> | undefined;
|
|
1020
|
+
return row ? this.mapRowToUser(row) : undefined;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
findByChannelId(channelId: string): ChannelUser[] {
|
|
1024
|
+
const stmt = this.db.prepare('SELECT * FROM channel_users WHERE channel_id = ? ORDER BY last_seen_at DESC');
|
|
1025
|
+
const rows = stmt.all(channelId) as Record<string, unknown>[];
|
|
1026
|
+
return rows.map(row => this.mapRowToUser(row));
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
findAllowedByChannelId(channelId: string): ChannelUser[] {
|
|
1030
|
+
const stmt = this.db.prepare('SELECT * FROM channel_users WHERE channel_id = ? AND allowed = 1 ORDER BY last_seen_at DESC');
|
|
1031
|
+
const rows = stmt.all(channelId) as Record<string, unknown>[];
|
|
1032
|
+
return rows.map(row => this.mapRowToUser(row));
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
deleteByChannelId(channelId: string): void {
|
|
1036
|
+
const stmt = this.db.prepare('DELETE FROM channel_users WHERE channel_id = ?');
|
|
1037
|
+
stmt.run(channelId);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
delete(id: string): void {
|
|
1041
|
+
const stmt = this.db.prepare('DELETE FROM channel_users WHERE id = ?');
|
|
1042
|
+
stmt.run(id);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Delete expired pending pairing entries
|
|
1047
|
+
* These are placeholder entries created when generating pairing codes that have expired
|
|
1048
|
+
* Returns the number of deleted entries
|
|
1049
|
+
*/
|
|
1050
|
+
deleteExpiredPending(channelId: string): number {
|
|
1051
|
+
const now = Date.now();
|
|
1052
|
+
const stmt = this.db.prepare(`
|
|
1053
|
+
DELETE FROM channel_users
|
|
1054
|
+
WHERE channel_id = ?
|
|
1055
|
+
AND allowed = 0
|
|
1056
|
+
AND channel_user_id LIKE 'pending_%'
|
|
1057
|
+
AND pairing_expires_at IS NOT NULL
|
|
1058
|
+
AND pairing_expires_at < ?
|
|
1059
|
+
`);
|
|
1060
|
+
const result = stmt.run(channelId, now);
|
|
1061
|
+
return result.changes;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
findByPairingCode(channelId: string, pairingCode: string): ChannelUser | undefined {
|
|
1065
|
+
const stmt = this.db.prepare('SELECT * FROM channel_users WHERE channel_id = ? AND UPPER(pairing_code) = UPPER(?)');
|
|
1066
|
+
const row = stmt.get(channelId, pairingCode) as Record<string, unknown> | undefined;
|
|
1067
|
+
return row ? this.mapRowToUser(row) : undefined;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
private mapRowToUser(row: Record<string, unknown>): ChannelUser {
|
|
1071
|
+
return {
|
|
1072
|
+
id: row.id as string,
|
|
1073
|
+
channelId: row.channel_id as string,
|
|
1074
|
+
channelUserId: row.channel_user_id as string,
|
|
1075
|
+
displayName: row.display_name as string,
|
|
1076
|
+
username: (row.username as string) || undefined,
|
|
1077
|
+
allowed: row.allowed === 1,
|
|
1078
|
+
pairingCode: (row.pairing_code as string) || undefined,
|
|
1079
|
+
pairingAttempts: row.pairing_attempts as number,
|
|
1080
|
+
pairingExpiresAt: (row.pairing_expires_at as number) || undefined,
|
|
1081
|
+
lockoutUntil: (row.lockout_until as number) || undefined,
|
|
1082
|
+
createdAt: row.created_at as number,
|
|
1083
|
+
lastSeenAt: row.last_seen_at as number,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
export class ChannelSessionRepository {
|
|
1089
|
+
constructor(private db: Database.Database) {}
|
|
1090
|
+
|
|
1091
|
+
create(session: Omit<ChannelSession, 'id' | 'createdAt' | 'lastActivityAt'>): ChannelSession {
|
|
1092
|
+
const now = Date.now();
|
|
1093
|
+
const newSession: ChannelSession = {
|
|
1094
|
+
...session,
|
|
1095
|
+
id: uuidv4(),
|
|
1096
|
+
createdAt: now,
|
|
1097
|
+
lastActivityAt: now,
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
const stmt = this.db.prepare(`
|
|
1101
|
+
INSERT INTO channel_sessions (id, channel_id, chat_id, user_id, task_id, workspace_id, state, context, created_at, last_activity_at)
|
|
1102
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1103
|
+
`);
|
|
1104
|
+
|
|
1105
|
+
stmt.run(
|
|
1106
|
+
newSession.id,
|
|
1107
|
+
newSession.channelId,
|
|
1108
|
+
newSession.chatId,
|
|
1109
|
+
newSession.userId || null,
|
|
1110
|
+
newSession.taskId || null,
|
|
1111
|
+
newSession.workspaceId || null,
|
|
1112
|
+
newSession.state,
|
|
1113
|
+
newSession.context ? JSON.stringify(newSession.context) : null,
|
|
1114
|
+
newSession.createdAt,
|
|
1115
|
+
newSession.lastActivityAt
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
return newSession;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
update(id: string, updates: Partial<ChannelSession>): void {
|
|
1122
|
+
const fields: string[] = [];
|
|
1123
|
+
const values: unknown[] = [];
|
|
1124
|
+
|
|
1125
|
+
// Use 'in' check to allow setting fields to null/undefined (clearing them)
|
|
1126
|
+
if ('taskId' in updates) {
|
|
1127
|
+
fields.push('task_id = ?');
|
|
1128
|
+
values.push(updates.taskId ?? null); // Convert undefined to null for SQLite
|
|
1129
|
+
}
|
|
1130
|
+
if ('workspaceId' in updates) {
|
|
1131
|
+
fields.push('workspace_id = ?');
|
|
1132
|
+
values.push(updates.workspaceId ?? null);
|
|
1133
|
+
}
|
|
1134
|
+
if ('state' in updates) {
|
|
1135
|
+
fields.push('state = ?');
|
|
1136
|
+
values.push(updates.state);
|
|
1137
|
+
}
|
|
1138
|
+
if ('lastActivityAt' in updates) {
|
|
1139
|
+
fields.push('last_activity_at = ?');
|
|
1140
|
+
values.push(updates.lastActivityAt);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Handle shellEnabled and debugMode by merging into context
|
|
1144
|
+
const hasContextUpdate = 'context' in updates || 'shellEnabled' in updates || 'debugMode' in updates;
|
|
1145
|
+
if (hasContextUpdate) {
|
|
1146
|
+
// Load existing session to merge context
|
|
1147
|
+
const existing = this.findById(id);
|
|
1148
|
+
const existingContext = existing?.context || {};
|
|
1149
|
+
const newContext = {
|
|
1150
|
+
...existingContext,
|
|
1151
|
+
...('context' in updates ? updates.context : {}),
|
|
1152
|
+
...('shellEnabled' in updates ? { shellEnabled: updates.shellEnabled } : {}),
|
|
1153
|
+
...('debugMode' in updates ? { debugMode: updates.debugMode } : {}),
|
|
1154
|
+
};
|
|
1155
|
+
fields.push('context = ?');
|
|
1156
|
+
values.push(JSON.stringify(newContext));
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (fields.length === 0) return;
|
|
1160
|
+
|
|
1161
|
+
values.push(id);
|
|
1162
|
+
const stmt = this.db.prepare(`UPDATE channel_sessions SET ${fields.join(', ')} WHERE id = ?`);
|
|
1163
|
+
stmt.run(...values);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
findById(id: string): ChannelSession | undefined {
|
|
1167
|
+
const stmt = this.db.prepare('SELECT * FROM channel_sessions WHERE id = ?');
|
|
1168
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
1169
|
+
return row ? this.mapRowToSession(row) : undefined;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
findByChatId(channelId: string, chatId: string): ChannelSession | undefined {
|
|
1173
|
+
const stmt = this.db.prepare('SELECT * FROM channel_sessions WHERE channel_id = ? AND chat_id = ? ORDER BY last_activity_at DESC LIMIT 1');
|
|
1174
|
+
const row = stmt.get(channelId, chatId) as Record<string, unknown> | undefined;
|
|
1175
|
+
return row ? this.mapRowToSession(row) : undefined;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
findByTaskId(taskId: string): ChannelSession | undefined {
|
|
1179
|
+
const stmt = this.db.prepare('SELECT * FROM channel_sessions WHERE task_id = ?');
|
|
1180
|
+
const row = stmt.get(taskId) as Record<string, unknown> | undefined;
|
|
1181
|
+
return row ? this.mapRowToSession(row) : undefined;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
findActiveByChannelId(channelId: string): ChannelSession[] {
|
|
1185
|
+
const stmt = this.db.prepare("SELECT * FROM channel_sessions WHERE channel_id = ? AND state != 'idle' ORDER BY last_activity_at DESC");
|
|
1186
|
+
const rows = stmt.all(channelId) as Record<string, unknown>[];
|
|
1187
|
+
return rows.map(row => this.mapRowToSession(row));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
deleteByChannelId(channelId: string): void {
|
|
1191
|
+
const stmt = this.db.prepare('DELETE FROM channel_sessions WHERE channel_id = ?');
|
|
1192
|
+
stmt.run(channelId);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
private mapRowToSession(row: Record<string, unknown>): ChannelSession {
|
|
1196
|
+
const context = row.context ? safeJsonParse(row.context as string, {} as Record<string, unknown>, 'session.context') : undefined;
|
|
1197
|
+
// Extract shellEnabled and debugMode from context
|
|
1198
|
+
const shellEnabled = context?.shellEnabled as boolean | undefined;
|
|
1199
|
+
const debugMode = context?.debugMode as boolean | undefined;
|
|
1200
|
+
return {
|
|
1201
|
+
id: row.id as string,
|
|
1202
|
+
channelId: row.channel_id as string,
|
|
1203
|
+
chatId: row.chat_id as string,
|
|
1204
|
+
userId: (row.user_id as string) || undefined,
|
|
1205
|
+
taskId: (row.task_id as string) || undefined,
|
|
1206
|
+
workspaceId: (row.workspace_id as string) || undefined,
|
|
1207
|
+
state: row.state as 'idle' | 'active' | 'waiting_approval',
|
|
1208
|
+
context,
|
|
1209
|
+
shellEnabled,
|
|
1210
|
+
debugMode,
|
|
1211
|
+
createdAt: row.created_at as number,
|
|
1212
|
+
lastActivityAt: row.last_activity_at as number,
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export class ChannelMessageRepository {
|
|
1218
|
+
constructor(private db: Database.Database) {}
|
|
1219
|
+
|
|
1220
|
+
create(message: Omit<ChannelMessage, 'id'>): ChannelMessage {
|
|
1221
|
+
const newMessage: ChannelMessage = {
|
|
1222
|
+
...message,
|
|
1223
|
+
id: uuidv4(),
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
const stmt = this.db.prepare(`
|
|
1227
|
+
INSERT INTO channel_messages (id, channel_id, session_id, channel_message_id, chat_id, user_id, direction, content, attachments, timestamp)
|
|
1228
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1229
|
+
`);
|
|
1230
|
+
|
|
1231
|
+
stmt.run(
|
|
1232
|
+
newMessage.id,
|
|
1233
|
+
newMessage.channelId,
|
|
1234
|
+
newMessage.sessionId || null,
|
|
1235
|
+
newMessage.channelMessageId,
|
|
1236
|
+
newMessage.chatId,
|
|
1237
|
+
newMessage.userId || null,
|
|
1238
|
+
newMessage.direction,
|
|
1239
|
+
newMessage.content,
|
|
1240
|
+
newMessage.attachments ? JSON.stringify(newMessage.attachments) : null,
|
|
1241
|
+
newMessage.timestamp
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
return newMessage;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
findBySessionId(sessionId: string, limit = 50): ChannelMessage[] {
|
|
1248
|
+
const stmt = this.db.prepare('SELECT * FROM channel_messages WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?');
|
|
1249
|
+
const rows = stmt.all(sessionId, limit) as Record<string, unknown>[];
|
|
1250
|
+
return rows.map(row => this.mapRowToMessage(row)).reverse();
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
findByChatId(channelId: string, chatId: string, limit = 50): ChannelMessage[] {
|
|
1254
|
+
const stmt = this.db.prepare('SELECT * FROM channel_messages WHERE channel_id = ? AND chat_id = ? ORDER BY timestamp DESC LIMIT ?');
|
|
1255
|
+
const rows = stmt.all(channelId, chatId, limit) as Record<string, unknown>[];
|
|
1256
|
+
return rows.map(row => this.mapRowToMessage(row)).reverse();
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
deleteByChannelId(channelId: string): void {
|
|
1260
|
+
const stmt = this.db.prepare('DELETE FROM channel_messages WHERE channel_id = ?');
|
|
1261
|
+
stmt.run(channelId);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
private mapRowToMessage(row: Record<string, unknown>): ChannelMessage {
|
|
1265
|
+
return {
|
|
1266
|
+
id: row.id as string,
|
|
1267
|
+
channelId: row.channel_id as string,
|
|
1268
|
+
sessionId: (row.session_id as string) || undefined,
|
|
1269
|
+
channelMessageId: row.channel_message_id as string,
|
|
1270
|
+
chatId: row.chat_id as string,
|
|
1271
|
+
userId: (row.user_id as string) || undefined,
|
|
1272
|
+
direction: row.direction as 'incoming' | 'outgoing',
|
|
1273
|
+
content: row.content as string,
|
|
1274
|
+
attachments: row.attachments ? safeJsonParse(row.attachments as string, undefined, 'message.attachments') : undefined,
|
|
1275
|
+
timestamp: row.timestamp as number,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ============================================================
|
|
1281
|
+
// Gateway Infrastructure Repositories
|
|
1282
|
+
// ============================================================
|
|
1283
|
+
|
|
1284
|
+
export interface QueuedMessage {
|
|
1285
|
+
id: string;
|
|
1286
|
+
channelType: string;
|
|
1287
|
+
chatId: string;
|
|
1288
|
+
message: Record<string, unknown>;
|
|
1289
|
+
priority: number;
|
|
1290
|
+
status: 'pending' | 'processing' | 'sent' | 'failed';
|
|
1291
|
+
attempts: number;
|
|
1292
|
+
maxAttempts: number;
|
|
1293
|
+
lastAttemptAt?: number;
|
|
1294
|
+
error?: string;
|
|
1295
|
+
createdAt: number;
|
|
1296
|
+
scheduledAt?: number;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
export interface ScheduledMessage {
|
|
1300
|
+
id: string;
|
|
1301
|
+
channelType: string;
|
|
1302
|
+
chatId: string;
|
|
1303
|
+
message: Record<string, unknown>;
|
|
1304
|
+
scheduledAt: number;
|
|
1305
|
+
status: 'pending' | 'sent' | 'failed' | 'cancelled';
|
|
1306
|
+
sentMessageId?: string;
|
|
1307
|
+
error?: string;
|
|
1308
|
+
createdAt: number;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
export interface DeliveryRecord {
|
|
1312
|
+
id: string;
|
|
1313
|
+
channelType: string;
|
|
1314
|
+
chatId: string;
|
|
1315
|
+
messageId: string;
|
|
1316
|
+
status: 'pending' | 'sent' | 'delivered' | 'read' | 'failed';
|
|
1317
|
+
sentAt?: number;
|
|
1318
|
+
deliveredAt?: number;
|
|
1319
|
+
readAt?: number;
|
|
1320
|
+
error?: string;
|
|
1321
|
+
createdAt: number;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
export interface RateLimitRecord {
|
|
1325
|
+
id: string;
|
|
1326
|
+
channelType: string;
|
|
1327
|
+
userId: string;
|
|
1328
|
+
messageCount: number;
|
|
1329
|
+
windowStart: number;
|
|
1330
|
+
isLimited: boolean;
|
|
1331
|
+
limitExpiresAt?: number;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
export interface AuditLogEntry {
|
|
1335
|
+
id: string;
|
|
1336
|
+
timestamp: number;
|
|
1337
|
+
action: string;
|
|
1338
|
+
channelType?: string;
|
|
1339
|
+
userId?: string;
|
|
1340
|
+
chatId?: string;
|
|
1341
|
+
details?: Record<string, unknown>;
|
|
1342
|
+
severity: 'debug' | 'info' | 'warn' | 'error';
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
export class MessageQueueRepository {
|
|
1346
|
+
constructor(private db: Database.Database) {}
|
|
1347
|
+
|
|
1348
|
+
enqueue(item: Omit<QueuedMessage, 'id' | 'createdAt' | 'attempts' | 'status'>): QueuedMessage {
|
|
1349
|
+
const newItem: QueuedMessage = {
|
|
1350
|
+
...item,
|
|
1351
|
+
id: uuidv4(),
|
|
1352
|
+
status: 'pending',
|
|
1353
|
+
attempts: 0,
|
|
1354
|
+
createdAt: Date.now(),
|
|
1355
|
+
};
|
|
1356
|
+
|
|
1357
|
+
const stmt = this.db.prepare(`
|
|
1358
|
+
INSERT INTO message_queue (id, channel_type, chat_id, message, priority, status, attempts, max_attempts, last_attempt_at, error, created_at, scheduled_at)
|
|
1359
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1360
|
+
`);
|
|
1361
|
+
|
|
1362
|
+
stmt.run(
|
|
1363
|
+
newItem.id,
|
|
1364
|
+
newItem.channelType,
|
|
1365
|
+
newItem.chatId,
|
|
1366
|
+
JSON.stringify(newItem.message),
|
|
1367
|
+
newItem.priority,
|
|
1368
|
+
newItem.status,
|
|
1369
|
+
newItem.attempts,
|
|
1370
|
+
newItem.maxAttempts,
|
|
1371
|
+
newItem.lastAttemptAt || null,
|
|
1372
|
+
newItem.error || null,
|
|
1373
|
+
newItem.createdAt,
|
|
1374
|
+
newItem.scheduledAt || null
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
return newItem;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
update(id: string, updates: Partial<QueuedMessage>): void {
|
|
1381
|
+
const fields: string[] = [];
|
|
1382
|
+
const values: unknown[] = [];
|
|
1383
|
+
|
|
1384
|
+
if (updates.status !== undefined) {
|
|
1385
|
+
fields.push('status = ?');
|
|
1386
|
+
values.push(updates.status);
|
|
1387
|
+
}
|
|
1388
|
+
if (updates.attempts !== undefined) {
|
|
1389
|
+
fields.push('attempts = ?');
|
|
1390
|
+
values.push(updates.attempts);
|
|
1391
|
+
}
|
|
1392
|
+
if (updates.lastAttemptAt !== undefined) {
|
|
1393
|
+
fields.push('last_attempt_at = ?');
|
|
1394
|
+
values.push(updates.lastAttemptAt);
|
|
1395
|
+
}
|
|
1396
|
+
if (updates.error !== undefined) {
|
|
1397
|
+
fields.push('error = ?');
|
|
1398
|
+
values.push(updates.error);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (fields.length === 0) return;
|
|
1402
|
+
|
|
1403
|
+
values.push(id);
|
|
1404
|
+
const stmt = this.db.prepare(`UPDATE message_queue SET ${fields.join(', ')} WHERE id = ?`);
|
|
1405
|
+
stmt.run(...values);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
findPending(limit = 50): QueuedMessage[] {
|
|
1409
|
+
const now = Date.now();
|
|
1410
|
+
const stmt = this.db.prepare(`
|
|
1411
|
+
SELECT * FROM message_queue
|
|
1412
|
+
WHERE status = 'pending' AND (scheduled_at IS NULL OR scheduled_at <= ?)
|
|
1413
|
+
ORDER BY priority DESC, created_at ASC
|
|
1414
|
+
LIMIT ?
|
|
1415
|
+
`);
|
|
1416
|
+
const rows = stmt.all(now, limit) as Record<string, unknown>[];
|
|
1417
|
+
return rows.map(row => this.mapRowToItem(row));
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
findById(id: string): QueuedMessage | undefined {
|
|
1421
|
+
const stmt = this.db.prepare('SELECT * FROM message_queue WHERE id = ?');
|
|
1422
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
1423
|
+
return row ? this.mapRowToItem(row) : undefined;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
delete(id: string): void {
|
|
1427
|
+
const stmt = this.db.prepare('DELETE FROM message_queue WHERE id = ?');
|
|
1428
|
+
stmt.run(id);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
deleteOld(olderThanMs: number): number {
|
|
1432
|
+
const cutoff = Date.now() - olderThanMs;
|
|
1433
|
+
const stmt = this.db.prepare("DELETE FROM message_queue WHERE status IN ('sent', 'failed') AND created_at < ?");
|
|
1434
|
+
const result = stmt.run(cutoff);
|
|
1435
|
+
return result.changes;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private mapRowToItem(row: Record<string, unknown>): QueuedMessage {
|
|
1439
|
+
return {
|
|
1440
|
+
id: row.id as string,
|
|
1441
|
+
channelType: row.channel_type as string,
|
|
1442
|
+
chatId: row.chat_id as string,
|
|
1443
|
+
message: safeJsonParse(row.message as string, {}, 'queue.message'),
|
|
1444
|
+
priority: row.priority as number,
|
|
1445
|
+
status: row.status as QueuedMessage['status'],
|
|
1446
|
+
attempts: row.attempts as number,
|
|
1447
|
+
maxAttempts: row.max_attempts as number,
|
|
1448
|
+
lastAttemptAt: (row.last_attempt_at as number) || undefined,
|
|
1449
|
+
error: (row.error as string) || undefined,
|
|
1450
|
+
createdAt: row.created_at as number,
|
|
1451
|
+
scheduledAt: (row.scheduled_at as number) || undefined,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
export class ScheduledMessageRepository {
|
|
1457
|
+
constructor(private db: Database.Database) {}
|
|
1458
|
+
|
|
1459
|
+
create(item: Omit<ScheduledMessage, 'id' | 'createdAt' | 'status'>): ScheduledMessage {
|
|
1460
|
+
const newItem: ScheduledMessage = {
|
|
1461
|
+
...item,
|
|
1462
|
+
id: uuidv4(),
|
|
1463
|
+
status: 'pending',
|
|
1464
|
+
createdAt: Date.now(),
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
const stmt = this.db.prepare(`
|
|
1468
|
+
INSERT INTO scheduled_messages (id, channel_type, chat_id, message, scheduled_at, status, sent_message_id, error, created_at)
|
|
1469
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1470
|
+
`);
|
|
1471
|
+
|
|
1472
|
+
stmt.run(
|
|
1473
|
+
newItem.id,
|
|
1474
|
+
newItem.channelType,
|
|
1475
|
+
newItem.chatId,
|
|
1476
|
+
JSON.stringify(newItem.message),
|
|
1477
|
+
newItem.scheduledAt,
|
|
1478
|
+
newItem.status,
|
|
1479
|
+
newItem.sentMessageId || null,
|
|
1480
|
+
newItem.error || null,
|
|
1481
|
+
newItem.createdAt
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1484
|
+
return newItem;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
update(id: string, updates: Partial<ScheduledMessage>): void {
|
|
1488
|
+
const fields: string[] = [];
|
|
1489
|
+
const values: unknown[] = [];
|
|
1490
|
+
|
|
1491
|
+
if (updates.status !== undefined) {
|
|
1492
|
+
fields.push('status = ?');
|
|
1493
|
+
values.push(updates.status);
|
|
1494
|
+
}
|
|
1495
|
+
if (updates.sentMessageId !== undefined) {
|
|
1496
|
+
fields.push('sent_message_id = ?');
|
|
1497
|
+
values.push(updates.sentMessageId);
|
|
1498
|
+
}
|
|
1499
|
+
if (updates.error !== undefined) {
|
|
1500
|
+
fields.push('error = ?');
|
|
1501
|
+
values.push(updates.error);
|
|
1502
|
+
}
|
|
1503
|
+
if (updates.scheduledAt !== undefined) {
|
|
1504
|
+
fields.push('scheduled_at = ?');
|
|
1505
|
+
values.push(updates.scheduledAt);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (fields.length === 0) return;
|
|
1509
|
+
|
|
1510
|
+
values.push(id);
|
|
1511
|
+
const stmt = this.db.prepare(`UPDATE scheduled_messages SET ${fields.join(', ')} WHERE id = ?`);
|
|
1512
|
+
stmt.run(...values);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
findDue(limit = 50): ScheduledMessage[] {
|
|
1516
|
+
const now = Date.now();
|
|
1517
|
+
const stmt = this.db.prepare(`
|
|
1518
|
+
SELECT * FROM scheduled_messages
|
|
1519
|
+
WHERE status = 'pending' AND scheduled_at <= ?
|
|
1520
|
+
ORDER BY scheduled_at ASC
|
|
1521
|
+
LIMIT ?
|
|
1522
|
+
`);
|
|
1523
|
+
const rows = stmt.all(now, limit) as Record<string, unknown>[];
|
|
1524
|
+
return rows.map(row => this.mapRowToItem(row));
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
findById(id: string): ScheduledMessage | undefined {
|
|
1528
|
+
const stmt = this.db.prepare('SELECT * FROM scheduled_messages WHERE id = ?');
|
|
1529
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
1530
|
+
return row ? this.mapRowToItem(row) : undefined;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
findByChatId(channelType: string, chatId: string): ScheduledMessage[] {
|
|
1534
|
+
const stmt = this.db.prepare(`
|
|
1535
|
+
SELECT * FROM scheduled_messages
|
|
1536
|
+
WHERE channel_type = ? AND chat_id = ? AND status = 'pending'
|
|
1537
|
+
ORDER BY scheduled_at ASC
|
|
1538
|
+
`);
|
|
1539
|
+
const rows = stmt.all(channelType, chatId) as Record<string, unknown>[];
|
|
1540
|
+
return rows.map(row => this.mapRowToItem(row));
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
cancel(id: string): void {
|
|
1544
|
+
const stmt = this.db.prepare("UPDATE scheduled_messages SET status = 'cancelled' WHERE id = ? AND status = 'pending'");
|
|
1545
|
+
stmt.run(id);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
delete(id: string): void {
|
|
1549
|
+
const stmt = this.db.prepare('DELETE FROM scheduled_messages WHERE id = ?');
|
|
1550
|
+
stmt.run(id);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
private mapRowToItem(row: Record<string, unknown>): ScheduledMessage {
|
|
1554
|
+
return {
|
|
1555
|
+
id: row.id as string,
|
|
1556
|
+
channelType: row.channel_type as string,
|
|
1557
|
+
chatId: row.chat_id as string,
|
|
1558
|
+
message: safeJsonParse(row.message as string, {}, 'scheduled.message'),
|
|
1559
|
+
scheduledAt: row.scheduled_at as number,
|
|
1560
|
+
status: row.status as ScheduledMessage['status'],
|
|
1561
|
+
sentMessageId: (row.sent_message_id as string) || undefined,
|
|
1562
|
+
error: (row.error as string) || undefined,
|
|
1563
|
+
createdAt: row.created_at as number,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
export class DeliveryTrackingRepository {
|
|
1569
|
+
constructor(private db: Database.Database) {}
|
|
1570
|
+
|
|
1571
|
+
create(item: Omit<DeliveryRecord, 'id' | 'createdAt'>): DeliveryRecord {
|
|
1572
|
+
const newItem: DeliveryRecord = {
|
|
1573
|
+
...item,
|
|
1574
|
+
id: uuidv4(),
|
|
1575
|
+
createdAt: Date.now(),
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
const stmt = this.db.prepare(`
|
|
1579
|
+
INSERT INTO delivery_tracking (id, channel_type, chat_id, message_id, status, sent_at, delivered_at, read_at, error, created_at)
|
|
1580
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1581
|
+
`);
|
|
1582
|
+
|
|
1583
|
+
stmt.run(
|
|
1584
|
+
newItem.id,
|
|
1585
|
+
newItem.channelType,
|
|
1586
|
+
newItem.chatId,
|
|
1587
|
+
newItem.messageId,
|
|
1588
|
+
newItem.status,
|
|
1589
|
+
newItem.sentAt || null,
|
|
1590
|
+
newItem.deliveredAt || null,
|
|
1591
|
+
newItem.readAt || null,
|
|
1592
|
+
newItem.error || null,
|
|
1593
|
+
newItem.createdAt
|
|
1594
|
+
);
|
|
1595
|
+
|
|
1596
|
+
return newItem;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
update(id: string, updates: Partial<DeliveryRecord>): void {
|
|
1600
|
+
const fields: string[] = [];
|
|
1601
|
+
const values: unknown[] = [];
|
|
1602
|
+
|
|
1603
|
+
if (updates.status !== undefined) {
|
|
1604
|
+
fields.push('status = ?');
|
|
1605
|
+
values.push(updates.status);
|
|
1606
|
+
}
|
|
1607
|
+
if (updates.sentAt !== undefined) {
|
|
1608
|
+
fields.push('sent_at = ?');
|
|
1609
|
+
values.push(updates.sentAt);
|
|
1610
|
+
}
|
|
1611
|
+
if (updates.deliveredAt !== undefined) {
|
|
1612
|
+
fields.push('delivered_at = ?');
|
|
1613
|
+
values.push(updates.deliveredAt);
|
|
1614
|
+
}
|
|
1615
|
+
if (updates.readAt !== undefined) {
|
|
1616
|
+
fields.push('read_at = ?');
|
|
1617
|
+
values.push(updates.readAt);
|
|
1618
|
+
}
|
|
1619
|
+
if (updates.error !== undefined) {
|
|
1620
|
+
fields.push('error = ?');
|
|
1621
|
+
values.push(updates.error);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
if (fields.length === 0) return;
|
|
1625
|
+
|
|
1626
|
+
values.push(id);
|
|
1627
|
+
const stmt = this.db.prepare(`UPDATE delivery_tracking SET ${fields.join(', ')} WHERE id = ?`);
|
|
1628
|
+
stmt.run(...values);
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
findByMessageId(messageId: string): DeliveryRecord | undefined {
|
|
1632
|
+
const stmt = this.db.prepare('SELECT * FROM delivery_tracking WHERE message_id = ?');
|
|
1633
|
+
const row = stmt.get(messageId) as Record<string, unknown> | undefined;
|
|
1634
|
+
return row ? this.mapRowToItem(row) : undefined;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
findByChatId(channelType: string, chatId: string, limit = 50): DeliveryRecord[] {
|
|
1638
|
+
const stmt = this.db.prepare(`
|
|
1639
|
+
SELECT * FROM delivery_tracking
|
|
1640
|
+
WHERE channel_type = ? AND chat_id = ?
|
|
1641
|
+
ORDER BY created_at DESC
|
|
1642
|
+
LIMIT ?
|
|
1643
|
+
`);
|
|
1644
|
+
const rows = stmt.all(channelType, chatId, limit) as Record<string, unknown>[];
|
|
1645
|
+
return rows.map(row => this.mapRowToItem(row));
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
deleteOld(olderThanMs: number): number {
|
|
1649
|
+
const cutoff = Date.now() - olderThanMs;
|
|
1650
|
+
const stmt = this.db.prepare('DELETE FROM delivery_tracking WHERE created_at < ?');
|
|
1651
|
+
const result = stmt.run(cutoff);
|
|
1652
|
+
return result.changes;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
private mapRowToItem(row: Record<string, unknown>): DeliveryRecord {
|
|
1656
|
+
return {
|
|
1657
|
+
id: row.id as string,
|
|
1658
|
+
channelType: row.channel_type as string,
|
|
1659
|
+
chatId: row.chat_id as string,
|
|
1660
|
+
messageId: row.message_id as string,
|
|
1661
|
+
status: row.status as DeliveryRecord['status'],
|
|
1662
|
+
sentAt: (row.sent_at as number) || undefined,
|
|
1663
|
+
deliveredAt: (row.delivered_at as number) || undefined,
|
|
1664
|
+
readAt: (row.read_at as number) || undefined,
|
|
1665
|
+
error: (row.error as string) || undefined,
|
|
1666
|
+
createdAt: row.created_at as number,
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
export class RateLimitRepository {
|
|
1672
|
+
constructor(private db: Database.Database) {}
|
|
1673
|
+
|
|
1674
|
+
getOrCreate(channelType: string, userId: string): RateLimitRecord {
|
|
1675
|
+
const stmt = this.db.prepare('SELECT * FROM rate_limits WHERE channel_type = ? AND user_id = ?');
|
|
1676
|
+
const row = stmt.get(channelType, userId) as Record<string, unknown> | undefined;
|
|
1677
|
+
|
|
1678
|
+
if (row) {
|
|
1679
|
+
return this.mapRowToItem(row);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Create new record
|
|
1683
|
+
const newItem: RateLimitRecord = {
|
|
1684
|
+
id: uuidv4(),
|
|
1685
|
+
channelType,
|
|
1686
|
+
userId,
|
|
1687
|
+
messageCount: 0,
|
|
1688
|
+
windowStart: Date.now(),
|
|
1689
|
+
isLimited: false,
|
|
1690
|
+
};
|
|
1691
|
+
|
|
1692
|
+
const insertStmt = this.db.prepare(`
|
|
1693
|
+
INSERT INTO rate_limits (id, channel_type, user_id, message_count, window_start, is_limited, limit_expires_at)
|
|
1694
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1695
|
+
`);
|
|
1696
|
+
|
|
1697
|
+
insertStmt.run(
|
|
1698
|
+
newItem.id,
|
|
1699
|
+
newItem.channelType,
|
|
1700
|
+
newItem.userId,
|
|
1701
|
+
newItem.messageCount,
|
|
1702
|
+
newItem.windowStart,
|
|
1703
|
+
newItem.isLimited ? 1 : 0,
|
|
1704
|
+
newItem.limitExpiresAt || null
|
|
1705
|
+
);
|
|
1706
|
+
|
|
1707
|
+
return newItem;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
update(channelType: string, userId: string, updates: Partial<RateLimitRecord>): void {
|
|
1711
|
+
const fields: string[] = [];
|
|
1712
|
+
const values: unknown[] = [];
|
|
1713
|
+
|
|
1714
|
+
if (updates.messageCount !== undefined) {
|
|
1715
|
+
fields.push('message_count = ?');
|
|
1716
|
+
values.push(updates.messageCount);
|
|
1717
|
+
}
|
|
1718
|
+
if (updates.windowStart !== undefined) {
|
|
1719
|
+
fields.push('window_start = ?');
|
|
1720
|
+
values.push(updates.windowStart);
|
|
1721
|
+
}
|
|
1722
|
+
if (updates.isLimited !== undefined) {
|
|
1723
|
+
fields.push('is_limited = ?');
|
|
1724
|
+
values.push(updates.isLimited ? 1 : 0);
|
|
1725
|
+
}
|
|
1726
|
+
if (updates.limitExpiresAt !== undefined) {
|
|
1727
|
+
fields.push('limit_expires_at = ?');
|
|
1728
|
+
values.push(updates.limitExpiresAt);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (fields.length === 0) return;
|
|
1732
|
+
|
|
1733
|
+
values.push(channelType, userId);
|
|
1734
|
+
const stmt = this.db.prepare(`UPDATE rate_limits SET ${fields.join(', ')} WHERE channel_type = ? AND user_id = ?`);
|
|
1735
|
+
stmt.run(...values);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
resetWindow(channelType: string, userId: string): void {
|
|
1739
|
+
const stmt = this.db.prepare(`
|
|
1740
|
+
UPDATE rate_limits
|
|
1741
|
+
SET message_count = 0, window_start = ?, is_limited = 0, limit_expires_at = NULL
|
|
1742
|
+
WHERE channel_type = ? AND user_id = ?
|
|
1743
|
+
`);
|
|
1744
|
+
stmt.run(Date.now(), channelType, userId);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
private mapRowToItem(row: Record<string, unknown>): RateLimitRecord {
|
|
1748
|
+
return {
|
|
1749
|
+
id: row.id as string,
|
|
1750
|
+
channelType: row.channel_type as string,
|
|
1751
|
+
userId: row.user_id as string,
|
|
1752
|
+
messageCount: row.message_count as number,
|
|
1753
|
+
windowStart: row.window_start as number,
|
|
1754
|
+
isLimited: row.is_limited === 1,
|
|
1755
|
+
limitExpiresAt: (row.limit_expires_at as number) || undefined,
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
export class AuditLogRepository {
|
|
1761
|
+
constructor(private db: Database.Database) {}
|
|
1762
|
+
|
|
1763
|
+
log(entry: Omit<AuditLogEntry, 'id' | 'timestamp'>): AuditLogEntry {
|
|
1764
|
+
const newEntry: AuditLogEntry = {
|
|
1765
|
+
...entry,
|
|
1766
|
+
id: uuidv4(),
|
|
1767
|
+
timestamp: Date.now(),
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
const stmt = this.db.prepare(`
|
|
1771
|
+
INSERT INTO audit_log (id, timestamp, action, channel_type, user_id, chat_id, details, severity)
|
|
1772
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1773
|
+
`);
|
|
1774
|
+
|
|
1775
|
+
stmt.run(
|
|
1776
|
+
newEntry.id,
|
|
1777
|
+
newEntry.timestamp,
|
|
1778
|
+
newEntry.action,
|
|
1779
|
+
newEntry.channelType || null,
|
|
1780
|
+
newEntry.userId || null,
|
|
1781
|
+
newEntry.chatId || null,
|
|
1782
|
+
newEntry.details ? JSON.stringify(newEntry.details) : null,
|
|
1783
|
+
newEntry.severity
|
|
1784
|
+
);
|
|
1785
|
+
|
|
1786
|
+
return newEntry;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
find(options: {
|
|
1790
|
+
action?: string;
|
|
1791
|
+
channelType?: string;
|
|
1792
|
+
userId?: string;
|
|
1793
|
+
chatId?: string;
|
|
1794
|
+
fromTimestamp?: number;
|
|
1795
|
+
toTimestamp?: number;
|
|
1796
|
+
severity?: AuditLogEntry['severity'];
|
|
1797
|
+
limit?: number;
|
|
1798
|
+
offset?: number;
|
|
1799
|
+
}): AuditLogEntry[] {
|
|
1800
|
+
const conditions: string[] = [];
|
|
1801
|
+
const values: unknown[] = [];
|
|
1802
|
+
|
|
1803
|
+
if (options.action) {
|
|
1804
|
+
conditions.push('action = ?');
|
|
1805
|
+
values.push(options.action);
|
|
1806
|
+
}
|
|
1807
|
+
if (options.channelType) {
|
|
1808
|
+
conditions.push('channel_type = ?');
|
|
1809
|
+
values.push(options.channelType);
|
|
1810
|
+
}
|
|
1811
|
+
if (options.userId) {
|
|
1812
|
+
conditions.push('user_id = ?');
|
|
1813
|
+
values.push(options.userId);
|
|
1814
|
+
}
|
|
1815
|
+
if (options.chatId) {
|
|
1816
|
+
conditions.push('chat_id = ?');
|
|
1817
|
+
values.push(options.chatId);
|
|
1818
|
+
}
|
|
1819
|
+
if (options.fromTimestamp) {
|
|
1820
|
+
conditions.push('timestamp >= ?');
|
|
1821
|
+
values.push(options.fromTimestamp);
|
|
1822
|
+
}
|
|
1823
|
+
if (options.toTimestamp) {
|
|
1824
|
+
conditions.push('timestamp <= ?');
|
|
1825
|
+
values.push(options.toTimestamp);
|
|
1826
|
+
}
|
|
1827
|
+
if (options.severity) {
|
|
1828
|
+
conditions.push('severity = ?');
|
|
1829
|
+
values.push(options.severity);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
1833
|
+
const limit = options.limit || 100;
|
|
1834
|
+
const offset = options.offset || 0;
|
|
1835
|
+
|
|
1836
|
+
const stmt = this.db.prepare(`
|
|
1837
|
+
SELECT * FROM audit_log
|
|
1838
|
+
${whereClause}
|
|
1839
|
+
ORDER BY timestamp DESC
|
|
1840
|
+
LIMIT ? OFFSET ?
|
|
1841
|
+
`);
|
|
1842
|
+
|
|
1843
|
+
values.push(limit, offset);
|
|
1844
|
+
const rows = stmt.all(...values) as Record<string, unknown>[];
|
|
1845
|
+
return rows.map(row => this.mapRowToEntry(row));
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
deleteOld(olderThanMs: number): number {
|
|
1849
|
+
const cutoff = Date.now() - olderThanMs;
|
|
1850
|
+
const stmt = this.db.prepare('DELETE FROM audit_log WHERE timestamp < ?');
|
|
1851
|
+
const result = stmt.run(cutoff);
|
|
1852
|
+
return result.changes;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
private mapRowToEntry(row: Record<string, unknown>): AuditLogEntry {
|
|
1856
|
+
return {
|
|
1857
|
+
id: row.id as string,
|
|
1858
|
+
timestamp: row.timestamp as number,
|
|
1859
|
+
action: row.action as string,
|
|
1860
|
+
channelType: (row.channel_type as string) || undefined,
|
|
1861
|
+
userId: (row.user_id as string) || undefined,
|
|
1862
|
+
chatId: (row.chat_id as string) || undefined,
|
|
1863
|
+
details: row.details ? safeJsonParse(row.details as string, undefined, 'audit.details') : undefined,
|
|
1864
|
+
severity: row.severity as AuditLogEntry['severity'],
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// ============================================================
|
|
1870
|
+
// Memory System Repositories
|
|
1871
|
+
// ============================================================
|
|
1872
|
+
|
|
1873
|
+
export type MemoryType = 'observation' | 'decision' | 'error' | 'insight' | 'summary';
|
|
1874
|
+
export type PrivacyMode = 'normal' | 'strict' | 'disabled';
|
|
1875
|
+
export type TimePeriod = 'hourly' | 'daily' | 'weekly';
|
|
1876
|
+
|
|
1877
|
+
export interface Memory {
|
|
1878
|
+
id: string;
|
|
1879
|
+
workspaceId: string;
|
|
1880
|
+
taskId?: string;
|
|
1881
|
+
type: MemoryType;
|
|
1882
|
+
content: string;
|
|
1883
|
+
summary?: string;
|
|
1884
|
+
tokens: number;
|
|
1885
|
+
isCompressed: boolean;
|
|
1886
|
+
isPrivate: boolean;
|
|
1887
|
+
createdAt: number;
|
|
1888
|
+
updatedAt: number;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
export interface MemorySummary {
|
|
1892
|
+
id: string;
|
|
1893
|
+
workspaceId: string;
|
|
1894
|
+
timePeriod: TimePeriod;
|
|
1895
|
+
periodStart: number;
|
|
1896
|
+
periodEnd: number;
|
|
1897
|
+
summary: string;
|
|
1898
|
+
memoryIds: string[];
|
|
1899
|
+
tokens: number;
|
|
1900
|
+
createdAt: number;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
export interface MemorySettings {
|
|
1904
|
+
workspaceId: string;
|
|
1905
|
+
enabled: boolean;
|
|
1906
|
+
autoCapture: boolean;
|
|
1907
|
+
compressionEnabled: boolean;
|
|
1908
|
+
retentionDays: number;
|
|
1909
|
+
maxStorageMb: number;
|
|
1910
|
+
privacyMode: PrivacyMode;
|
|
1911
|
+
excludedPatterns?: string[];
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
export interface MemorySearchResult {
|
|
1915
|
+
id: string;
|
|
1916
|
+
snippet: string;
|
|
1917
|
+
type: MemoryType;
|
|
1918
|
+
relevanceScore: number;
|
|
1919
|
+
createdAt: number;
|
|
1920
|
+
taskId?: string;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
export interface MemoryTimelineEntry {
|
|
1924
|
+
id: string;
|
|
1925
|
+
content: string;
|
|
1926
|
+
type: MemoryType;
|
|
1927
|
+
createdAt: number;
|
|
1928
|
+
taskId?: string;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
export interface MemoryStats {
|
|
1932
|
+
count: number;
|
|
1933
|
+
totalTokens: number;
|
|
1934
|
+
compressedCount: number;
|
|
1935
|
+
compressionRatio: number;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
export class MemoryRepository {
|
|
1939
|
+
constructor(private db: Database.Database) {}
|
|
1940
|
+
|
|
1941
|
+
create(memory: Omit<Memory, 'id' | 'createdAt' | 'updatedAt'>): Memory {
|
|
1942
|
+
const now = Date.now();
|
|
1943
|
+
const newMemory: Memory = {
|
|
1944
|
+
...memory,
|
|
1945
|
+
id: uuidv4(),
|
|
1946
|
+
createdAt: now,
|
|
1947
|
+
updatedAt: now,
|
|
1948
|
+
};
|
|
1949
|
+
|
|
1950
|
+
const stmt = this.db.prepare(`
|
|
1951
|
+
INSERT INTO memories (id, workspace_id, task_id, type, content, summary, tokens, is_compressed, is_private, created_at, updated_at)
|
|
1952
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1953
|
+
`);
|
|
1954
|
+
|
|
1955
|
+
stmt.run(
|
|
1956
|
+
newMemory.id,
|
|
1957
|
+
newMemory.workspaceId,
|
|
1958
|
+
newMemory.taskId || null,
|
|
1959
|
+
newMemory.type,
|
|
1960
|
+
newMemory.content,
|
|
1961
|
+
newMemory.summary || null,
|
|
1962
|
+
newMemory.tokens,
|
|
1963
|
+
newMemory.isCompressed ? 1 : 0,
|
|
1964
|
+
newMemory.isPrivate ? 1 : 0,
|
|
1965
|
+
newMemory.createdAt,
|
|
1966
|
+
newMemory.updatedAt
|
|
1967
|
+
);
|
|
1968
|
+
|
|
1969
|
+
return newMemory;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
update(id: string, updates: Partial<Pick<Memory, 'summary' | 'tokens' | 'isCompressed'>>): void {
|
|
1973
|
+
const fields: string[] = [];
|
|
1974
|
+
const values: unknown[] = [];
|
|
1975
|
+
|
|
1976
|
+
if (updates.summary !== undefined) {
|
|
1977
|
+
fields.push('summary = ?');
|
|
1978
|
+
values.push(updates.summary);
|
|
1979
|
+
}
|
|
1980
|
+
if (updates.tokens !== undefined) {
|
|
1981
|
+
fields.push('tokens = ?');
|
|
1982
|
+
values.push(updates.tokens);
|
|
1983
|
+
}
|
|
1984
|
+
if (updates.isCompressed !== undefined) {
|
|
1985
|
+
fields.push('is_compressed = ?');
|
|
1986
|
+
values.push(updates.isCompressed ? 1 : 0);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if (fields.length === 0) return;
|
|
1990
|
+
|
|
1991
|
+
fields.push('updated_at = ?');
|
|
1992
|
+
values.push(Date.now());
|
|
1993
|
+
values.push(id);
|
|
1994
|
+
|
|
1995
|
+
const stmt = this.db.prepare(`UPDATE memories SET ${fields.join(', ')} WHERE id = ?`);
|
|
1996
|
+
stmt.run(...values);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
findById(id: string): Memory | undefined {
|
|
2000
|
+
const stmt = this.db.prepare('SELECT * FROM memories WHERE id = ?');
|
|
2001
|
+
const row = stmt.get(id) as Record<string, unknown> | undefined;
|
|
2002
|
+
return row ? this.mapRowToMemory(row) : undefined;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
findByIds(ids: string[]): Memory[] {
|
|
2006
|
+
if (ids.length === 0) return [];
|
|
2007
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
2008
|
+
const stmt = this.db.prepare(`SELECT * FROM memories WHERE id IN (${placeholders})`);
|
|
2009
|
+
const rows = stmt.all(...ids) as Record<string, unknown>[];
|
|
2010
|
+
return rows.map(row => this.mapRowToMemory(row));
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Layer 1: Search returns IDs + brief snippets (~50 tokens each)
|
|
2015
|
+
* Uses FTS5 for full-text search with relevance ranking
|
|
2016
|
+
*/
|
|
2017
|
+
search(workspaceId: string, query: string, limit = 20): MemorySearchResult[] {
|
|
2018
|
+
try {
|
|
2019
|
+
// Try FTS5 search first
|
|
2020
|
+
const stmt = this.db.prepare(`
|
|
2021
|
+
SELECT m.id, m.summary, m.content, m.type, m.created_at, m.task_id,
|
|
2022
|
+
bm25(memories_fts) as score
|
|
2023
|
+
FROM memories_fts f
|
|
2024
|
+
JOIN memories m ON f.rowid = m.rowid
|
|
2025
|
+
WHERE memories_fts MATCH ? AND m.workspace_id = ? AND m.is_private = 0
|
|
2026
|
+
ORDER BY score
|
|
2027
|
+
LIMIT ?
|
|
2028
|
+
`);
|
|
2029
|
+
|
|
2030
|
+
const rows = stmt.all(query, workspaceId, limit) as Record<string, unknown>[];
|
|
2031
|
+
return rows.map(row => ({
|
|
2032
|
+
id: row.id as string,
|
|
2033
|
+
snippet: (row.summary as string) || this.truncateToSnippet(row.content as string, 200),
|
|
2034
|
+
type: row.type as MemoryType,
|
|
2035
|
+
relevanceScore: Math.abs(row.score as number),
|
|
2036
|
+
createdAt: row.created_at as number,
|
|
2037
|
+
taskId: (row.task_id as string) || undefined,
|
|
2038
|
+
}));
|
|
2039
|
+
} catch {
|
|
2040
|
+
// Fall back to LIKE search if FTS5 is not available
|
|
2041
|
+
const stmt = this.db.prepare(`
|
|
2042
|
+
SELECT id, summary, content, type, created_at, task_id
|
|
2043
|
+
FROM memories
|
|
2044
|
+
WHERE workspace_id = ? AND is_private = 0
|
|
2045
|
+
AND (content LIKE ? OR summary LIKE ?)
|
|
2046
|
+
ORDER BY created_at DESC
|
|
2047
|
+
LIMIT ?
|
|
2048
|
+
`);
|
|
2049
|
+
|
|
2050
|
+
const likeQuery = `%${query}%`;
|
|
2051
|
+
const rows = stmt.all(workspaceId, likeQuery, likeQuery, limit) as Record<string, unknown>[];
|
|
2052
|
+
return rows.map(row => ({
|
|
2053
|
+
id: row.id as string,
|
|
2054
|
+
snippet: (row.summary as string) || this.truncateToSnippet(row.content as string, 200),
|
|
2055
|
+
type: row.type as MemoryType,
|
|
2056
|
+
relevanceScore: 1,
|
|
2057
|
+
createdAt: row.created_at as number,
|
|
2058
|
+
taskId: (row.task_id as string) || undefined,
|
|
2059
|
+
}));
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
/**
|
|
2064
|
+
* Layer 2: Get timeline context around a specific memory
|
|
2065
|
+
* Returns surrounding memories within a time window
|
|
2066
|
+
*/
|
|
2067
|
+
getTimelineContext(memoryId: string, windowSize = 5): MemoryTimelineEntry[] {
|
|
2068
|
+
const memory = this.findById(memoryId);
|
|
2069
|
+
if (!memory) return [];
|
|
2070
|
+
|
|
2071
|
+
const stmt = this.db.prepare(`
|
|
2072
|
+
SELECT id, content, type, created_at, task_id
|
|
2073
|
+
FROM memories
|
|
2074
|
+
WHERE workspace_id = ? AND is_private = 0
|
|
2075
|
+
AND created_at BETWEEN ? AND ?
|
|
2076
|
+
ORDER BY created_at ASC
|
|
2077
|
+
LIMIT ?
|
|
2078
|
+
`);
|
|
2079
|
+
|
|
2080
|
+
const timeWindow = 30 * 60 * 1000; // 30 minutes
|
|
2081
|
+
const rows = stmt.all(
|
|
2082
|
+
memory.workspaceId,
|
|
2083
|
+
memory.createdAt - timeWindow,
|
|
2084
|
+
memory.createdAt + timeWindow,
|
|
2085
|
+
windowSize * 2 + 1
|
|
2086
|
+
) as Record<string, unknown>[];
|
|
2087
|
+
|
|
2088
|
+
return rows.map(row => ({
|
|
2089
|
+
id: row.id as string,
|
|
2090
|
+
content: row.content as string,
|
|
2091
|
+
type: row.type as MemoryType,
|
|
2092
|
+
createdAt: row.created_at as number,
|
|
2093
|
+
taskId: (row.task_id as string) || undefined,
|
|
2094
|
+
}));
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Layer 3: Get full details for selected IDs
|
|
2099
|
+
* Only called for specific memories when full content is needed
|
|
2100
|
+
*/
|
|
2101
|
+
getFullDetails(ids: string[]): Memory[] {
|
|
2102
|
+
return this.findByIds(ids);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
/**
|
|
2106
|
+
* Get recent memories for context injection
|
|
2107
|
+
*/
|
|
2108
|
+
getRecentForWorkspace(workspaceId: string, limit = 10): Memory[] {
|
|
2109
|
+
const stmt = this.db.prepare(`
|
|
2110
|
+
SELECT * FROM memories
|
|
2111
|
+
WHERE workspace_id = ? AND is_private = 0
|
|
2112
|
+
ORDER BY created_at DESC
|
|
2113
|
+
LIMIT ?
|
|
2114
|
+
`);
|
|
2115
|
+
const rows = stmt.all(workspaceId, limit) as Record<string, unknown>[];
|
|
2116
|
+
return rows.map(row => this.mapRowToMemory(row));
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
/**
|
|
2120
|
+
* Get uncompressed memories for batch compression
|
|
2121
|
+
*/
|
|
2122
|
+
getUncompressed(limit = 50): Memory[] {
|
|
2123
|
+
const stmt = this.db.prepare(`
|
|
2124
|
+
SELECT * FROM memories
|
|
2125
|
+
WHERE is_compressed = 0 AND summary IS NULL
|
|
2126
|
+
ORDER BY created_at ASC
|
|
2127
|
+
LIMIT ?
|
|
2128
|
+
`);
|
|
2129
|
+
const rows = stmt.all(limit) as Record<string, unknown>[];
|
|
2130
|
+
return rows.map(row => this.mapRowToMemory(row));
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Find memories by workspace
|
|
2135
|
+
*/
|
|
2136
|
+
findByWorkspace(workspaceId: string, limit = 100, offset = 0): Memory[] {
|
|
2137
|
+
const stmt = this.db.prepare(`
|
|
2138
|
+
SELECT * FROM memories
|
|
2139
|
+
WHERE workspace_id = ?
|
|
2140
|
+
ORDER BY created_at DESC
|
|
2141
|
+
LIMIT ? OFFSET ?
|
|
2142
|
+
`);
|
|
2143
|
+
const rows = stmt.all(workspaceId, limit, offset) as Record<string, unknown>[];
|
|
2144
|
+
return rows.map(row => this.mapRowToMemory(row));
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* Find memories by task
|
|
2149
|
+
*/
|
|
2150
|
+
findByTask(taskId: string): Memory[] {
|
|
2151
|
+
const stmt = this.db.prepare(`
|
|
2152
|
+
SELECT * FROM memories
|
|
2153
|
+
WHERE task_id = ?
|
|
2154
|
+
ORDER BY created_at ASC
|
|
2155
|
+
`);
|
|
2156
|
+
const rows = stmt.all(taskId) as Record<string, unknown>[];
|
|
2157
|
+
return rows.map(row => this.mapRowToMemory(row));
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
/**
|
|
2161
|
+
* Cleanup old memories based on retention policy
|
|
2162
|
+
*/
|
|
2163
|
+
deleteOlderThan(workspaceId: string, cutoffTimestamp: number): number {
|
|
2164
|
+
const stmt = this.db.prepare(`
|
|
2165
|
+
DELETE FROM memories
|
|
2166
|
+
WHERE workspace_id = ? AND created_at < ?
|
|
2167
|
+
`);
|
|
2168
|
+
const result = stmt.run(workspaceId, cutoffTimestamp);
|
|
2169
|
+
return result.changes;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Delete all memories for a workspace
|
|
2174
|
+
*/
|
|
2175
|
+
deleteByWorkspace(workspaceId: string): number {
|
|
2176
|
+
const stmt = this.db.prepare('DELETE FROM memories WHERE workspace_id = ?');
|
|
2177
|
+
const result = stmt.run(workspaceId);
|
|
2178
|
+
return result.changes;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* Get storage statistics for a workspace
|
|
2183
|
+
*/
|
|
2184
|
+
getStats(workspaceId: string): MemoryStats {
|
|
2185
|
+
const stmt = this.db.prepare(`
|
|
2186
|
+
SELECT COUNT(*) as count,
|
|
2187
|
+
COALESCE(SUM(tokens), 0) as total_tokens,
|
|
2188
|
+
SUM(CASE WHEN is_compressed = 1 THEN 1 ELSE 0 END) as compressed_count
|
|
2189
|
+
FROM memories
|
|
2190
|
+
WHERE workspace_id = ?
|
|
2191
|
+
`);
|
|
2192
|
+
const row = stmt.get(workspaceId) as Record<string, unknown>;
|
|
2193
|
+
const count = row.count as number;
|
|
2194
|
+
const compressedCount = row.compressed_count as number;
|
|
2195
|
+
return {
|
|
2196
|
+
count,
|
|
2197
|
+
totalTokens: row.total_tokens as number,
|
|
2198
|
+
compressedCount,
|
|
2199
|
+
compressionRatio: count > 0 ? compressedCount / count : 0,
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
private truncateToSnippet(content: string, maxChars: number): string {
|
|
2204
|
+
if (content.length <= maxChars) return content;
|
|
2205
|
+
return content.slice(0, maxChars - 3) + '...';
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
private mapRowToMemory(row: Record<string, unknown>): Memory {
|
|
2209
|
+
return {
|
|
2210
|
+
id: row.id as string,
|
|
2211
|
+
workspaceId: row.workspace_id as string,
|
|
2212
|
+
taskId: (row.task_id as string) || undefined,
|
|
2213
|
+
type: row.type as MemoryType,
|
|
2214
|
+
content: row.content as string,
|
|
2215
|
+
summary: (row.summary as string) || undefined,
|
|
2216
|
+
tokens: row.tokens as number,
|
|
2217
|
+
isCompressed: row.is_compressed === 1,
|
|
2218
|
+
isPrivate: row.is_private === 1,
|
|
2219
|
+
createdAt: row.created_at as number,
|
|
2220
|
+
updatedAt: row.updated_at as number,
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
export class MemorySummaryRepository {
|
|
2226
|
+
constructor(private db: Database.Database) {}
|
|
2227
|
+
|
|
2228
|
+
create(summary: Omit<MemorySummary, 'id' | 'createdAt'>): MemorySummary {
|
|
2229
|
+
const newSummary: MemorySummary = {
|
|
2230
|
+
...summary,
|
|
2231
|
+
id: uuidv4(),
|
|
2232
|
+
createdAt: Date.now(),
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
const stmt = this.db.prepare(`
|
|
2236
|
+
INSERT INTO memory_summaries (id, workspace_id, time_period, period_start, period_end, summary, memory_ids, tokens, created_at)
|
|
2237
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2238
|
+
`);
|
|
2239
|
+
|
|
2240
|
+
stmt.run(
|
|
2241
|
+
newSummary.id,
|
|
2242
|
+
newSummary.workspaceId,
|
|
2243
|
+
newSummary.timePeriod,
|
|
2244
|
+
newSummary.periodStart,
|
|
2245
|
+
newSummary.periodEnd,
|
|
2246
|
+
newSummary.summary,
|
|
2247
|
+
JSON.stringify(newSummary.memoryIds),
|
|
2248
|
+
newSummary.tokens,
|
|
2249
|
+
newSummary.createdAt
|
|
2250
|
+
);
|
|
2251
|
+
|
|
2252
|
+
return newSummary;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
findByWorkspaceAndPeriod(workspaceId: string, timePeriod: TimePeriod, limit = 10): MemorySummary[] {
|
|
2256
|
+
const stmt = this.db.prepare(`
|
|
2257
|
+
SELECT * FROM memory_summaries
|
|
2258
|
+
WHERE workspace_id = ? AND time_period = ?
|
|
2259
|
+
ORDER BY period_start DESC
|
|
2260
|
+
LIMIT ?
|
|
2261
|
+
`);
|
|
2262
|
+
const rows = stmt.all(workspaceId, timePeriod, limit) as Record<string, unknown>[];
|
|
2263
|
+
return rows.map(row => this.mapRowToSummary(row));
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
findByWorkspace(workspaceId: string, limit = 50): MemorySummary[] {
|
|
2267
|
+
const stmt = this.db.prepare(`
|
|
2268
|
+
SELECT * FROM memory_summaries
|
|
2269
|
+
WHERE workspace_id = ?
|
|
2270
|
+
ORDER BY period_start DESC
|
|
2271
|
+
LIMIT ?
|
|
2272
|
+
`);
|
|
2273
|
+
const rows = stmt.all(workspaceId, limit) as Record<string, unknown>[];
|
|
2274
|
+
return rows.map(row => this.mapRowToSummary(row));
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
deleteByWorkspace(workspaceId: string): number {
|
|
2278
|
+
const stmt = this.db.prepare('DELETE FROM memory_summaries WHERE workspace_id = ?');
|
|
2279
|
+
const result = stmt.run(workspaceId);
|
|
2280
|
+
return result.changes;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
private mapRowToSummary(row: Record<string, unknown>): MemorySummary {
|
|
2284
|
+
return {
|
|
2285
|
+
id: row.id as string,
|
|
2286
|
+
workspaceId: row.workspace_id as string,
|
|
2287
|
+
timePeriod: row.time_period as TimePeriod,
|
|
2288
|
+
periodStart: row.period_start as number,
|
|
2289
|
+
periodEnd: row.period_end as number,
|
|
2290
|
+
summary: row.summary as string,
|
|
2291
|
+
memoryIds: safeJsonParse(row.memory_ids as string, [] as string[], 'memorySummary.memoryIds'),
|
|
2292
|
+
tokens: row.tokens as number,
|
|
2293
|
+
createdAt: row.created_at as number,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
export class MemorySettingsRepository {
|
|
2299
|
+
constructor(private db: Database.Database) {}
|
|
2300
|
+
|
|
2301
|
+
getOrCreate(workspaceId: string): MemorySettings {
|
|
2302
|
+
const stmt = this.db.prepare('SELECT * FROM memory_settings WHERE workspace_id = ?');
|
|
2303
|
+
const row = stmt.get(workspaceId) as Record<string, unknown> | undefined;
|
|
2304
|
+
|
|
2305
|
+
if (row) {
|
|
2306
|
+
return this.mapRowToSettings(row);
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// Create default settings
|
|
2310
|
+
const defaults: MemorySettings = {
|
|
2311
|
+
workspaceId,
|
|
2312
|
+
enabled: true,
|
|
2313
|
+
autoCapture: true,
|
|
2314
|
+
compressionEnabled: true,
|
|
2315
|
+
retentionDays: 90,
|
|
2316
|
+
maxStorageMb: 100,
|
|
2317
|
+
privacyMode: 'normal',
|
|
2318
|
+
excludedPatterns: [],
|
|
2319
|
+
};
|
|
2320
|
+
|
|
2321
|
+
const insertStmt = this.db.prepare(`
|
|
2322
|
+
INSERT INTO memory_settings (workspace_id, enabled, auto_capture, compression_enabled, retention_days, max_storage_mb, privacy_mode, excluded_patterns)
|
|
2323
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2324
|
+
`);
|
|
2325
|
+
|
|
2326
|
+
insertStmt.run(
|
|
2327
|
+
defaults.workspaceId,
|
|
2328
|
+
defaults.enabled ? 1 : 0,
|
|
2329
|
+
defaults.autoCapture ? 1 : 0,
|
|
2330
|
+
defaults.compressionEnabled ? 1 : 0,
|
|
2331
|
+
defaults.retentionDays,
|
|
2332
|
+
defaults.maxStorageMb,
|
|
2333
|
+
defaults.privacyMode,
|
|
2334
|
+
JSON.stringify(defaults.excludedPatterns)
|
|
2335
|
+
);
|
|
2336
|
+
|
|
2337
|
+
return defaults;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
update(workspaceId: string, updates: Partial<Omit<MemorySettings, 'workspaceId'>>): void {
|
|
2341
|
+
const fields: string[] = [];
|
|
2342
|
+
const values: unknown[] = [];
|
|
2343
|
+
|
|
2344
|
+
if (updates.enabled !== undefined) {
|
|
2345
|
+
fields.push('enabled = ?');
|
|
2346
|
+
values.push(updates.enabled ? 1 : 0);
|
|
2347
|
+
}
|
|
2348
|
+
if (updates.autoCapture !== undefined) {
|
|
2349
|
+
fields.push('auto_capture = ?');
|
|
2350
|
+
values.push(updates.autoCapture ? 1 : 0);
|
|
2351
|
+
}
|
|
2352
|
+
if (updates.compressionEnabled !== undefined) {
|
|
2353
|
+
fields.push('compression_enabled = ?');
|
|
2354
|
+
values.push(updates.compressionEnabled ? 1 : 0);
|
|
2355
|
+
}
|
|
2356
|
+
if (updates.retentionDays !== undefined) {
|
|
2357
|
+
fields.push('retention_days = ?');
|
|
2358
|
+
values.push(updates.retentionDays);
|
|
2359
|
+
}
|
|
2360
|
+
if (updates.maxStorageMb !== undefined) {
|
|
2361
|
+
fields.push('max_storage_mb = ?');
|
|
2362
|
+
values.push(updates.maxStorageMb);
|
|
2363
|
+
}
|
|
2364
|
+
if (updates.privacyMode !== undefined) {
|
|
2365
|
+
fields.push('privacy_mode = ?');
|
|
2366
|
+
values.push(updates.privacyMode);
|
|
2367
|
+
}
|
|
2368
|
+
if (updates.excludedPatterns !== undefined) {
|
|
2369
|
+
fields.push('excluded_patterns = ?');
|
|
2370
|
+
values.push(JSON.stringify(updates.excludedPatterns));
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
if (fields.length === 0) return;
|
|
2374
|
+
|
|
2375
|
+
values.push(workspaceId);
|
|
2376
|
+
const stmt = this.db.prepare(`UPDATE memory_settings SET ${fields.join(', ')} WHERE workspace_id = ?`);
|
|
2377
|
+
stmt.run(...values);
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
delete(workspaceId: string): void {
|
|
2381
|
+
const stmt = this.db.prepare('DELETE FROM memory_settings WHERE workspace_id = ?');
|
|
2382
|
+
stmt.run(workspaceId);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
private mapRowToSettings(row: Record<string, unknown>): MemorySettings {
|
|
2386
|
+
return {
|
|
2387
|
+
workspaceId: row.workspace_id as string,
|
|
2388
|
+
enabled: row.enabled === 1,
|
|
2389
|
+
autoCapture: row.auto_capture === 1,
|
|
2390
|
+
compressionEnabled: row.compression_enabled === 1,
|
|
2391
|
+
retentionDays: row.retention_days as number,
|
|
2392
|
+
maxStorageMb: row.max_storage_mb as number,
|
|
2393
|
+
privacyMode: row.privacy_mode as PrivacyMode,
|
|
2394
|
+
excludedPatterns: safeJsonParse(row.excluded_patterns as string, [] as string[], 'memorySettings.excludedPatterns'),
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
}
|