@myrialabs/clopen 0.0.1

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 (456) hide show
  1. package/.env.example +6 -0
  2. package/.github/workflows/release.yml +60 -0
  3. package/.github/workflows/test.yml +40 -0
  4. package/CONTRIBUTING.md +499 -0
  5. package/LICENSE +21 -0
  6. package/README.md +209 -0
  7. package/backend/index.ts +156 -0
  8. package/backend/lib/chat/helpers.ts +42 -0
  9. package/backend/lib/chat/index.ts +2 -0
  10. package/backend/lib/chat/stream-manager.ts +1126 -0
  11. package/backend/lib/database/README.md +77 -0
  12. package/backend/lib/database/index.ts +119 -0
  13. package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
  14. package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
  15. package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
  16. package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
  17. package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
  18. package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
  19. package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
  20. package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
  21. package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
  22. package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
  23. package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
  24. package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
  25. package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
  26. package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
  27. package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
  28. package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
  29. package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
  30. package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
  31. package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
  32. package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
  33. package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
  34. package/backend/lib/database/migrations/index.ts +154 -0
  35. package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
  36. package/backend/lib/database/queries/engine-queries.ts +75 -0
  37. package/backend/lib/database/queries/index.ts +9 -0
  38. package/backend/lib/database/queries/message-queries.ts +472 -0
  39. package/backend/lib/database/queries/project-queries.ts +117 -0
  40. package/backend/lib/database/queries/session-queries.ts +271 -0
  41. package/backend/lib/database/queries/settings-queries.ts +34 -0
  42. package/backend/lib/database/queries/snapshot-queries.ts +326 -0
  43. package/backend/lib/database/queries/utils-queries.ts +59 -0
  44. package/backend/lib/database/seeders/index.ts +13 -0
  45. package/backend/lib/database/seeders/settings_seeder.ts +84 -0
  46. package/backend/lib/database/utils/connection.ts +174 -0
  47. package/backend/lib/database/utils/index.ts +4 -0
  48. package/backend/lib/database/utils/migration-runner.ts +118 -0
  49. package/backend/lib/database/utils/seeder-runner.ts +121 -0
  50. package/backend/lib/engine/adapters/claude/environment.ts +164 -0
  51. package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
  52. package/backend/lib/engine/adapters/claude/index.ts +1 -0
  53. package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
  54. package/backend/lib/engine/adapters/claude/stream.ts +177 -0
  55. package/backend/lib/engine/adapters/opencode/index.ts +2 -0
  56. package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
  57. package/backend/lib/engine/adapters/opencode/server.ts +104 -0
  58. package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
  59. package/backend/lib/engine/index.ts +196 -0
  60. package/backend/lib/engine/types.ts +58 -0
  61. package/backend/lib/files/file-operations.ts +478 -0
  62. package/backend/lib/files/file-reading.ts +308 -0
  63. package/backend/lib/files/file-watcher.ts +383 -0
  64. package/backend/lib/files/path-browsing.ts +382 -0
  65. package/backend/lib/git/git-executor.ts +88 -0
  66. package/backend/lib/git/git-parser.ts +411 -0
  67. package/backend/lib/git/git-service.ts +505 -0
  68. package/backend/lib/mcp/README.md +1144 -0
  69. package/backend/lib/mcp/config.ts +316 -0
  70. package/backend/lib/mcp/index.ts +35 -0
  71. package/backend/lib/mcp/project-context.ts +236 -0
  72. package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
  73. package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
  74. package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
  75. package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
  76. package/backend/lib/mcp/servers/helper.ts +143 -0
  77. package/backend/lib/mcp/servers/index.ts +45 -0
  78. package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
  79. package/backend/lib/mcp/servers/weather/index.ts +31 -0
  80. package/backend/lib/mcp/stdio-server.ts +103 -0
  81. package/backend/lib/mcp/types.ts +65 -0
  82. package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
  83. package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
  84. package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
  85. package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
  86. package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
  87. package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
  88. package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
  89. package/backend/lib/preview/browser/browser-pool.ts +357 -0
  90. package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
  91. package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
  92. package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
  93. package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
  94. package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
  95. package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
  96. package/backend/lib/preview/browser/types.ts +359 -0
  97. package/backend/lib/preview/index.ts +24 -0
  98. package/backend/lib/project/index.ts +2 -0
  99. package/backend/lib/project/status-manager.ts +182 -0
  100. package/backend/lib/shared/index.ts +2 -0
  101. package/backend/lib/shared/port-utils.ts +25 -0
  102. package/backend/lib/shared/process-manager.ts +281 -0
  103. package/backend/lib/snapshot/blob-store.ts +227 -0
  104. package/backend/lib/snapshot/gitignore.ts +307 -0
  105. package/backend/lib/snapshot/helpers.ts +397 -0
  106. package/backend/lib/snapshot/snapshot-service.ts +483 -0
  107. package/backend/lib/terminal/helpers.ts +14 -0
  108. package/backend/lib/terminal/index.ts +8 -0
  109. package/backend/lib/terminal/pty-manager.ts +4 -0
  110. package/backend/lib/terminal/pty-session-manager.ts +387 -0
  111. package/backend/lib/terminal/shell-utils.ts +313 -0
  112. package/backend/lib/terminal/stream-manager.ts +293 -0
  113. package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
  114. package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
  115. package/backend/lib/user/helpers.ts +87 -0
  116. package/backend/lib/utils/ws.ts +944 -0
  117. package/backend/lib/vite-dev.ts +353 -0
  118. package/backend/middleware/cors.ts +15 -0
  119. package/backend/middleware/error-handler.ts +49 -0
  120. package/backend/middleware/logger.ts +9 -0
  121. package/backend/types/api.ts +24 -0
  122. package/backend/ws/README.md +1505 -0
  123. package/backend/ws/chat/background.ts +198 -0
  124. package/backend/ws/chat/index.ts +21 -0
  125. package/backend/ws/chat/stream.ts +707 -0
  126. package/backend/ws/engine/claude/accounts.ts +401 -0
  127. package/backend/ws/engine/claude/index.ts +13 -0
  128. package/backend/ws/engine/claude/status.ts +43 -0
  129. package/backend/ws/engine/index.ts +14 -0
  130. package/backend/ws/engine/opencode/index.ts +11 -0
  131. package/backend/ws/engine/opencode/status.ts +30 -0
  132. package/backend/ws/engine/utils.ts +36 -0
  133. package/backend/ws/files/index.ts +30 -0
  134. package/backend/ws/files/read.ts +189 -0
  135. package/backend/ws/files/search.ts +453 -0
  136. package/backend/ws/files/watch.ts +124 -0
  137. package/backend/ws/files/write.ts +143 -0
  138. package/backend/ws/git/branch.ts +106 -0
  139. package/backend/ws/git/commit.ts +39 -0
  140. package/backend/ws/git/conflict.ts +68 -0
  141. package/backend/ws/git/diff.ts +69 -0
  142. package/backend/ws/git/index.ts +24 -0
  143. package/backend/ws/git/log.ts +41 -0
  144. package/backend/ws/git/remote.ts +214 -0
  145. package/backend/ws/git/staging.ts +84 -0
  146. package/backend/ws/git/status.ts +90 -0
  147. package/backend/ws/index.ts +69 -0
  148. package/backend/ws/mcp/index.ts +61 -0
  149. package/backend/ws/messages/crud.ts +74 -0
  150. package/backend/ws/messages/index.ts +14 -0
  151. package/backend/ws/preview/browser/cleanup.ts +129 -0
  152. package/backend/ws/preview/browser/console.ts +114 -0
  153. package/backend/ws/preview/browser/interact.ts +513 -0
  154. package/backend/ws/preview/browser/mcp.ts +129 -0
  155. package/backend/ws/preview/browser/native-ui.ts +235 -0
  156. package/backend/ws/preview/browser/stats.ts +55 -0
  157. package/backend/ws/preview/browser/tab-info.ts +126 -0
  158. package/backend/ws/preview/browser/tab.ts +166 -0
  159. package/backend/ws/preview/browser/webcodecs.ts +293 -0
  160. package/backend/ws/preview/index.ts +146 -0
  161. package/backend/ws/projects/crud.ts +113 -0
  162. package/backend/ws/projects/index.ts +25 -0
  163. package/backend/ws/projects/presence.ts +46 -0
  164. package/backend/ws/projects/status.ts +116 -0
  165. package/backend/ws/sessions/crud.ts +327 -0
  166. package/backend/ws/sessions/index.ts +33 -0
  167. package/backend/ws/settings/crud.ts +112 -0
  168. package/backend/ws/settings/index.ts +14 -0
  169. package/backend/ws/snapshot/index.ts +17 -0
  170. package/backend/ws/snapshot/restore.ts +173 -0
  171. package/backend/ws/snapshot/timeline.ts +141 -0
  172. package/backend/ws/system/index.ts +14 -0
  173. package/backend/ws/system/operations.ts +49 -0
  174. package/backend/ws/terminal/index.ts +40 -0
  175. package/backend/ws/terminal/persistence.ts +153 -0
  176. package/backend/ws/terminal/session.ts +382 -0
  177. package/backend/ws/terminal/stream.ts +79 -0
  178. package/backend/ws/tunnel/index.ts +14 -0
  179. package/backend/ws/tunnel/operations.ts +91 -0
  180. package/backend/ws/types.ts +20 -0
  181. package/backend/ws/user/crud.ts +156 -0
  182. package/backend/ws/user/index.ts +14 -0
  183. package/bin/clopen.ts +307 -0
  184. package/bun.lock +1352 -0
  185. package/frontend/App.svelte +34 -0
  186. package/frontend/app.css +313 -0
  187. package/frontend/lib/app-environment.ts +10 -0
  188. package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
  189. package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
  190. package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
  191. package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
  192. package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
  193. package/frontend/lib/components/chat/formatters/index.ts +3 -0
  194. package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
  195. package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
  196. package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
  197. package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
  198. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
  199. package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
  200. package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
  201. package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
  202. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
  203. package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
  204. package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
  205. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
  206. package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
  207. package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
  208. package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
  209. package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
  210. package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
  211. package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
  212. package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
  213. package/frontend/lib/components/chat/shared/index.ts +2 -0
  214. package/frontend/lib/components/chat/shared/utils.ts +116 -0
  215. package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
  216. package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
  217. package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
  218. package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
  219. package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
  220. package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
  221. package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
  222. package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
  223. package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
  224. package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
  225. package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
  226. package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
  227. package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
  228. package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
  229. package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
  230. package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
  231. package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
  232. package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
  233. package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
  234. package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
  235. package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
  236. package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
  237. package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
  238. package/frontend/lib/components/chat/tools/components/index.ts +7 -0
  239. package/frontend/lib/components/chat/tools/index.ts +26 -0
  240. package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
  241. package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
  242. package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
  243. package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
  244. package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
  245. package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
  246. package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
  247. package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
  248. package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
  249. package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
  250. package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
  251. package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
  252. package/frontend/lib/components/common/Alert.svelte +139 -0
  253. package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
  254. package/frontend/lib/components/common/Button.svelte +71 -0
  255. package/frontend/lib/components/common/Card.svelte +102 -0
  256. package/frontend/lib/components/common/Checkbox.svelte +48 -0
  257. package/frontend/lib/components/common/Dialog.svelte +249 -0
  258. package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
  259. package/frontend/lib/components/common/Icon.svelte +58 -0
  260. package/frontend/lib/components/common/Input.svelte +72 -0
  261. package/frontend/lib/components/common/Lightbox.svelte +233 -0
  262. package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
  263. package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
  264. package/frontend/lib/components/common/Modal.svelte +177 -0
  265. package/frontend/lib/components/common/ModalProvider.svelte +28 -0
  266. package/frontend/lib/components/common/ModelSelector.svelte +110 -0
  267. package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
  268. package/frontend/lib/components/common/NotificationToast.svelte +113 -0
  269. package/frontend/lib/components/common/PageTemplate.svelte +76 -0
  270. package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
  271. package/frontend/lib/components/common/Select.svelte +98 -0
  272. package/frontend/lib/components/common/Textarea.svelte +80 -0
  273. package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
  274. package/frontend/lib/components/common/lucide-icons.ts +1642 -0
  275. package/frontend/lib/components/common/material-icons.ts +1082 -0
  276. package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
  277. package/frontend/lib/components/common/xterm/index.ts +16 -0
  278. package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
  279. package/frontend/lib/components/common/xterm/types.ts +31 -0
  280. package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
  281. package/frontend/lib/components/files/FileNode.svelte +384 -0
  282. package/frontend/lib/components/files/FileTree.svelte +681 -0
  283. package/frontend/lib/components/files/FileViewer.svelte +728 -0
  284. package/frontend/lib/components/files/SearchResults.svelte +303 -0
  285. package/frontend/lib/components/git/BranchManager.svelte +458 -0
  286. package/frontend/lib/components/git/ChangesSection.svelte +107 -0
  287. package/frontend/lib/components/git/CommitForm.svelte +76 -0
  288. package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
  289. package/frontend/lib/components/git/DiffViewer.svelte +364 -0
  290. package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
  291. package/frontend/lib/components/git/GitButton.svelte +33 -0
  292. package/frontend/lib/components/git/GitLog.svelte +361 -0
  293. package/frontend/lib/components/git/GitModal.svelte +80 -0
  294. package/frontend/lib/components/history/HistoryModal.svelte +563 -0
  295. package/frontend/lib/components/history/HistoryView.svelte +615 -0
  296. package/frontend/lib/components/index.ts +34 -0
  297. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
  298. package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
  299. package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
  300. package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
  301. package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
  302. package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
  303. package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
  304. package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
  305. package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
  306. package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
  307. package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
  308. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
  309. package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
  310. package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
  311. package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
  312. package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
  313. package/frontend/lib/components/preview/index.ts +2 -0
  314. package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
  315. package/frontend/lib/components/settings/SettingsView.svelte +36 -0
  316. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
  317. package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
  318. package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
  319. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
  320. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
  321. package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
  322. package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
  323. package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
  324. package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
  325. package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
  326. package/frontend/lib/components/terminal/Terminal.svelte +368 -0
  327. package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
  328. package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
  329. package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
  330. package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
  331. package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
  332. package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
  333. package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
  334. package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
  335. package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
  336. package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
  337. package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
  338. package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
  339. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
  340. package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
  341. package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
  342. package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
  343. package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
  344. package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
  345. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
  346. package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
  347. package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
  348. package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
  349. package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
  350. package/frontend/lib/constants/preview.ts +45 -0
  351. package/frontend/lib/services/chat/chat.service.ts +704 -0
  352. package/frontend/lib/services/chat/index.ts +7 -0
  353. package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
  354. package/frontend/lib/services/notification/index.ts +8 -0
  355. package/frontend/lib/services/notification/push.service.ts +144 -0
  356. package/frontend/lib/services/notification/sound.service.ts +127 -0
  357. package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
  358. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
  359. package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
  360. package/frontend/lib/services/preview/index.ts +23 -0
  361. package/frontend/lib/services/project/index.ts +8 -0
  362. package/frontend/lib/services/project/status.service.ts +159 -0
  363. package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
  364. package/frontend/lib/services/terminal/background/index.ts +130 -0
  365. package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
  366. package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
  367. package/frontend/lib/services/terminal/index.ts +14 -0
  368. package/frontend/lib/services/terminal/persistence.service.ts +260 -0
  369. package/frontend/lib/services/terminal/project.service.ts +953 -0
  370. package/frontend/lib/services/terminal/session.service.ts +364 -0
  371. package/frontend/lib/services/terminal/terminal.service.ts +369 -0
  372. package/frontend/lib/stores/core/app.svelte.ts +117 -0
  373. package/frontend/lib/stores/core/files.svelte.ts +73 -0
  374. package/frontend/lib/stores/core/presence.svelte.ts +48 -0
  375. package/frontend/lib/stores/core/projects.svelte.ts +317 -0
  376. package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
  377. package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
  378. package/frontend/lib/stores/features/models.svelte.ts +89 -0
  379. package/frontend/lib/stores/features/settings.svelte.ts +87 -0
  380. package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
  381. package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
  382. package/frontend/lib/stores/features/user.svelte.ts +96 -0
  383. package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
  384. package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
  385. package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
  386. package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
  387. package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
  388. package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
  389. package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
  390. package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
  391. package/frontend/lib/types/native-ui.ts +73 -0
  392. package/frontend/lib/utils/chat/date-separator.ts +39 -0
  393. package/frontend/lib/utils/chat/message-grouper.ts +219 -0
  394. package/frontend/lib/utils/chat/message-processor.ts +135 -0
  395. package/frontend/lib/utils/chat/tool-handler.ts +161 -0
  396. package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
  397. package/frontend/lib/utils/click-outside.ts +20 -0
  398. package/frontend/lib/utils/context-manager.ts +257 -0
  399. package/frontend/lib/utils/file-icon-mappings.ts +769 -0
  400. package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
  401. package/frontend/lib/utils/git-status.ts +68 -0
  402. package/frontend/lib/utils/platform.ts +113 -0
  403. package/frontend/lib/utils/port-check.ts +65 -0
  404. package/frontend/lib/utils/terminalFormatter.ts +207 -0
  405. package/frontend/lib/utils/theme.ts +6 -0
  406. package/frontend/lib/utils/tree-visualizer.ts +320 -0
  407. package/frontend/lib/utils/ws.ts +44 -0
  408. package/frontend/main.ts +13 -0
  409. package/index.html +70 -0
  410. package/package.json +111 -0
  411. package/scripts/generate-icons.ts +87 -0
  412. package/scripts/pre-publish-check.sh +142 -0
  413. package/scripts/setup-hooks.sh +134 -0
  414. package/scripts/validate-branch-name.sh +47 -0
  415. package/scripts/validate-commit-msg.sh +42 -0
  416. package/shared/constants/engines.ts +134 -0
  417. package/shared/types/database/connection.ts +16 -0
  418. package/shared/types/database/index.ts +6 -0
  419. package/shared/types/database/schema.ts +141 -0
  420. package/shared/types/engine/index.ts +45 -0
  421. package/shared/types/filesystem/index.ts +22 -0
  422. package/shared/types/git.ts +171 -0
  423. package/shared/types/messaging/index.ts +239 -0
  424. package/shared/types/messaging/tool.ts +526 -0
  425. package/shared/types/network/api.ts +18 -0
  426. package/shared/types/network/index.ts +5 -0
  427. package/shared/types/stores/app.ts +23 -0
  428. package/shared/types/stores/dialog.ts +21 -0
  429. package/shared/types/stores/index.ts +3 -0
  430. package/shared/types/stores/settings.ts +15 -0
  431. package/shared/types/terminal/index.ts +44 -0
  432. package/shared/types/ui/components.ts +61 -0
  433. package/shared/types/ui/icons.ts +23 -0
  434. package/shared/types/ui/index.ts +22 -0
  435. package/shared/types/ui/notifications.ts +14 -0
  436. package/shared/types/ui/theme.ts +12 -0
  437. package/shared/types/websocket/index.ts +43 -0
  438. package/shared/types/window.d.ts +13 -0
  439. package/shared/utils/anonymous-user.ts +168 -0
  440. package/shared/utils/async.ts +10 -0
  441. package/shared/utils/diff-calculator.ts +184 -0
  442. package/shared/utils/file-type-detection.ts +166 -0
  443. package/shared/utils/logger.ts +158 -0
  444. package/shared/utils/message-formatter.ts +79 -0
  445. package/shared/utils/path.ts +47 -0
  446. package/shared/utils/ws-client.ts +768 -0
  447. package/shared/utils/ws-server.ts +660 -0
  448. package/static/audio/notification.ogg +0 -0
  449. package/static/favicon.svg +8 -0
  450. package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
  451. package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
  452. package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
  453. package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
  454. package/static/fonts/dm-sans.css +96 -0
  455. package/svelte.config.js +20 -0
  456. package/vite.config.ts +33 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * TARGETED Process Management for Terminal Command Cancellation
3
+ * Tracks specific session processes without affecting external terminals
4
+ */
5
+
6
+ import type { Subprocess } from 'bun';
7
+
8
+ import { debug } from '$shared/utils/logger';
9
+ // Store active child processes globally
10
+ const activeProcesses = new Map<string, Subprocess>();
11
+ // Store child process trees for comprehensive cleanup
12
+ const processTreeMap = new Map<string, number[]>();
13
+ // Track manually terminated sessions
14
+ const manuallyTerminatedSessions = new Set<string>();
15
+ // Platform detection
16
+ const isWindows = process.platform === 'win32';
17
+
18
+ export function setActiveProcess(sessionId: string, process: Subprocess) {
19
+ debug.log('server', '🔧 Setting active process for session:', sessionId, 'PID:', process.pid);
20
+
21
+ // Clean up any existing process for this session first
22
+ const existingProcess = activeProcesses.get(sessionId);
23
+ if (existingProcess && existingProcess.pid) {
24
+ debug.log('server', '🧹 Cleaning up existing process for session:', sessionId);
25
+ try {
26
+ // Use nuclear cleanup for existing process
27
+ nuclearProcessCleanup(sessionId);
28
+ } catch (error) {
29
+ debug.log('server', '⚠️ Failed to clean existing process:', error);
30
+ }
31
+ }
32
+
33
+ activeProcesses.set(sessionId, process);
34
+ processTreeMap.set(sessionId, []);
35
+
36
+ // Add cleanup handlers - ONLY cleanup tracking, not nuclear
37
+ process.exited.then((code) => {
38
+ debug.log('server', `🏁 Process for session ${sessionId} exited with code ${code}`);
39
+ // Just cleanup tracking - don't force kill other processes
40
+ activeProcesses.delete(sessionId);
41
+ processTreeMap.delete(sessionId);
42
+ // Clean up manual termination tracking after some time
43
+ setTimeout(() => {
44
+ manuallyTerminatedSessions.delete(sessionId);
45
+ }, 5000);
46
+ }).catch((error) => {
47
+ debug.log('server', `❌ Process error for session ${sessionId}:`, error);
48
+ // Just cleanup tracking - don't force kill other processes
49
+ activeProcesses.delete(sessionId);
50
+ processTreeMap.delete(sessionId);
51
+ // Clean up manual termination tracking
52
+ setTimeout(() => {
53
+ manuallyTerminatedSessions.delete(sessionId);
54
+ }, 5000);
55
+ });
56
+
57
+ // Start monitoring child processes for this session
58
+ if (process.pid) {
59
+ startChildProcessMonitoring(sessionId, process.pid);
60
+ }
61
+ }
62
+
63
+ // Track child processes without aggressive monitoring
64
+ function startChildProcessMonitoring(sessionId: string, parentPid: number) {
65
+ const monitorInterval = setInterval(async () => {
66
+ try {
67
+ // Only monitor if session still exists and process is alive
68
+ if (!activeProcesses.has(sessionId)) {
69
+ clearInterval(monitorInterval);
70
+ return;
71
+ }
72
+
73
+ const childPids = await discoverChildProcesses(parentPid);
74
+ const currentChildren = processTreeMap.get(sessionId) || [];
75
+
76
+ // Add new children to tracking (passive monitoring only)
77
+ for (const childPid of childPids) {
78
+ if (!currentChildren.includes(childPid)) {
79
+ debug.log('server', `🔍 Discovered new child process: ${childPid} for session: ${sessionId}`);
80
+ currentChildren.push(childPid);
81
+ }
82
+ }
83
+
84
+ processTreeMap.set(sessionId, currentChildren);
85
+
86
+ } catch (error) {
87
+ debug.log('server', '⚠️ Child process monitoring error:', error);
88
+ // Don't kill anything on monitoring errors
89
+ }
90
+ }, 5000); // Check every 5 seconds (less aggressive)
91
+
92
+ // Clean up monitoring after reasonable time
93
+ setTimeout(() => {
94
+ clearInterval(monitorInterval);
95
+ }, 300000); // Stop after 5 minutes max
96
+ }
97
+
98
+ // NUCLEAR: Discover all child processes of a parent
99
+ async function discoverChildProcesses(parentPid: number): Promise<number[]> {
100
+ const childPids: number[] = [];
101
+
102
+ try {
103
+ if (isWindows) {
104
+ // Windows: Use WMIC to find all children recursively
105
+ const proc = Bun.spawn(['wmic', 'process', 'where', `ParentProcessId=${parentPid}`, 'get', 'ProcessId', '/format:csv'], {
106
+ stdout: 'pipe',
107
+ stderr: 'ignore'
108
+ });
109
+ const output = await new Response(proc.stdout).text();
110
+
111
+ const lines = output.split('\n');
112
+ for (const line of lines) {
113
+ const match = line.match(/,(\d+)$/);
114
+ if (match) {
115
+ const childPid = parseInt(match[1]);
116
+ childPids.push(childPid);
117
+
118
+ // Recursively find grandchildren
119
+ const grandChildren = await discoverChildProcesses(childPid);
120
+ childPids.push(...grandChildren);
121
+ }
122
+ }
123
+ } else {
124
+ // Unix: Use pgrep to find children
125
+ try {
126
+ const proc = Bun.spawn(['pgrep', '-P', parentPid.toString()], {
127
+ stdout: 'pipe',
128
+ stderr: 'ignore'
129
+ });
130
+ const output = await new Response(proc.stdout).text();
131
+
132
+ const lines = output.trim().split('\n');
133
+ for (const line of lines) {
134
+ if (line.trim()) {
135
+ const childPid = parseInt(line.trim());
136
+ if (!isNaN(childPid)) {
137
+ childPids.push(childPid);
138
+
139
+ // Recursively find grandchildren
140
+ const grandChildren = await discoverChildProcesses(childPid);
141
+ childPids.push(...grandChildren);
142
+ }
143
+ }
144
+ }
145
+ } catch (error) {
146
+ // No children found, which is fine
147
+ }
148
+ }
149
+ } catch (error) {
150
+ debug.log('server', '⚠️ Error discovering child processes:', error);
151
+ }
152
+
153
+ return [...new Set(childPids)]; // Remove duplicates
154
+ }
155
+
156
+ // TARGETED: Complete process tree cleanup for specific session only
157
+ export async function nuclearProcessCleanup(sessionId: string) {
158
+ debug.log('server', '🎯 TARGETED CLEANUP for session:', sessionId);
159
+
160
+ // Mark this session as manually terminated
161
+ manuallyTerminatedSessions.add(sessionId);
162
+
163
+ const process = activeProcesses.get(sessionId);
164
+ const childPids = processTreeMap.get(sessionId) || [];
165
+
166
+ // Kill all child processes first (bottom-up)
167
+ for (const childPid of childPids) {
168
+ try {
169
+ debug.log('server', `💥 Killing child process: ${childPid}`);
170
+ if (isWindows) {
171
+ const proc = Bun.spawn(['taskkill', '/F', '/PID', childPid.toString()], {
172
+ stdout: 'ignore',
173
+ stderr: 'ignore'
174
+ });
175
+ await proc.exited;
176
+ } else {
177
+ const proc = Bun.spawn(['kill', '-9', childPid.toString()], {
178
+ stdout: 'ignore',
179
+ stderr: 'ignore'
180
+ });
181
+ await proc.exited;
182
+ }
183
+ debug.log('server', `✅ Killed child process: ${childPid}`);
184
+ } catch (error) {
185
+ debug.log('server', `⚠️ Failed to kill child ${childPid}:`, error);
186
+ }
187
+ }
188
+
189
+ // Kill parent process
190
+ if (process && process.pid) {
191
+ try {
192
+ debug.log('server', `💥 Killing parent process: ${process.pid}`);
193
+ process.kill();
194
+ debug.log('server', `✅ Killed parent process: ${process.pid}`);
195
+ } catch (error) {
196
+ debug.log('server', '⚠️ Failed to kill parent process:', error);
197
+ }
198
+ }
199
+
200
+ // TARGETED: Only kill processes that belong to this specific session
201
+ // No more nuclear killing of all npm/node processes
202
+ debug.log('server', '🎯 Targeted cleanup completed - only session-specific processes terminated')
203
+
204
+ // Clean up tracking
205
+ activeProcesses.delete(sessionId);
206
+ processTreeMap.delete(sessionId);
207
+
208
+ debug.log('server', '🎯 TARGETED CLEANUP completed for session:', sessionId);
209
+ }
210
+
211
+ export function removeActiveProcess(sessionId: string) {
212
+ debug.log('server', '🗑️ Removing active process for session:', sessionId);
213
+
214
+ // Simple cleanup - just remove from tracking
215
+ activeProcesses.delete(sessionId);
216
+ processTreeMap.delete(sessionId);
217
+ }
218
+
219
+ export function getActiveProcess(sessionId: string): Subprocess | undefined {
220
+ const process = activeProcesses.get(sessionId);
221
+ if (process && !process.pid) {
222
+ // Process is dead, remove it from the map
223
+ debug.log('server', '🪦 Process for session', sessionId, 'is dead, removing from active list');
224
+ activeProcesses.delete(sessionId);
225
+ processTreeMap.delete(sessionId);
226
+ return undefined;
227
+ }
228
+ return process;
229
+ }
230
+
231
+ export function getAllActiveProcesses(): Map<string, Subprocess> {
232
+ return activeProcesses;
233
+ }
234
+
235
+ export function getActiveProcessCount(): number {
236
+ return activeProcesses.size;
237
+ }
238
+
239
+ // Enhanced cleanup function with targeted options
240
+ export async function cleanupAllProcesses() {
241
+ debug.log('server', '🎯 TARGETED CLEANUP of all tracked processes...');
242
+
243
+ // Targeted cleanup for each session
244
+ for (const sessionId of activeProcesses.keys()) {
245
+ await nuclearProcessCleanup(sessionId);
246
+ }
247
+
248
+ // REMOVED: Global process killing - only clean up tracked processes
249
+ debug.log('server', '🎯 All tracked processes cleaned up - no global process termination')
250
+
251
+ activeProcesses.clear();
252
+ processTreeMap.clear();
253
+ }
254
+
255
+ // Get comprehensive process status for debugging
256
+ export function getProcessStatus(sessionId: string) {
257
+ const process = activeProcesses.get(sessionId);
258
+ const childPids = processTreeMap.get(sessionId) || [];
259
+
260
+ if (!process) {
261
+ return { exists: false, childProcesses: [] };
262
+ }
263
+
264
+ return {
265
+ exists: true,
266
+ pid: process.pid,
267
+ killed: process.killed,
268
+ exitCode: process.exitCode,
269
+ signalCode: process.signalCode,
270
+ childProcesses: childPids
271
+ };
272
+ }
273
+
274
+ // Get all tracked child processes for a session
275
+ export function getChildProcesses(sessionId: string): number[] {
276
+ return processTreeMap.get(sessionId) || [];
277
+ }
278
+
279
+ export function wasManuallyTerminated(sessionId: string): boolean {
280
+ return manuallyTerminatedSessions.has(sessionId);
281
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Content-Addressable Blob Store
3
+ * Git-like storage for snapshot file contents
4
+ *
5
+ * Structure:
6
+ * ~/.clopen/snapshots/blobs/{hash[0:2]}/{hash}.gz - compressed file blobs
7
+ * ~/.clopen/snapshots/trees/{snapshotId}.json - tree maps (filepath -> hash)
8
+ *
9
+ * Deduplication: Same file content across any snapshot is stored only once.
10
+ * Compression: All blobs are gzip compressed to minimize disk usage.
11
+ */
12
+
13
+ import { join } from 'path';
14
+ import { homedir } from 'os';
15
+ import fs from 'fs/promises';
16
+ import { gzipSync, gunzipSync } from 'zlib';
17
+ import { debug } from '$shared/utils/logger';
18
+
19
+ const SNAPSHOTS_DIR = join(homedir(), '.clopen', 'snapshots');
20
+ const BLOBS_DIR = join(SNAPSHOTS_DIR, 'blobs');
21
+ const TREES_DIR = join(SNAPSHOTS_DIR, 'trees');
22
+
23
+ export interface TreeMap {
24
+ [filepath: string]: string; // filepath -> blob hash
25
+ }
26
+
27
+ interface FileHashCacheEntry {
28
+ mtimeMs: number;
29
+ size: number;
30
+ hash: string;
31
+ }
32
+
33
+ class BlobStore {
34
+ private initialized = false;
35
+
36
+ /**
37
+ * Cache: filepath -> { mtimeMs, size, hash }
38
+ * Avoids re-reading files that haven't changed (based on mtime + size).
39
+ */
40
+ private fileHashCache = new Map<string, FileHashCacheEntry>();
41
+
42
+ /**
43
+ * Ensure storage directories exist
44
+ */
45
+ async init(): Promise<void> {
46
+ if (this.initialized) return;
47
+ await fs.mkdir(BLOBS_DIR, { recursive: true });
48
+ await fs.mkdir(TREES_DIR, { recursive: true });
49
+ this.initialized = true;
50
+ }
51
+
52
+ /**
53
+ * Compute SHA-256 hash of content (Buffer for binary safety)
54
+ */
55
+ hashContent(content: Buffer): string {
56
+ const hasher = new Bun.CryptoHasher('sha256');
57
+ hasher.update(content);
58
+ return hasher.digest('hex');
59
+ }
60
+
61
+ /**
62
+ * Get blob file path from hash (using 2-char prefix subdirectory like git)
63
+ */
64
+ private getBlobPath(hash: string): string {
65
+ const prefix = hash.substring(0, 2);
66
+ return join(BLOBS_DIR, prefix, hash + '.gz');
67
+ }
68
+
69
+ /**
70
+ * Check if a blob exists
71
+ */
72
+ async hasBlob(hash: string): Promise<boolean> {
73
+ try {
74
+ await fs.access(this.getBlobPath(hash));
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Store content as a blob. Returns the hash.
83
+ * If blob already exists (same hash), it's a no-op (deduplication).
84
+ * Accepts Buffer to safely handle both text and binary files.
85
+ */
86
+ async storeBlob(content: Buffer): Promise<string> {
87
+ await this.init();
88
+ const hash = this.hashContent(content);
89
+
90
+ // Check if already exists (deduplication)
91
+ if (await this.hasBlob(hash)) {
92
+ return hash;
93
+ }
94
+
95
+ // Create prefix directory
96
+ const prefixDir = join(BLOBS_DIR, hash.substring(0, 2));
97
+ await fs.mkdir(prefixDir, { recursive: true });
98
+
99
+ // Compress and write (Buffer directly, no encoding conversion)
100
+ const compressed = gzipSync(content);
101
+ await fs.writeFile(this.getBlobPath(hash), compressed);
102
+
103
+ return hash;
104
+ }
105
+
106
+ /**
107
+ * Read blob content by hash. Returns Buffer to safely handle binary files.
108
+ */
109
+ async readBlob(hash: string): Promise<Buffer> {
110
+ const blobPath = this.getBlobPath(hash);
111
+ const compressed = await fs.readFile(blobPath);
112
+ return gunzipSync(compressed);
113
+ }
114
+
115
+ /**
116
+ * Store a tree (snapshot state) as a JSON file.
117
+ * Returns the tree hash for reference.
118
+ */
119
+ async storeTree(snapshotId: string, tree: TreeMap): Promise<string> {
120
+ await this.init();
121
+ const treePath = join(TREES_DIR, `${snapshotId}.json`);
122
+ const content = JSON.stringify(tree);
123
+ const treeHash = this.hashContent(Buffer.from(content, 'utf-8'));
124
+ await fs.writeFile(treePath, content, 'utf-8');
125
+ return treeHash;
126
+ }
127
+
128
+ /**
129
+ * Read a tree by snapshot ID
130
+ */
131
+ async readTree(snapshotId: string): Promise<TreeMap> {
132
+ const treePath = join(TREES_DIR, `${snapshotId}.json`);
133
+ const content = await fs.readFile(treePath, 'utf-8');
134
+ return JSON.parse(content) as TreeMap;
135
+ }
136
+
137
+ /**
138
+ * Check if a tree exists
139
+ */
140
+ async hasTree(snapshotId: string): Promise<boolean> {
141
+ try {
142
+ await fs.access(join(TREES_DIR, `${snapshotId}.json`));
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Resolve a tree to full file contents (as Buffers).
151
+ * Reads all blobs in parallel for performance.
152
+ * Returns { filepath: Buffer } map for binary-safe handling.
153
+ */
154
+ async resolveTree(tree: TreeMap): Promise<Record<string, Buffer>> {
155
+ const result: Record<string, Buffer> = {};
156
+
157
+ const entries = Object.entries(tree);
158
+ const blobPromises = entries.map(async ([filepath, hash]) => {
159
+ try {
160
+ const content = await this.readBlob(hash);
161
+ return { filepath, content };
162
+ } catch (err) {
163
+ debug.warn('snapshot', `Could not read blob ${hash} for ${filepath}:`, err);
164
+ return null;
165
+ }
166
+ });
167
+
168
+ const results = await Promise.all(blobPromises);
169
+ for (const r of results) {
170
+ if (r) {
171
+ result[r.filepath] = r.content;
172
+ }
173
+ }
174
+
175
+ return result;
176
+ }
177
+
178
+ /**
179
+ * Hash a file using mtime cache. Returns { hash, content? }.
180
+ * If the file hasn't changed (same mtime+size), returns cached hash without reading content.
181
+ * If the file has changed, reads content, hashes it, stores blob, and caches.
182
+ * Reads as Buffer to safely handle binary files (images, PDFs, etc.).
183
+ *
184
+ * @returns hash and content Buffer (content is null if cache hit and blob already exists)
185
+ */
186
+ async hashFile(filepath: string, fullPath: string): Promise<{ hash: string; content: Buffer | null; cached: boolean }> {
187
+ await this.init();
188
+
189
+ const stat = await fs.stat(fullPath);
190
+
191
+ // Check mtime cache
192
+ const cached = this.fileHashCache.get(filepath);
193
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
194
+ return { hash: cached.hash, content: null, cached: true };
195
+ }
196
+
197
+ // File changed - read as Buffer (binary-safe, no encoding conversion)
198
+ const content = await fs.readFile(fullPath);
199
+ const hash = this.hashContent(content);
200
+
201
+ // Store blob (deduplication handled internally)
202
+ await this.storeBlob(content);
203
+
204
+ // Update cache
205
+ this.fileHashCache.set(filepath, {
206
+ mtimeMs: stat.mtimeMs,
207
+ size: stat.size,
208
+ hash
209
+ });
210
+
211
+ return { hash, content, cached: false };
212
+ }
213
+
214
+ /**
215
+ * Delete a tree file (cleanup)
216
+ */
217
+ async deleteTree(snapshotId: string): Promise<void> {
218
+ try {
219
+ await fs.unlink(join(TREES_DIR, `${snapshotId}.json`));
220
+ } catch {
221
+ // Ignore - might not exist
222
+ }
223
+ }
224
+ }
225
+
226
+ // Export singleton
227
+ export const blobStore = new BlobStore();