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.
Files changed (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. 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.