klaude-code 1.2.6__py3-none-any.whl
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.
- klaude_code/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from .file.apply_patch import DiffError, process_patch
|
|
2
|
+
from .file.apply_patch_tool import ApplyPatchTool
|
|
3
|
+
from .file.edit_tool import EditTool
|
|
4
|
+
from .file.multi_edit_tool import MultiEditTool
|
|
5
|
+
from .file.read_tool import ReadTool
|
|
6
|
+
from .file.write_tool import WriteTool
|
|
7
|
+
from .memory.memory_tool import MEMORY_DIR_NAME, MemoryTool
|
|
8
|
+
from .memory.skill_loader import Skill, SkillLoader
|
|
9
|
+
from .memory.skill_tool import SkillTool
|
|
10
|
+
from .shell.bash_tool import BashTool
|
|
11
|
+
from .shell.command_safety import SafetyCheckResult, is_safe_command
|
|
12
|
+
from .sub_agent_tool import SubAgentTool
|
|
13
|
+
from .todo.todo_write_tool import TodoWriteTool
|
|
14
|
+
from .todo.update_plan_tool import UpdatePlanTool
|
|
15
|
+
from .tool_abc import ToolABC
|
|
16
|
+
from .tool_context import (
|
|
17
|
+
TodoContext,
|
|
18
|
+
ToolContextToken,
|
|
19
|
+
current_run_subtask_callback,
|
|
20
|
+
reset_tool_context,
|
|
21
|
+
set_tool_context_from_session,
|
|
22
|
+
tool_context,
|
|
23
|
+
)
|
|
24
|
+
from .tool_registry import get_registry, get_tool_schemas, load_agent_tools
|
|
25
|
+
from .tool_runner import run_tool
|
|
26
|
+
from .truncation import SimpleTruncationStrategy, TruncationStrategy, get_truncation_strategy, set_truncation_strategy
|
|
27
|
+
from .web.mermaid_tool import MermaidTool
|
|
28
|
+
from .web.web_fetch_tool import WebFetchTool
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# Tools
|
|
32
|
+
"ApplyPatchTool",
|
|
33
|
+
"BashTool",
|
|
34
|
+
"EditTool",
|
|
35
|
+
"MemoryTool",
|
|
36
|
+
"MermaidTool",
|
|
37
|
+
"MultiEditTool",
|
|
38
|
+
"ReadTool",
|
|
39
|
+
"SkillTool",
|
|
40
|
+
"SubAgentTool",
|
|
41
|
+
"TodoWriteTool",
|
|
42
|
+
"UpdatePlanTool",
|
|
43
|
+
"WebFetchTool",
|
|
44
|
+
"WriteTool",
|
|
45
|
+
# Tool ABC
|
|
46
|
+
"ToolABC",
|
|
47
|
+
# Tool context
|
|
48
|
+
"TodoContext",
|
|
49
|
+
"ToolContextToken",
|
|
50
|
+
"current_run_subtask_callback",
|
|
51
|
+
"reset_tool_context",
|
|
52
|
+
"set_tool_context_from_session",
|
|
53
|
+
"tool_context",
|
|
54
|
+
# Tool registry
|
|
55
|
+
"load_agent_tools",
|
|
56
|
+
"get_registry",
|
|
57
|
+
"get_tool_schemas",
|
|
58
|
+
"run_tool",
|
|
59
|
+
# Truncation
|
|
60
|
+
"SimpleTruncationStrategy",
|
|
61
|
+
"TruncationStrategy",
|
|
62
|
+
"get_truncation_strategy",
|
|
63
|
+
"set_truncation_strategy",
|
|
64
|
+
# Command safety
|
|
65
|
+
"SafetyCheckResult",
|
|
66
|
+
"is_safe_command",
|
|
67
|
+
# Skill
|
|
68
|
+
"Skill",
|
|
69
|
+
"SkillLoader",
|
|
70
|
+
# Memory
|
|
71
|
+
"MEMORY_DIR_NAME",
|
|
72
|
+
# Apply patch
|
|
73
|
+
"DiffError",
|
|
74
|
+
"process_patch",
|
|
75
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""
|
|
2
|
+
https://github.com/openai/openai-cookbook/blob/main/examples/gpt-5/apply_patch.py
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Callable, Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ActionType(str, Enum):
|
|
13
|
+
ADD = "add"
|
|
14
|
+
DELETE = "delete"
|
|
15
|
+
UPDATE = "update"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileChange(BaseModel):
|
|
19
|
+
type: ActionType
|
|
20
|
+
old_content: Optional[str] = None
|
|
21
|
+
new_content: Optional[str] = None
|
|
22
|
+
move_path: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Commit(BaseModel):
|
|
26
|
+
changes: dict[str, FileChange] = Field(default_factory=dict)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def assemble_changes(orig: dict[str, Optional[str]], dest: dict[str, Optional[str]]) -> Commit:
|
|
30
|
+
commit = Commit()
|
|
31
|
+
for path in sorted(set(orig.keys()).union(dest.keys())):
|
|
32
|
+
old_content = orig.get(path)
|
|
33
|
+
new_content = dest.get(path)
|
|
34
|
+
if old_content != new_content:
|
|
35
|
+
if old_content is not None and new_content is not None:
|
|
36
|
+
commit.changes[path] = FileChange(
|
|
37
|
+
type=ActionType.UPDATE,
|
|
38
|
+
old_content=old_content,
|
|
39
|
+
new_content=new_content,
|
|
40
|
+
)
|
|
41
|
+
elif new_content:
|
|
42
|
+
commit.changes[path] = FileChange(
|
|
43
|
+
type=ActionType.ADD,
|
|
44
|
+
new_content=new_content,
|
|
45
|
+
)
|
|
46
|
+
elif old_content:
|
|
47
|
+
commit.changes[path] = FileChange(
|
|
48
|
+
type=ActionType.DELETE,
|
|
49
|
+
old_content=old_content,
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
assert False
|
|
53
|
+
return commit
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _new_str_list() -> list[str]:
|
|
57
|
+
# Returns a new list[str] for pydantic Field default_factory
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Chunk(BaseModel):
|
|
62
|
+
orig_index: int = -1 # line index of the first line in the original file
|
|
63
|
+
del_lines: list[str] = Field(default_factory=_new_str_list)
|
|
64
|
+
ins_lines: list[str] = Field(default_factory=_new_str_list)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _new_chunk_list() -> list["Chunk"]:
|
|
68
|
+
# Returns a new list[Chunk] for pydantic Field default_factory
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PatchAction(BaseModel):
|
|
73
|
+
type: ActionType
|
|
74
|
+
new_file: Optional[str] = None
|
|
75
|
+
chunks: list[Chunk] = Field(default_factory=_new_chunk_list)
|
|
76
|
+
move_path: Optional[str] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Patch(BaseModel):
|
|
80
|
+
actions: dict[str, PatchAction] = Field(default_factory=dict)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Parser(BaseModel):
|
|
84
|
+
current_files: dict[str, str] = Field(default_factory=dict)
|
|
85
|
+
lines: list[str] = Field(default_factory=list)
|
|
86
|
+
index: int = 0
|
|
87
|
+
patch: Patch = Field(default_factory=Patch)
|
|
88
|
+
fuzz: int = 0
|
|
89
|
+
|
|
90
|
+
def is_done(self, prefixes: Optional[tuple[str, ...]] = None) -> bool:
|
|
91
|
+
if self.index >= len(self.lines):
|
|
92
|
+
return True
|
|
93
|
+
if prefixes and self.lines[self.index].startswith(prefixes):
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def startswith(self, prefix: str | tuple[str, ...]) -> bool:
|
|
98
|
+
assert self.index < len(self.lines), f"Index: {self.index} >= {len(self.lines)}"
|
|
99
|
+
if self.lines[self.index].startswith(prefix):
|
|
100
|
+
return True
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def read_str(self, prefix: str = "", return_everything: bool = False) -> str:
|
|
104
|
+
assert self.index < len(self.lines), f"Index: {self.index} >= {len(self.lines)}"
|
|
105
|
+
if self.lines[self.index].startswith(prefix):
|
|
106
|
+
if return_everything:
|
|
107
|
+
text = self.lines[self.index]
|
|
108
|
+
else:
|
|
109
|
+
text = self.lines[self.index][len(prefix) :]
|
|
110
|
+
self.index += 1
|
|
111
|
+
return text
|
|
112
|
+
return ""
|
|
113
|
+
|
|
114
|
+
def parse(self):
|
|
115
|
+
while not self.is_done(("*** End Patch",)):
|
|
116
|
+
path = self.read_str("*** Update File: ")
|
|
117
|
+
if path:
|
|
118
|
+
if path in self.patch.actions:
|
|
119
|
+
raise DiffError(f"Update File Error: Duplicate Path: {path}")
|
|
120
|
+
move_to = self.read_str("*** Move to: ")
|
|
121
|
+
if path not in self.current_files:
|
|
122
|
+
raise DiffError(f"Update File Error: Missing File: {path}")
|
|
123
|
+
text = self.current_files[path]
|
|
124
|
+
action = self.parse_update_file(text)
|
|
125
|
+
# TODO: Check move_to is valid
|
|
126
|
+
action.move_path = move_to
|
|
127
|
+
self.patch.actions[path] = action
|
|
128
|
+
continue
|
|
129
|
+
path = self.read_str("*** Delete File: ")
|
|
130
|
+
if path:
|
|
131
|
+
if path in self.patch.actions:
|
|
132
|
+
raise DiffError(f"Delete File Error: Duplicate Path: {path}")
|
|
133
|
+
if path not in self.current_files:
|
|
134
|
+
raise DiffError(f"Delete File Error: Missing File: {path}")
|
|
135
|
+
self.patch.actions[path] = PatchAction(
|
|
136
|
+
type=ActionType.DELETE,
|
|
137
|
+
)
|
|
138
|
+
continue
|
|
139
|
+
path = self.read_str("*** Add File: ")
|
|
140
|
+
if path:
|
|
141
|
+
if path in self.patch.actions:
|
|
142
|
+
raise DiffError(f"Add File Error: Duplicate Path: {path}")
|
|
143
|
+
self.patch.actions[path] = self.parse_add_file()
|
|
144
|
+
continue
|
|
145
|
+
raise DiffError(f"Unknown Line: {self.lines[self.index]}")
|
|
146
|
+
if not self.startswith("*** End Patch"):
|
|
147
|
+
raise DiffError("Missing End Patch")
|
|
148
|
+
self.index += 1
|
|
149
|
+
|
|
150
|
+
def parse_update_file(self, text: str) -> PatchAction:
|
|
151
|
+
# self.lines / self.index refers to the patch
|
|
152
|
+
# lines / index refers to the file being modified
|
|
153
|
+
# print("parse update file")
|
|
154
|
+
action = PatchAction(
|
|
155
|
+
type=ActionType.UPDATE,
|
|
156
|
+
)
|
|
157
|
+
lines = text.split("\n")
|
|
158
|
+
index = 0
|
|
159
|
+
while not self.is_done(
|
|
160
|
+
(
|
|
161
|
+
"*** End Patch",
|
|
162
|
+
"*** Update File:",
|
|
163
|
+
"*** Delete File:",
|
|
164
|
+
"*** Add File:",
|
|
165
|
+
"*** End of File",
|
|
166
|
+
)
|
|
167
|
+
):
|
|
168
|
+
def_str = self.read_str("@@ ")
|
|
169
|
+
section_str = ""
|
|
170
|
+
if not def_str:
|
|
171
|
+
if self.lines[self.index] == "@@":
|
|
172
|
+
section_str = self.lines[self.index]
|
|
173
|
+
self.index += 1
|
|
174
|
+
if not (def_str or section_str or index == 0):
|
|
175
|
+
raise DiffError(f"Invalid Line:\n{self.lines[self.index]}")
|
|
176
|
+
if def_str.strip():
|
|
177
|
+
found = False
|
|
178
|
+
if not [s for s in lines[:index] if s == def_str]:
|
|
179
|
+
# def str is a skip ahead operator
|
|
180
|
+
for i, s in enumerate(lines[index:], index):
|
|
181
|
+
if s == def_str:
|
|
182
|
+
# print(f"Jump ahead @@: {index} -> {i}: {def_str}")
|
|
183
|
+
index = i + 1
|
|
184
|
+
found = True
|
|
185
|
+
break
|
|
186
|
+
if not found and not [s for s in lines[:index] if s.strip() == def_str.strip()]:
|
|
187
|
+
# def str is a skip ahead operator
|
|
188
|
+
for i, s in enumerate(lines[index:], index):
|
|
189
|
+
if s.strip() == def_str.strip():
|
|
190
|
+
# print(f"Jump ahead @@: {index} -> {i}: {def_str}")
|
|
191
|
+
index = i + 1
|
|
192
|
+
self.fuzz += 1
|
|
193
|
+
found = True
|
|
194
|
+
break
|
|
195
|
+
next_chunk_context, chunks, end_patch_index, eof = peek_next_section(self.lines, self.index)
|
|
196
|
+
next_chunk_text = "\n".join(next_chunk_context)
|
|
197
|
+
new_index, fuzz = find_context(lines, next_chunk_context, index, eof)
|
|
198
|
+
if new_index == -1:
|
|
199
|
+
if eof:
|
|
200
|
+
raise DiffError(f"Invalid EOF Context {index}:\n{next_chunk_text}")
|
|
201
|
+
else:
|
|
202
|
+
raise DiffError(f"Invalid Context {index}:\n{next_chunk_text}")
|
|
203
|
+
self.fuzz += fuzz
|
|
204
|
+
# print(f"Jump ahead: {index} -> {new_index}")
|
|
205
|
+
for ch in chunks:
|
|
206
|
+
ch.orig_index += new_index
|
|
207
|
+
action.chunks.append(ch)
|
|
208
|
+
index = new_index + len(next_chunk_context)
|
|
209
|
+
self.index = end_patch_index
|
|
210
|
+
continue
|
|
211
|
+
return action
|
|
212
|
+
|
|
213
|
+
def parse_add_file(self) -> PatchAction:
|
|
214
|
+
lines: list[str] = []
|
|
215
|
+
while not self.is_done(("*** End Patch", "*** Update File:", "*** Delete File:", "*** Add File:")):
|
|
216
|
+
s = self.read_str()
|
|
217
|
+
if not s.startswith("+"):
|
|
218
|
+
raise DiffError(f"Invalid Add File Line: {s}")
|
|
219
|
+
s = s[1:]
|
|
220
|
+
lines.append(s)
|
|
221
|
+
return PatchAction(
|
|
222
|
+
type=ActionType.ADD,
|
|
223
|
+
new_file="\n".join(lines),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def find_context_core(lines: list[str], context: list[str], start: int) -> tuple[int, int]:
|
|
228
|
+
if not context:
|
|
229
|
+
# print("context is empty")
|
|
230
|
+
return start, 0
|
|
231
|
+
|
|
232
|
+
# Prefer identical
|
|
233
|
+
for i in range(start, len(lines)):
|
|
234
|
+
if lines[i : i + len(context)] == context:
|
|
235
|
+
return i, 0
|
|
236
|
+
# RStrip is ok
|
|
237
|
+
for i in range(start, len(lines)):
|
|
238
|
+
if [s.rstrip() for s in lines[i : i + len(context)]] == [s.rstrip() for s in context]:
|
|
239
|
+
return i, 1
|
|
240
|
+
# Fine, Strip is ok too.
|
|
241
|
+
for i in range(start, len(lines)):
|
|
242
|
+
if [s.strip() for s in lines[i : i + len(context)]] == [s.strip() for s in context]:
|
|
243
|
+
return i, 100
|
|
244
|
+
return -1, 0
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def find_context(lines: list[str], context: list[str], start: int, eof: bool) -> tuple[int, int]:
|
|
248
|
+
if eof:
|
|
249
|
+
new_index, fuzz = find_context_core(lines, context, len(lines) - len(context))
|
|
250
|
+
if new_index != -1:
|
|
251
|
+
return new_index, fuzz
|
|
252
|
+
new_index, fuzz = find_context_core(lines, context, start)
|
|
253
|
+
return new_index, fuzz + 10000
|
|
254
|
+
return find_context_core(lines, context, start)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def peek_next_section(lines: list[str], index: int) -> tuple[list[str], list[Chunk], int, bool]:
|
|
258
|
+
old: list[str] = []
|
|
259
|
+
del_lines: list[str] = []
|
|
260
|
+
ins_lines: list[str] = []
|
|
261
|
+
chunks: list[Chunk] = []
|
|
262
|
+
mode = "keep"
|
|
263
|
+
orig_index = index
|
|
264
|
+
while index < len(lines):
|
|
265
|
+
s = lines[index]
|
|
266
|
+
if s.startswith(
|
|
267
|
+
(
|
|
268
|
+
"@@",
|
|
269
|
+
"*** End Patch",
|
|
270
|
+
"*** Update File:",
|
|
271
|
+
"*** Delete File:",
|
|
272
|
+
"*** Add File:",
|
|
273
|
+
"*** End of File",
|
|
274
|
+
)
|
|
275
|
+
):
|
|
276
|
+
break
|
|
277
|
+
if s == "***":
|
|
278
|
+
break
|
|
279
|
+
elif s.startswith("***"):
|
|
280
|
+
raise DiffError(f"Invalid Line: {s}")
|
|
281
|
+
index += 1
|
|
282
|
+
last_mode = mode
|
|
283
|
+
if s == "":
|
|
284
|
+
s = " "
|
|
285
|
+
if s[0] == "+":
|
|
286
|
+
mode = "add"
|
|
287
|
+
elif s[0] == "-":
|
|
288
|
+
mode = "delete"
|
|
289
|
+
elif s[0] == " ":
|
|
290
|
+
mode = "keep"
|
|
291
|
+
else:
|
|
292
|
+
raise DiffError(f"Invalid Line: {s}")
|
|
293
|
+
s = s[1:]
|
|
294
|
+
if mode == "keep" and last_mode != mode:
|
|
295
|
+
if ins_lines or del_lines:
|
|
296
|
+
chunks.append(
|
|
297
|
+
Chunk(
|
|
298
|
+
orig_index=len(old) - len(del_lines),
|
|
299
|
+
del_lines=del_lines,
|
|
300
|
+
ins_lines=ins_lines,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
del_lines = []
|
|
304
|
+
ins_lines = []
|
|
305
|
+
if mode == "delete":
|
|
306
|
+
del_lines.append(s)
|
|
307
|
+
old.append(s)
|
|
308
|
+
elif mode == "add":
|
|
309
|
+
ins_lines.append(s)
|
|
310
|
+
elif mode == "keep":
|
|
311
|
+
old.append(s)
|
|
312
|
+
if ins_lines or del_lines:
|
|
313
|
+
chunks.append(
|
|
314
|
+
Chunk(
|
|
315
|
+
orig_index=len(old) - len(del_lines),
|
|
316
|
+
del_lines=del_lines,
|
|
317
|
+
ins_lines=ins_lines,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
del_lines = []
|
|
321
|
+
ins_lines = []
|
|
322
|
+
if index < len(lines) and lines[index] == "*** End of File":
|
|
323
|
+
index += 1
|
|
324
|
+
return old, chunks, index, True
|
|
325
|
+
if index == orig_index:
|
|
326
|
+
raise DiffError(f"Nothing in this section - {index=} {lines[index]}")
|
|
327
|
+
return old, chunks, index, False
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def text_to_patch(text: str, orig: dict[str, str]) -> tuple[Patch, int]:
|
|
331
|
+
lines = text.strip().split("\n")
|
|
332
|
+
if len(lines) < 2 or not lines[0].startswith("*** Begin Patch") or lines[-1] != "*** End Patch":
|
|
333
|
+
raise DiffError('Invalid patch text, expected "*** Begin Patch" and "*** End Patch"')
|
|
334
|
+
|
|
335
|
+
parser = Parser(
|
|
336
|
+
current_files=orig,
|
|
337
|
+
lines=lines,
|
|
338
|
+
index=1,
|
|
339
|
+
)
|
|
340
|
+
parser.parse()
|
|
341
|
+
return parser.patch, parser.fuzz
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def identify_files_needed(text: str) -> list[str]:
|
|
345
|
+
lines = text.strip().split("\n")
|
|
346
|
+
result: set[str] = set()
|
|
347
|
+
for line in lines:
|
|
348
|
+
if line.startswith("*** Update File: "):
|
|
349
|
+
result.add(line[len("*** Update File: ") :])
|
|
350
|
+
if line.startswith("*** Delete File: "):
|
|
351
|
+
result.add(line[len("*** Delete File: ") :])
|
|
352
|
+
return list(result)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _get_updated_file(text: str, action: PatchAction, path: str) -> str:
|
|
356
|
+
assert action.type == ActionType.UPDATE
|
|
357
|
+
orig_lines = text.split("\n")
|
|
358
|
+
dest_lines: list[str] = []
|
|
359
|
+
orig_index = 0
|
|
360
|
+
dest_index = 0
|
|
361
|
+
for chunk in action.chunks:
|
|
362
|
+
# Process the unchanged lines before the chunk
|
|
363
|
+
if chunk.orig_index > len(orig_lines):
|
|
364
|
+
# print(f"_get_updated_file: {path}: chunk.orig_index {chunk.orig_index} > len(lines) {len(orig_lines)}")
|
|
365
|
+
raise DiffError(
|
|
366
|
+
f"_get_updated_file: {path}: chunk.orig_index {chunk.orig_index} > len(lines) {len(orig_lines)}"
|
|
367
|
+
)
|
|
368
|
+
if orig_index > chunk.orig_index:
|
|
369
|
+
raise DiffError(f"_get_updated_file: {path}: orig_index {orig_index} > chunk.orig_index {chunk.orig_index}")
|
|
370
|
+
assert orig_index <= chunk.orig_index
|
|
371
|
+
dest_lines.extend(orig_lines[orig_index : chunk.orig_index])
|
|
372
|
+
delta = chunk.orig_index - orig_index
|
|
373
|
+
orig_index += delta
|
|
374
|
+
dest_index += delta
|
|
375
|
+
# Process the inserted lines
|
|
376
|
+
if chunk.ins_lines:
|
|
377
|
+
for i in range(len(chunk.ins_lines)):
|
|
378
|
+
dest_lines.append(chunk.ins_lines[i])
|
|
379
|
+
dest_index += len(chunk.ins_lines)
|
|
380
|
+
orig_index += len(chunk.del_lines)
|
|
381
|
+
# Final part
|
|
382
|
+
dest_lines.extend(orig_lines[orig_index:])
|
|
383
|
+
delta = len(orig_lines) - orig_index
|
|
384
|
+
orig_index += delta
|
|
385
|
+
dest_index += delta
|
|
386
|
+
assert orig_index == len(orig_lines)
|
|
387
|
+
assert dest_index == len(dest_lines)
|
|
388
|
+
return "\n".join(dest_lines)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def patch_to_commit(patch: Patch, orig: dict[str, str]) -> Commit:
|
|
392
|
+
commit = Commit()
|
|
393
|
+
for path, action in patch.actions.items():
|
|
394
|
+
if action.type == ActionType.DELETE:
|
|
395
|
+
commit.changes[path] = FileChange(type=ActionType.DELETE, old_content=orig[path])
|
|
396
|
+
elif action.type == ActionType.ADD:
|
|
397
|
+
commit.changes[path] = FileChange(type=ActionType.ADD, new_content=action.new_file)
|
|
398
|
+
elif action.type == ActionType.UPDATE:
|
|
399
|
+
new_content = _get_updated_file(text=orig[path], action=action, path=path)
|
|
400
|
+
commit.changes[path] = FileChange(
|
|
401
|
+
type=ActionType.UPDATE,
|
|
402
|
+
old_content=orig[path],
|
|
403
|
+
new_content=new_content,
|
|
404
|
+
move_path=action.move_path,
|
|
405
|
+
)
|
|
406
|
+
return commit
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class DiffError(ValueError):
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def load_files(paths: list[str], open_fn: Callable[[str], str]) -> dict[str, str]:
|
|
414
|
+
orig: dict[str, str] = {}
|
|
415
|
+
for path in paths:
|
|
416
|
+
orig[path] = open_fn(path)
|
|
417
|
+
return orig
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def apply_commit(
|
|
421
|
+
commit: Commit,
|
|
422
|
+
write_fn: Callable[[str, str], None],
|
|
423
|
+
remove_fn: Callable[[str], None],
|
|
424
|
+
) -> None:
|
|
425
|
+
for path, change in commit.changes.items():
|
|
426
|
+
if change.type == ActionType.DELETE:
|
|
427
|
+
remove_fn(path)
|
|
428
|
+
elif change.type == ActionType.ADD:
|
|
429
|
+
if change.new_content is None:
|
|
430
|
+
raise DiffError(f"Missing new_content for ADD: {path}")
|
|
431
|
+
write_fn(path, change.new_content)
|
|
432
|
+
elif change.type == ActionType.UPDATE:
|
|
433
|
+
if change.move_path:
|
|
434
|
+
if change.new_content is None:
|
|
435
|
+
raise DiffError(f"Missing new_content for UPDATE: {path}")
|
|
436
|
+
write_fn(change.move_path, change.new_content)
|
|
437
|
+
remove_fn(path)
|
|
438
|
+
else:
|
|
439
|
+
if change.new_content is None:
|
|
440
|
+
raise DiffError(f"Missing new_content for UPDATE: {path}")
|
|
441
|
+
write_fn(path, change.new_content)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def process_patch(
|
|
445
|
+
text: str,
|
|
446
|
+
open_fn: Callable[[str], str],
|
|
447
|
+
write_fn: Callable[[str, str], None],
|
|
448
|
+
remove_fn: Callable[[str], None],
|
|
449
|
+
) -> str:
|
|
450
|
+
assert text.startswith("*** Begin Patch")
|
|
451
|
+
paths = identify_files_needed(text)
|
|
452
|
+
orig = load_files(paths, open_fn)
|
|
453
|
+
patch, _ = text_to_patch(text, orig)
|
|
454
|
+
commit = patch_to_commit(patch, orig)
|
|
455
|
+
apply_commit(commit, write_fn, remove_fn)
|
|
456
|
+
return "Done!"
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def open_file(path: str) -> str:
|
|
460
|
+
with open(path, "rt") as f:
|
|
461
|
+
return f.read()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def write_file(path: str, content: str) -> None:
|
|
465
|
+
if "/" in path:
|
|
466
|
+
parent = "/".join(path.split("/")[:-1])
|
|
467
|
+
os.makedirs(parent, exist_ok=True)
|
|
468
|
+
with open(path, "wt") as f:
|
|
469
|
+
f.write(content)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def remove_file(path: str) -> None:
|
|
473
|
+
os.remove(path)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def main():
|
|
477
|
+
import sys
|
|
478
|
+
|
|
479
|
+
patch_text = sys.stdin.read()
|
|
480
|
+
if not patch_text:
|
|
481
|
+
print("Please pass patch text through stdin")
|
|
482
|
+
return
|
|
483
|
+
try:
|
|
484
|
+
result = process_patch(patch_text, open_file, write_file, remove_file)
|
|
485
|
+
except DiffError as e:
|
|
486
|
+
print(str(e))
|
|
487
|
+
return
|
|
488
|
+
print(result)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
if __name__ == "__main__":
|
|
492
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Apply a unified diff patch to a file within the workspace.
|