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.
Files changed (526) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1638 -0
  3. package/bin/cowork.js +42 -0
  4. package/build/entitlements.mac.plist +16 -0
  5. package/build/icon.icns +0 -0
  6. package/build/icon.png +0 -0
  7. package/dist/electron/electron/activity/ActivityRepository.js +190 -0
  8. package/dist/electron/electron/agent/browser/browser-service.js +639 -0
  9. package/dist/electron/electron/agent/context-manager.js +225 -0
  10. package/dist/electron/electron/agent/custom-skill-loader.js +566 -0
  11. package/dist/electron/electron/agent/daemon.js +975 -0
  12. package/dist/electron/electron/agent/executor.js +3561 -0
  13. package/dist/electron/electron/agent/llm/anthropic-provider.js +155 -0
  14. package/dist/electron/electron/agent/llm/bedrock-provider.js +202 -0
  15. package/dist/electron/electron/agent/llm/gemini-provider.js +375 -0
  16. package/dist/electron/electron/agent/llm/index.js +34 -0
  17. package/dist/electron/electron/agent/llm/ollama-provider.js +263 -0
  18. package/dist/electron/electron/agent/llm/openai-oauth.js +101 -0
  19. package/dist/electron/electron/agent/llm/openai-provider.js +657 -0
  20. package/dist/electron/electron/agent/llm/openrouter-provider.js +232 -0
  21. package/dist/electron/electron/agent/llm/pricing.js +160 -0
  22. package/dist/electron/electron/agent/llm/provider-factory.js +880 -0
  23. package/dist/electron/electron/agent/llm/types.js +178 -0
  24. package/dist/electron/electron/agent/queue-manager.js +378 -0
  25. package/dist/electron/electron/agent/sandbox/docker-sandbox.js +402 -0
  26. package/dist/electron/electron/agent/sandbox/macos-sandbox.js +407 -0
  27. package/dist/electron/electron/agent/sandbox/runner.js +410 -0
  28. package/dist/electron/electron/agent/sandbox/sandbox-factory.js +228 -0
  29. package/dist/electron/electron/agent/sandbox/security-utils.js +258 -0
  30. package/dist/electron/electron/agent/search/brave-provider.js +119 -0
  31. package/dist/electron/electron/agent/search/google-provider.js +100 -0
  32. package/dist/electron/electron/agent/search/index.js +28 -0
  33. package/dist/electron/electron/agent/search/provider-factory.js +395 -0
  34. package/dist/electron/electron/agent/search/serpapi-provider.js +112 -0
  35. package/dist/electron/electron/agent/search/tavily-provider.js +90 -0
  36. package/dist/electron/electron/agent/search/types.js +40 -0
  37. package/dist/electron/electron/agent/security/index.js +12 -0
  38. package/dist/electron/electron/agent/security/input-sanitizer.js +303 -0
  39. package/dist/electron/electron/agent/security/output-filter.js +217 -0
  40. package/dist/electron/electron/agent/skill-eligibility.js +281 -0
  41. package/dist/electron/electron/agent/skill-registry.js +396 -0
  42. package/dist/electron/electron/agent/skills/document.js +878 -0
  43. package/dist/electron/electron/agent/skills/image-generator.js +225 -0
  44. package/dist/electron/electron/agent/skills/organizer.js +141 -0
  45. package/dist/electron/electron/agent/skills/presentation.js +367 -0
  46. package/dist/electron/electron/agent/skills/spreadsheet.js +165 -0
  47. package/dist/electron/electron/agent/tools/browser-tools.js +523 -0
  48. package/dist/electron/electron/agent/tools/builtin-settings.js +384 -0
  49. package/dist/electron/electron/agent/tools/canvas-tools.js +530 -0
  50. package/dist/electron/electron/agent/tools/cron-tools.js +577 -0
  51. package/dist/electron/electron/agent/tools/edit-tools.js +194 -0
  52. package/dist/electron/electron/agent/tools/file-tools.js +719 -0
  53. package/dist/electron/electron/agent/tools/glob-tools.js +283 -0
  54. package/dist/electron/electron/agent/tools/grep-tools.js +387 -0
  55. package/dist/electron/electron/agent/tools/image-tools.js +111 -0
  56. package/dist/electron/electron/agent/tools/mention-tools.js +282 -0
  57. package/dist/electron/electron/agent/tools/node-tools.js +476 -0
  58. package/dist/electron/electron/agent/tools/registry.js +2719 -0
  59. package/dist/electron/electron/agent/tools/search-tools.js +91 -0
  60. package/dist/electron/electron/agent/tools/shell-tools.js +574 -0
  61. package/dist/electron/electron/agent/tools/skill-tools.js +274 -0
  62. package/dist/electron/electron/agent/tools/system-tools.js +578 -0
  63. package/dist/electron/electron/agent/tools/web-fetch-tools.js +444 -0
  64. package/dist/electron/electron/agent/tools/x-tools.js +264 -0
  65. package/dist/electron/electron/agents/AgentRoleRepository.js +420 -0
  66. package/dist/electron/electron/agents/HeartbeatService.js +356 -0
  67. package/dist/electron/electron/agents/MentionRepository.js +197 -0
  68. package/dist/electron/electron/agents/TaskSubscriptionRepository.js +168 -0
  69. package/dist/electron/electron/agents/WorkingStateRepository.js +229 -0
  70. package/dist/electron/electron/canvas/canvas-manager.js +714 -0
  71. package/dist/electron/electron/canvas/canvas-preload.js +53 -0
  72. package/dist/electron/electron/canvas/canvas-protocol.js +195 -0
  73. package/dist/electron/electron/canvas/canvas-store.js +174 -0
  74. package/dist/electron/electron/canvas/index.js +13 -0
  75. package/dist/electron/electron/control-plane/client.js +364 -0
  76. package/dist/electron/electron/control-plane/handlers.js +572 -0
  77. package/dist/electron/electron/control-plane/index.js +41 -0
  78. package/dist/electron/electron/control-plane/node-manager.js +264 -0
  79. package/dist/electron/electron/control-plane/protocol.js +194 -0
  80. package/dist/electron/electron/control-plane/remote-client.js +437 -0
  81. package/dist/electron/electron/control-plane/server.js +640 -0
  82. package/dist/electron/electron/control-plane/settings.js +369 -0
  83. package/dist/electron/electron/control-plane/ssh-tunnel.js +549 -0
  84. package/dist/electron/electron/cron/index.js +30 -0
  85. package/dist/electron/electron/cron/schedule.js +190 -0
  86. package/dist/electron/electron/cron/service.js +614 -0
  87. package/dist/electron/electron/cron/store.js +155 -0
  88. package/dist/electron/electron/cron/types.js +82 -0
  89. package/dist/electron/electron/cron/webhook.js +258 -0
  90. package/dist/electron/electron/database/SecureSettingsRepository.js +444 -0
  91. package/dist/electron/electron/database/TaskLabelRepository.js +120 -0
  92. package/dist/electron/electron/database/repositories.js +1781 -0
  93. package/dist/electron/electron/database/schema.js +978 -0
  94. package/dist/electron/electron/extensions/index.js +33 -0
  95. package/dist/electron/electron/extensions/loader.js +313 -0
  96. package/dist/electron/electron/extensions/registry.js +485 -0
  97. package/dist/electron/electron/extensions/types.js +11 -0
  98. package/dist/electron/electron/gateway/channel-registry.js +1102 -0
  99. package/dist/electron/electron/gateway/channels/bluebubbles-client.js +479 -0
  100. package/dist/electron/electron/gateway/channels/bluebubbles.js +432 -0
  101. package/dist/electron/electron/gateway/channels/discord.js +975 -0
  102. package/dist/electron/electron/gateway/channels/email-client.js +593 -0
  103. package/dist/electron/electron/gateway/channels/email.js +443 -0
  104. package/dist/electron/electron/gateway/channels/google-chat.js +631 -0
  105. package/dist/electron/electron/gateway/channels/imessage-client.js +363 -0
  106. package/dist/electron/electron/gateway/channels/imessage.js +465 -0
  107. package/dist/electron/electron/gateway/channels/index.js +36 -0
  108. package/dist/electron/electron/gateway/channels/line-client.js +470 -0
  109. package/dist/electron/electron/gateway/channels/line.js +479 -0
  110. package/dist/electron/electron/gateway/channels/matrix-client.js +432 -0
  111. package/dist/electron/electron/gateway/channels/matrix.js +592 -0
  112. package/dist/electron/electron/gateway/channels/mattermost-client.js +394 -0
  113. package/dist/electron/electron/gateway/channels/mattermost.js +496 -0
  114. package/dist/electron/electron/gateway/channels/signal-client.js +500 -0
  115. package/dist/electron/electron/gateway/channels/signal.js +582 -0
  116. package/dist/electron/electron/gateway/channels/slack.js +415 -0
  117. package/dist/electron/electron/gateway/channels/teams.js +596 -0
  118. package/dist/electron/electron/gateway/channels/telegram.js +1390 -0
  119. package/dist/electron/electron/gateway/channels/twitch-client.js +502 -0
  120. package/dist/electron/electron/gateway/channels/twitch.js +396 -0
  121. package/dist/electron/electron/gateway/channels/types.js +8 -0
  122. package/dist/electron/electron/gateway/channels/whatsapp.js +953 -0
  123. package/dist/electron/electron/gateway/context-policy.js +268 -0
  124. package/dist/electron/electron/gateway/index.js +1063 -0
  125. package/dist/electron/electron/gateway/infrastructure.js +496 -0
  126. package/dist/electron/electron/gateway/router.js +2700 -0
  127. package/dist/electron/electron/gateway/security.js +375 -0
  128. package/dist/electron/electron/gateway/session.js +115 -0
  129. package/dist/electron/electron/gateway/tunnel.js +503 -0
  130. package/dist/electron/electron/guardrails/guardrail-manager.js +348 -0
  131. package/dist/electron/electron/hooks/gmail-watcher.js +300 -0
  132. package/dist/electron/electron/hooks/index.js +46 -0
  133. package/dist/electron/electron/hooks/mappings.js +381 -0
  134. package/dist/electron/electron/hooks/server.js +480 -0
  135. package/dist/electron/electron/hooks/settings.js +447 -0
  136. package/dist/electron/electron/hooks/types.js +41 -0
  137. package/dist/electron/electron/ipc/canvas-handlers.js +158 -0
  138. package/dist/electron/electron/ipc/handlers.js +3138 -0
  139. package/dist/electron/electron/ipc/mission-control-handlers.js +141 -0
  140. package/dist/electron/electron/main.js +448 -0
  141. package/dist/electron/electron/mcp/client/MCPClientManager.js +330 -0
  142. package/dist/electron/electron/mcp/client/MCPServerConnection.js +437 -0
  143. package/dist/electron/electron/mcp/client/transports/SSETransport.js +304 -0
  144. package/dist/electron/electron/mcp/client/transports/StdioTransport.js +307 -0
  145. package/dist/electron/electron/mcp/client/transports/WebSocketTransport.js +329 -0
  146. package/dist/electron/electron/mcp/host/MCPHostServer.js +354 -0
  147. package/dist/electron/electron/mcp/host/ToolAdapter.js +100 -0
  148. package/dist/electron/electron/mcp/registry/MCPRegistryManager.js +497 -0
  149. package/dist/electron/electron/mcp/settings.js +446 -0
  150. package/dist/electron/electron/mcp/types.js +59 -0
  151. package/dist/electron/electron/memory/MemoryService.js +435 -0
  152. package/dist/electron/electron/notifications/index.js +17 -0
  153. package/dist/electron/electron/notifications/service.js +118 -0
  154. package/dist/electron/electron/notifications/store.js +144 -0
  155. package/dist/electron/electron/preload.js +842 -0
  156. package/dist/electron/electron/reports/StandupReportService.js +272 -0
  157. package/dist/electron/electron/security/concurrency.js +293 -0
  158. package/dist/electron/electron/security/index.js +15 -0
  159. package/dist/electron/electron/security/policy-manager.js +435 -0
  160. package/dist/electron/electron/settings/appearance-manager.js +193 -0
  161. package/dist/electron/electron/settings/personality-manager.js +724 -0
  162. package/dist/electron/electron/settings/x-manager.js +58 -0
  163. package/dist/electron/electron/tailscale/exposure.js +188 -0
  164. package/dist/electron/electron/tailscale/index.js +28 -0
  165. package/dist/electron/electron/tailscale/settings.js +205 -0
  166. package/dist/electron/electron/tailscale/tailscale.js +355 -0
  167. package/dist/electron/electron/tray/QuickInputWindow.js +568 -0
  168. package/dist/electron/electron/tray/TrayManager.js +895 -0
  169. package/dist/electron/electron/tray/index.js +9 -0
  170. package/dist/electron/electron/updater/index.js +6 -0
  171. package/dist/electron/electron/updater/update-manager.js +418 -0
  172. package/dist/electron/electron/utils/env-migration.js +209 -0
  173. package/dist/electron/electron/utils/process.js +102 -0
  174. package/dist/electron/electron/utils/rate-limiter.js +104 -0
  175. package/dist/electron/electron/utils/validation.js +419 -0
  176. package/dist/electron/electron/utils/x-cli.js +177 -0
  177. package/dist/electron/electron/voice/VoiceService.js +507 -0
  178. package/dist/electron/electron/voice/index.js +14 -0
  179. package/dist/electron/electron/voice/voice-settings-manager.js +359 -0
  180. package/dist/electron/shared/channelMessages.js +170 -0
  181. package/dist/electron/shared/types.js +1185 -0
  182. package/package.json +159 -0
  183. package/resources/skills/1password.json +10 -0
  184. package/resources/skills/add-documentation.json +31 -0
  185. package/resources/skills/analyze-csv.json +17 -0
  186. package/resources/skills/apple-notes.json +10 -0
  187. package/resources/skills/apple-reminders.json +10 -0
  188. package/resources/skills/auto-commenter.json +10 -0
  189. package/resources/skills/bear-notes.json +10 -0
  190. package/resources/skills/bird.json +35 -0
  191. package/resources/skills/blogwatcher.json +10 -0
  192. package/resources/skills/blucli.json +10 -0
  193. package/resources/skills/bluebubbles.json +10 -0
  194. package/resources/skills/camsnap.json +10 -0
  195. package/resources/skills/clean-imports.json +18 -0
  196. package/resources/skills/code-review.json +18 -0
  197. package/resources/skills/coding-agent.json +10 -0
  198. package/resources/skills/compare-files.json +23 -0
  199. package/resources/skills/convert-code.json +34 -0
  200. package/resources/skills/create-changelog.json +24 -0
  201. package/resources/skills/debug-error.json +17 -0
  202. package/resources/skills/dependency-check.json +10 -0
  203. package/resources/skills/discord.json +10 -0
  204. package/resources/skills/eightctl.json +10 -0
  205. package/resources/skills/explain-code.json +29 -0
  206. package/resources/skills/extract-todos.json +18 -0
  207. package/resources/skills/food-order.json +10 -0
  208. package/resources/skills/gemini.json +10 -0
  209. package/resources/skills/generate-readme.json +10 -0
  210. package/resources/skills/gifgrep.json +10 -0
  211. package/resources/skills/git-commit.json +10 -0
  212. package/resources/skills/github.json +10 -0
  213. package/resources/skills/gog.json +10 -0
  214. package/resources/skills/goplaces.json +10 -0
  215. package/resources/skills/himalaya.json +10 -0
  216. package/resources/skills/imsg.json +10 -0
  217. package/resources/skills/karpathy-guidelines.json +12 -0
  218. package/resources/skills/last30days.json +26 -0
  219. package/resources/skills/local-places.json +10 -0
  220. package/resources/skills/mcporter.json +10 -0
  221. package/resources/skills/model-usage.json +10 -0
  222. package/resources/skills/nano-banana-pro.json +10 -0
  223. package/resources/skills/nano-pdf.json +10 -0
  224. package/resources/skills/notion.json +10 -0
  225. package/resources/skills/obsidian.json +10 -0
  226. package/resources/skills/openai-image-gen.json +10 -0
  227. package/resources/skills/openai-whisper-api.json +10 -0
  228. package/resources/skills/openai-whisper.json +10 -0
  229. package/resources/skills/openhue.json +10 -0
  230. package/resources/skills/oracle.json +10 -0
  231. package/resources/skills/ordercli.json +10 -0
  232. package/resources/skills/peekaboo.json +10 -0
  233. package/resources/skills/project-structure.json +10 -0
  234. package/resources/skills/proofread.json +17 -0
  235. package/resources/skills/refactor-code.json +31 -0
  236. package/resources/skills/rename-symbol.json +23 -0
  237. package/resources/skills/sag.json +10 -0
  238. package/resources/skills/security-audit.json +18 -0
  239. package/resources/skills/session-logs.json +10 -0
  240. package/resources/skills/sherpa-onnx-tts.json +10 -0
  241. package/resources/skills/skill-creator.json +15 -0
  242. package/resources/skills/skill-hub.json +29 -0
  243. package/resources/skills/slack.json +10 -0
  244. package/resources/skills/songsee.json +10 -0
  245. package/resources/skills/sonoscli.json +10 -0
  246. package/resources/skills/spotify-player.json +10 -0
  247. package/resources/skills/startup-cfo.json +55 -0
  248. package/resources/skills/summarize-folder.json +18 -0
  249. package/resources/skills/summarize.json +10 -0
  250. package/resources/skills/things-mac.json +10 -0
  251. package/resources/skills/tmux.json +10 -0
  252. package/resources/skills/translate.json +36 -0
  253. package/resources/skills/trello.json +10 -0
  254. package/resources/skills/video-frames.json +10 -0
  255. package/resources/skills/voice-call.json +10 -0
  256. package/resources/skills/wacli.json +10 -0
  257. package/resources/skills/weather.json +10 -0
  258. package/resources/skills/write-tests.json +31 -0
  259. package/src/electron/activity/ActivityRepository.ts +238 -0
  260. package/src/electron/agent/browser/browser-service.ts +721 -0
  261. package/src/electron/agent/context-manager.ts +257 -0
  262. package/src/electron/agent/custom-skill-loader.ts +634 -0
  263. package/src/electron/agent/daemon.ts +1097 -0
  264. package/src/electron/agent/executor.ts +4017 -0
  265. package/src/electron/agent/llm/anthropic-provider.ts +175 -0
  266. package/src/electron/agent/llm/bedrock-provider.ts +236 -0
  267. package/src/electron/agent/llm/gemini-provider.ts +422 -0
  268. package/src/electron/agent/llm/index.ts +9 -0
  269. package/src/electron/agent/llm/ollama-provider.ts +347 -0
  270. package/src/electron/agent/llm/openai-oauth.ts +127 -0
  271. package/src/electron/agent/llm/openai-provider.ts +686 -0
  272. package/src/electron/agent/llm/openrouter-provider.ts +273 -0
  273. package/src/electron/agent/llm/pricing.ts +180 -0
  274. package/src/electron/agent/llm/provider-factory.ts +971 -0
  275. package/src/electron/agent/llm/types.ts +291 -0
  276. package/src/electron/agent/queue-manager.ts +408 -0
  277. package/src/electron/agent/sandbox/docker-sandbox.ts +453 -0
  278. package/src/electron/agent/sandbox/macos-sandbox.ts +426 -0
  279. package/src/electron/agent/sandbox/runner.ts +453 -0
  280. package/src/electron/agent/sandbox/sandbox-factory.ts +337 -0
  281. package/src/electron/agent/sandbox/security-utils.ts +251 -0
  282. package/src/electron/agent/search/brave-provider.ts +141 -0
  283. package/src/electron/agent/search/google-provider.ts +131 -0
  284. package/src/electron/agent/search/index.ts +6 -0
  285. package/src/electron/agent/search/provider-factory.ts +450 -0
  286. package/src/electron/agent/search/serpapi-provider.ts +138 -0
  287. package/src/electron/agent/search/tavily-provider.ts +108 -0
  288. package/src/electron/agent/search/types.ts +118 -0
  289. package/src/electron/agent/security/index.ts +20 -0
  290. package/src/electron/agent/security/input-sanitizer.ts +380 -0
  291. package/src/electron/agent/security/output-filter.ts +259 -0
  292. package/src/electron/agent/skill-eligibility.ts +334 -0
  293. package/src/electron/agent/skill-registry.ts +457 -0
  294. package/src/electron/agent/skills/document.ts +1070 -0
  295. package/src/electron/agent/skills/image-generator.ts +272 -0
  296. package/src/electron/agent/skills/organizer.ts +131 -0
  297. package/src/electron/agent/skills/presentation.ts +418 -0
  298. package/src/electron/agent/skills/spreadsheet.ts +166 -0
  299. package/src/electron/agent/tools/browser-tools.ts +546 -0
  300. package/src/electron/agent/tools/builtin-settings.ts +422 -0
  301. package/src/electron/agent/tools/canvas-tools.ts +572 -0
  302. package/src/electron/agent/tools/cron-tools.ts +723 -0
  303. package/src/electron/agent/tools/edit-tools.ts +196 -0
  304. package/src/electron/agent/tools/file-tools.ts +811 -0
  305. package/src/electron/agent/tools/glob-tools.ts +303 -0
  306. package/src/electron/agent/tools/grep-tools.ts +432 -0
  307. package/src/electron/agent/tools/image-tools.ts +126 -0
  308. package/src/electron/agent/tools/mention-tools.ts +371 -0
  309. package/src/electron/agent/tools/node-tools.ts +550 -0
  310. package/src/electron/agent/tools/registry.ts +3052 -0
  311. package/src/electron/agent/tools/search-tools.ts +111 -0
  312. package/src/electron/agent/tools/shell-tools.ts +651 -0
  313. package/src/electron/agent/tools/skill-tools.ts +340 -0
  314. package/src/electron/agent/tools/system-tools.ts +665 -0
  315. package/src/electron/agent/tools/web-fetch-tools.ts +528 -0
  316. package/src/electron/agent/tools/x-tools.ts +267 -0
  317. package/src/electron/agents/AgentRoleRepository.ts +557 -0
  318. package/src/electron/agents/HeartbeatService.ts +469 -0
  319. package/src/electron/agents/MentionRepository.ts +242 -0
  320. package/src/electron/agents/TaskSubscriptionRepository.ts +231 -0
  321. package/src/electron/agents/WorkingStateRepository.ts +278 -0
  322. package/src/electron/canvas/canvas-manager.ts +818 -0
  323. package/src/electron/canvas/canvas-preload.ts +102 -0
  324. package/src/electron/canvas/canvas-protocol.ts +174 -0
  325. package/src/electron/canvas/canvas-store.ts +200 -0
  326. package/src/electron/canvas/index.ts +8 -0
  327. package/src/electron/control-plane/client.ts +527 -0
  328. package/src/electron/control-plane/handlers.ts +723 -0
  329. package/src/electron/control-plane/index.ts +51 -0
  330. package/src/electron/control-plane/node-manager.ts +322 -0
  331. package/src/electron/control-plane/protocol.ts +269 -0
  332. package/src/electron/control-plane/remote-client.ts +517 -0
  333. package/src/electron/control-plane/server.ts +853 -0
  334. package/src/electron/control-plane/settings.ts +401 -0
  335. package/src/electron/control-plane/ssh-tunnel.ts +624 -0
  336. package/src/electron/cron/index.ts +9 -0
  337. package/src/electron/cron/schedule.ts +217 -0
  338. package/src/electron/cron/service.ts +743 -0
  339. package/src/electron/cron/store.ts +165 -0
  340. package/src/electron/cron/types.ts +291 -0
  341. package/src/electron/cron/webhook.ts +303 -0
  342. package/src/electron/database/SecureSettingsRepository.ts +514 -0
  343. package/src/electron/database/TaskLabelRepository.ts +148 -0
  344. package/src/electron/database/repositories.ts +2397 -0
  345. package/src/electron/database/schema.ts +1017 -0
  346. package/src/electron/extensions/index.ts +18 -0
  347. package/src/electron/extensions/loader.ts +336 -0
  348. package/src/electron/extensions/registry.ts +546 -0
  349. package/src/electron/extensions/types.ts +372 -0
  350. package/src/electron/gateway/channel-registry.ts +1267 -0
  351. package/src/electron/gateway/channels/bluebubbles-client.ts +641 -0
  352. package/src/electron/gateway/channels/bluebubbles.ts +509 -0
  353. package/src/electron/gateway/channels/discord.ts +1150 -0
  354. package/src/electron/gateway/channels/email-client.ts +708 -0
  355. package/src/electron/gateway/channels/email.ts +516 -0
  356. package/src/electron/gateway/channels/google-chat.ts +760 -0
  357. package/src/electron/gateway/channels/imessage-client.ts +473 -0
  358. package/src/electron/gateway/channels/imessage.ts +520 -0
  359. package/src/electron/gateway/channels/index.ts +21 -0
  360. package/src/electron/gateway/channels/line-client.ts +598 -0
  361. package/src/electron/gateway/channels/line.ts +559 -0
  362. package/src/electron/gateway/channels/matrix-client.ts +632 -0
  363. package/src/electron/gateway/channels/matrix.ts +655 -0
  364. package/src/electron/gateway/channels/mattermost-client.ts +526 -0
  365. package/src/electron/gateway/channels/mattermost.ts +550 -0
  366. package/src/electron/gateway/channels/signal-client.ts +722 -0
  367. package/src/electron/gateway/channels/signal.ts +666 -0
  368. package/src/electron/gateway/channels/slack.ts +458 -0
  369. package/src/electron/gateway/channels/teams.ts +681 -0
  370. package/src/electron/gateway/channels/telegram.ts +1727 -0
  371. package/src/electron/gateway/channels/twitch-client.ts +665 -0
  372. package/src/electron/gateway/channels/twitch.ts +468 -0
  373. package/src/electron/gateway/channels/types.ts +1002 -0
  374. package/src/electron/gateway/channels/whatsapp.ts +1101 -0
  375. package/src/electron/gateway/context-policy.ts +382 -0
  376. package/src/electron/gateway/index.ts +1274 -0
  377. package/src/electron/gateway/infrastructure.ts +645 -0
  378. package/src/electron/gateway/router.ts +3206 -0
  379. package/src/electron/gateway/security.ts +422 -0
  380. package/src/electron/gateway/session.ts +144 -0
  381. package/src/electron/gateway/tunnel.ts +626 -0
  382. package/src/electron/guardrails/guardrail-manager.ts +380 -0
  383. package/src/electron/hooks/gmail-watcher.ts +355 -0
  384. package/src/electron/hooks/index.ts +30 -0
  385. package/src/electron/hooks/mappings.ts +404 -0
  386. package/src/electron/hooks/server.ts +574 -0
  387. package/src/electron/hooks/settings.ts +466 -0
  388. package/src/electron/hooks/types.ts +245 -0
  389. package/src/electron/ipc/canvas-handlers.ts +223 -0
  390. package/src/electron/ipc/handlers.ts +3661 -0
  391. package/src/electron/ipc/mission-control-handlers.ts +182 -0
  392. package/src/electron/main.ts +496 -0
  393. package/src/electron/mcp/client/MCPClientManager.ts +406 -0
  394. package/src/electron/mcp/client/MCPServerConnection.ts +514 -0
  395. package/src/electron/mcp/client/transports/SSETransport.ts +360 -0
  396. package/src/electron/mcp/client/transports/StdioTransport.ts +355 -0
  397. package/src/electron/mcp/client/transports/WebSocketTransport.ts +384 -0
  398. package/src/electron/mcp/host/MCPHostServer.ts +388 -0
  399. package/src/electron/mcp/host/ToolAdapter.ts +140 -0
  400. package/src/electron/mcp/registry/MCPRegistryManager.ts +565 -0
  401. package/src/electron/mcp/settings.ts +468 -0
  402. package/src/electron/mcp/types.ts +371 -0
  403. package/src/electron/memory/MemoryService.ts +523 -0
  404. package/src/electron/notifications/index.ts +16 -0
  405. package/src/electron/notifications/service.ts +161 -0
  406. package/src/electron/notifications/store.ts +163 -0
  407. package/src/electron/preload.ts +2845 -0
  408. package/src/electron/reports/StandupReportService.ts +356 -0
  409. package/src/electron/security/concurrency.ts +333 -0
  410. package/src/electron/security/index.ts +17 -0
  411. package/src/electron/security/policy-manager.ts +539 -0
  412. package/src/electron/settings/appearance-manager.ts +182 -0
  413. package/src/electron/settings/personality-manager.ts +800 -0
  414. package/src/electron/settings/x-manager.ts +62 -0
  415. package/src/electron/tailscale/exposure.ts +262 -0
  416. package/src/electron/tailscale/index.ts +34 -0
  417. package/src/electron/tailscale/settings.ts +218 -0
  418. package/src/electron/tailscale/tailscale.ts +379 -0
  419. package/src/electron/tray/QuickInputWindow.ts +609 -0
  420. package/src/electron/tray/TrayManager.ts +1005 -0
  421. package/src/electron/tray/index.ts +6 -0
  422. package/src/electron/updater/index.ts +1 -0
  423. package/src/electron/updater/update-manager.ts +447 -0
  424. package/src/electron/utils/env-migration.ts +203 -0
  425. package/src/electron/utils/process.ts +124 -0
  426. package/src/electron/utils/rate-limiter.ts +130 -0
  427. package/src/electron/utils/validation.ts +493 -0
  428. package/src/electron/utils/x-cli.ts +198 -0
  429. package/src/electron/voice/VoiceService.ts +583 -0
  430. package/src/electron/voice/index.ts +9 -0
  431. package/src/electron/voice/voice-settings-manager.ts +403 -0
  432. package/src/renderer/App.tsx +775 -0
  433. package/src/renderer/components/ActivityFeed.tsx +407 -0
  434. package/src/renderer/components/ActivityFeedItem.tsx +285 -0
  435. package/src/renderer/components/AgentRoleCard.tsx +343 -0
  436. package/src/renderer/components/AgentRoleEditor.tsx +805 -0
  437. package/src/renderer/components/AgentSquadSettings.tsx +295 -0
  438. package/src/renderer/components/AgentWorkingStatePanel.tsx +411 -0
  439. package/src/renderer/components/AppearanceSettings.tsx +122 -0
  440. package/src/renderer/components/ApprovalDialog.tsx +100 -0
  441. package/src/renderer/components/BlueBubblesSettings.tsx +505 -0
  442. package/src/renderer/components/BuiltinToolsSettings.tsx +307 -0
  443. package/src/renderer/components/CanvasPreview.tsx +1189 -0
  444. package/src/renderer/components/CommandOutput.tsx +202 -0
  445. package/src/renderer/components/ContextPolicySettings.tsx +523 -0
  446. package/src/renderer/components/ControlPlaneSettings.tsx +1134 -0
  447. package/src/renderer/components/DisclaimerModal.tsx +124 -0
  448. package/src/renderer/components/DiscordSettings.tsx +436 -0
  449. package/src/renderer/components/EmailSettings.tsx +606 -0
  450. package/src/renderer/components/ExtensionsSettings.tsx +542 -0
  451. package/src/renderer/components/FileViewer.tsx +224 -0
  452. package/src/renderer/components/GoogleChatSettings.tsx +535 -0
  453. package/src/renderer/components/GuardrailSettings.tsx +487 -0
  454. package/src/renderer/components/HooksSettings.tsx +581 -0
  455. package/src/renderer/components/ImessageSettings.tsx +484 -0
  456. package/src/renderer/components/LineSettings.tsx +483 -0
  457. package/src/renderer/components/MCPRegistryBrowser.tsx +386 -0
  458. package/src/renderer/components/MCPSettings.tsx +943 -0
  459. package/src/renderer/components/MainContent.tsx +2433 -0
  460. package/src/renderer/components/MatrixSettings.tsx +510 -0
  461. package/src/renderer/components/MattermostSettings.tsx +473 -0
  462. package/src/renderer/components/MemorySettings.tsx +247 -0
  463. package/src/renderer/components/MentionBadge.tsx +87 -0
  464. package/src/renderer/components/MentionInput.tsx +409 -0
  465. package/src/renderer/components/MentionList.tsx +476 -0
  466. package/src/renderer/components/MissionControlPanel.tsx +1995 -0
  467. package/src/renderer/components/NodesSettings.tsx +316 -0
  468. package/src/renderer/components/NotificationPanel.tsx +481 -0
  469. package/src/renderer/components/Onboarding/AwakeningOrb.tsx +44 -0
  470. package/src/renderer/components/Onboarding/Onboarding.tsx +443 -0
  471. package/src/renderer/components/Onboarding/TypewriterText.tsx +102 -0
  472. package/src/renderer/components/Onboarding/index.ts +3 -0
  473. package/src/renderer/components/OnboardingModal.tsx +698 -0
  474. package/src/renderer/components/PairingCodeDisplay.tsx +324 -0
  475. package/src/renderer/components/PersonalitySettings.tsx +597 -0
  476. package/src/renderer/components/QueueSettings.tsx +119 -0
  477. package/src/renderer/components/QuickTaskFAB.tsx +71 -0
  478. package/src/renderer/components/RightPanel.tsx +413 -0
  479. package/src/renderer/components/ScheduledTasksSettings.tsx +1328 -0
  480. package/src/renderer/components/SearchSettings.tsx +328 -0
  481. package/src/renderer/components/Settings.tsx +1504 -0
  482. package/src/renderer/components/Sidebar.tsx +344 -0
  483. package/src/renderer/components/SignalSettings.tsx +673 -0
  484. package/src/renderer/components/SkillHubBrowser.tsx +458 -0
  485. package/src/renderer/components/SkillParameterModal.tsx +185 -0
  486. package/src/renderer/components/SkillsSettings.tsx +451 -0
  487. package/src/renderer/components/SlackSettings.tsx +442 -0
  488. package/src/renderer/components/StandupReportViewer.tsx +614 -0
  489. package/src/renderer/components/TaskBoard.tsx +498 -0
  490. package/src/renderer/components/TaskBoardCard.tsx +357 -0
  491. package/src/renderer/components/TaskBoardColumn.tsx +211 -0
  492. package/src/renderer/components/TaskLabelManager.tsx +472 -0
  493. package/src/renderer/components/TaskQueuePanel.tsx +144 -0
  494. package/src/renderer/components/TaskQuickActions.tsx +492 -0
  495. package/src/renderer/components/TaskTimeline.tsx +216 -0
  496. package/src/renderer/components/TaskView.tsx +162 -0
  497. package/src/renderer/components/TeamsSettings.tsx +518 -0
  498. package/src/renderer/components/TelegramSettings.tsx +421 -0
  499. package/src/renderer/components/Toast.tsx +76 -0
  500. package/src/renderer/components/TraySettings.tsx +189 -0
  501. package/src/renderer/components/TwitchSettings.tsx +511 -0
  502. package/src/renderer/components/UpdateSettings.tsx +295 -0
  503. package/src/renderer/components/VoiceIndicator.tsx +270 -0
  504. package/src/renderer/components/VoiceSettings.tsx +867 -0
  505. package/src/renderer/components/WhatsAppSettings.tsx +721 -0
  506. package/src/renderer/components/WorkingStateEditor.tsx +309 -0
  507. package/src/renderer/components/WorkingStateHistory.tsx +481 -0
  508. package/src/renderer/components/WorkspaceSelector.tsx +150 -0
  509. package/src/renderer/components/XSettings.tsx +311 -0
  510. package/src/renderer/global.d.ts +9 -0
  511. package/src/renderer/hooks/useAgentContext.ts +153 -0
  512. package/src/renderer/hooks/useOnboardingFlow.ts +548 -0
  513. package/src/renderer/hooks/useVoiceInput.ts +268 -0
  514. package/src/renderer/index.html +12 -0
  515. package/src/renderer/main.tsx +10 -0
  516. package/src/renderer/public/cowork-os-logo.png +0 -0
  517. package/src/renderer/quick-input.html +164 -0
  518. package/src/renderer/styles/index.css +14504 -0
  519. package/src/renderer/utils/agentMessages.ts +749 -0
  520. package/src/renderer/utils/voice-directives.ts +169 -0
  521. package/src/shared/channelMessages.ts +213 -0
  522. package/src/shared/types.ts +3608 -0
  523. package/tsconfig.electron.json +26 -0
  524. package/tsconfig.json +26 -0
  525. package/tsconfig.node.json +10 -0
  526. 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
+ }