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