centaurus-cli 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (552) hide show
  1. package/dist/ai/types.js +0 -1
  2. package/dist/ai/types.js.map +1 -1
  3. package/dist/cli-adapter.js +5047 -5037
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/commands/CommandParser.js +372 -315
  6. package/dist/commands/CommandParser.js.map +1 -1
  7. package/dist/config/build-config.js +11 -42
  8. package/dist/config/build-config.js.map +1 -1
  9. package/dist/config/defaultConfig.js +94 -82
  10. package/dist/config/defaultConfig.js.map +1 -1
  11. package/dist/config/manager.js +144 -160
  12. package/dist/config/manager.js.map +1 -1
  13. package/dist/config/mcp-config-manager.js +411 -364
  14. package/dist/config/mcp-config-manager.js.map +1 -1
  15. package/dist/config/models.js +118 -185
  16. package/dist/config/models.js.map +1 -1
  17. package/dist/config/slash-commands.js +186 -184
  18. package/dist/config/slash-commands.js.map +1 -1
  19. package/dist/config/types.js +33 -26
  20. package/dist/config/types.js.map +1 -1
  21. package/dist/context/command-detector.js +63 -67
  22. package/dist/context/command-detector.js.map +1 -1
  23. package/dist/context/context-manager.js +533 -518
  24. package/dist/context/context-manager.js.map +1 -1
  25. package/dist/context/handlers/docker-handler.js +518 -576
  26. package/dist/context/handlers/docker-handler.js.map +1 -1
  27. package/dist/context/handlers/ssh-handler.js +1050 -1109
  28. package/dist/context/handlers/ssh-handler.js.map +1 -1
  29. package/dist/context/handlers/wsl-handler.js +558 -630
  30. package/dist/context/handlers/wsl-handler.js.map +1 -1
  31. package/dist/context/index.js +42 -6
  32. package/dist/context/index.js.map +1 -1
  33. package/dist/context/subshell-handler.js +0 -4
  34. package/dist/context/subshell-handler.js.map +1 -1
  35. package/dist/context/types.js +20 -31
  36. package/dist/context/types.js.map +1 -1
  37. package/dist/hooks/useConnectivity.js +13 -10
  38. package/dist/hooks/useConnectivity.js.map +1 -1
  39. package/dist/hooks/useTerminalDimensions.js +67 -79
  40. package/dist/hooks/useTerminalDimensions.js.map +1 -1
  41. package/dist/index.js +228 -251
  42. package/dist/index.js.map +1 -1
  43. package/dist/mcp/mcp-command-handler.js +297 -260
  44. package/dist/mcp/mcp-command-handler.js.map +1 -1
  45. package/dist/mcp/mcp-server-manager.js +139 -155
  46. package/dist/mcp/mcp-server-manager.js.map +1 -1
  47. package/dist/mcp/mcp-tool-wrapper.js +74 -94
  48. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  49. package/dist/services/ai-autocomplete-agent.js +169 -181
  50. package/dist/services/ai-autocomplete-agent.js.map +1 -1
  51. package/dist/services/ai-context-injector.js +180 -93
  52. package/dist/services/ai-context-injector.js.map +1 -1
  53. package/dist/services/ai-service-client.js +513 -456
  54. package/dist/services/ai-service-client.js.map +1 -1
  55. package/dist/services/api-client.js +443 -441
  56. package/dist/services/api-client.js.map +1 -1
  57. package/dist/services/auth-handler.js +162 -198
  58. package/dist/services/auth-handler.js.map +1 -1
  59. package/dist/services/background-task-manager.js +258 -282
  60. package/dist/services/background-task-manager.js.map +1 -1
  61. package/dist/services/checkpoint-manager.js +1513 -973
  62. package/dist/services/checkpoint-manager.js.map +1 -1
  63. package/dist/services/clipboard-service.js +151 -200
  64. package/dist/services/clipboard-service.js.map +1 -1
  65. package/dist/services/connectivity-manager.js +63 -65
  66. package/dist/services/connectivity-manager.js.map +1 -1
  67. package/dist/services/conversation-manager.js +118 -121
  68. package/dist/services/conversation-manager.js.map +1 -1
  69. package/dist/services/environment-context-injector.js +160 -187
  70. package/dist/services/environment-context-injector.js.map +1 -1
  71. package/dist/services/fast-context-agent.js +203 -243
  72. package/dist/services/fast-context-agent.js.map +1 -1
  73. package/dist/services/input-detection-agent.js +190 -202
  74. package/dist/services/input-detection-agent.js.map +1 -1
  75. package/dist/services/input-requirement-detector.js +155 -189
  76. package/dist/services/input-requirement-detector.js.map +1 -1
  77. package/dist/services/local-chat-storage.js +342 -365
  78. package/dist/services/local-chat-storage.js.map +1 -1
  79. package/dist/services/monitored-shell-manager.js +225 -233
  80. package/dist/services/monitored-shell-manager.js.map +1 -1
  81. package/dist/services/ollama-service.js +293 -310
  82. package/dist/services/ollama-service.js.map +1 -1
  83. package/dist/services/rules-storage.js +142 -0
  84. package/dist/services/rules-storage.js.map +1 -0
  85. package/dist/services/session-quota-manager.js +219 -235
  86. package/dist/services/session-quota-manager.js.map +1 -1
  87. package/dist/services/shell-input-agent.js +299 -334
  88. package/dist/services/shell-input-agent.js.map +1 -1
  89. package/dist/services/sub-agent-manager.js +459 -501
  90. package/dist/services/sub-agent-manager.js.map +1 -1
  91. package/dist/services/warpify-detector.js +133 -183
  92. package/dist/services/warpify-detector.js.map +1 -1
  93. package/dist/services/workflow-storage.js +202 -217
  94. package/dist/services/workflow-storage.js.map +1 -1
  95. package/dist/test-ssh-handler.js +148 -193
  96. package/dist/test-ssh-handler.js.map +1 -1
  97. package/dist/tools/add-mcp.js +161 -0
  98. package/dist/tools/add-mcp.js.map +1 -0
  99. package/dist/tools/background-command.js +240 -273
  100. package/dist/tools/background-command.js.map +1 -1
  101. package/dist/tools/command.js +447 -440
  102. package/dist/tools/command.js.map +1 -1
  103. package/dist/tools/create-image.js +172 -202
  104. package/dist/tools/create-image.js.map +1 -1
  105. package/dist/tools/enter-remote-session.js +169 -215
  106. package/dist/tools/enter-remote-session.js.map +1 -1
  107. package/dist/tools/fast-context.js +60 -67
  108. package/dist/tools/fast-context.js.map +1 -1
  109. package/dist/tools/file-ops.js +605 -537
  110. package/dist/tools/file-ops.js.map +1 -1
  111. package/dist/tools/find-files.js +262 -303
  112. package/dist/tools/find-files.js.map +1 -1
  113. package/dist/tools/get-diff.js +423 -400
  114. package/dist/tools/get-diff.js.map +1 -1
  115. package/dist/tools/grep-search.js +966 -948
  116. package/dist/tools/grep-search.js.map +1 -1
  117. package/dist/tools/inspect-symbol.js +308 -323
  118. package/dist/tools/inspect-symbol.js.map +1 -1
  119. package/dist/tools/plan-mode.js +459 -503
  120. package/dist/tools/plan-mode.js.map +1 -1
  121. package/dist/tools/read-binary-file.js +160 -190
  122. package/dist/tools/read-binary-file.js.map +1 -1
  123. package/dist/tools/registry.js +100 -84
  124. package/dist/tools/registry.js.map +1 -1
  125. package/dist/tools/reproduce_issue.js +170 -151
  126. package/dist/tools/reproduce_issue.js.map +1 -1
  127. package/dist/tools/sub-agent.js +223 -228
  128. package/dist/tools/sub-agent.js.map +1 -1
  129. package/dist/tools/task-complete.js +28 -27
  130. package/dist/tools/task-complete.js.map +1 -1
  131. package/dist/tools/types.js +0 -1
  132. package/dist/tools/types.js.map +1 -1
  133. package/dist/tools/validation.js +96 -118
  134. package/dist/tools/validation.js.map +1 -1
  135. package/dist/tools/web-search.js +194 -194
  136. package/dist/tools/web-search.js.map +1 -1
  137. package/dist/tools/workflow-tool.js +77 -82
  138. package/dist/tools/workflow-tool.js.map +1 -1
  139. package/dist/types/index.js +0 -1
  140. package/dist/types/index.js.map +1 -1
  141. package/dist/types/rule.js +1 -0
  142. package/dist/types/rule.js.map +1 -0
  143. package/dist/types/workflow.js +0 -7
  144. package/dist/types/workflow.js.map +1 -1
  145. package/dist/ui/components/AgentTimer.js +24 -25
  146. package/dist/ui/components/AgentTimer.js.map +1 -1
  147. package/dist/ui/components/App.js +3266 -3249
  148. package/dist/ui/components/App.js.map +1 -1
  149. package/dist/ui/components/AuthScreen.js +22 -34
  150. package/dist/ui/components/AuthScreen.js.map +1 -1
  151. package/dist/ui/components/AuthWelcomeScreen.js +30 -24
  152. package/dist/ui/components/AuthWelcomeScreen.js.map +1 -1
  153. package/dist/ui/components/Breadcrumbs.js +53 -82
  154. package/dist/ui/components/Breadcrumbs.js.map +1 -1
  155. package/dist/ui/components/CircularSelectInput.js +59 -67
  156. package/dist/ui/components/CircularSelectInput.js.map +1 -1
  157. package/dist/ui/components/ClipboardFileAutocomplete.js +78 -39
  158. package/dist/ui/components/ClipboardFileAutocomplete.js.map +1 -1
  159. package/dist/ui/components/CodeBlock.js +24 -42
  160. package/dist/ui/components/CodeBlock.js.map +1 -1
  161. package/dist/ui/components/ConfigViewer.js +18 -25
  162. package/dist/ui/components/ConfigViewer.js.map +1 -1
  163. package/dist/ui/components/ConfirmPrompt.js +49 -71
  164. package/dist/ui/components/ConfirmPrompt.js.map +1 -1
  165. package/dist/ui/components/ConnectionStatusMessage.js +32 -83
  166. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  167. package/dist/ui/components/ContextWindowIndicator.js +34 -49
  168. package/dist/ui/components/ContextWindowIndicator.js.map +1 -1
  169. package/dist/ui/components/DetailedPlanReviewScreen.js +104 -106
  170. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
  171. package/dist/ui/components/DiffViewer.js +68 -121
  172. package/dist/ui/components/DiffViewer.js.map +1 -1
  173. package/dist/ui/components/ErrorBoundary.js +40 -48
  174. package/dist/ui/components/ErrorBoundary.js.map +1 -1
  175. package/dist/ui/components/FileCreationPreview.js +29 -60
  176. package/dist/ui/components/FileCreationPreview.js.map +1 -1
  177. package/dist/ui/components/FileOperation.js +34 -29
  178. package/dist/ui/components/FileOperation.js.map +1 -1
  179. package/dist/ui/components/FileTagAutocomplete.js +55 -25
  180. package/dist/ui/components/FileTagAutocomplete.js.map +1 -1
  181. package/dist/ui/components/FontRecommendation.js.map +1 -1
  182. package/dist/ui/components/GitDiffBreadcrumb.js +29 -0
  183. package/dist/ui/components/GitDiffBreadcrumb.js.map +1 -0
  184. package/dist/ui/components/InputBox.js +1620 -2150
  185. package/dist/ui/components/InputBox.js.map +1 -1
  186. package/dist/ui/components/InteractiveShell.js +234 -352
  187. package/dist/ui/components/InteractiveShell.js.map +1 -1
  188. package/dist/ui/components/KeyboardHelp.js +34 -35
  189. package/dist/ui/components/KeyboardHelp.js.map +1 -1
  190. package/dist/ui/components/LoadingIndicator.js +22 -25
  191. package/dist/ui/components/LoadingIndicator.js.map +1 -1
  192. package/dist/ui/components/MCPAddScreen.js +40 -51
  193. package/dist/ui/components/MCPAddScreen.js.map +1 -1
  194. package/dist/ui/components/MCPListScreen.js +40 -48
  195. package/dist/ui/components/MCPListScreen.js.map +1 -1
  196. package/dist/ui/components/MCPServerListScreen.js +49 -56
  197. package/dist/ui/components/MCPServerListScreen.js.map +1 -1
  198. package/dist/ui/components/MarkdownRenderer.js +69 -96
  199. package/dist/ui/components/MarkdownRenderer.js.map +1 -1
  200. package/dist/ui/components/MessageBox.js +66 -48
  201. package/dist/ui/components/MessageBox.js.map +1 -1
  202. package/dist/ui/components/MessageDisplay.js +150 -142
  203. package/dist/ui/components/MessageDisplay.js.map +1 -1
  204. package/dist/ui/components/MonitorModeAIPanel.js +46 -65
  205. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -1
  206. package/dist/ui/components/MultiLineInput.js +243 -277
  207. package/dist/ui/components/MultiLineInput.js.map +1 -1
  208. package/dist/ui/components/PasswordPrompt.js +37 -18
  209. package/dist/ui/components/PasswordPrompt.js.map +1 -1
  210. package/dist/ui/components/PlanAcceptedMessage.js +27 -38
  211. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  212. package/dist/ui/components/PlanReviewScreen.js +46 -50
  213. package/dist/ui/components/PlanReviewScreen.js.map +1 -1
  214. package/dist/ui/components/RulesEditorScreen.js +81 -0
  215. package/dist/ui/components/RulesEditorScreen.js.map +1 -0
  216. package/dist/ui/components/SelectPrompt.js +19 -8
  217. package/dist/ui/components/SelectPrompt.js.map +1 -1
  218. package/dist/ui/components/ShimmerText.js +44 -0
  219. package/dist/ui/components/ShimmerText.js.map +1 -0
  220. package/dist/ui/components/SlashCommandAutocomplete.js +49 -22
  221. package/dist/ui/components/SlashCommandAutocomplete.js.map +1 -1
  222. package/dist/ui/components/StatusBar.js +56 -87
  223. package/dist/ui/components/StatusBar.js.map +1 -1
  224. package/dist/ui/components/StreamingMessageDisplay.js +116 -99
  225. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  226. package/dist/ui/components/TaskCompletedMessage.js +28 -23
  227. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  228. package/dist/ui/components/TaskProgressIndicator.js +44 -70
  229. package/dist/ui/components/TaskProgressIndicator.js.map +1 -1
  230. package/dist/ui/components/ThinkingDisplay.js +44 -41
  231. package/dist/ui/components/ThinkingDisplay.js.map +1 -1
  232. package/dist/ui/components/ToolExecutionMessage.js +772 -1326
  233. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  234. package/dist/ui/components/ToolExecutionStatus.js +53 -84
  235. package/dist/ui/components/ToolExecutionStatus.js.map +1 -1
  236. package/dist/ui/components/ToolResult.js +22 -15
  237. package/dist/ui/components/ToolResult.js.map +1 -1
  238. package/dist/ui/components/VersionUpdatePrompt.js +88 -120
  239. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  240. package/dist/ui/components/WelcomeBanner.js +176 -26
  241. package/dist/ui/components/WelcomeBanner.js.map +1 -1
  242. package/dist/ui/components/WorkflowCreatorScreen.js +94 -161
  243. package/dist/ui/components/WorkflowCreatorScreen.js.map +1 -1
  244. package/dist/utils/ansi-encoder.js +30 -61
  245. package/dist/utils/ansi-encoder.js.map +1 -1
  246. package/dist/utils/chat-formatter.js +327 -305
  247. package/dist/utils/chat-formatter.js.map +1 -1
  248. package/dist/utils/command-history.js +152 -174
  249. package/dist/utils/command-history.js.map +1 -1
  250. package/dist/utils/context-sanitizer.js +49 -112
  251. package/dist/utils/context-sanitizer.js.map +1 -1
  252. package/dist/utils/conversation-logger.js +292 -324
  253. package/dist/utils/conversation-logger.js.map +1 -1
  254. package/dist/utils/custom-commands-manager.js +126 -131
  255. package/dist/utils/custom-commands-manager.js.map +1 -1
  256. package/dist/utils/editor-utils.js +732 -837
  257. package/dist/utils/editor-utils.js.map +1 -1
  258. package/dist/utils/file.js +174 -213
  259. package/dist/utils/file.js.map +1 -1
  260. package/dist/utils/git-stats.js +169 -0
  261. package/dist/utils/git-stats.js.map +1 -0
  262. package/dist/utils/input-classifier.js +960 -482
  263. package/dist/utils/input-classifier.js.map +1 -1
  264. package/dist/utils/logger.js +48 -73
  265. package/dist/utils/logger.js.map +1 -1
  266. package/dist/utils/markdown-parser.js +277 -310
  267. package/dist/utils/markdown-parser.js.map +1 -1
  268. package/dist/utils/rule-reference-resolver.js +54 -0
  269. package/dist/utils/rule-reference-resolver.js.map +1 -0
  270. package/dist/utils/shell.js +144 -156
  271. package/dist/utils/shell.js.map +1 -1
  272. package/dist/utils/state.js +23 -22
  273. package/dist/utils/state.js.map +1 -1
  274. package/dist/utils/syntax-checker.js +279 -327
  275. package/dist/utils/syntax-checker.js.map +1 -1
  276. package/dist/utils/terminal-output.js +199 -302
  277. package/dist/utils/terminal-output.js.map +1 -1
  278. package/dist/utils/text-clipboard.js +47 -70
  279. package/dist/utils/text-clipboard.js.map +1 -1
  280. package/dist/utils/unicode-sanitizer.js +134 -197
  281. package/dist/utils/unicode-sanitizer.js.map +1 -1
  282. package/dist/utils/version-checker.js +46 -56
  283. package/dist/utils/version-checker.js.map +1 -1
  284. package/package.json +6 -4
  285. package/dist/ai/types.d.ts +0 -20
  286. package/dist/ai/types.d.ts.map +0 -1
  287. package/dist/cli-adapter.d.ts +0 -511
  288. package/dist/cli-adapter.d.ts.map +0 -1
  289. package/dist/commands/CommandParser.d.ts +0 -27
  290. package/dist/commands/CommandParser.d.ts.map +0 -1
  291. package/dist/config/build-config.d.ts +0 -42
  292. package/dist/config/build-config.d.ts.map +0 -1
  293. package/dist/config/defaultConfig.d.ts +0 -79
  294. package/dist/config/defaultConfig.d.ts.map +0 -1
  295. package/dist/config/manager.d.ts +0 -62
  296. package/dist/config/manager.d.ts.map +0 -1
  297. package/dist/config/mcp-config-manager.d.ts +0 -79
  298. package/dist/config/mcp-config-manager.d.ts.map +0 -1
  299. package/dist/config/models.d.ts +0 -83
  300. package/dist/config/models.d.ts.map +0 -1
  301. package/dist/config/slash-commands.d.ts +0 -23
  302. package/dist/config/slash-commands.d.ts.map +0 -1
  303. package/dist/config/types.d.ts +0 -35
  304. package/dist/config/types.d.ts.map +0 -1
  305. package/dist/context/command-detector.d.ts +0 -50
  306. package/dist/context/command-detector.d.ts.map +0 -1
  307. package/dist/context/context-manager.d.ts +0 -157
  308. package/dist/context/context-manager.d.ts.map +0 -1
  309. package/dist/context/handlers/docker-handler.d.ts +0 -130
  310. package/dist/context/handlers/docker-handler.d.ts.map +0 -1
  311. package/dist/context/handlers/ssh-handler.d.ts +0 -201
  312. package/dist/context/handlers/ssh-handler.d.ts.map +0 -1
  313. package/dist/context/handlers/wsl-handler.d.ts +0 -146
  314. package/dist/context/handlers/wsl-handler.d.ts.map +0 -1
  315. package/dist/context/index.d.ts +0 -8
  316. package/dist/context/index.d.ts.map +0 -1
  317. package/dist/context/subshell-handler.d.ts +0 -165
  318. package/dist/context/subshell-handler.d.ts.map +0 -1
  319. package/dist/context/types.d.ts +0 -70
  320. package/dist/context/types.d.ts.map +0 -1
  321. package/dist/hooks/useConnectivity.d.ts +0 -2
  322. package/dist/hooks/useConnectivity.d.ts.map +0 -1
  323. package/dist/hooks/useTerminalDimensions.d.ts +0 -41
  324. package/dist/hooks/useTerminalDimensions.d.ts.map +0 -1
  325. package/dist/index.d.ts +0 -9
  326. package/dist/index.d.ts.map +0 -1
  327. package/dist/mcp/mcp-command-handler.d.ts +0 -47
  328. package/dist/mcp/mcp-command-handler.d.ts.map +0 -1
  329. package/dist/mcp/mcp-server-manager.d.ts +0 -30
  330. package/dist/mcp/mcp-server-manager.d.ts.map +0 -1
  331. package/dist/mcp/mcp-tool-wrapper.d.ts +0 -12
  332. package/dist/mcp/mcp-tool-wrapper.d.ts.map +0 -1
  333. package/dist/services/ai-autocomplete-agent.d.ts +0 -39
  334. package/dist/services/ai-autocomplete-agent.d.ts.map +0 -1
  335. package/dist/services/ai-context-injector.d.ts +0 -41
  336. package/dist/services/ai-context-injector.d.ts.map +0 -1
  337. package/dist/services/ai-service-client.d.ts +0 -128
  338. package/dist/services/ai-service-client.d.ts.map +0 -1
  339. package/dist/services/api-client.d.ts +0 -353
  340. package/dist/services/api-client.d.ts.map +0 -1
  341. package/dist/services/auth-handler.d.ts +0 -30
  342. package/dist/services/auth-handler.d.ts.map +0 -1
  343. package/dist/services/background-task-manager.d.ts +0 -114
  344. package/dist/services/background-task-manager.d.ts.map +0 -1
  345. package/dist/services/checkpoint-manager.d.ts +0 -167
  346. package/dist/services/checkpoint-manager.d.ts.map +0 -1
  347. package/dist/services/clipboard-service.d.ts +0 -37
  348. package/dist/services/clipboard-service.d.ts.map +0 -1
  349. package/dist/services/connectivity-manager.d.ts +0 -18
  350. package/dist/services/connectivity-manager.d.ts.map +0 -1
  351. package/dist/services/conversation-manager.d.ts +0 -73
  352. package/dist/services/conversation-manager.d.ts.map +0 -1
  353. package/dist/services/environment-context-injector.d.ts +0 -69
  354. package/dist/services/environment-context-injector.d.ts.map +0 -1
  355. package/dist/services/fast-context-agent.d.ts +0 -12
  356. package/dist/services/fast-context-agent.d.ts.map +0 -1
  357. package/dist/services/input-detection-agent.d.ts +0 -40
  358. package/dist/services/input-detection-agent.d.ts.map +0 -1
  359. package/dist/services/input-requirement-detector.d.ts +0 -28
  360. package/dist/services/input-requirement-detector.d.ts.map +0 -1
  361. package/dist/services/local-chat-storage.d.ts +0 -182
  362. package/dist/services/local-chat-storage.d.ts.map +0 -1
  363. package/dist/services/monitored-shell-manager.d.ts +0 -120
  364. package/dist/services/monitored-shell-manager.d.ts.map +0 -1
  365. package/dist/services/ollama-service.d.ts +0 -197
  366. package/dist/services/ollama-service.d.ts.map +0 -1
  367. package/dist/services/session-quota-manager.d.ts +0 -101
  368. package/dist/services/session-quota-manager.d.ts.map +0 -1
  369. package/dist/services/shell-input-agent.d.ts +0 -89
  370. package/dist/services/shell-input-agent.d.ts.map +0 -1
  371. package/dist/services/sub-agent-manager.d.ts +0 -140
  372. package/dist/services/sub-agent-manager.d.ts.map +0 -1
  373. package/dist/services/warpify-detector.d.ts +0 -43
  374. package/dist/services/warpify-detector.d.ts.map +0 -1
  375. package/dist/services/workflow-storage.d.ts +0 -72
  376. package/dist/services/workflow-storage.d.ts.map +0 -1
  377. package/dist/test-ssh-handler.d.ts +0 -8
  378. package/dist/test-ssh-handler.d.ts.map +0 -1
  379. package/dist/tools/background-command.d.ts +0 -11
  380. package/dist/tools/background-command.d.ts.map +0 -1
  381. package/dist/tools/command.d.ts +0 -3
  382. package/dist/tools/command.d.ts.map +0 -1
  383. package/dist/tools/create-image.d.ts +0 -10
  384. package/dist/tools/create-image.d.ts.map +0 -1
  385. package/dist/tools/enter-remote-session.d.ts +0 -48
  386. package/dist/tools/enter-remote-session.d.ts.map +0 -1
  387. package/dist/tools/fast-context.d.ts +0 -3
  388. package/dist/tools/fast-context.d.ts.map +0 -1
  389. package/dist/tools/file-ops.d.ts +0 -7
  390. package/dist/tools/file-ops.d.ts.map +0 -1
  391. package/dist/tools/find-files.d.ts +0 -49
  392. package/dist/tools/find-files.d.ts.map +0 -1
  393. package/dist/tools/get-diff.d.ts +0 -14
  394. package/dist/tools/get-diff.d.ts.map +0 -1
  395. package/dist/tools/grep-search.d.ts +0 -155
  396. package/dist/tools/grep-search.d.ts.map +0 -1
  397. package/dist/tools/inspect-symbol.d.ts +0 -32
  398. package/dist/tools/inspect-symbol.d.ts.map +0 -1
  399. package/dist/tools/plan-mode.d.ts +0 -140
  400. package/dist/tools/plan-mode.d.ts.map +0 -1
  401. package/dist/tools/read-binary-file.d.ts +0 -10
  402. package/dist/tools/read-binary-file.d.ts.map +0 -1
  403. package/dist/tools/registry.d.ts +0 -31
  404. package/dist/tools/registry.d.ts.map +0 -1
  405. package/dist/tools/reproduce_issue.d.ts +0 -2
  406. package/dist/tools/reproduce_issue.d.ts.map +0 -1
  407. package/dist/tools/sub-agent.d.ts +0 -9
  408. package/dist/tools/sub-agent.d.ts.map +0 -1
  409. package/dist/tools/task-complete.d.ts +0 -3
  410. package/dist/tools/task-complete.d.ts.map +0 -1
  411. package/dist/tools/types.d.ts +0 -40
  412. package/dist/tools/types.d.ts.map +0 -1
  413. package/dist/tools/validation.d.ts +0 -47
  414. package/dist/tools/validation.d.ts.map +0 -1
  415. package/dist/tools/web-search.d.ts +0 -24
  416. package/dist/tools/web-search.d.ts.map +0 -1
  417. package/dist/tools/workflow-tool.d.ts +0 -11
  418. package/dist/tools/workflow-tool.d.ts.map +0 -1
  419. package/dist/types/index.d.ts +0 -123
  420. package/dist/types/index.d.ts.map +0 -1
  421. package/dist/types/workflow.d.ts +0 -110
  422. package/dist/types/workflow.d.ts.map +0 -1
  423. package/dist/ui/components/AgentTimer.d.ts +0 -7
  424. package/dist/ui/components/AgentTimer.d.ts.map +0 -1
  425. package/dist/ui/components/App.d.ts +0 -197
  426. package/dist/ui/components/App.d.ts.map +0 -1
  427. package/dist/ui/components/AuthScreen.d.ts +0 -8
  428. package/dist/ui/components/AuthScreen.d.ts.map +0 -1
  429. package/dist/ui/components/AuthWelcomeScreen.d.ts +0 -8
  430. package/dist/ui/components/AuthWelcomeScreen.d.ts.map +0 -1
  431. package/dist/ui/components/Breadcrumbs.d.ts +0 -13
  432. package/dist/ui/components/Breadcrumbs.d.ts.map +0 -1
  433. package/dist/ui/components/CircularSelectInput.d.ts +0 -24
  434. package/dist/ui/components/CircularSelectInput.d.ts.map +0 -1
  435. package/dist/ui/components/ClipboardFileAutocomplete.d.ts +0 -10
  436. package/dist/ui/components/ClipboardFileAutocomplete.d.ts.map +0 -1
  437. package/dist/ui/components/CodeBlock.d.ts +0 -9
  438. package/dist/ui/components/CodeBlock.d.ts.map +0 -1
  439. package/dist/ui/components/ConfigViewer.d.ts +0 -11
  440. package/dist/ui/components/ConfigViewer.d.ts.map +0 -1
  441. package/dist/ui/components/ConfirmPrompt.d.ts +0 -13
  442. package/dist/ui/components/ConfirmPrompt.d.ts.map +0 -1
  443. package/dist/ui/components/ConnectionStatusMessage.d.ts +0 -17
  444. package/dist/ui/components/ConnectionStatusMessage.d.ts.map +0 -1
  445. package/dist/ui/components/ContextWindowIndicator.d.ts +0 -8
  446. package/dist/ui/components/ContextWindowIndicator.d.ts.map +0 -1
  447. package/dist/ui/components/DetailedPlanReviewScreen.d.ts +0 -17
  448. package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +0 -1
  449. package/dist/ui/components/DiffViewer.d.ts +0 -9
  450. package/dist/ui/components/DiffViewer.d.ts.map +0 -1
  451. package/dist/ui/components/ErrorBoundary.d.ts +0 -17
  452. package/dist/ui/components/ErrorBoundary.d.ts.map +0 -1
  453. package/dist/ui/components/FileCreationPreview.d.ts +0 -8
  454. package/dist/ui/components/FileCreationPreview.d.ts.map +0 -1
  455. package/dist/ui/components/FileOperation.d.ts +0 -10
  456. package/dist/ui/components/FileOperation.d.ts.map +0 -1
  457. package/dist/ui/components/FileTagAutocomplete.d.ts +0 -11
  458. package/dist/ui/components/FileTagAutocomplete.d.ts.map +0 -1
  459. package/dist/ui/components/FontRecommendation.d.ts +0 -1
  460. package/dist/ui/components/FontRecommendation.d.ts.map +0 -1
  461. package/dist/ui/components/InputBox.d.ts +0 -42
  462. package/dist/ui/components/InputBox.d.ts.map +0 -1
  463. package/dist/ui/components/InteractiveShell.d.ts +0 -30
  464. package/dist/ui/components/InteractiveShell.d.ts.map +0 -1
  465. package/dist/ui/components/KeyboardHelp.d.ts +0 -7
  466. package/dist/ui/components/KeyboardHelp.d.ts.map +0 -1
  467. package/dist/ui/components/LoadingIndicator.d.ts +0 -3
  468. package/dist/ui/components/LoadingIndicator.d.ts.map +0 -1
  469. package/dist/ui/components/MCPAddScreen.d.ts +0 -13
  470. package/dist/ui/components/MCPAddScreen.d.ts.map +0 -1
  471. package/dist/ui/components/MCPListScreen.d.ts +0 -17
  472. package/dist/ui/components/MCPListScreen.d.ts.map +0 -1
  473. package/dist/ui/components/MCPServerListScreen.d.ts +0 -16
  474. package/dist/ui/components/MCPServerListScreen.d.ts.map +0 -1
  475. package/dist/ui/components/MarkdownRenderer.d.ts +0 -8
  476. package/dist/ui/components/MarkdownRenderer.d.ts.map +0 -1
  477. package/dist/ui/components/MessageBox.d.ts +0 -10
  478. package/dist/ui/components/MessageBox.d.ts.map +0 -1
  479. package/dist/ui/components/MessageDisplay.d.ts +0 -14
  480. package/dist/ui/components/MessageDisplay.d.ts.map +0 -1
  481. package/dist/ui/components/MonitorModeAIPanel.d.ts +0 -23
  482. package/dist/ui/components/MonitorModeAIPanel.d.ts.map +0 -1
  483. package/dist/ui/components/MultiLineInput.d.ts +0 -13
  484. package/dist/ui/components/MultiLineInput.d.ts.map +0 -1
  485. package/dist/ui/components/PasswordPrompt.d.ts +0 -9
  486. package/dist/ui/components/PasswordPrompt.d.ts.map +0 -1
  487. package/dist/ui/components/PlanAcceptedMessage.d.ts +0 -20
  488. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +0 -1
  489. package/dist/ui/components/PlanReviewScreen.d.ts +0 -14
  490. package/dist/ui/components/PlanReviewScreen.d.ts.map +0 -1
  491. package/dist/ui/components/SelectPrompt.d.ts +0 -12
  492. package/dist/ui/components/SelectPrompt.d.ts.map +0 -1
  493. package/dist/ui/components/SlashCommandAutocomplete.d.ts +0 -13
  494. package/dist/ui/components/SlashCommandAutocomplete.d.ts.map +0 -1
  495. package/dist/ui/components/StatusBar.d.ts +0 -14
  496. package/dist/ui/components/StatusBar.d.ts.map +0 -1
  497. package/dist/ui/components/StreamingMessageDisplay.d.ts +0 -15
  498. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +0 -1
  499. package/dist/ui/components/TaskCompletedMessage.d.ts +0 -14
  500. package/dist/ui/components/TaskCompletedMessage.d.ts.map +0 -1
  501. package/dist/ui/components/TaskProgressIndicator.d.ts +0 -18
  502. package/dist/ui/components/TaskProgressIndicator.d.ts.map +0 -1
  503. package/dist/ui/components/ThinkingDisplay.d.ts +0 -15
  504. package/dist/ui/components/ThinkingDisplay.d.ts.map +0 -1
  505. package/dist/ui/components/ToolExecutionMessage.d.ts +0 -8
  506. package/dist/ui/components/ToolExecutionMessage.d.ts.map +0 -1
  507. package/dist/ui/components/ToolExecutionStatus.d.ts +0 -10
  508. package/dist/ui/components/ToolExecutionStatus.d.ts.map +0 -1
  509. package/dist/ui/components/ToolResult.d.ts +0 -10
  510. package/dist/ui/components/ToolResult.d.ts.map +0 -1
  511. package/dist/ui/components/VersionUpdatePrompt.d.ts +0 -9
  512. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +0 -1
  513. package/dist/ui/components/WelcomeBanner.d.ts +0 -3
  514. package/dist/ui/components/WelcomeBanner.d.ts.map +0 -1
  515. package/dist/ui/components/WorkflowCreatorScreen.d.ts +0 -25
  516. package/dist/ui/components/WorkflowCreatorScreen.d.ts.map +0 -1
  517. package/dist/utils/ansi-encoder.d.ts +0 -7
  518. package/dist/utils/ansi-encoder.d.ts.map +0 -1
  519. package/dist/utils/chat-formatter.d.ts +0 -12
  520. package/dist/utils/chat-formatter.d.ts.map +0 -1
  521. package/dist/utils/command-history.d.ts +0 -24
  522. package/dist/utils/command-history.d.ts.map +0 -1
  523. package/dist/utils/context-sanitizer.d.ts +0 -50
  524. package/dist/utils/context-sanitizer.d.ts.map +0 -1
  525. package/dist/utils/conversation-logger.d.ts +0 -142
  526. package/dist/utils/conversation-logger.d.ts.map +0 -1
  527. package/dist/utils/custom-commands-manager.d.ts +0 -59
  528. package/dist/utils/custom-commands-manager.d.ts.map +0 -1
  529. package/dist/utils/editor-utils.d.ts +0 -101
  530. package/dist/utils/editor-utils.d.ts.map +0 -1
  531. package/dist/utils/file.d.ts +0 -61
  532. package/dist/utils/file.d.ts.map +0 -1
  533. package/dist/utils/input-classifier.d.ts +0 -25
  534. package/dist/utils/input-classifier.d.ts.map +0 -1
  535. package/dist/utils/logger.d.ts +0 -17
  536. package/dist/utils/logger.d.ts.map +0 -1
  537. package/dist/utils/markdown-parser.d.ts +0 -60
  538. package/dist/utils/markdown-parser.d.ts.map +0 -1
  539. package/dist/utils/shell.d.ts +0 -47
  540. package/dist/utils/shell.d.ts.map +0 -1
  541. package/dist/utils/state.d.ts +0 -13
  542. package/dist/utils/state.d.ts.map +0 -1
  543. package/dist/utils/syntax-checker.d.ts +0 -24
  544. package/dist/utils/syntax-checker.d.ts.map +0 -1
  545. package/dist/utils/terminal-output.d.ts +0 -25
  546. package/dist/utils/terminal-output.d.ts.map +0 -1
  547. package/dist/utils/text-clipboard.d.ts +0 -12
  548. package/dist/utils/text-clipboard.d.ts.map +0 -1
  549. package/dist/utils/unicode-sanitizer.d.ts +0 -44
  550. package/dist/utils/unicode-sanitizer.d.ts.map +0 -1
  551. package/dist/utils/version-checker.d.ts +0 -14
  552. package/dist/utils/version-checker.d.ts.map +0 -1
@@ -1,1035 +1,1575 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
- import fg from 'fast-glob';
5
- import { logError, logWarning } from '../utils/logger.js';
6
- const DEFAULT_IGNORE_PATTERNS = [
7
- '**/.git/**',
8
- '**/node_modules/**',
9
- '**/dist/**',
10
- '**/build/**',
11
- '**/out/**',
12
- '**/.next/**',
13
- '**/.turbo/**',
14
- '**/.cache/**',
15
- '**/coverage/**',
16
- '**/__pycache__/**',
17
- '**/.venv/**',
18
- '**/venv/**',
19
- '**/.idea/**',
20
- '**/.vscode/**',
21
- '**/.DS_Store',
22
- '**/Thumbs.db',
23
- '**/.centaurus/**',
24
- ];
25
- export class CheckpointManager {
26
- checkpoints = [];
27
- currentChatId = null;
28
- discardedIds = new Set();
29
- baseDir;
30
- constructor() {
31
- this.baseDir = path.join(os.homedir(), '.centaurus', 'checkpoints');
32
- this.ensureDirSync(this.baseDir);
33
- }
34
- setCurrentChatId(chatId) {
35
- this.currentChatId = chatId;
36
- this.loadIndex();
37
- }
38
- clear() {
39
- this.checkpoints = [];
40
- this.discardedIds.clear();
41
- }
42
- list() {
43
- return [...this.checkpoints].sort((a, b) => b.createdAtMs - a.createdAtMs);
44
- }
45
- /**
46
- * Get session changes for an active checkpoint (live calculation).
47
- * Returns added/modified/deleted file lists plus per-file line stats.
48
- */
49
- async getSessionChanges(checkpointId, handler) {
50
- const checkpoint = this.checkpoints.find(cp => cp.id === checkpointId);
51
- if (!checkpoint)
52
- return null;
53
- const isRemote = checkpoint.contextType !== 'local';
54
- if (isRemote && (!handler || !handler.isConnected())) {
55
- return null;
56
- }
57
- const changes = await this.calculateChanges(checkpoint, handler);
58
- // Calculate line-level stats for modified files
59
- const stats = [];
60
- if (!isRemote) {
61
- for (const filePath of changes.modified) {
62
- const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
63
- const currentPath = path.join(checkpoint.cwd, filePath);
64
- const lineStat = this.calculateLineStats(snapshotPath, currentPath);
65
- stats.push({ filePath, ...lineStat });
66
- }
67
- // For added files, count all lines as insertions
68
- for (const filePath of changes.added) {
69
- const currentPath = path.join(checkpoint.cwd, filePath);
70
- try {
71
- const content = fs.readFileSync(currentPath, 'utf-8');
72
- const lines = content.split('\n').length;
73
- stats.push({ filePath, insertions: lines, deletions: 0 });
74
- }
75
- catch {
76
- stats.push({ filePath, insertions: 0, deletions: 0 });
77
- }
78
- }
79
- // For deleted files, count all lines as deletions
80
- for (const filePath of changes.deleted) {
81
- const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
82
- try {
83
- const content = fs.readFileSync(snapshotPath, 'utf-8');
84
- const lines = content.split('\n').length;
85
- stats.push({ filePath, insertions: 0, deletions: lines });
86
- }
87
- catch {
88
- stats.push({ filePath, insertions: 0, deletions: 0 });
89
- }
90
- }
91
- }
92
- else if (handler) {
93
- // Remote stats: use handler to read remote file contents
94
- for (const filePath of changes.modified) {
95
- const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
96
- const remotePath = checkpoint.cwd + '/' + filePath;
97
- try {
98
- const snapshotContent = fs.readFileSync(snapshotPath, 'utf-8');
99
- const remoteContent = await handler.readFile(remotePath);
100
- const lineStat = this.calculateLineStatsFromContent(snapshotContent, remoteContent);
101
- stats.push({ filePath, ...lineStat });
102
- }
103
- catch {
104
- stats.push({ filePath, insertions: 0, deletions: 0 });
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { logError, logWarning } from "../utils/logger.js";
5
+ import { quickLog } from "../utils/conversation-logger.js";
6
+ class CheckpointManager {
7
+ checkpoints = [];
8
+ currentChatId = null;
9
+ discardedIds = /* @__PURE__ */ new Set();
10
+ baseDir;
11
+ constructor() {
12
+ this.baseDir = path.join(os.homedir(), ".centaurus", "checkpoints");
13
+ this.ensureDirSync(this.baseDir);
14
+ }
15
+ setCurrentChatId(chatId) {
16
+ this.currentChatId = chatId;
17
+ this.loadIndex();
18
+ }
19
+ clear() {
20
+ this.checkpoints = [];
21
+ this.discardedIds.clear();
22
+ }
23
+ list() {
24
+ return [...this.checkpoints].sort((a, b) => b.createdAtMs - a.createdAtMs);
25
+ }
26
+ /**
27
+ * Merge checkpoint metadata restored from chat history into in-memory state.
28
+ * This acts as a fallback when checkpoint index.json is missing or stale.
29
+ * Existing in-memory entries (loaded from disk index) take precedence.
30
+ *
31
+ * @returns Number of checkpoints merged from chat history.
32
+ */
33
+ hydrateFromChatHistory(entries) {
34
+ if (!Array.isArray(entries) || entries.length === 0) {
35
+ return 0;
36
+ }
37
+ const existingIds = new Set(this.checkpoints.map((cp) => cp.id));
38
+ let merged = 0;
39
+ for (const rawEntry of entries) {
40
+ const entry = this.normalizeCheckpointMeta(rawEntry);
41
+ if (!entry || existingIds.has(entry.id)) {
42
+ continue;
43
+ }
44
+ if (!fs.existsSync(entry.manifestPath)) {
45
+ continue;
46
+ }
47
+ this.checkpoints.push(entry);
48
+ existingIds.add(entry.id);
49
+ merged++;
50
+ }
51
+ if (merged > 0) {
52
+ this.saveIndex();
53
+ }
54
+ return merged;
55
+ }
56
+ // ── Backup-on-write: called by file tools before modifying a file ───
57
+ /**
58
+ * Back up a single file before the AI modifies it.
59
+ * This is the core of the new checkpoint system: instead of copying the
60
+ * entire project at checkpoint start, we only back up the specific files
61
+ * that the AI actually touches, right before it touches them.
62
+ *
63
+ * If the file has already been backed up in this checkpoint, this is a no-op
64
+ * (we always want the ORIGINAL state, not intermediate states).
65
+ */
66
+ async backupFileBeforeChange(checkpointId, absoluteFilePath, cwd, handler) {
67
+ const checkpoint = this.checkpoints.find((cp) => cp.id === checkpointId);
68
+ if (!checkpoint || checkpoint.status !== "active") return;
69
+ const baseCwd = checkpoint.cwd || cwd;
70
+ const { absolutePath, relativePath } = this.resolveCheckpointFilePath(baseCwd, absoluteFilePath);
71
+ const manifest = this.readManifestV2(checkpoint.manifestPath);
72
+ const alreadyBacked = manifest.fileBackups.some((b) => b.filePath === relativePath);
73
+ if (alreadyBacked) return;
74
+ const isRemote = checkpoint.contextType !== "local" && handler && handler.isConnected();
75
+ const backupDir = path.join(path.dirname(checkpoint.manifestPath), "backups");
76
+ try {
77
+ let existed = false;
78
+ let fileSize;
79
+ if (isRemote) {
80
+ const remotePath = path.posix.resolve(baseCwd, relativePath);
81
+ try {
82
+ const content = await handler.readFile(remotePath);
83
+ existed = true;
84
+ fileSize = Buffer.from(content, "utf-8").length;
85
+ const backupPath = path.join(backupDir, relativePath);
86
+ this.ensureDirSync(path.dirname(backupPath));
87
+ fs.writeFileSync(backupPath, content, "utf-8");
88
+ } catch {
89
+ existed = false;
90
+ }
91
+ } else {
92
+ if (fs.existsSync(absolutePath)) {
93
+ existed = true;
94
+ try {
95
+ const stat = fs.statSync(absolutePath);
96
+ fileSize = stat.size;
97
+ const backupPath = path.join(backupDir, relativePath);
98
+ this.ensureDirSync(path.dirname(backupPath));
99
+ fs.copyFileSync(absolutePath, backupPath);
100
+ } catch (err) {
101
+ logWarning(`Failed to backup file ${relativePath}: ${err.message}`);
102
+ return;
103
+ }
104
+ } else {
105
+ existed = false;
106
+ }
107
+ }
108
+ manifest.fileBackups.push({
109
+ filePath: relativePath,
110
+ existed,
111
+ size: fileSize,
112
+ backedUpAt: (/* @__PURE__ */ new Date()).toISOString()
113
+ });
114
+ fs.writeFileSync(checkpoint.manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
115
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Checkpoint] Backed up "${relativePath}" (existed=${existed}) in ${checkpointId}
116
+ `);
117
+ } catch (error) {
118
+ logWarning(`Failed to backup file before change: ${error.message}`);
119
+ }
120
+ }
121
+ resolveCheckpointFilePath(cwd, filePath) {
122
+ const baseCwd = path.resolve(cwd);
123
+ const absolutePath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(baseCwd, filePath);
124
+ let relativePath = path.relative(baseCwd, absolutePath).replace(/\\/g, "/");
125
+ relativePath = relativePath.replace(/^\.\/+/, "");
126
+ if (!relativePath) {
127
+ relativePath = path.basename(absolutePath).replace(/\\/g, "/");
128
+ }
129
+ return { absolutePath, relativePath };
130
+ }
131
+ /**
132
+ * Record a file operation in the checkpoint's operation log.
133
+ * Called by file tools after successfully modifying a file.
134
+ */
135
+ recordFileOperation(checkpointId, type, filePath, toolName) {
136
+ const checkpoint = this.checkpoints.find((cp) => cp.id === checkpointId);
137
+ if (!checkpoint || checkpoint.status !== "active") return;
138
+ try {
139
+ const manifest = this.readManifestV2(checkpoint.manifestPath);
140
+ manifest.operations.push({
141
+ type,
142
+ filePath,
143
+ toolName,
144
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
145
+ });
146
+ fs.writeFileSync(checkpoint.manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
147
+ } catch (error) {
148
+ logWarning(`Failed to record file operation: ${error.message}`);
149
+ }
150
+ }
151
+ // ── Session changes and diff ────────────────────────────────────────
152
+ /**
153
+ * Get session changes for a checkpoint (live calculation).
154
+ * Returns added/modified/deleted file lists plus per-file line stats.
155
+ */
156
+ async getSessionChanges(checkpointId, handler) {
157
+ const checkpoint = this.checkpoints.find((cp) => cp.id === checkpointId);
158
+ if (!checkpoint) return null;
159
+ const isRemote = checkpoint.contextType !== "local";
160
+ if (isRemote && (!handler || !handler.isConnected())) {
161
+ return null;
162
+ }
163
+ const changes = await this.calculateChanges(checkpoint, handler);
164
+ const stats = [];
165
+ const manifest = this.readManifest(checkpoint.manifestPath);
166
+ if (this.isV2Manifest(manifest)) {
167
+ for (const backup of manifest.fileBackups) {
168
+ const backupPath = path.join(path.dirname(checkpoint.manifestPath), "backups", backup.filePath);
169
+ if (isRemote && handler) {
170
+ const remotePath = checkpoint.cwd + "/" + backup.filePath;
171
+ try {
172
+ if (backup.existed) {
173
+ const backupContent = fs.readFileSync(backupPath, "utf-8");
174
+ try {
175
+ const currentContent = await handler.readFile(remotePath);
176
+ if (backupContent !== currentContent) {
177
+ const lineStat = this.calculateLineStatsFromContent(backupContent, currentContent);
178
+ stats.push({ filePath: backup.filePath, ...lineStat });
105
179
  }
180
+ } catch {
181
+ const lines = backupContent.split("\n").length;
182
+ stats.push({ filePath: backup.filePath, insertions: 0, deletions: lines });
183
+ }
184
+ } else {
185
+ try {
186
+ const currentContent = await handler.readFile(remotePath);
187
+ const lines = currentContent.split("\n").length;
188
+ stats.push({ filePath: backup.filePath, insertions: lines, deletions: 0 });
189
+ } catch {
190
+ }
106
191
  }
107
- for (const filePath of changes.added) {
108
- const remotePath = checkpoint.cwd + '/' + filePath;
109
- try {
110
- const remoteContent = await handler.readFile(remotePath);
111
- const lines = remoteContent.split('\n').length;
112
- stats.push({ filePath, insertions: lines, deletions: 0 });
113
- }
114
- catch {
115
- stats.push({ filePath, insertions: 0, deletions: 0 });
192
+ } catch {
193
+ stats.push({ filePath: backup.filePath, insertions: 0, deletions: 0 });
194
+ }
195
+ } else {
196
+ const currentPath = path.join(checkpoint.cwd, backup.filePath);
197
+ if (backup.existed) {
198
+ if (fs.existsSync(currentPath)) {
199
+ try {
200
+ const lineStat = this.calculateLineStats(backupPath, currentPath);
201
+ if (lineStat.insertions > 0 || lineStat.deletions > 0) {
202
+ stats.push({ filePath: backup.filePath, ...lineStat });
116
203
  }
204
+ } catch {
205
+ stats.push({ filePath: backup.filePath, insertions: 0, deletions: 0 });
206
+ }
207
+ } else {
208
+ try {
209
+ const content = fs.readFileSync(backupPath, "utf-8");
210
+ const lines = content.split("\n").length;
211
+ stats.push({ filePath: backup.filePath, insertions: 0, deletions: lines });
212
+ } catch {
213
+ stats.push({ filePath: backup.filePath, insertions: 0, deletions: 0 });
214
+ }
117
215
  }
118
- for (const filePath of changes.deleted) {
119
- const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
120
- try {
121
- const content = fs.readFileSync(snapshotPath, 'utf-8');
122
- const lines = content.split('\n').length;
123
- stats.push({ filePath, insertions: 0, deletions: lines });
124
- }
125
- catch {
126
- stats.push({ filePath, insertions: 0, deletions: 0 });
127
- }
216
+ } else {
217
+ if (fs.existsSync(currentPath)) {
218
+ try {
219
+ const content = fs.readFileSync(currentPath, "utf-8");
220
+ const lines = content.split("\n").length;
221
+ stats.push({ filePath: backup.filePath, insertions: lines, deletions: 0 });
222
+ } catch {
223
+ stats.push({ filePath: backup.filePath, insertions: 0, deletions: 0 });
224
+ }
128
225
  }
226
+ }
129
227
  }
130
- return { changes, stats };
131
- }
132
- /**
133
- * Get a unified diff for a single file within a checkpoint.
134
- * Compares the snapshot version to the current version.
135
- */
136
- async getFileDiff(checkpointId, filePath, handler) {
137
- const checkpoint = this.checkpoints.find(cp => cp.id === checkpointId);
138
- if (!checkpoint)
139
- return null;
140
- const isRemote = checkpoint.contextType !== 'local';
141
- const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
142
- const snapshotExists = fs.existsSync(snapshotPath);
143
- if (isRemote) {
144
- if (!handler || !handler.isConnected()) {
145
- return null;
146
- }
147
- const remotePath = checkpoint.cwd + '/' + filePath;
148
- let remoteContent = null;
228
+ }
229
+ } else {
230
+ await this.getSessionChangesV1(checkpoint, changes, stats, isRemote, handler);
231
+ }
232
+ return { changes, stats };
233
+ }
234
+ /**
235
+ * Get session changes across ALL checkpoints (for session scope).
236
+ * Aggregates backups from all checkpoints to find the original state of each file.
237
+ */
238
+ async getAggregatedSessionChanges(handler) {
239
+ if (this.checkpoints.length === 0) return null;
240
+ const sorted = [...this.checkpoints].sort((a, b) => a.createdAtMs - b.createdAtMs);
241
+ const firstCheckpoint = sorted[0];
242
+ const isRemote = firstCheckpoint.contextType !== "local";
243
+ if (isRemote && (!handler || !handler.isConnected())) return null;
244
+ const originalBackups = /* @__PURE__ */ new Map();
245
+ for (const cp of sorted) {
246
+ const manifest = this.readManifest(cp.manifestPath);
247
+ if (!this.isV2Manifest(manifest)) continue;
248
+ for (const backup of manifest.fileBackups) {
249
+ if (!originalBackups.has(backup.filePath)) {
250
+ const backupPath = path.join(path.dirname(cp.manifestPath), "backups", backup.filePath);
251
+ originalBackups.set(backup.filePath, {
252
+ existed: backup.existed,
253
+ backupPath,
254
+ cwd: cp.cwd
255
+ });
256
+ }
257
+ }
258
+ }
259
+ const changes = { added: [], modified: [], deleted: [] };
260
+ const stats = [];
261
+ const cwd = firstCheckpoint.cwd;
262
+ for (const [filePath, original] of originalBackups) {
263
+ if (isRemote && handler) {
264
+ const remotePath = cwd + "/" + filePath;
265
+ try {
266
+ if (original.existed) {
267
+ const backupContent = fs.readFileSync(original.backupPath, "utf-8");
149
268
  try {
150
- remoteContent = await handler.readFile(remotePath);
151
- }
152
- catch {
153
- remoteContent = null;
154
- }
155
- if (!snapshotExists && remoteContent === null) {
156
- return null;
157
- }
158
- // Added file
159
- if (!snapshotExists && remoteContent !== null) {
160
- const lines = remoteContent.split('\n');
161
- let diff = `--- /dev/null\n+++ b/${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
162
- diff += lines.map(l => `+${l}`).join('\n');
163
- return diff;
164
- }
165
- // Deleted file
166
- if (snapshotExists && remoteContent === null) {
167
- try {
168
- const content = fs.readFileSync(snapshotPath, 'utf-8');
169
- const lines = content.split('\n');
170
- let diff = `--- a/${filePath}\n+++ /dev/null\n@@ -1,${lines.length} +0,0 @@\n`;
171
- diff += lines.map(l => `-${l}`).join('\n');
172
- return diff;
173
- }
174
- catch {
175
- return `[Binary or unreadable file: ${filePath}]`;
176
- }
269
+ const currentContent = await handler.readFile(remotePath);
270
+ if (backupContent !== currentContent) {
271
+ changes.modified.push(filePath);
272
+ const lineStat = this.calculateLineStatsFromContent(backupContent, currentContent);
273
+ stats.push({ filePath, ...lineStat });
274
+ }
275
+ } catch {
276
+ changes.deleted.push(filePath);
277
+ const lines = backupContent.split("\n").length;
278
+ stats.push({ filePath, insertions: 0, deletions: lines });
177
279
  }
178
- // Modified file
280
+ } else {
179
281
  try {
180
- const oldContent = fs.readFileSync(snapshotPath, 'utf-8');
181
- const newContent = remoteContent ?? '';
182
- return this.generateUnifiedDiff(filePath, oldContent, newContent);
282
+ const currentContent = await handler.readFile(remotePath);
283
+ changes.added.push(filePath);
284
+ const lines = currentContent.split("\n").length;
285
+ stats.push({ filePath, insertions: lines, deletions: 0 });
286
+ } catch {
183
287
  }
184
- catch {
185
- return `[Binary or unreadable file: ${filePath}]`;
288
+ }
289
+ } catch {
290
+ }
291
+ } else {
292
+ const currentPath = path.join(cwd, filePath);
293
+ if (original.existed) {
294
+ if (fs.existsSync(currentPath)) {
295
+ if (this.filesDiffer(original.backupPath, currentPath)) {
296
+ changes.modified.push(filePath);
297
+ const lineStat = this.calculateLineStats(original.backupPath, currentPath);
298
+ stats.push({ filePath, ...lineStat });
186
299
  }
187
- }
188
- // Local diff (original logic)
189
- const currentPath = path.join(checkpoint.cwd, filePath);
190
- const currentExists = fs.existsSync(currentPath);
191
- if (!snapshotExists && !currentExists) {
192
- return null;
193
- }
194
- // Added file: show full content as additions
195
- if (!snapshotExists && currentExists) {
300
+ } else {
301
+ changes.deleted.push(filePath);
196
302
  try {
197
- const content = fs.readFileSync(currentPath, 'utf-8');
198
- const lines = content.split('\n');
199
- let diff = `--- /dev/null\n+++ b/${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
200
- diff += lines.map(l => `+${l}`).join('\n');
201
- return diff;
202
- }
203
- catch {
204
- return `[Binary or unreadable file: ${filePath}]`;
303
+ const content = fs.readFileSync(original.backupPath, "utf-8");
304
+ const lines = content.split("\n").length;
305
+ stats.push({ filePath, insertions: 0, deletions: lines });
306
+ } catch {
307
+ stats.push({ filePath, insertions: 0, deletions: 0 });
205
308
  }
206
- }
207
- // Deleted file: show full content as deletions
208
- if (snapshotExists && !currentExists) {
309
+ }
310
+ } else {
311
+ if (fs.existsSync(currentPath)) {
312
+ changes.added.push(filePath);
209
313
  try {
210
- const content = fs.readFileSync(snapshotPath, 'utf-8');
211
- const lines = content.split('\n');
212
- let diff = `--- a/${filePath}\n+++ /dev/null\n@@ -1,${lines.length} +0,0 @@\n`;
213
- diff += lines.map(l => `-${l}`).join('\n');
214
- return diff;
215
- }
216
- catch {
217
- return `[Binary or unreadable file: ${filePath}]`;
314
+ const content = fs.readFileSync(currentPath, "utf-8");
315
+ const lines = content.split("\n").length;
316
+ stats.push({ filePath, insertions: lines, deletions: 0 });
317
+ } catch {
318
+ stats.push({ filePath, insertions: 0, deletions: 0 });
218
319
  }
320
+ }
219
321
  }
220
- // Modified file: produce unified diff
221
- try {
222
- const oldContent = fs.readFileSync(snapshotPath, 'utf-8');
223
- const newContent = fs.readFileSync(currentPath, 'utf-8');
224
- return this.generateUnifiedDiff(filePath, oldContent, newContent);
225
- }
226
- catch {
227
- return `[Binary or unreadable file: ${filePath}]`;
228
- }
322
+ }
229
323
  }
230
- /**
231
- * Get the initial checkpoint for the current chat session.
232
- * This represents the state at the start of the conversation.
233
- */
234
- getInitialCheckpoint() {
235
- if (this.checkpoints.length === 0)
236
- return null;
237
- // Sort by creation time to find the first one
238
- return [...this.checkpoints].sort((a, b) => a.createdAtMs - b.createdAtMs)[0];
239
- }
240
- /**
241
- * Calculate insertions/deletions between two file versions
242
- */
243
- calculateLineStats(snapshotPath, currentPath) {
244
- try {
245
- const oldContent = fs.readFileSync(snapshotPath, 'utf-8');
246
- const newContent = fs.readFileSync(currentPath, 'utf-8');
247
- return this.calculateLineStatsFromContent(oldContent, newContent);
248
- }
249
- catch {
250
- return { insertions: 0, deletions: 0 };
251
- }
324
+ return { changes, stats };
325
+ }
326
+ /**
327
+ * Get a unified diff for a single file within a checkpoint.
328
+ * For V2 checkpoints: compares the backed-up original to the current version.
329
+ * For V1 checkpoints: compares the full snapshot to the current version (legacy).
330
+ */
331
+ async getFileDiff(checkpointId, filePath, handler) {
332
+ const checkpoint = this.checkpoints.find((cp) => cp.id === checkpointId);
333
+ if (!checkpoint) return null;
334
+ const normalizedPath = this.normalizeToRelativePath(filePath, checkpoint.cwd);
335
+ const manifest = this.readManifest(checkpoint.manifestPath);
336
+ if (this.isV2Manifest(manifest)) {
337
+ return this.getFileDiffV2(checkpoint, manifest, normalizedPath, handler);
338
+ } else {
339
+ return this.getFileDiffV1(checkpoint, normalizedPath, handler);
252
340
  }
253
- calculateLineStatsFromContent(oldContent, newContent) {
254
- const oldLines = oldContent.split('\n');
255
- const newLines = newContent.split('\n');
256
- // Simple LCS-based diff to count insertions and deletions
257
- const oldSet = new Map();
258
- for (const line of oldLines) {
259
- oldSet.set(line, (oldSet.get(line) || 0) + 1);
260
- }
261
- const newSet = new Map();
262
- for (const line of newLines) {
263
- newSet.set(line, (newSet.get(line) || 0) + 1);
264
- }
265
- let deletions = 0;
266
- for (const [line, count] of oldSet) {
267
- const newCount = newSet.get(line) || 0;
268
- if (newCount < count) {
269
- deletions += count - newCount;
270
- }
271
- }
272
- let insertions = 0;
273
- for (const [line, count] of newSet) {
274
- const oldCount = oldSet.get(line) || 0;
275
- if (oldCount < count) {
276
- insertions += count - oldCount;
277
- }
278
- }
279
- return { insertions, deletions };
280
- }
281
- /**
282
- * Generate a unified diff between old and new content
283
- */
284
- generateUnifiedDiff(filePath, oldContent, newContent) {
285
- const oldLines = oldContent.split('\n');
286
- const newLines = newContent.split('\n');
287
- // Use a simple diff algorithm: find common prefix/suffix, then show changes
288
- let result = `--- a/${filePath}\n+++ b/${filePath}\n`;
289
- // Find changed regions using a simple approach
290
- const hunks = this.computeHunks(oldLines, newLines);
291
- if (hunks.length === 0) {
292
- return `No differences found in ${filePath}`;
293
- }
294
- for (const hunk of hunks) {
295
- result += `@@ -${hunk.oldStart + 1},${hunk.oldCount} +${hunk.newStart + 1},${hunk.newCount} @@\n`;
296
- for (const line of hunk.lines) {
297
- result += line + '\n';
298
- }
299
- }
300
- return result;
301
- }
302
- /**
303
- * Compute diff hunks between old and new lines using Myers-like approach
304
- */
305
- computeHunks(oldLines, newLines) {
306
- // Build an edit script using LCS
307
- const lcs = this.longestCommonSubsequence(oldLines, newLines);
308
- const editScript = [];
309
- let oldIdx = 0;
310
- let newIdx = 0;
311
- let lcsIdx = 0;
312
- while (oldIdx < oldLines.length || newIdx < newLines.length) {
313
- if (lcsIdx < lcs.length && oldIdx < oldLines.length && newIdx < newLines.length &&
314
- oldLines[oldIdx] === lcs[lcsIdx] && newLines[newIdx] === lcs[lcsIdx]) {
315
- editScript.push({ type: 'keep', oldIdx, newIdx, line: oldLines[oldIdx] });
316
- oldIdx++;
317
- newIdx++;
318
- lcsIdx++;
319
- }
320
- else if (oldIdx < oldLines.length && (lcsIdx >= lcs.length || oldLines[oldIdx] !== lcs[lcsIdx])) {
321
- editScript.push({ type: 'delete', oldIdx, newIdx, line: oldLines[oldIdx] });
322
- oldIdx++;
323
- }
324
- else if (newIdx < newLines.length) {
325
- editScript.push({ type: 'insert', oldIdx, newIdx, line: newLines[newIdx] });
326
- newIdx++;
327
- }
328
- }
329
- // Group into hunks with context (3 lines)
330
- const CONTEXT = 3;
331
- const hunks = [];
332
- let currentHunk = null;
333
- let contextCounter = 0;
334
- for (let i = 0; i < editScript.length; i++) {
335
- const edit = editScript[i];
336
- if (edit.type !== 'keep') {
337
- // Start a new hunk or extend current
338
- if (!currentHunk) {
339
- // Look back for context
340
- const contextStart = Math.max(0, i - CONTEXT);
341
- currentHunk = {
342
- oldStart: editScript[contextStart]?.oldIdx ?? edit.oldIdx,
343
- oldCount: 0,
344
- newStart: editScript[contextStart]?.newIdx ?? edit.newIdx,
345
- newCount: 0,
346
- lines: [],
347
- };
348
- for (let j = contextStart; j < i; j++) {
349
- if (editScript[j].type === 'keep') {
350
- currentHunk.lines.push(` ${editScript[j].line}`);
351
- currentHunk.oldCount++;
352
- currentHunk.newCount++;
353
- }
354
- }
355
- }
356
- if (edit.type === 'delete') {
357
- currentHunk.lines.push(`-${edit.line}`);
358
- currentHunk.oldCount++;
359
- }
360
- else {
361
- currentHunk.lines.push(`+${edit.line}`);
362
- currentHunk.newCount++;
363
- }
364
- contextCounter = 0;
365
- }
366
- else if (currentHunk) {
367
- contextCounter++;
368
- if (contextCounter <= CONTEXT) {
369
- currentHunk.lines.push(` ${edit.line}`);
370
- currentHunk.oldCount++;
371
- currentHunk.newCount++;
372
- }
373
- else {
374
- // End the hunk
375
- hunks.push(currentHunk);
376
- currentHunk = null;
377
- contextCounter = 0;
378
- }
379
- }
380
- }
381
- if (currentHunk) {
382
- hunks.push(currentHunk);
383
- }
384
- return hunks;
385
- }
386
- /**
387
- * Compute longest common subsequence of two string arrays
388
- * Uses optimized approach for large files (limit LCS table size)
389
- */
390
- longestCommonSubsequence(a, b) {
391
- // For very large files, use a simpler approach
392
- if (a.length > 1000 || b.length > 1000) {
393
- return this.simpleLCS(a, b);
394
- }
395
- const m = a.length;
396
- const n = b.length;
397
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
398
- for (let i = 1; i <= m; i++) {
399
- for (let j = 1; j <= n; j++) {
400
- if (a[i - 1] === b[j - 1]) {
401
- dp[i][j] = dp[i - 1][j - 1] + 1;
402
- }
403
- else {
404
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
405
- }
406
- }
407
- }
408
- // Backtrack
409
- const result = [];
410
- let i = m, j = n;
411
- while (i > 0 && j > 0) {
412
- if (a[i - 1] === b[j - 1]) {
413
- result.unshift(a[i - 1]);
414
- i--;
415
- j--;
416
- }
417
- else if (dp[i - 1][j] > dp[i][j - 1]) {
418
- i--;
419
- }
420
- else {
421
- j--;
341
+ }
342
+ /**
343
+ * Get a session-wide diff for a single file (across all checkpoints).
344
+ * Finds the earliest backup of the file and compares to current state.
345
+ */
346
+ async getSessionFileDiff(filePath, handler) {
347
+ const sorted = [...this.checkpoints].sort((a, b) => a.createdAtMs - b.createdAtMs);
348
+ for (const cp of sorted) {
349
+ const manifest = this.readManifest(cp.manifestPath);
350
+ if (!this.isV2Manifest(manifest)) continue;
351
+ const normalizedPath = this.normalizeToRelativePath(filePath, cp.cwd);
352
+ const backup = manifest.fileBackups.find((b) => b.filePath === normalizedPath);
353
+ if (backup) {
354
+ return this.getFileDiffV2(cp, manifest, normalizedPath, handler);
355
+ }
356
+ }
357
+ return null;
358
+ }
359
+ /**
360
+ * Normalise a file path to be relative to the given cwd.
361
+ * If the path is already relative, it is returned unchanged.
362
+ * This handles the common case where the AI passes an absolute remote path
363
+ * (e.g. /home/user/project/foo.ts) to the diff tool while the checkpoint
364
+ * manifest stores relative paths (e.g. project/foo.ts).
365
+ */
366
+ normalizeToRelativePath(filePath, cwd) {
367
+ if (!path.isAbsolute(filePath)) return filePath;
368
+ try {
369
+ const rel = path.relative(cwd, filePath).replace(/\\/g, "/");
370
+ return rel || filePath;
371
+ } catch {
372
+ return filePath;
373
+ }
374
+ }
375
+ /**
376
+ * Get the initial checkpoint for the current chat session.
377
+ */
378
+ getInitialCheckpoint() {
379
+ if (this.checkpoints.length === 0) return null;
380
+ return [...this.checkpoints].sort((a, b) => a.createdAtMs - b.createdAtMs)[0];
381
+ }
382
+ // ── Checkpoint lifecycle ────────────────────────────────────────────
383
+ /**
384
+ * Start a new checkpoint. With V2 (backup-on-write), this is nearly instant:
385
+ * no file scanning or copying. Just creates the checkpoint metadata and an
386
+ * empty manifest.
387
+ */
388
+ async startCheckpoint(params) {
389
+ if (!this.currentChatId) {
390
+ return null;
391
+ }
392
+ const checkpointId = this.generateCheckpointId();
393
+ const checkpointDir = path.join(this.getChatDir(), checkpointId);
394
+ const backupsDir = path.join(checkpointDir, "backups");
395
+ const manifestPath = path.join(checkpointDir, "manifest.json");
396
+ try {
397
+ this.ensureDirSync(backupsDir);
398
+ const manifest = {
399
+ version: 2,
400
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
401
+ cwd: params.cwd,
402
+ fileBackups: [],
403
+ operations: []
404
+ };
405
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
406
+ const meta = {
407
+ id: checkpointId,
408
+ prompt: params.prompt,
409
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
410
+ createdAtMs: Date.now(),
411
+ cwd: params.cwd,
412
+ contextType: params.contextType,
413
+ remoteSessionInfo: params.remoteSessionInfo,
414
+ conversationIndex: params.conversationIndex,
415
+ uiMessageIndex: params.uiMessageIndex,
416
+ uiMessageId: params.uiMessageId,
417
+ // V2: snapshotDir points to 'backups' directory (only has backed-up files)
418
+ snapshotDir: backupsDir,
419
+ manifestPath,
420
+ commands: [],
421
+ toolCalls: [],
422
+ status: "active"
423
+ };
424
+ this.checkpoints.push(meta);
425
+ this.saveIndex();
426
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Checkpoint] Created ${checkpointId} (${params.contextType}) cwd="${params.cwd}"
427
+ `);
428
+ return meta;
429
+ } catch (error) {
430
+ logError("Failed to create checkpoint", error);
431
+ return null;
432
+ }
433
+ }
434
+ async finalizeCheckpoint(id, handler) {
435
+ const checkpoint = this.checkpoints.find((cp) => cp.id === id);
436
+ if (!checkpoint) return;
437
+ if (this.discardedIds.has(id)) {
438
+ this.discardCheckpoint(id);
439
+ return;
440
+ }
441
+ try {
442
+ const changes = await this.calculateChanges(checkpoint, handler);
443
+ checkpoint.changes = changes;
444
+ checkpoint.status = "finalized";
445
+ this.saveIndex();
446
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Checkpoint] Finalized ${id} \u2014 ${checkpoint.changes.modified.length} modified, ${checkpoint.changes.added.length} added, ${checkpoint.changes.deleted.length} deleted
447
+ `);
448
+ } catch (error) {
449
+ logWarning(`Failed to finalize checkpoint ${id}: ${error.message}`);
450
+ }
451
+ }
452
+ recordToolCall(id, toolCall) {
453
+ const checkpoint = this.checkpoints.find((cp) => cp.id === id);
454
+ if (!checkpoint || checkpoint.status === "discarded") return;
455
+ if (toolCall.id && checkpoint.toolCalls.some((tc) => tc.id === toolCall.id)) {
456
+ return;
457
+ }
458
+ checkpoint.toolCalls.push(toolCall);
459
+ if (toolCall.name === "execute_command") {
460
+ const command = toolCall.arguments?.CommandLine || toolCall.arguments?.command;
461
+ const isShellInput = Boolean(toolCall.arguments?.shell_input);
462
+ if (command && !isShellInput) {
463
+ checkpoint.commands.push(String(command));
464
+ }
465
+ }
466
+ if (toolCall.name === "background_command") {
467
+ const command = toolCall.arguments?.command;
468
+ const action = toolCall.arguments?.action;
469
+ if (command && action === "start") {
470
+ checkpoint.commands.push(String(command));
471
+ }
472
+ }
473
+ this.saveIndex();
474
+ }
475
+ /**
476
+ * Revert to a checkpoint. For V2 checkpoints, this is efficient:
477
+ * only the files the AI actually touched are restored.
478
+ *
479
+ * - Files the AI created (existed=false): deleted
480
+ * - Files the AI modified (existed=true): restored from backup
481
+ */
482
+ async revertToCheckpoint(id, handler) {
483
+ const checkpoint = this.checkpoints.find((cp) => cp.id === id);
484
+ if (!checkpoint) {
485
+ throw new Error(`Checkpoint "${id}" not found`);
486
+ }
487
+ if (checkpoint.contextType !== "local") {
488
+ if (!handler || !handler.isConnected()) {
489
+ const sessionType = checkpoint.contextType.toUpperCase();
490
+ const sessionInfo = checkpoint.remoteSessionInfo;
491
+ const target = sessionInfo?.connectionString || sessionInfo?.hostname || "the remote machine";
492
+ throw new Error(
493
+ `This checkpoint was created during a ${sessionType} session (${target}). You are not currently connected to that session. Please reconnect to the ${sessionType} session first, then retry /revert.`
494
+ );
495
+ }
496
+ }
497
+ const manifest = this.readManifest(checkpoint.manifestPath);
498
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Checkpoint] Reverting to ${id} (${checkpoint.contextType}, ${this.isV2Manifest(manifest) ? "V2" : "V1"})
499
+ `);
500
+ let result;
501
+ if (this.isV2Manifest(manifest)) {
502
+ result = await this.revertV2(checkpoint, manifest, handler);
503
+ } else {
504
+ result = await (checkpoint.contextType !== "local" && handler ? this.revertRemoteCheckpointV1(checkpoint, manifest, handler) : this.revertLocalCheckpointV1(checkpoint, manifest));
505
+ }
506
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Checkpoint] Revert complete \u2014 restored=${result.restored}, removed=${result.removed}, errors=${result.errors.length}
507
+ `);
508
+ return result;
509
+ }
510
+ markDiscarded(id) {
511
+ this.discardedIds.add(id);
512
+ }
513
+ removeCheckpointsFrom(id) {
514
+ const index = this.checkpoints.findIndex((cp) => cp.id === id);
515
+ if (index === -1) return;
516
+ const toRemove = this.checkpoints.slice(index);
517
+ for (const checkpoint of toRemove) {
518
+ this.discardCheckpoint(checkpoint.id);
519
+ }
520
+ }
521
+ discardCheckpointById(id) {
522
+ this.discardCheckpoint(id);
523
+ }
524
+ deleteCheckpointsForChat(chatId) {
525
+ const chatDir = path.join(this.baseDir, chatId);
526
+ try {
527
+ if (fs.existsSync(chatDir)) {
528
+ fs.rmSync(chatDir, { recursive: true, force: true });
529
+ }
530
+ } catch (error) {
531
+ logWarning(`Failed to delete checkpoints for chat ${chatId}: ${error.message}`);
532
+ }
533
+ if (this.currentChatId === chatId) {
534
+ this.clear();
535
+ }
536
+ }
537
+ // ── V2 Revert (backup-on-write) ────────────────────────────────────
538
+ async revertV2(checkpoint, manifest, handler) {
539
+ const errors = [];
540
+ let restored = 0;
541
+ let removed = 0;
542
+ const incrementalResult = await this.revertV2IncrementalBackups(checkpoint, manifest, handler);
543
+ restored += incrementalResult.restored;
544
+ removed += incrementalResult.removed;
545
+ errors.push(...incrementalResult.errors);
546
+ return { checkpoint, restored, removed, errors };
547
+ }
548
+ async revertV2IncrementalBackups(checkpoint, manifest, handler) {
549
+ const isRemote = checkpoint.contextType !== "local" && handler && handler.isConnected();
550
+ const errors = [];
551
+ let restored = 0;
552
+ let removed = 0;
553
+ const backupsDir = path.join(path.dirname(checkpoint.manifestPath), "backups");
554
+ for (const backup of manifest.fileBackups) {
555
+ const backupPath = path.join(backupsDir, backup.filePath);
556
+ if (isRemote && handler) {
557
+ const remotePath = checkpoint.cwd + "/" + backup.filePath;
558
+ try {
559
+ if (backup.existed) {
560
+ const content = fs.readFileSync(backupPath, "utf-8");
561
+ const remoteDir = remotePath.substring(0, remotePath.lastIndexOf("/"));
562
+ await handler.executeCommand(`mkdir -p "${remoteDir}"`);
563
+ await handler.writeFile(remotePath, content);
564
+ restored++;
565
+ } else {
566
+ const result = await handler.executeCommand(`rm -f "${remotePath}"`);
567
+ if (result.exitCode === 0) {
568
+ removed++;
569
+ } else {
570
+ errors.push(`Failed to remove ${backup.filePath}: ${result.stderr}`);
422
571
  }
572
+ }
573
+ } catch (error) {
574
+ errors.push(`Failed to revert ${backup.filePath}: ${error.message}`);
423
575
  }
424
- return result;
425
- }
426
- /**
427
- * Simple LCS for large files - uses hash-based matching
428
- */
429
- simpleLCS(a, b) {
430
- const bMap = new Map();
431
- for (let i = 0; i < b.length; i++) {
432
- const positions = bMap.get(b[i]) || [];
433
- positions.push(i);
434
- bMap.set(b[i], positions);
435
- }
436
- const result = [];
437
- let lastMatchB = -1;
438
- for (let i = 0; i < a.length; i++) {
439
- const positions = bMap.get(a[i]);
440
- if (positions) {
441
- // Find earliest position after lastMatchB
442
- for (const pos of positions) {
443
- if (pos > lastMatchB) {
444
- result.push(a[i]);
445
- lastMatchB = pos;
446
- break;
447
- }
448
- }
576
+ } else {
577
+ const targetPath = path.join(checkpoint.cwd, backup.filePath);
578
+ try {
579
+ if (backup.existed) {
580
+ this.ensureDirSync(path.dirname(targetPath));
581
+ fs.copyFileSync(backupPath, targetPath);
582
+ restored++;
583
+ } else {
584
+ if (fs.existsSync(targetPath)) {
585
+ this.removeFileOrDirSync(targetPath);
586
+ removed++;
449
587
  }
588
+ }
589
+ } catch (error) {
590
+ errors.push(`Failed to revert ${backup.filePath}: ${error.message}`);
450
591
  }
451
- return result;
592
+ }
452
593
  }
453
- markDiscarded(id) {
454
- this.discardedIds.add(id);
594
+ if (!isRemote) {
595
+ this.cleanupEmptyDirectories(checkpoint.cwd, manifest.fileBackups.filter((b) => !b.existed).map((b) => b.filePath));
596
+ } else if (handler) {
597
+ await this.cleanupEmptyDirectoriesRemote(checkpoint.cwd, manifest.fileBackups.filter((b) => !b.existed).map((b) => b.filePath), handler);
455
598
  }
456
- async startCheckpoint(params) {
457
- if (!this.currentChatId) {
458
- return null;
459
- }
460
- const checkpointId = this.generateCheckpointId();
461
- const checkpointDir = path.join(this.getChatDir(), checkpointId);
462
- const snapshotDir = path.join(checkpointDir, 'snapshot');
463
- const manifestPath = path.join(checkpointDir, 'manifest.json');
599
+ return { restored, removed, errors };
600
+ }
601
+ // ── V2 Diff ─────────────────────────────────────────────────────────
602
+ async getFileDiffV2(checkpoint, manifest, filePath, handler) {
603
+ const backup = manifest.fileBackups.find((b) => b.filePath === filePath);
604
+ if (!backup) {
605
+ return null;
606
+ }
607
+ const isRemote = checkpoint.contextType !== "local";
608
+ const backupPath = path.join(path.dirname(checkpoint.manifestPath), "backups", filePath);
609
+ if (isRemote) {
610
+ if (!handler || !handler.isConnected()) return null;
611
+ const remotePath = checkpoint.cwd + "/" + filePath;
612
+ let currentContent = null;
613
+ try {
614
+ currentContent = await handler.readFile(remotePath);
615
+ } catch {
616
+ currentContent = null;
617
+ }
618
+ if (!backup.existed && currentContent !== null) {
619
+ const lines = currentContent.split("\n");
620
+ let diff = `--- /dev/null
621
+ +++ b/${filePath}
622
+ @@ -0,0 +1,${lines.length} @@
623
+ `;
624
+ diff += lines.map((l) => `+${l}`).join("\n");
625
+ return diff;
626
+ }
627
+ if (backup.existed && currentContent === null) {
464
628
  try {
465
- this.ensureDirSync(snapshotDir);
466
- // Use remote snapshot when a handler is provided (SSH/WSL/Docker sessions)
467
- const isRemote = params.contextType !== 'local' && params.handler;
468
- const manifest = isRemote
469
- ? await this.createRemoteSnapshot(params.cwd, snapshotDir, params.handler)
470
- : await this.createSnapshot(params.cwd, snapshotDir);
471
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
472
- const meta = {
473
- id: checkpointId,
474
- prompt: params.prompt,
475
- createdAt: new Date().toISOString(),
476
- createdAtMs: Date.now(),
477
- cwd: params.cwd,
478
- contextType: params.contextType,
479
- remoteSessionInfo: params.remoteSessionInfo,
480
- conversationIndex: params.conversationIndex,
481
- uiMessageIndex: params.uiMessageIndex,
482
- uiMessageId: params.uiMessageId,
483
- snapshotDir,
484
- manifestPath,
485
- commands: [],
486
- toolCalls: [],
487
- status: 'active',
488
- };
489
- this.checkpoints.push(meta);
490
- this.saveIndex();
491
- return meta;
492
- }
493
- catch (error) {
494
- logError('Failed to create checkpoint snapshot', error);
495
- return null;
629
+ const content = fs.readFileSync(backupPath, "utf-8");
630
+ const lines = content.split("\n");
631
+ let diff = `--- a/${filePath}
632
+ +++ /dev/null
633
+ @@ -1,${lines.length} +0,0 @@
634
+ `;
635
+ diff += lines.map((l) => `-${l}`).join("\n");
636
+ return diff;
637
+ } catch {
638
+ return `[Binary or unreadable file: ${filePath}]`;
639
+ }
640
+ }
641
+ if (backup.existed && currentContent !== null) {
642
+ try {
643
+ const oldContent = fs.readFileSync(backupPath, "utf-8");
644
+ return this.generateUnifiedDiff(filePath, oldContent, currentContent);
645
+ } catch {
646
+ return `[Binary or unreadable file: ${filePath}]`;
496
647
  }
648
+ }
649
+ return null;
497
650
  }
498
- async finalizeCheckpoint(id) {
499
- const checkpoint = this.checkpoints.find(cp => cp.id === id);
500
- if (!checkpoint)
501
- return;
502
- if (this.discardedIds.has(id)) {
503
- this.discardCheckpoint(id);
504
- return;
505
- }
651
+ const currentPath = path.join(checkpoint.cwd, filePath);
652
+ const currentExists = fs.existsSync(currentPath);
653
+ if (!backup.existed && currentExists) {
654
+ try {
655
+ const content = fs.readFileSync(currentPath, "utf-8");
656
+ const lines = content.split("\n");
657
+ let diff = `--- /dev/null
658
+ +++ b/${filePath}
659
+ @@ -0,0 +1,${lines.length} @@
660
+ `;
661
+ diff += lines.map((l) => `+${l}`).join("\n");
662
+ return diff;
663
+ } catch {
664
+ return `[Binary or unreadable file: ${filePath}]`;
665
+ }
666
+ }
667
+ if (backup.existed && !currentExists) {
668
+ try {
669
+ const content = fs.readFileSync(backupPath, "utf-8");
670
+ const lines = content.split("\n");
671
+ let diff = `--- a/${filePath}
672
+ +++ /dev/null
673
+ @@ -1,${lines.length} +0,0 @@
674
+ `;
675
+ diff += lines.map((l) => `-${l}`).join("\n");
676
+ return diff;
677
+ } catch {
678
+ return `[Binary or unreadable file: ${filePath}]`;
679
+ }
680
+ }
681
+ if (backup.existed && currentExists) {
682
+ try {
683
+ const oldContent = fs.readFileSync(backupPath, "utf-8");
684
+ const newContent = fs.readFileSync(currentPath, "utf-8");
685
+ return this.generateUnifiedDiff(filePath, oldContent, newContent);
686
+ } catch {
687
+ return `[Binary or unreadable file: ${filePath}]`;
688
+ }
689
+ }
690
+ return null;
691
+ }
692
+ // ── V1 Legacy Support ───────────────────────────────────────────────
693
+ /**
694
+ * V1 diff: uses full snapshot files (legacy checkpoints).
695
+ */
696
+ async getFileDiffV1(checkpoint, filePath, handler) {
697
+ const isRemote = checkpoint.contextType !== "local";
698
+ const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
699
+ const snapshotExists = fs.existsSync(snapshotPath);
700
+ if (isRemote) {
701
+ if (!handler || !handler.isConnected()) return null;
702
+ const remotePath = checkpoint.cwd + "/" + filePath;
703
+ let remoteContent = null;
704
+ try {
705
+ remoteContent = await handler.readFile(remotePath);
706
+ } catch {
707
+ remoteContent = null;
708
+ }
709
+ if (!snapshotExists && remoteContent === null) return null;
710
+ if (!snapshotExists && remoteContent !== null) {
711
+ const lines = remoteContent.split("\n");
712
+ let diff = `--- /dev/null
713
+ +++ b/${filePath}
714
+ @@ -0,0 +1,${lines.length} @@
715
+ `;
716
+ diff += lines.map((l) => `+${l}`).join("\n");
717
+ return diff;
718
+ }
719
+ if (snapshotExists && remoteContent === null) {
506
720
  try {
507
- const changes = await this.calculateChanges(checkpoint);
508
- checkpoint.changes = changes;
509
- checkpoint.status = 'finalized';
510
- this.saveIndex();
511
- }
512
- catch (error) {
513
- logWarning(`Failed to finalize checkpoint ${id}: ${error.message}`);
514
- }
721
+ const content = fs.readFileSync(snapshotPath, "utf-8");
722
+ const lines = content.split("\n");
723
+ let diff = `--- a/${filePath}
724
+ +++ /dev/null
725
+ @@ -1,${lines.length} +0,0 @@
726
+ `;
727
+ diff += lines.map((l) => `-${l}`).join("\n");
728
+ return diff;
729
+ } catch {
730
+ return `[Binary or unreadable file: ${filePath}]`;
731
+ }
732
+ }
733
+ try {
734
+ const oldContent = fs.readFileSync(snapshotPath, "utf-8");
735
+ const newContent = remoteContent ?? "";
736
+ return this.generateUnifiedDiff(filePath, oldContent, newContent);
737
+ } catch {
738
+ return `[Binary or unreadable file: ${filePath}]`;
739
+ }
515
740
  }
516
- recordToolCall(id, toolCall) {
517
- const checkpoint = this.checkpoints.find(cp => cp.id === id);
518
- if (!checkpoint || checkpoint.status === 'discarded')
519
- return;
520
- if (toolCall.id && checkpoint.toolCalls.some(tc => tc.id === toolCall.id)) {
521
- return;
522
- }
523
- checkpoint.toolCalls.push(toolCall);
524
- if (toolCall.name === 'execute_command') {
525
- const command = toolCall.arguments?.CommandLine || toolCall.arguments?.command;
526
- const isShellInput = Boolean(toolCall.arguments?.shell_input);
527
- if (command && !isShellInput) {
528
- checkpoint.commands.push(String(command));
529
- }
530
- }
531
- if (toolCall.name === 'background_command') {
532
- const command = toolCall.arguments?.command;
533
- const action = toolCall.arguments?.action;
534
- if (command && action === 'start') {
535
- checkpoint.commands.push(String(command));
536
- }
537
- }
538
- this.saveIndex();
741
+ const currentPath = path.join(checkpoint.cwd, filePath);
742
+ const currentExists = fs.existsSync(currentPath);
743
+ if (!snapshotExists && !currentExists) return null;
744
+ if (!snapshotExists && currentExists) {
745
+ try {
746
+ const content = fs.readFileSync(currentPath, "utf-8");
747
+ const lines = content.split("\n");
748
+ let diff = `--- /dev/null
749
+ +++ b/${filePath}
750
+ @@ -0,0 +1,${lines.length} @@
751
+ `;
752
+ diff += lines.map((l) => `+${l}`).join("\n");
753
+ return diff;
754
+ } catch {
755
+ return `[Binary or unreadable file: ${filePath}]`;
756
+ }
539
757
  }
540
- async revertToCheckpoint(id, handler) {
541
- const checkpoint = this.checkpoints.find(cp => cp.id === id);
542
- if (!checkpoint) {
543
- throw new Error(`Checkpoint "${id}" not found`);
544
- }
545
- // Remote checkpoint requires a connected handler to revert
546
- if (checkpoint.contextType !== 'local') {
547
- if (!handler || !handler.isConnected()) {
548
- const sessionType = checkpoint.contextType.toUpperCase();
549
- const sessionInfo = checkpoint.remoteSessionInfo;
550
- const target = sessionInfo?.connectionString || sessionInfo?.hostname || 'the remote machine';
551
- throw new Error(`This checkpoint was created during a ${sessionType} session (${target}). ` +
552
- `You are not currently connected to that session. Please reconnect to the ${sessionType} session first, then retry /revert.`);
553
- }
554
- return this.revertRemoteCheckpoint(checkpoint, handler);
555
- }
556
- // Local revert (original logic)
557
- const manifest = this.readManifest(checkpoint.manifestPath);
558
- const manifestSet = new Set(manifest.files.map(file => file.path));
559
- const currentFiles = await this.scanFiles(checkpoint.cwd);
560
- const errors = [];
561
- let removed = 0;
562
- for (const filePath of currentFiles) {
563
- if (!manifestSet.has(filePath)) {
564
- const absolutePath = path.join(checkpoint.cwd, filePath);
565
- try {
566
- this.removeFileOrDirSync(absolutePath);
567
- removed++;
568
- }
569
- catch (error) {
570
- errors.push(`Failed to remove ${filePath}: ${error.message}`);
571
- }
572
- }
573
- }
574
- let restored = 0;
575
- for (const file of manifest.files) {
576
- const sourcePath = path.join(checkpoint.snapshotDir, file.path);
577
- const targetPath = path.join(checkpoint.cwd, file.path);
578
- try {
579
- this.ensureDirSync(path.dirname(targetPath));
580
- this.removeFileOrDirSync(targetPath);
581
- fs.copyFileSync(sourcePath, targetPath);
582
- restored++;
583
- }
584
- catch (error) {
585
- errors.push(`Failed to restore ${file.path}: ${error.message}`);
586
- }
587
- }
588
- // Clean up empty directories that were created after the checkpoint
589
- // Build the set of directories that existed in the manifest
590
- const manifestDirs = new Set();
591
- for (const file of manifest.files) {
592
- let dir = path.dirname(file.path);
593
- while (dir && dir !== '.' && dir !== '/') {
594
- manifestDirs.add(dir);
595
- dir = path.dirname(dir);
596
- }
597
- }
598
- // Walk the cwd and find directories not in the manifest, remove if empty (deepest first)
758
+ if (snapshotExists && !currentExists) {
759
+ try {
760
+ const content = fs.readFileSync(snapshotPath, "utf-8");
761
+ const lines = content.split("\n");
762
+ let diff = `--- a/${filePath}
763
+ +++ /dev/null
764
+ @@ -1,${lines.length} +0,0 @@
765
+ `;
766
+ diff += lines.map((l) => `-${l}`).join("\n");
767
+ return diff;
768
+ } catch {
769
+ return `[Binary or unreadable file: ${filePath}]`;
770
+ }
771
+ }
772
+ try {
773
+ const oldContent = fs.readFileSync(snapshotPath, "utf-8");
774
+ const newContent = fs.readFileSync(currentPath, "utf-8");
775
+ return this.generateUnifiedDiff(filePath, oldContent, newContent);
776
+ } catch {
777
+ return `[Binary or unreadable file: ${filePath}]`;
778
+ }
779
+ }
780
+ async getSessionChangesV1(checkpoint, changes, stats, isRemote, handler) {
781
+ if (!isRemote) {
782
+ for (const filePath of changes.modified) {
783
+ const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
784
+ const currentPath = path.join(checkpoint.cwd, filePath);
785
+ const lineStat = this.calculateLineStats(snapshotPath, currentPath);
786
+ stats.push({ filePath, ...lineStat });
787
+ }
788
+ for (const filePath of changes.added) {
789
+ const currentPath = path.join(checkpoint.cwd, filePath);
599
790
  try {
600
- const allDirs = this.scanLocalDirectories(checkpoint.cwd, checkpoint.cwd);
601
- // Sort deepest first so nested empty dirs are removed before their parents
602
- allDirs.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
603
- for (const dir of allDirs) {
604
- if (!manifestDirs.has(dir)) {
605
- const absDir = path.join(checkpoint.cwd, dir);
606
- try {
607
- const entries = fs.readdirSync(absDir);
608
- if (entries.length === 0) {
609
- fs.rmdirSync(absDir);
610
- removed++;
611
- }
612
- }
613
- catch {
614
- // Directory may already be gone or inaccessible, skip
615
- }
616
- }
617
- }
618
- }
619
- catch {
620
- // Non-critical: directory cleanup is best-effort
621
- }
622
- return { checkpoint, restored, removed, errors };
623
- }
624
- /**
625
- * Revert a remote checkpoint by uploading snapshot files back to the remote machine.
626
- */
627
- async revertRemoteCheckpoint(checkpoint, handler) {
628
- const manifest = this.readManifest(checkpoint.manifestPath);
629
- const manifestSet = new Set(manifest.files.map(file => file.path));
630
- const currentFiles = await this.scanRemoteFiles(checkpoint.cwd, handler);
631
- const errors = [];
632
- // Remove files that were added after the checkpoint
633
- let removed = 0;
634
- for (const filePath of currentFiles) {
635
- if (!manifestSet.has(filePath)) {
636
- const remotePath = checkpoint.cwd + '/' + filePath;
637
- try {
638
- const result = await handler.executeCommand(`rm -f "${remotePath}"`);
639
- if (result.exitCode === 0) {
640
- removed++;
641
- }
642
- else {
643
- errors.push(`Failed to remove ${filePath}: ${result.stderr}`);
644
- }
645
- }
646
- catch (error) {
647
- errors.push(`Failed to remove ${filePath}: ${error.message}`);
648
- }
649
- }
650
- }
651
- // Restore files from the snapshot
652
- let restored = 0;
653
- for (const file of manifest.files) {
654
- const localSnapshotPath = path.join(checkpoint.snapshotDir, file.path);
655
- const remotePath = checkpoint.cwd + '/' + file.path;
656
- try {
657
- // Read the snapshot content from local storage
658
- const content = fs.readFileSync(localSnapshotPath, 'utf-8');
659
- // Ensure remote directory exists
660
- const remoteDir = remotePath.substring(0, remotePath.lastIndexOf('/'));
661
- await handler.executeCommand(`mkdir -p "${remoteDir}"`);
662
- // Write the file back to the remote machine
663
- await handler.writeFile(remotePath, content);
664
- restored++;
665
- }
666
- catch (error) {
667
- errors.push(`Failed to restore ${file.path}: ${error.message}`);
668
- }
669
- }
670
- // Clean up empty directories that were created after the checkpoint
671
- // Build the set of directories referenced by manifest files
672
- const manifestDirs = new Set();
673
- for (const file of manifest.files) {
674
- let dir = file.path.substring(0, file.path.lastIndexOf('/'));
675
- while (dir && dir !== '.' && dir !== '/') {
676
- manifestDirs.add(dir);
677
- const lastSlash = dir.lastIndexOf('/');
678
- dir = lastSlash > 0 ? dir.substring(0, lastSlash) : '';
679
- }
680
- }
681
- // Find all current directories on the remote and remove empty ones not in the manifest
791
+ const content = fs.readFileSync(currentPath, "utf-8");
792
+ const lines = content.split("\n").length;
793
+ stats.push({ filePath, insertions: lines, deletions: 0 });
794
+ } catch {
795
+ stats.push({ filePath, insertions: 0, deletions: 0 });
796
+ }
797
+ }
798
+ for (const filePath of changes.deleted) {
799
+ const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
682
800
  try {
683
- const findDirsCmd = `find "${checkpoint.cwd}" -mindepth 1 -type d 2>/dev/null`;
684
- const dirsResult = await handler.executeCommand(findDirsCmd);
685
- if (dirsResult.exitCode === 0 && dirsResult.stdout.trim()) {
686
- const cwdPrefix = checkpoint.cwd.endsWith('/') ? checkpoint.cwd : checkpoint.cwd + '/';
687
- const remoteDirs = dirsResult.stdout
688
- .split('\n')
689
- .map((l) => l.trim())
690
- .filter((l) => l.length > 0 && l.startsWith(cwdPrefix))
691
- .map((l) => l.substring(cwdPrefix.length))
692
- .filter((relPath) => relPath.length > 0);
693
- // Sort deepest first so nested empty dirs are removed before their parents
694
- remoteDirs.sort((a, b) => b.split('/').length - a.split('/').length);
695
- for (const dir of remoteDirs) {
696
- if (!manifestDirs.has(dir)) {
697
- const remoteDirPath = checkpoint.cwd + '/' + dir;
698
- try {
699
- // rmdir only removes empty directories (safe)
700
- const rmResult = await handler.executeCommand(`rmdir "${remoteDirPath}" 2>/dev/null`);
701
- if (rmResult.exitCode === 0) {
702
- removed++;
703
- }
704
- }
705
- catch {
706
- // Directory not empty or inaccessible, skip
707
- }
708
- }
709
- }
710
- }
711
- }
712
- catch {
713
- // Non-critical: directory cleanup is best-effort
801
+ const content = fs.readFileSync(snapshotPath, "utf-8");
802
+ const lines = content.split("\n").length;
803
+ stats.push({ filePath, insertions: 0, deletions: lines });
804
+ } catch {
805
+ stats.push({ filePath, insertions: 0, deletions: 0 });
806
+ }
807
+ }
808
+ } else if (handler) {
809
+ for (const filePath of changes.modified) {
810
+ const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
811
+ const remotePath = checkpoint.cwd + "/" + filePath;
812
+ try {
813
+ const snapshotContent = fs.readFileSync(snapshotPath, "utf-8");
814
+ const remoteContent = await handler.readFile(remotePath);
815
+ const lineStat = this.calculateLineStatsFromContent(snapshotContent, remoteContent);
816
+ stats.push({ filePath, ...lineStat });
817
+ } catch {
818
+ stats.push({ filePath, insertions: 0, deletions: 0 });
819
+ }
820
+ }
821
+ for (const filePath of changes.added) {
822
+ const remotePath = checkpoint.cwd + "/" + filePath;
823
+ try {
824
+ const remoteContent = await handler.readFile(remotePath);
825
+ const lines = remoteContent.split("\n").length;
826
+ stats.push({ filePath, insertions: lines, deletions: 0 });
827
+ } catch {
828
+ stats.push({ filePath, insertions: 0, deletions: 0 });
829
+ }
830
+ }
831
+ for (const filePath of changes.deleted) {
832
+ const snapshotPath = path.join(checkpoint.snapshotDir, filePath);
833
+ try {
834
+ const content = fs.readFileSync(snapshotPath, "utf-8");
835
+ const lines = content.split("\n").length;
836
+ stats.push({ filePath, insertions: 0, deletions: lines });
837
+ } catch {
838
+ stats.push({ filePath, insertions: 0, deletions: 0 });
714
839
  }
715
- return { checkpoint, restored, removed, errors };
840
+ }
716
841
  }
717
- removeCheckpointsFrom(id) {
718
- const index = this.checkpoints.findIndex(cp => cp.id === id);
719
- if (index === -1)
720
- return;
721
- // Remove checkpoints FROM the given checkpoint (INCLUDING the checkpoint itself)
722
- // This is used during revert - the reverted checkpoint is also removed
723
- // because the user will re-submit the prompt
724
- const toRemove = this.checkpoints.slice(index);
725
- for (const checkpoint of toRemove) {
726
- this.discardCheckpoint(checkpoint.id);
842
+ }
843
+ /**
844
+ * V1 local revert: legacy full-scan approach.
845
+ */
846
+ async revertLocalCheckpointV1(checkpoint, manifest) {
847
+ const manifestSet = new Set(manifest.files.map((file) => file.path));
848
+ const currentFiles = await this.scanLocalFiles(checkpoint.cwd);
849
+ const errors = [];
850
+ let removed = 0;
851
+ for (const filePath of currentFiles) {
852
+ if (!manifestSet.has(filePath)) {
853
+ const absolutePath = path.join(checkpoint.cwd, filePath);
854
+ try {
855
+ this.removeFileOrDirSync(absolutePath);
856
+ removed++;
857
+ } catch (error) {
858
+ errors.push(`Failed to remove ${filePath}: ${error.message}`);
727
859
  }
860
+ }
728
861
  }
729
- /**
730
- * Discard a single checkpoint by id.
731
- * Used when a background/late checkpoint should not be kept.
732
- */
733
- discardCheckpointById(id) {
734
- this.discardCheckpoint(id);
862
+ let restored = 0;
863
+ for (const file of manifest.files) {
864
+ const sourcePath = path.join(checkpoint.snapshotDir, file.path);
865
+ const targetPath = path.join(checkpoint.cwd, file.path);
866
+ try {
867
+ this.ensureDirSync(path.dirname(targetPath));
868
+ this.removeFileOrDirSync(targetPath);
869
+ fs.copyFileSync(sourcePath, targetPath);
870
+ restored++;
871
+ } catch (error) {
872
+ errors.push(`Failed to restore ${file.path}: ${error.message}`);
873
+ }
735
874
  }
736
- deleteCheckpointsForChat(chatId) {
737
- const chatDir = path.join(this.baseDir, chatId);
738
- try {
739
- if (fs.existsSync(chatDir)) {
740
- fs.rmSync(chatDir, { recursive: true, force: true });
875
+ const manifestDirs = /* @__PURE__ */ new Set();
876
+ for (const file of manifest.files) {
877
+ let dir = path.dirname(file.path);
878
+ while (dir && dir !== "." && dir !== "/") {
879
+ manifestDirs.add(dir);
880
+ dir = path.dirname(dir);
881
+ }
882
+ }
883
+ try {
884
+ const allDirs = this.scanLocalDirectories(checkpoint.cwd, checkpoint.cwd);
885
+ allDirs.sort((a, b) => b.split(path.sep).length - a.split(path.sep).length);
886
+ for (const dir of allDirs) {
887
+ if (!manifestDirs.has(dir)) {
888
+ const absDir = path.join(checkpoint.cwd, dir);
889
+ try {
890
+ const entries = fs.readdirSync(absDir);
891
+ if (entries.length === 0) {
892
+ fs.rmdirSync(absDir);
893
+ removed++;
741
894
  }
895
+ } catch {
896
+ }
742
897
  }
743
- catch (error) {
744
- logWarning(`Failed to delete checkpoints for chat ${chatId}: ${error.message}`);
745
- }
746
- if (this.currentChatId === chatId) {
747
- this.clear();
748
- }
898
+ }
899
+ } catch {
749
900
  }
750
- discardCheckpoint(id) {
751
- const checkpointIndex = this.checkpoints.findIndex(cp => cp.id === id);
752
- if (checkpointIndex === -1)
753
- return;
754
- const [checkpoint] = this.checkpoints.splice(checkpointIndex, 1);
755
- this.discardedIds.delete(id);
901
+ return { checkpoint, restored, removed, errors };
902
+ }
903
+ /**
904
+ * V1 remote revert: legacy full-scan approach.
905
+ */
906
+ async revertRemoteCheckpointV1(checkpoint, manifest, handler) {
907
+ const manifestSet = new Set(manifest.files.map((file) => file.path));
908
+ const currentFiles = await this.scanRemoteFiles(checkpoint.cwd, handler);
909
+ const errors = [];
910
+ let removed = 0;
911
+ for (const filePath of currentFiles) {
912
+ if (!manifestSet.has(filePath)) {
913
+ const remotePath = checkpoint.cwd + "/" + filePath;
756
914
  try {
757
- if (fs.existsSync(path.dirname(checkpoint.manifestPath))) {
758
- fs.rmSync(path.dirname(checkpoint.manifestPath), { recursive: true, force: true });
759
- }
760
- }
761
- catch (error) {
762
- logWarning(`Failed to delete checkpoint ${id}: ${error.message}`);
763
- }
764
- this.saveIndex();
765
- }
766
- async calculateChanges(checkpoint, handler) {
767
- const manifest = this.readManifest(checkpoint.manifestPath);
768
- const manifestSet = new Set(manifest.files.map(file => file.path));
769
- // Use remote scanning when checkpoint is remote and handler is available
770
- const isRemote = checkpoint.contextType !== 'local' && handler && handler.isConnected();
771
- const currentFiles = isRemote
772
- ? await this.scanRemoteFiles(checkpoint.cwd, handler)
773
- : await this.scanFiles(checkpoint.cwd);
774
- const currentSet = new Set(currentFiles);
775
- const added = [];
776
- const deleted = [];
777
- const modified = [];
778
- for (const filePath of currentFiles) {
779
- if (!manifestSet.has(filePath)) {
780
- added.push(filePath);
781
- }
782
- }
783
- for (const file of manifest.files) {
784
- if (!currentSet.has(file.path)) {
785
- deleted.push(file.path);
786
- continue;
787
- }
788
- if (isRemote) {
789
- // For remote: read current file from remote and compare with local snapshot
790
- try {
791
- const remotePath = checkpoint.cwd + '/' + file.path;
792
- const remoteContent = await handler.readFile(remotePath);
793
- const snapshotPath = path.join(checkpoint.snapshotDir, file.path);
794
- const snapshotContent = fs.readFileSync(snapshotPath, 'utf-8');
795
- if (remoteContent !== snapshotContent) {
796
- modified.push(file.path);
797
- }
798
- }
799
- catch {
800
- modified.push(file.path); // Assume modified if we can't read
801
- }
802
- }
803
- else {
804
- const currentPath = path.join(checkpoint.cwd, file.path);
805
- const snapshotPath = path.join(checkpoint.snapshotDir, file.path);
806
- if (this.filesDiffer(snapshotPath, currentPath)) {
807
- modified.push(file.path);
808
- }
809
- }
810
- }
811
- return { added, modified, deleted };
915
+ const result = await handler.executeCommand(`rm -f "${remotePath}"`);
916
+ if (result.exitCode === 0) {
917
+ removed++;
918
+ } else {
919
+ errors.push(`Failed to remove ${filePath}: ${result.stderr}`);
920
+ }
921
+ } catch (error) {
922
+ errors.push(`Failed to remove ${filePath}: ${error.message}`);
923
+ }
924
+ }
812
925
  }
813
- readManifest(manifestPath) {
814
- const raw = fs.readFileSync(manifestPath, 'utf-8');
815
- return JSON.parse(raw);
926
+ let restored = 0;
927
+ for (const file of manifest.files) {
928
+ const localSnapshotPath = path.join(checkpoint.snapshotDir, file.path);
929
+ const remotePath = checkpoint.cwd + "/" + file.path;
930
+ try {
931
+ const content = fs.readFileSync(localSnapshotPath, "utf-8");
932
+ const remoteDir = remotePath.substring(0, remotePath.lastIndexOf("/"));
933
+ await handler.executeCommand(`mkdir -p "${remoteDir}"`);
934
+ await handler.writeFile(remotePath, content);
935
+ restored++;
936
+ } catch (error) {
937
+ errors.push(`Failed to restore ${file.path}: ${error.message}`);
938
+ }
816
939
  }
817
- async createSnapshot(cwd, snapshotDir) {
818
- const files = await this.scanFiles(cwd);
819
- const manifestEntries = [];
820
- for (const filePath of files) {
821
- const absolutePath = path.join(cwd, filePath);
940
+ const manifestDirs = /* @__PURE__ */ new Set();
941
+ for (const file of manifest.files) {
942
+ let dir = file.path.substring(0, file.path.lastIndexOf("/"));
943
+ while (dir && dir !== "." && dir !== "/") {
944
+ manifestDirs.add(dir);
945
+ const lastSlash = dir.lastIndexOf("/");
946
+ dir = lastSlash > 0 ? dir.substring(0, lastSlash) : "";
947
+ }
948
+ }
949
+ try {
950
+ const findDirsCmd = `find "${checkpoint.cwd}" -mindepth 1 -type d 2>/dev/null`;
951
+ const dirsResult = await handler.executeCommand(findDirsCmd);
952
+ if (dirsResult.exitCode === 0 && dirsResult.stdout.trim()) {
953
+ const cwdPrefix = checkpoint.cwd.endsWith("/") ? checkpoint.cwd : checkpoint.cwd + "/";
954
+ const remoteDirs = dirsResult.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && l.startsWith(cwdPrefix)).map((l) => l.substring(cwdPrefix.length)).filter((relPath) => relPath.length > 0);
955
+ remoteDirs.sort((a, b) => b.split("/").length - a.split("/").length);
956
+ for (const dir of remoteDirs) {
957
+ if (!manifestDirs.has(dir)) {
958
+ const remoteDirPath = checkpoint.cwd + "/" + dir;
822
959
  try {
823
- const stat = fs.statSync(absolutePath);
824
- if (!stat.isFile())
825
- continue;
826
- const snapshotPath = path.join(snapshotDir, filePath);
827
- this.ensureDirSync(path.dirname(snapshotPath));
828
- fs.copyFileSync(absolutePath, snapshotPath);
829
- manifestEntries.push({
830
- path: filePath,
831
- size: stat.size,
832
- mtimeMs: stat.mtimeMs,
833
- mode: stat.mode,
834
- });
835
- }
836
- catch (error) {
837
- logWarning(`Failed to snapshot ${filePath}: ${error.message}`);
960
+ const rmResult = await handler.executeCommand(`rmdir "${remoteDirPath}" 2>/dev/null`);
961
+ if (rmResult.exitCode === 0) {
962
+ removed++;
963
+ }
964
+ } catch {
838
965
  }
966
+ }
839
967
  }
840
- return {
841
- version: 1,
842
- createdAt: new Date().toISOString(),
843
- cwd,
844
- files: manifestEntries,
845
- };
846
- }
847
- /**
848
- * Create a snapshot of remote files by downloading them via the handler.
849
- * Files are stored locally in the snapshot directory for later comparison/revert.
850
- */
851
- async createRemoteSnapshot(cwd, snapshotDir, handler) {
852
- const files = await this.scanRemoteFiles(cwd, handler);
853
- const manifestEntries = [];
854
- for (const filePath of files) {
855
- const remotePath = cwd + '/' + filePath;
968
+ }
969
+ } catch {
970
+ }
971
+ return { checkpoint, restored, removed, errors };
972
+ }
973
+ // ── Change calculation ──────────────────────────────────────────────
974
+ async calculateChanges(checkpoint, handler) {
975
+ const manifest = this.readManifest(checkpoint.manifestPath);
976
+ if (this.isV2Manifest(manifest)) {
977
+ return this.calculateChangesV2(checkpoint, manifest, handler);
978
+ } else {
979
+ return this.calculateChangesV1(checkpoint, manifest, handler);
980
+ }
981
+ }
982
+ /**
983
+ * V2: Calculate changes from backup entries only.
984
+ * No directory scanning needed — we only track files the AI touched.
985
+ */
986
+ async calculateChangesV2(checkpoint, manifest, handler) {
987
+ const isRemote = checkpoint.contextType !== "local" && handler && handler.isConnected();
988
+ const added = [];
989
+ const modified = [];
990
+ const deleted = [];
991
+ for (const backup of manifest.fileBackups) {
992
+ const backupPath = path.join(path.dirname(checkpoint.manifestPath), "backups", backup.filePath);
993
+ if (isRemote && handler) {
994
+ const remotePath = checkpoint.cwd + "/" + backup.filePath;
995
+ try {
996
+ if (backup.existed) {
856
997
  try {
857
- const content = await handler.readFile(remotePath);
858
- const snapshotPath = path.join(snapshotDir, filePath);
859
- this.ensureDirSync(path.dirname(snapshotPath));
860
- fs.writeFileSync(snapshotPath, content, 'utf-8');
861
- const contentBuffer = Buffer.from(content, 'utf-8');
862
- manifestEntries.push({
863
- path: filePath,
864
- size: contentBuffer.length,
865
- mtimeMs: Date.now(),
866
- mode: 0o644,
867
- });
868
- }
869
- catch (error) {
870
- logWarning(`Failed to snapshot remote file ${filePath}: ${error.message}`);
998
+ const remoteContent = await handler.readFile(remotePath);
999
+ const backupContent = fs.readFileSync(backupPath, "utf-8");
1000
+ if (remoteContent !== backupContent) {
1001
+ modified.push(backup.filePath);
1002
+ }
1003
+ } catch {
1004
+ deleted.push(backup.filePath);
871
1005
  }
872
- }
873
- return {
874
- version: 1,
875
- createdAt: new Date().toISOString(),
876
- cwd,
877
- files: manifestEntries,
878
- };
879
- }
880
- async scanFiles(cwd) {
881
- const files = await fg(['**/*'], {
882
- cwd,
883
- dot: true,
884
- onlyFiles: true,
885
- followSymbolicLinks: false,
886
- unique: true,
887
- ignore: DEFAULT_IGNORE_PATTERNS,
888
- });
889
- return files.map((file) => file.replace(/\\/g, '/'));
890
- }
891
- /**
892
- * Recursively scan all subdirectories under root, returning relative paths.
893
- * Used by local revert to find directories that may need cleanup.
894
- */
895
- scanLocalDirectories(rootDir, cwd) {
896
- const dirs = [];
897
- const ignoreDirs = new Set([
898
- '.git', 'node_modules', 'dist', 'build', 'out',
899
- '.next', '.turbo', '.cache', 'coverage',
900
- '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.centaurus',
901
- ]);
902
- const walk = (dir) => {
1006
+ } else {
903
1007
  try {
904
- const entries = fs.readdirSync(dir, { withFileTypes: true });
905
- for (const entry of entries) {
906
- if (entry.isDirectory() && !ignoreDirs.has(entry.name)) {
907
- const fullPath = path.join(dir, entry.name);
908
- const relPath = path.relative(cwd, fullPath).replace(/\\/g, '/');
909
- dirs.push(relPath);
910
- walk(fullPath);
911
- }
912
- }
1008
+ await handler.readFile(remotePath);
1009
+ added.push(backup.filePath);
1010
+ } catch {
913
1011
  }
914
- catch {
915
- // Skip inaccessible directories
916
- }
917
- };
918
- walk(rootDir);
919
- return dirs;
920
- }
921
- /**
922
- * Scan files on a remote machine using the handler's executeCommand.
923
- * Uses `find` with ignore patterns equivalent to DEFAULT_IGNORE_PATTERNS.
924
- */
925
- async scanRemoteFiles(cwd, handler) {
926
- // Build find command with exclusion patterns matching DEFAULT_IGNORE_PATTERNS
927
- const excludeDirs = [
928
- '.git', 'node_modules', 'dist', 'build', 'out',
929
- '.next', '.turbo', '.cache', 'coverage',
930
- '__pycache__', '.venv', 'venv', '.idea', '.vscode', '.centaurus',
931
- ];
932
- const excludeFiles = ['.DS_Store', 'Thumbs.db'];
933
- const pruneArgs = excludeDirs.map(d => `-name "${d}" -prune`).join(' -o ');
934
- const notFileArgs = excludeFiles.map(f => `! -name "${f}"`).join(' ');
935
- // find <cwd> ( -name .git -prune -o ... ) -o -type f ! -name .DS_Store ... -print
936
- const findCmd = `find "${cwd}" \\( ${pruneArgs} \\) -o -type f ${notFileArgs} -print 2>/dev/null`;
937
- try {
938
- const result = await handler.executeCommand(findCmd);
939
- if (result.exitCode !== 0 && !result.stdout.trim()) {
940
- logWarning(`Remote file scan failed: ${result.stderr}`);
941
- return [];
1012
+ }
1013
+ } catch {
1014
+ }
1015
+ } else {
1016
+ const currentPath = path.join(checkpoint.cwd, backup.filePath);
1017
+ if (backup.existed) {
1018
+ if (fs.existsSync(currentPath)) {
1019
+ if (this.filesDiffer(backupPath, currentPath)) {
1020
+ modified.push(backup.filePath);
942
1021
  }
943
- const cwdPrefix = cwd.endsWith('/') ? cwd : cwd + '/';
944
- return result.stdout
945
- .split('\n')
946
- .map(line => line.trim())
947
- .filter(line => line.length > 0 && line.startsWith(cwdPrefix))
948
- .map(line => line.substring(cwdPrefix.length))
949
- .filter(relPath => relPath.length > 0);
950
- }
951
- catch (error) {
952
- logWarning(`Failed to scan remote files: ${error.message}`);
953
- return [];
954
- }
1022
+ } else {
1023
+ deleted.push(backup.filePath);
1024
+ }
1025
+ } else {
1026
+ if (fs.existsSync(currentPath)) {
1027
+ added.push(backup.filePath);
1028
+ }
1029
+ }
1030
+ }
1031
+ }
1032
+ return {
1033
+ added: Array.from(new Set(added)),
1034
+ modified: Array.from(new Set(modified)),
1035
+ deleted: Array.from(new Set(deleted))
1036
+ };
1037
+ }
1038
+ /**
1039
+ * V1 legacy: full directory scan approach.
1040
+ */
1041
+ async calculateChangesV1(checkpoint, manifest, handler) {
1042
+ const manifestSet = new Set(manifest.files.map((file) => file.path));
1043
+ const isRemote = checkpoint.contextType !== "local" && handler && handler.isConnected();
1044
+ const currentFiles = isRemote ? await this.scanRemoteFiles(checkpoint.cwd, handler) : await this.scanLocalFiles(checkpoint.cwd);
1045
+ const currentSet = new Set(currentFiles);
1046
+ const added = [];
1047
+ const deleted = [];
1048
+ const modified = [];
1049
+ for (const filePath of currentFiles) {
1050
+ if (!manifestSet.has(filePath)) {
1051
+ added.push(filePath);
1052
+ }
955
1053
  }
956
- filesDiffer(snapshotPath, currentPath) {
1054
+ for (const file of manifest.files) {
1055
+ if (!currentSet.has(file.path)) {
1056
+ deleted.push(file.path);
1057
+ continue;
1058
+ }
1059
+ if (isRemote && handler) {
957
1060
  try {
958
- const snapStat = fs.statSync(snapshotPath);
959
- const currStat = fs.statSync(currentPath);
960
- if (snapStat.size !== currStat.size) {
961
- return true;
962
- }
963
- const snapBuffer = fs.readFileSync(snapshotPath);
964
- const currBuffer = fs.readFileSync(currentPath);
965
- if (snapBuffer.length !== currBuffer.length) {
966
- return true;
1061
+ const remotePath = checkpoint.cwd + "/" + file.path;
1062
+ const remoteContent = await handler.readFile(remotePath);
1063
+ const snapshotPath = path.join(checkpoint.snapshotDir, file.path);
1064
+ const snapshotContent = fs.readFileSync(snapshotPath, "utf-8");
1065
+ if (remoteContent !== snapshotContent) {
1066
+ modified.push(file.path);
1067
+ }
1068
+ } catch {
1069
+ modified.push(file.path);
1070
+ }
1071
+ } else {
1072
+ const currentPath = path.join(checkpoint.cwd, file.path);
1073
+ const snapshotPath = path.join(checkpoint.snapshotDir, file.path);
1074
+ if (this.filesDiffer(snapshotPath, currentPath)) {
1075
+ modified.push(file.path);
1076
+ }
1077
+ }
1078
+ }
1079
+ return { added, modified, deleted };
1080
+ }
1081
+ // ── Helpers: directory cleanup ──────────────────────────────────────
1082
+ /**
1083
+ * Clean up empty directories after deleting files that the AI created.
1084
+ */
1085
+ cleanupEmptyDirectories(cwd, deletedRelPaths) {
1086
+ const dirs = /* @__PURE__ */ new Set();
1087
+ for (const relPath of deletedRelPaths) {
1088
+ let dir = path.dirname(relPath);
1089
+ while (dir && dir !== "." && dir !== "/" && dir !== "\\") {
1090
+ dirs.add(dir);
1091
+ dir = path.dirname(dir);
1092
+ }
1093
+ }
1094
+ const sortedDirs = Array.from(dirs).sort(
1095
+ (a, b) => b.split("/").length - a.split("/").length
1096
+ );
1097
+ for (const dir of sortedDirs) {
1098
+ const absDir = path.join(cwd, dir);
1099
+ try {
1100
+ if (fs.existsSync(absDir)) {
1101
+ const entries = fs.readdirSync(absDir);
1102
+ if (entries.length === 0) {
1103
+ fs.rmdirSync(absDir);
1104
+ }
1105
+ }
1106
+ } catch {
1107
+ }
1108
+ }
1109
+ }
1110
+ async cleanupEmptyDirectoriesRemote(cwd, deletedRelPaths, handler) {
1111
+ const dirs = /* @__PURE__ */ new Set();
1112
+ for (const relPath of deletedRelPaths) {
1113
+ let dir = relPath.substring(0, relPath.lastIndexOf("/"));
1114
+ while (dir && dir !== "." && dir !== "/") {
1115
+ dirs.add(dir);
1116
+ const lastSlash = dir.lastIndexOf("/");
1117
+ dir = lastSlash > 0 ? dir.substring(0, lastSlash) : "";
1118
+ }
1119
+ }
1120
+ const sortedDirs = Array.from(dirs).sort(
1121
+ (a, b) => b.split("/").length - a.split("/").length
1122
+ );
1123
+ for (const dir of sortedDirs) {
1124
+ const remoteDirPath = cwd + "/" + dir;
1125
+ try {
1126
+ await handler.executeCommand(`rmdir "${remoteDirPath}" 2>/dev/null`);
1127
+ } catch {
1128
+ }
1129
+ }
1130
+ }
1131
+ // ── Helpers: line stats and diff generation ─────────────────────────
1132
+ calculateLineStats(snapshotPath, currentPath) {
1133
+ try {
1134
+ const oldContent = fs.readFileSync(snapshotPath, "utf-8");
1135
+ const newContent = fs.readFileSync(currentPath, "utf-8");
1136
+ return this.calculateLineStatsFromContent(oldContent, newContent);
1137
+ } catch {
1138
+ return { insertions: 0, deletions: 0 };
1139
+ }
1140
+ }
1141
+ calculateLineStatsFromContent(oldContent, newContent) {
1142
+ const oldLines = oldContent.split("\n");
1143
+ const newLines = newContent.split("\n");
1144
+ const oldSet = /* @__PURE__ */ new Map();
1145
+ for (const line of oldLines) {
1146
+ oldSet.set(line, (oldSet.get(line) || 0) + 1);
1147
+ }
1148
+ const newSet = /* @__PURE__ */ new Map();
1149
+ for (const line of newLines) {
1150
+ newSet.set(line, (newSet.get(line) || 0) + 1);
1151
+ }
1152
+ let deletions = 0;
1153
+ for (const [line, count] of oldSet) {
1154
+ const newCount = newSet.get(line) || 0;
1155
+ if (newCount < count) {
1156
+ deletions += count - newCount;
1157
+ }
1158
+ }
1159
+ let insertions = 0;
1160
+ for (const [line, count] of newSet) {
1161
+ const oldCount = oldSet.get(line) || 0;
1162
+ if (oldCount < count) {
1163
+ insertions += count - oldCount;
1164
+ }
1165
+ }
1166
+ return { insertions, deletions };
1167
+ }
1168
+ generateUnifiedDiff(filePath, oldContent, newContent) {
1169
+ const oldLines = oldContent.split("\n");
1170
+ const newLines = newContent.split("\n");
1171
+ let result = `--- a/${filePath}
1172
+ +++ b/${filePath}
1173
+ `;
1174
+ const hunks = this.computeHunks(oldLines, newLines);
1175
+ if (hunks.length === 0) {
1176
+ return `No differences found in ${filePath}`;
1177
+ }
1178
+ for (const hunk of hunks) {
1179
+ result += `@@ -${hunk.oldStart + 1},${hunk.oldCount} +${hunk.newStart + 1},${hunk.newCount} @@
1180
+ `;
1181
+ for (const line of hunk.lines) {
1182
+ result += line + "\n";
1183
+ }
1184
+ }
1185
+ return result;
1186
+ }
1187
+ computeHunks(oldLines, newLines) {
1188
+ const lcs = this.longestCommonSubsequence(oldLines, newLines);
1189
+ const editScript = [];
1190
+ let oldIdx = 0;
1191
+ let newIdx = 0;
1192
+ let lcsIdx = 0;
1193
+ while (oldIdx < oldLines.length || newIdx < newLines.length) {
1194
+ if (lcsIdx < lcs.length && oldIdx < oldLines.length && newIdx < newLines.length && oldLines[oldIdx] === lcs[lcsIdx] && newLines[newIdx] === lcs[lcsIdx]) {
1195
+ editScript.push({ type: "keep", oldIdx, newIdx, line: oldLines[oldIdx] });
1196
+ oldIdx++;
1197
+ newIdx++;
1198
+ lcsIdx++;
1199
+ } else if (oldIdx < oldLines.length && (lcsIdx >= lcs.length || oldLines[oldIdx] !== lcs[lcsIdx])) {
1200
+ editScript.push({ type: "delete", oldIdx, newIdx, line: oldLines[oldIdx] });
1201
+ oldIdx++;
1202
+ } else if (newIdx < newLines.length) {
1203
+ editScript.push({ type: "insert", oldIdx, newIdx, line: newLines[newIdx] });
1204
+ newIdx++;
1205
+ }
1206
+ }
1207
+ const CONTEXT = 3;
1208
+ const hunks = [];
1209
+ let currentHunk = null;
1210
+ let contextCounter = 0;
1211
+ for (let i = 0; i < editScript.length; i++) {
1212
+ const edit = editScript[i];
1213
+ if (edit.type !== "keep") {
1214
+ if (!currentHunk) {
1215
+ const contextStart = Math.max(0, i - CONTEXT);
1216
+ currentHunk = {
1217
+ oldStart: editScript[contextStart]?.oldIdx ?? edit.oldIdx,
1218
+ oldCount: 0,
1219
+ newStart: editScript[contextStart]?.newIdx ?? edit.newIdx,
1220
+ newCount: 0,
1221
+ lines: []
1222
+ };
1223
+ for (let j = contextStart; j < i; j++) {
1224
+ if (editScript[j].type === "keep") {
1225
+ currentHunk.lines.push(` ${editScript[j].line}`);
1226
+ currentHunk.oldCount++;
1227
+ currentHunk.newCount++;
967
1228
  }
968
- return !snapBuffer.equals(currBuffer);
969
- }
970
- catch {
971
- return true;
972
- }
1229
+ }
1230
+ }
1231
+ if (edit.type === "delete") {
1232
+ currentHunk.lines.push(`-${edit.line}`);
1233
+ currentHunk.oldCount++;
1234
+ } else {
1235
+ currentHunk.lines.push(`+${edit.line}`);
1236
+ currentHunk.newCount++;
1237
+ }
1238
+ contextCounter = 0;
1239
+ } else if (currentHunk) {
1240
+ contextCounter++;
1241
+ if (contextCounter <= CONTEXT) {
1242
+ currentHunk.lines.push(` ${edit.line}`);
1243
+ currentHunk.oldCount++;
1244
+ currentHunk.newCount++;
1245
+ } else {
1246
+ hunks.push(currentHunk);
1247
+ currentHunk = null;
1248
+ contextCounter = 0;
1249
+ }
1250
+ }
973
1251
  }
974
- removeFileOrDirSync(targetPath) {
975
- if (!fs.existsSync(targetPath))
976
- return;
977
- const stat = fs.lstatSync(targetPath);
978
- if (stat.isDirectory()) {
979
- fs.rmSync(targetPath, { recursive: true, force: true });
980
- return;
981
- }
982
- fs.rmSync(targetPath, { force: true });
1252
+ if (currentHunk) {
1253
+ hunks.push(currentHunk);
983
1254
  }
984
- generateCheckpointId() {
985
- const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '');
986
- const random = Math.random().toString(36).substring(2, 6);
987
- return `cp-${timestamp}-${random}`;
1255
+ return hunks;
1256
+ }
1257
+ longestCommonSubsequence(a, b) {
1258
+ if (a.length > 1e3 || b.length > 1e3) {
1259
+ return this.simpleLCS(a, b);
988
1260
  }
989
- ensureDirSync(dirPath) {
990
- if (!fs.existsSync(dirPath)) {
991
- fs.mkdirSync(dirPath, { recursive: true });
992
- }
1261
+ const m = a.length;
1262
+ const n = b.length;
1263
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
1264
+ for (let i2 = 1; i2 <= m; i2++) {
1265
+ for (let j2 = 1; j2 <= n; j2++) {
1266
+ if (a[i2 - 1] === b[j2 - 1]) {
1267
+ dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
1268
+ } else {
1269
+ dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
1270
+ }
1271
+ }
993
1272
  }
994
- getChatDir() {
995
- if (!this.currentChatId) {
996
- return this.baseDir;
997
- }
998
- const dir = path.join(this.baseDir, this.currentChatId);
999
- this.ensureDirSync(dir);
1000
- return dir;
1001
- }
1002
- getIndexPath() {
1003
- if (!this.currentChatId)
1004
- return null;
1005
- return path.join(this.getChatDir(), 'index.json');
1006
- }
1007
- loadIndex() {
1008
- this.checkpoints = [];
1009
- this.discardedIds.clear();
1010
- const indexPath = this.getIndexPath();
1011
- if (!indexPath || !fs.existsSync(indexPath)) {
1012
- return;
1013
- }
1014
- try {
1015
- const raw = fs.readFileSync(indexPath, 'utf-8');
1016
- const data = JSON.parse(raw);
1017
- this.checkpoints = data.filter(entry => fs.existsSync(entry.manifestPath));
1018
- }
1019
- catch (error) {
1020
- logWarning(`Failed to load checkpoint index: ${error.message}`);
1021
- }
1273
+ const result = [];
1274
+ let i = m, j = n;
1275
+ while (i > 0 && j > 0) {
1276
+ if (a[i - 1] === b[j - 1]) {
1277
+ result.unshift(a[i - 1]);
1278
+ i--;
1279
+ j--;
1280
+ } else if (dp[i - 1][j] > dp[i][j - 1]) {
1281
+ i--;
1282
+ } else {
1283
+ j--;
1284
+ }
1022
1285
  }
1023
- saveIndex() {
1024
- const indexPath = this.getIndexPath();
1025
- if (!indexPath)
1026
- return;
1027
- try {
1028
- fs.writeFileSync(indexPath, JSON.stringify(this.checkpoints, null, 2), 'utf-8');
1029
- }
1030
- catch (error) {
1031
- logWarning(`Failed to save checkpoint index: ${error.message}`);
1032
- }
1286
+ return result;
1287
+ }
1288
+ simpleLCS(a, b) {
1289
+ const bMap = /* @__PURE__ */ new Map();
1290
+ for (let i = 0; i < b.length; i++) {
1291
+ const positions = bMap.get(b[i]) || [];
1292
+ positions.push(i);
1293
+ bMap.set(b[i], positions);
1294
+ }
1295
+ const result = [];
1296
+ let lastMatchB = -1;
1297
+ for (let i = 0; i < a.length; i++) {
1298
+ const positions = bMap.get(a[i]);
1299
+ if (positions) {
1300
+ for (const pos of positions) {
1301
+ if (pos > lastMatchB) {
1302
+ result.push(a[i]);
1303
+ lastMatchB = pos;
1304
+ break;
1305
+ }
1306
+ }
1307
+ }
1308
+ }
1309
+ return result;
1310
+ }
1311
+ // ── Helpers: file operations ────────────────────────────────────────
1312
+ filesDiffer(snapshotPath, currentPath) {
1313
+ try {
1314
+ const snapStat = fs.statSync(snapshotPath);
1315
+ const currStat = fs.statSync(currentPath);
1316
+ if (snapStat.size !== currStat.size) {
1317
+ return true;
1318
+ }
1319
+ const snapBuffer = fs.readFileSync(snapshotPath);
1320
+ const currBuffer = fs.readFileSync(currentPath);
1321
+ if (snapBuffer.length !== currBuffer.length) {
1322
+ return true;
1323
+ }
1324
+ return !snapBuffer.equals(currBuffer);
1325
+ } catch {
1326
+ return true;
1327
+ }
1328
+ }
1329
+ removeFileOrDirSync(targetPath) {
1330
+ if (!fs.existsSync(targetPath)) return;
1331
+ const stat = fs.lstatSync(targetPath);
1332
+ if (stat.isDirectory()) {
1333
+ fs.rmSync(targetPath, { recursive: true, force: true });
1334
+ return;
1335
+ }
1336
+ fs.rmSync(targetPath, { force: true });
1337
+ }
1338
+ // ── Helpers: manifest & index I/O ───────────────────────────────────
1339
+ readManifest(manifestPath) {
1340
+ const raw = fs.readFileSync(manifestPath, "utf-8");
1341
+ return JSON.parse(raw);
1342
+ }
1343
+ readManifestV2(manifestPath) {
1344
+ const raw = fs.readFileSync(manifestPath, "utf-8");
1345
+ const manifest = JSON.parse(raw);
1346
+ if (manifest.version === 2) {
1347
+ return manifest;
1348
+ }
1349
+ return {
1350
+ version: 2,
1351
+ createdAt: manifest.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
1352
+ cwd: manifest.cwd || "",
1353
+ fileBackups: [],
1354
+ operations: []
1355
+ };
1356
+ }
1357
+ isV2Manifest(manifest) {
1358
+ return manifest.version === 2;
1359
+ }
1360
+ // ── Helpers: file scanning (only used for V1 legacy) ────────────────
1361
+ async scanLocalFiles(cwd) {
1362
+ const fg = (await import("fast-glob")).default;
1363
+ const files = await fg(["**/*"], {
1364
+ cwd,
1365
+ dot: true,
1366
+ onlyFiles: true,
1367
+ followSymbolicLinks: false,
1368
+ unique: true,
1369
+ ignore: [
1370
+ "**/.git/**",
1371
+ "**/node_modules/**",
1372
+ "**/dist/**",
1373
+ "**/build/**",
1374
+ "**/out/**",
1375
+ "**/.next/**",
1376
+ "**/.turbo/**",
1377
+ "**/.cache/**",
1378
+ "**/coverage/**",
1379
+ "**/__pycache__/**",
1380
+ "**/.venv/**",
1381
+ "**/venv/**",
1382
+ "**/.idea/**",
1383
+ "**/.vscode/**",
1384
+ "**/.DS_Store",
1385
+ "**/Thumbs.db",
1386
+ "**/.centaurus/**"
1387
+ ]
1388
+ });
1389
+ return files.map((file) => file.replace(/\\/g, "/"));
1390
+ }
1391
+ scanLocalDirectories(rootDir, cwd) {
1392
+ const dirs = [];
1393
+ const ignoreDirs = /* @__PURE__ */ new Set([
1394
+ ".git",
1395
+ "node_modules",
1396
+ "dist",
1397
+ "build",
1398
+ "out",
1399
+ ".next",
1400
+ ".turbo",
1401
+ ".cache",
1402
+ "coverage",
1403
+ "__pycache__",
1404
+ ".venv",
1405
+ "venv",
1406
+ ".idea",
1407
+ ".vscode",
1408
+ ".centaurus"
1409
+ ]);
1410
+ const walk = (dir) => {
1411
+ try {
1412
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1413
+ for (const entry of entries) {
1414
+ if (entry.isDirectory() && !ignoreDirs.has(entry.name)) {
1415
+ const fullPath = path.join(dir, entry.name);
1416
+ const relPath = path.relative(cwd, fullPath).replace(/\\/g, "/");
1417
+ dirs.push(relPath);
1418
+ walk(fullPath);
1419
+ }
1420
+ }
1421
+ } catch {
1422
+ }
1423
+ };
1424
+ walk(rootDir);
1425
+ return dirs;
1426
+ }
1427
+ async scanRemoteFiles(cwd, handler) {
1428
+ const excludeDirs = [
1429
+ ".git",
1430
+ "node_modules",
1431
+ "dist",
1432
+ "build",
1433
+ "out",
1434
+ ".next",
1435
+ ".turbo",
1436
+ ".cache",
1437
+ "coverage",
1438
+ "__pycache__",
1439
+ ".venv",
1440
+ "venv",
1441
+ ".idea",
1442
+ ".vscode",
1443
+ ".centaurus"
1444
+ ];
1445
+ const excludeFiles = [".DS_Store", "Thumbs.db"];
1446
+ const pruneArgs = excludeDirs.map((d) => `-name "${d}" -prune`).join(" -o ");
1447
+ const notFileArgs = excludeFiles.map((f) => `! -name "${f}"`).join(" ");
1448
+ const findCmd = `find "${cwd}" \\( ${pruneArgs} \\) -o -type f ${notFileArgs} -print 2>/dev/null`;
1449
+ try {
1450
+ const result = await handler.executeCommand(findCmd);
1451
+ if (result.exitCode !== 0 && !result.stdout.trim()) {
1452
+ logWarning(`Remote file scan failed: ${result.stderr}`);
1453
+ return [];
1454
+ }
1455
+ const cwdPrefix = cwd.endsWith("/") ? cwd : cwd + "/";
1456
+ return result.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && line.startsWith(cwdPrefix)).map((line) => line.substring(cwdPrefix.length)).filter((relPath) => relPath.length > 0);
1457
+ } catch (error) {
1458
+ logWarning(`Failed to scan remote files: ${error.message}`);
1459
+ return [];
1460
+ }
1461
+ }
1462
+ // ── Helpers: checkpoint management ──────────────────────────────────
1463
+ discardCheckpoint(id) {
1464
+ const checkpointIndex = this.checkpoints.findIndex((cp) => cp.id === id);
1465
+ if (checkpointIndex === -1) return;
1466
+ const [checkpoint] = this.checkpoints.splice(checkpointIndex, 1);
1467
+ this.discardedIds.delete(id);
1468
+ try {
1469
+ if (fs.existsSync(path.dirname(checkpoint.manifestPath))) {
1470
+ fs.rmSync(path.dirname(checkpoint.manifestPath), { recursive: true, force: true });
1471
+ }
1472
+ } catch (error) {
1473
+ logWarning(`Failed to delete checkpoint ${id}: ${error.message}`);
1474
+ }
1475
+ quickLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] [Checkpoint] Discarded ${id}
1476
+ `);
1477
+ this.saveIndex();
1478
+ }
1479
+ generateCheckpointId() {
1480
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\..+/, "");
1481
+ const random = Math.random().toString(36).substring(2, 6);
1482
+ return `cp-${timestamp}-${random}`;
1483
+ }
1484
+ ensureDirSync(dirPath) {
1485
+ if (!fs.existsSync(dirPath)) {
1486
+ fs.mkdirSync(dirPath, { recursive: true });
1487
+ }
1488
+ }
1489
+ getChatDir() {
1490
+ if (!this.currentChatId) {
1491
+ return this.baseDir;
1492
+ }
1493
+ const dir = path.join(this.baseDir, this.currentChatId);
1494
+ this.ensureDirSync(dir);
1495
+ return dir;
1496
+ }
1497
+ getIndexPath() {
1498
+ if (!this.currentChatId) return null;
1499
+ return path.join(this.getChatDir(), "index.json");
1500
+ }
1501
+ loadIndex() {
1502
+ this.checkpoints = [];
1503
+ this.discardedIds.clear();
1504
+ const indexPath = this.getIndexPath();
1505
+ if (!indexPath || !fs.existsSync(indexPath)) {
1506
+ return;
1507
+ }
1508
+ try {
1509
+ const raw = fs.readFileSync(indexPath, "utf-8");
1510
+ const data = JSON.parse(raw);
1511
+ this.checkpoints = data.filter((entry) => fs.existsSync(entry.manifestPath));
1512
+ } catch (error) {
1513
+ logWarning(`Failed to load checkpoint index: ${error.message}`);
1514
+ }
1515
+ }
1516
+ saveIndex() {
1517
+ const indexPath = this.getIndexPath();
1518
+ if (!indexPath) return;
1519
+ try {
1520
+ fs.writeFileSync(indexPath, JSON.stringify(this.checkpoints, null, 2), "utf-8");
1521
+ } catch (error) {
1522
+ logWarning(`Failed to save checkpoint index: ${error.message}`);
1523
+ }
1524
+ }
1525
+ normalizeCheckpointMeta(rawEntry) {
1526
+ if (!rawEntry || typeof rawEntry !== "object" || typeof rawEntry.id !== "string" || typeof rawEntry.manifestPath !== "string") {
1527
+ return null;
1033
1528
  }
1529
+ const createdAt = typeof rawEntry.createdAt === "string" ? rawEntry.createdAt : (/* @__PURE__ */ new Date()).toISOString();
1530
+ const parsedCreatedAtMs = typeof rawEntry.createdAtMs === "number" ? rawEntry.createdAtMs : Date.parse(createdAt);
1531
+ const createdAtMs = Number.isFinite(parsedCreatedAtMs) ? parsedCreatedAtMs : Date.now();
1532
+ const contextType = rawEntry.contextType === "ssh" || rawEntry.contextType === "wsl" || rawEntry.contextType === "docker" ? rawEntry.contextType : "local";
1533
+ const status = rawEntry.status === "active" || rawEntry.status === "discarded" ? rawEntry.status : "finalized";
1534
+ const toolCalls = Array.isArray(rawEntry.toolCalls) ? rawEntry.toolCalls.filter((toolCall) => toolCall && typeof toolCall.name === "string").map((toolCall) => ({
1535
+ id: typeof toolCall.id === "string" ? toolCall.id : void 0,
1536
+ name: toolCall.name,
1537
+ arguments: toolCall.arguments && typeof toolCall.arguments === "object" ? toolCall.arguments : void 0,
1538
+ timestamp: typeof toolCall.timestamp === "string" ? toolCall.timestamp : createdAt
1539
+ })) : [];
1540
+ const commands = Array.isArray(rawEntry.commands) ? rawEntry.commands.filter((command) => typeof command === "string") : [];
1541
+ const snapshotDir = typeof rawEntry.snapshotDir === "string" ? rawEntry.snapshotDir : path.join(path.dirname(rawEntry.manifestPath), "backups");
1542
+ const remoteSessionInfo = rawEntry.remoteSessionInfo && typeof rawEntry.remoteSessionInfo === "object" ? {
1543
+ hostname: typeof rawEntry.remoteSessionInfo.hostname === "string" ? rawEntry.remoteSessionInfo.hostname : void 0,
1544
+ username: typeof rawEntry.remoteSessionInfo.username === "string" ? rawEntry.remoteSessionInfo.username : void 0,
1545
+ sessionId: typeof rawEntry.remoteSessionInfo.sessionId === "string" ? rawEntry.remoteSessionInfo.sessionId : "",
1546
+ connectionString: typeof rawEntry.remoteSessionInfo.connectionString === "string" ? rawEntry.remoteSessionInfo.connectionString : void 0
1547
+ } : void 0;
1548
+ return {
1549
+ id: rawEntry.id,
1550
+ prompt: typeof rawEntry.prompt === "string" ? rawEntry.prompt : "",
1551
+ createdAt,
1552
+ createdAtMs,
1553
+ cwd: typeof rawEntry.cwd === "string" ? rawEntry.cwd : "",
1554
+ contextType,
1555
+ remoteSessionInfo: remoteSessionInfo?.sessionId ? remoteSessionInfo : void 0,
1556
+ conversationIndex: typeof rawEntry.conversationIndex === "number" ? rawEntry.conversationIndex : 0,
1557
+ uiMessageIndex: typeof rawEntry.uiMessageIndex === "number" ? rawEntry.uiMessageIndex : void 0,
1558
+ uiMessageId: typeof rawEntry.uiMessageId === "string" ? rawEntry.uiMessageId : void 0,
1559
+ snapshotDir,
1560
+ manifestPath: rawEntry.manifestPath,
1561
+ changes: rawEntry.changes && typeof rawEntry.changes === "object" ? {
1562
+ added: Array.isArray(rawEntry.changes.added) ? rawEntry.changes.added.filter((f) => typeof f === "string") : [],
1563
+ modified: Array.isArray(rawEntry.changes.modified) ? rawEntry.changes.modified.filter((f) => typeof f === "string") : [],
1564
+ deleted: Array.isArray(rawEntry.changes.deleted) ? rawEntry.changes.deleted.filter((f) => typeof f === "string") : []
1565
+ } : void 0,
1566
+ commands,
1567
+ toolCalls,
1568
+ status
1569
+ };
1570
+ }
1034
1571
  }
1572
+ export {
1573
+ CheckpointManager
1574
+ };
1035
1575
  //# sourceMappingURL=checkpoint-manager.js.map