klaude-code 1.2.8__py3-none-any.whl → 1.2.10__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/auth/codex/__init__.py +1 -1
- klaude_code/cli/main.py +12 -1
- klaude_code/cli/runtime.py +7 -11
- klaude_code/command/__init__.py +68 -21
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +5 -2
- klaude_code/command/diff_cmd.py +5 -2
- klaude_code/command/export_cmd.py +7 -4
- klaude_code/command/help_cmd.py +6 -2
- klaude_code/command/model_cmd.py +5 -2
- klaude_code/command/prompt-deslop.md +14 -0
- klaude_code/command/prompt_command.py +8 -3
- klaude_code/command/refresh_cmd.py +6 -2
- klaude_code/command/registry.py +17 -5
- klaude_code/command/release_notes_cmd.py +89 -0
- klaude_code/command/status_cmd.py +98 -56
- klaude_code/command/terminal_setup_cmd.py +7 -4
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/agent.py +66 -26
- klaude_code/core/executor.py +2 -2
- klaude_code/core/manager/agent_manager.py +6 -7
- klaude_code/core/manager/llm_clients.py +47 -22
- klaude_code/core/manager/llm_clients_builder.py +19 -7
- klaude_code/core/manager/sub_agent_manager.py +6 -2
- klaude_code/core/prompt.py +38 -28
- klaude_code/core/reminders.py +4 -7
- klaude_code/core/task.py +59 -40
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/file/_utils.py +30 -0
- klaude_code/core/tool/file/apply_patch_tool.py +1 -1
- klaude_code/core/tool/file/edit_tool.py +6 -31
- klaude_code/core/tool/file/multi_edit_tool.py +7 -32
- klaude_code/core/tool/file/read_tool.py +6 -18
- klaude_code/core/tool/file/write_tool.py +6 -31
- klaude_code/core/tool/memory/__init__.py +5 -0
- klaude_code/core/tool/memory/memory_tool.py +2 -2
- klaude_code/core/tool/memory/skill_loader.py +2 -1
- klaude_code/core/tool/memory/skill_tool.py +13 -0
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_context.py +21 -4
- klaude_code/core/tool/tool_runner.py +5 -8
- klaude_code/core/tool/web/mermaid_tool.py +1 -4
- klaude_code/core/turn.py +40 -37
- klaude_code/llm/__init__.py +2 -12
- klaude_code/llm/anthropic/client.py +14 -44
- klaude_code/llm/client.py +2 -2
- klaude_code/llm/codex/client.py +4 -3
- klaude_code/llm/input_common.py +0 -6
- klaude_code/llm/openai_compatible/client.py +31 -74
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream_processor.py +82 -0
- klaude_code/llm/openrouter/client.py +32 -62
- klaude_code/llm/openrouter/input.py +4 -27
- klaude_code/llm/registry.py +33 -7
- klaude_code/llm/responses/client.py +16 -48
- klaude_code/llm/responses/input.py +1 -1
- klaude_code/llm/usage.py +61 -11
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/events.py +11 -2
- klaude_code/protocol/model.py +147 -24
- klaude_code/protocol/op.py +1 -0
- klaude_code/protocol/sub_agent.py +5 -1
- klaude_code/session/export.py +56 -32
- klaude_code/session/session.py +43 -21
- klaude_code/session/templates/export_session.html +4 -1
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/modes/repl/__init__.py +1 -5
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/event_handler.py +153 -54
- klaude_code/ui/modes/repl/renderer.py +4 -4
- klaude_code/ui/renderers/developer.py +35 -25
- klaude_code/ui/renderers/metadata.py +68 -30
- klaude_code/ui/renderers/tools.py +53 -87
- klaude_code/ui/rich/markdown.py +5 -5
- klaude_code/ui/terminal/control.py +2 -2
- klaude_code/version.py +3 -3
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/METADATA +1 -1
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/RECORD +82 -78
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.8.dist-info → klaude_code-1.2.10.dist-info}/entry_points.txt +0 -0
klaude_code/core/prompt.py
CHANGED
|
@@ -26,9 +26,34 @@ PROMPT_FILES: dict[str, str] = {
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@lru_cache(maxsize=None)
|
|
29
|
-
def
|
|
30
|
-
"""
|
|
29
|
+
def _load_base_prompt(file_key: str) -> str:
|
|
30
|
+
"""Load and cache the base prompt content from file."""
|
|
31
|
+
try:
|
|
32
|
+
prompt_path = PROMPT_FILES[file_key]
|
|
33
|
+
except KeyError as exc:
|
|
34
|
+
raise ValueError(f"Unknown prompt key: {file_key}") from exc
|
|
35
|
+
|
|
36
|
+
return files(__package__).joinpath(prompt_path).read_text(encoding="utf-8").strip()
|
|
31
37
|
|
|
38
|
+
|
|
39
|
+
def _get_file_key(model_name: str, sub_agent_type: str | None) -> str:
|
|
40
|
+
"""Determine which prompt file to use based on model and agent type."""
|
|
41
|
+
if sub_agent_type is not None:
|
|
42
|
+
return sub_agent_type
|
|
43
|
+
|
|
44
|
+
match model_name:
|
|
45
|
+
case "gpt-5.1-codex-max":
|
|
46
|
+
return "main_gpt_5_1_codex_max"
|
|
47
|
+
case name if "gpt-5" in name:
|
|
48
|
+
return "main_gpt_5_1"
|
|
49
|
+
case name if "gemini" in name:
|
|
50
|
+
return "main_gemini"
|
|
51
|
+
case _:
|
|
52
|
+
return "main_claude"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _build_env_info(model_name: str) -> str:
|
|
56
|
+
"""Build environment info section with dynamic runtime values."""
|
|
32
57
|
cwd = Path.cwd()
|
|
33
58
|
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
34
59
|
is_git_repo = (cwd / ".git").exists()
|
|
@@ -38,30 +63,6 @@ def get_system_prompt(model_name: str, sub_agent_type: str | None = None) -> str
|
|
|
38
63
|
if shutil.which(command) is not None:
|
|
39
64
|
available_tools.append(f"{command}: {desc}")
|
|
40
65
|
|
|
41
|
-
if sub_agent_type is None:
|
|
42
|
-
match model_name:
|
|
43
|
-
case "gpt-5.1-codex-max":
|
|
44
|
-
file_key = "main_gpt_5_1_codex_max"
|
|
45
|
-
case name if "gpt-5" in name:
|
|
46
|
-
file_key = "main_gpt_5_1"
|
|
47
|
-
case name if "gemini" in name:
|
|
48
|
-
file_key = "main_gemini"
|
|
49
|
-
case _:
|
|
50
|
-
file_key = "main_claude"
|
|
51
|
-
else:
|
|
52
|
-
file_key = sub_agent_type
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
prompt_path = PROMPT_FILES[file_key]
|
|
56
|
-
except KeyError as exc:
|
|
57
|
-
raise ValueError(f"Unknown prompt key: {file_key}") from exc
|
|
58
|
-
|
|
59
|
-
base_prompt = files(__package__).joinpath(prompt_path).read_text(encoding="utf-8").strip()
|
|
60
|
-
|
|
61
|
-
if model_name == "gpt-5.1-codex-max":
|
|
62
|
-
# Do not add env info for gpt-5.1-codex-max
|
|
63
|
-
return base_prompt
|
|
64
|
-
|
|
65
66
|
env_lines: list[str] = [
|
|
66
67
|
"",
|
|
67
68
|
"",
|
|
@@ -80,6 +81,15 @@ def get_system_prompt(model_name: str, sub_agent_type: str | None = None) -> str
|
|
|
80
81
|
|
|
81
82
|
env_lines.append("</env>")
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
return "\n".join(env_lines)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_system_prompt(model_name: str, sub_agent_type: str | None = None) -> str:
|
|
88
|
+
"""Get system prompt content for the given model and sub-agent type."""
|
|
89
|
+
file_key = _get_file_key(model_name, sub_agent_type)
|
|
90
|
+
base_prompt = _load_base_prompt(file_key)
|
|
91
|
+
|
|
92
|
+
if model_name == "gpt-5.1-codex-max":
|
|
93
|
+
return base_prompt
|
|
84
94
|
|
|
85
|
-
return base_prompt +
|
|
95
|
+
return base_prompt + _build_env_info(model_name)
|
klaude_code/core/reminders.py
CHANGED
|
@@ -150,16 +150,16 @@ async def todo_not_used_recently_reminder(
|
|
|
150
150
|
return None
|
|
151
151
|
|
|
152
152
|
# Count non-todo tool calls since the last TodoWrite
|
|
153
|
-
|
|
153
|
+
other_tool_call_count_before_last_todo = 0
|
|
154
154
|
for item in reversed(session.conversation_history):
|
|
155
155
|
if isinstance(item, model.ToolCallItem):
|
|
156
156
|
if item.name in (tools.TODO_WRITE, tools.UPDATE_PLAN):
|
|
157
157
|
break
|
|
158
|
-
|
|
159
|
-
if
|
|
158
|
+
other_tool_call_count_before_last_todo += 1
|
|
159
|
+
if other_tool_call_count_before_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD:
|
|
160
160
|
break
|
|
161
161
|
|
|
162
|
-
not_used_recently =
|
|
162
|
+
not_used_recently = other_tool_call_count_before_last_todo >= const.TODO_REMINDER_TOOL_CALL_THRESHOLD
|
|
163
163
|
|
|
164
164
|
if not not_used_recently:
|
|
165
165
|
return None
|
|
@@ -344,9 +344,6 @@ async def last_path_memory_reminder(
|
|
|
344
344
|
paths.append(path)
|
|
345
345
|
except json.JSONDecodeError:
|
|
346
346
|
continue
|
|
347
|
-
elif tool_call.name == tools.BASH:
|
|
348
|
-
# TODO: haiku check file path
|
|
349
|
-
pass
|
|
350
347
|
paths = list(set(paths))
|
|
351
348
|
memories: list[Memory] = []
|
|
352
349
|
if len(paths) == 0:
|
klaude_code/core/task.py
CHANGED
|
@@ -25,28 +25,30 @@ class MetadataAccumulator:
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
def __init__(self, model_name: str) -> None:
|
|
28
|
-
self.
|
|
28
|
+
self._main = model.TaskMetadata(model_name=model_name)
|
|
29
|
+
self._sub_agent_metadata: list[model.TaskMetadata] = []
|
|
29
30
|
self._throughput_weighted_sum: float = 0.0
|
|
30
31
|
self._throughput_tracked_tokens: int = 0
|
|
31
32
|
|
|
32
33
|
def add(self, turn_metadata: model.ResponseMetadataItem) -> None:
|
|
33
34
|
"""Merge a turn's metadata into the accumulated state."""
|
|
34
|
-
|
|
35
|
+
main = self._main
|
|
35
36
|
usage = turn_metadata.usage
|
|
36
37
|
|
|
37
38
|
if usage is not None:
|
|
38
|
-
if
|
|
39
|
-
|
|
40
|
-
acc_usage =
|
|
39
|
+
if main.usage is None:
|
|
40
|
+
main.usage = model.Usage()
|
|
41
|
+
acc_usage = main.usage
|
|
41
42
|
acc_usage.input_tokens += usage.input_tokens
|
|
42
43
|
acc_usage.cached_tokens += usage.cached_tokens
|
|
43
44
|
acc_usage.reasoning_tokens += usage.reasoning_tokens
|
|
44
45
|
acc_usage.output_tokens += usage.output_tokens
|
|
45
|
-
acc_usage.total_tokens += usage.total_tokens
|
|
46
46
|
acc_usage.currency = usage.currency
|
|
47
47
|
|
|
48
|
-
if usage.
|
|
49
|
-
acc_usage.
|
|
48
|
+
if usage.context_token is not None:
|
|
49
|
+
acc_usage.context_token = usage.context_token
|
|
50
|
+
if usage.context_limit is not None:
|
|
51
|
+
acc_usage.context_limit = usage.context_limit
|
|
50
52
|
|
|
51
53
|
if usage.first_token_latency_ms is not None:
|
|
52
54
|
if acc_usage.first_token_latency_ms is None:
|
|
@@ -63,47 +65,56 @@ class MetadataAccumulator:
|
|
|
63
65
|
self._throughput_weighted_sum += usage.throughput_tps * current_output
|
|
64
66
|
self._throughput_tracked_tokens += current_output
|
|
65
67
|
|
|
66
|
-
# Accumulate costs
|
|
67
68
|
if usage.input_cost is not None:
|
|
68
69
|
acc_usage.input_cost = (acc_usage.input_cost or 0.0) + usage.input_cost
|
|
69
70
|
if usage.output_cost is not None:
|
|
70
71
|
acc_usage.output_cost = (acc_usage.output_cost or 0.0) + usage.output_cost
|
|
71
72
|
if usage.cache_read_cost is not None:
|
|
72
73
|
acc_usage.cache_read_cost = (acc_usage.cache_read_cost or 0.0) + usage.cache_read_cost
|
|
73
|
-
if usage.total_cost is not None:
|
|
74
|
-
acc_usage.total_cost = (acc_usage.total_cost or 0.0) + usage.total_cost
|
|
75
74
|
|
|
76
75
|
if turn_metadata.provider is not None:
|
|
77
|
-
|
|
76
|
+
main.provider = turn_metadata.provider
|
|
78
77
|
if turn_metadata.model_name:
|
|
79
|
-
|
|
80
|
-
if turn_metadata.response_id:
|
|
81
|
-
accumulated.response_id = turn_metadata.response_id
|
|
78
|
+
main.model_name = turn_metadata.model_name
|
|
82
79
|
|
|
83
|
-
def
|
|
80
|
+
def add_sub_agent_metadata(self, sub_agent_metadata: model.TaskMetadata) -> None:
|
|
81
|
+
"""Add sub-agent task metadata to the accumulated state."""
|
|
82
|
+
self._sub_agent_metadata.append(sub_agent_metadata)
|
|
83
|
+
|
|
84
|
+
def finalize(self, task_duration_s: float) -> model.TaskMetadataItem:
|
|
84
85
|
"""Return the final accumulated metadata with computed throughput and duration."""
|
|
85
|
-
|
|
86
|
-
if
|
|
86
|
+
main = self._main
|
|
87
|
+
if main.usage is not None:
|
|
87
88
|
if self._throughput_tracked_tokens > 0:
|
|
88
|
-
|
|
89
|
+
main.usage.throughput_tps = self._throughput_weighted_sum / self._throughput_tracked_tokens
|
|
89
90
|
else:
|
|
90
|
-
|
|
91
|
+
main.usage.throughput_tps = None
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
return
|
|
93
|
+
main.task_duration_s = task_duration_s
|
|
94
|
+
return model.TaskMetadataItem(main=main, sub_agent_task_metadata=self._sub_agent_metadata)
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
@dataclass
|
|
97
|
-
class
|
|
98
|
-
"""
|
|
98
|
+
class SessionContext:
|
|
99
|
+
"""Shared session-level context for task and turn execution.
|
|
100
|
+
|
|
101
|
+
Contains common fields that both TaskExecutionContext and TurnExecutionContext need.
|
|
102
|
+
"""
|
|
99
103
|
|
|
100
104
|
session_id: str
|
|
101
|
-
profile: AgentProfile
|
|
102
105
|
get_conversation_history: Callable[[], list[model.ConversationItem]]
|
|
103
106
|
append_history: Callable[[Sequence[model.ConversationItem]], None]
|
|
104
|
-
tool_registry: dict[str, type[ToolABC]]
|
|
105
107
|
file_tracker: MutableMapping[str, float]
|
|
106
108
|
todo_context: TodoContext
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class TaskExecutionContext:
|
|
113
|
+
"""Execution context required to run a task."""
|
|
114
|
+
|
|
115
|
+
session_ctx: SessionContext
|
|
116
|
+
profile: AgentProfile
|
|
117
|
+
tool_registry: dict[str, type[ToolABC]]
|
|
107
118
|
# For reminder processing - needs access to session
|
|
108
119
|
process_reminder: Callable[[Reminder], AsyncGenerator[events.DeveloperMessageEvent, None]]
|
|
109
120
|
sub_agent_state: model.SubAgentState | None
|
|
@@ -135,18 +146,18 @@ class TaskExecutor:
|
|
|
135
146
|
async def run(self, user_input: model.UserInputPayload) -> AsyncGenerator[events.Event, None]:
|
|
136
147
|
"""Execute the task, yielding events as they occur."""
|
|
137
148
|
ctx = self._context
|
|
149
|
+
session_ctx = ctx.session_ctx
|
|
138
150
|
self._started_at = time.perf_counter()
|
|
139
151
|
|
|
140
152
|
yield events.TaskStartEvent(
|
|
141
|
-
session_id=
|
|
153
|
+
session_id=session_ctx.session_id,
|
|
142
154
|
sub_agent_state=ctx.sub_agent_state,
|
|
143
155
|
)
|
|
144
156
|
|
|
145
|
-
|
|
157
|
+
session_ctx.append_history([model.UserMessageItem(content=user_input.text, images=user_input.images)])
|
|
146
158
|
|
|
147
159
|
profile = ctx.profile
|
|
148
160
|
metadata_accumulator = MetadataAccumulator(model_name=profile.llm_client.model_name)
|
|
149
|
-
last_assistant_message: events.AssistantMessageEvent | None = None
|
|
150
161
|
|
|
151
162
|
while True:
|
|
152
163
|
# Process reminders at the start of each turn
|
|
@@ -155,15 +166,11 @@ class TaskExecutor:
|
|
|
155
166
|
yield event
|
|
156
167
|
|
|
157
168
|
turn_context = TurnExecutionContext(
|
|
158
|
-
|
|
159
|
-
get_conversation_history=ctx.get_conversation_history,
|
|
160
|
-
append_history=ctx.append_history,
|
|
169
|
+
session_ctx=session_ctx,
|
|
161
170
|
llm_client=profile.llm_client,
|
|
162
171
|
system_prompt=profile.system_prompt,
|
|
163
172
|
tools=profile.tools,
|
|
164
173
|
tool_registry=ctx.tool_registry,
|
|
165
|
-
file_tracker=ctx.file_tracker,
|
|
166
|
-
todo_context=ctx.todo_context,
|
|
167
174
|
)
|
|
168
175
|
|
|
169
176
|
turn: TurnExecutor | None = None
|
|
@@ -178,11 +185,14 @@ class TaskExecutor:
|
|
|
178
185
|
async for turn_event in turn.run():
|
|
179
186
|
match turn_event:
|
|
180
187
|
case events.AssistantMessageEvent() as am:
|
|
181
|
-
if am.content.strip() != "":
|
|
182
|
-
last_assistant_message = am
|
|
183
188
|
yield am
|
|
184
189
|
case events.ResponseMetadataEvent() as e:
|
|
185
190
|
metadata_accumulator.add(e.metadata)
|
|
191
|
+
case events.ToolResultEvent() as e:
|
|
192
|
+
# Collect sub-agent task metadata from tool results
|
|
193
|
+
if e.task_metadata is not None:
|
|
194
|
+
metadata_accumulator.add_sub_agent_metadata(e.task_metadata)
|
|
195
|
+
yield turn_event
|
|
186
196
|
case _:
|
|
187
197
|
yield turn_event
|
|
188
198
|
|
|
@@ -219,14 +229,23 @@ class TaskExecutor:
|
|
|
219
229
|
task_duration_s = time.perf_counter() - self._started_at
|
|
220
230
|
accumulated = metadata_accumulator.finalize(task_duration_s)
|
|
221
231
|
|
|
222
|
-
yield events.
|
|
223
|
-
|
|
232
|
+
yield events.TaskMetadataEvent(metadata=accumulated, session_id=session_ctx.session_id)
|
|
233
|
+
session_ctx.append_history([accumulated])
|
|
224
234
|
yield events.TaskFinishEvent(
|
|
225
|
-
session_id=
|
|
226
|
-
task_result=
|
|
235
|
+
session_id=session_ctx.session_id,
|
|
236
|
+
task_result=_get_last_assistant_message(session_ctx.get_conversation_history()) or "",
|
|
227
237
|
)
|
|
228
238
|
|
|
229
239
|
|
|
240
|
+
def _get_last_assistant_message(history: list[model.ConversationItem]) -> str | None:
|
|
241
|
+
"""Return the content of the most recent assistant message in history."""
|
|
242
|
+
|
|
243
|
+
for item in reversed(history):
|
|
244
|
+
if isinstance(item, model.AssistantMessageItem):
|
|
245
|
+
return item.content or ""
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
230
249
|
def _retry_delay_seconds(attempt: int) -> float:
|
|
231
250
|
"""Compute exponential backoff delay for the given attempt count."""
|
|
232
251
|
capped_attempt = max(1, attempt)
|
|
@@ -16,6 +16,7 @@ from .tool_abc import ToolABC
|
|
|
16
16
|
from .tool_context import (
|
|
17
17
|
TodoContext,
|
|
18
18
|
ToolContextToken,
|
|
19
|
+
build_todo_context,
|
|
19
20
|
current_run_subtask_callback,
|
|
20
21
|
reset_tool_context,
|
|
21
22
|
set_tool_context_from_session,
|
|
@@ -46,6 +47,7 @@ __all__ = [
|
|
|
46
47
|
"ToolABC",
|
|
47
48
|
# Tool context
|
|
48
49
|
"TodoContext",
|
|
50
|
+
"build_todo_context",
|
|
49
51
|
"ToolContextToken",
|
|
50
52
|
"current_run_subtask_callback",
|
|
51
53
|
"reset_tool_context",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Shared utility functions for file tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_directory(path: str) -> bool:
|
|
10
|
+
"""Check if path is a directory."""
|
|
11
|
+
return os.path.isdir(path)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def file_exists(path: str) -> bool:
|
|
15
|
+
"""Check if path exists."""
|
|
16
|
+
return os.path.exists(path)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_text(path: str) -> str:
|
|
20
|
+
"""Read text from file with UTF-8 encoding."""
|
|
21
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
22
|
+
return f.read()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_text(path: str, content: str) -> None:
|
|
26
|
+
"""Write text to file, creating parent directories if needed."""
|
|
27
|
+
parent = Path(path).parent
|
|
28
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
30
|
+
f.write(content)
|
|
@@ -26,7 +26,7 @@ class ApplyPatchHandler:
|
|
|
26
26
|
return model.ToolResultItem(
|
|
27
27
|
status="success",
|
|
28
28
|
output=output,
|
|
29
|
-
ui_extra=model.
|
|
29
|
+
ui_extra=model.DiffTextUIExtra(diff_text=diff_text),
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
@staticmethod
|
|
@@ -7,38 +7,13 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
|
+
from klaude_code.core.tool.file._utils import file_exists, is_directory, read_text, write_text
|
|
10
11
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
11
12
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
12
13
|
from klaude_code.core.tool.tool_registry import register
|
|
13
14
|
from klaude_code.protocol import llm_param, model, tools
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def _is_directory(path: str) -> bool:
|
|
17
|
-
try:
|
|
18
|
-
return Path(path).is_dir()
|
|
19
|
-
except Exception:
|
|
20
|
-
return False
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _file_exists(path: str) -> bool:
|
|
24
|
-
try:
|
|
25
|
-
return Path(path).exists()
|
|
26
|
-
except Exception:
|
|
27
|
-
return False
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _read_text(path: str) -> str:
|
|
31
|
-
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
32
|
-
return f.read()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _write_text(path: str, content: str) -> None:
|
|
36
|
-
parent = Path(path).parent
|
|
37
|
-
parent.mkdir(parents=True, exist_ok=True)
|
|
38
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
39
|
-
f.write(content)
|
|
40
|
-
|
|
41
|
-
|
|
42
17
|
@register(tools.EDIT)
|
|
43
18
|
class EditTool(ToolABC):
|
|
44
19
|
class EditArguments(BaseModel):
|
|
@@ -119,7 +94,7 @@ class EditTool(ToolABC):
|
|
|
119
94
|
file_path = os.path.abspath(args.file_path)
|
|
120
95
|
|
|
121
96
|
# Common file errors
|
|
122
|
-
if
|
|
97
|
+
if is_directory(file_path):
|
|
123
98
|
return model.ToolResultItem(
|
|
124
99
|
status="error",
|
|
125
100
|
output="<tool_use_error>Illegal operation on a directory. edit</tool_use_error>",
|
|
@@ -136,7 +111,7 @@ class EditTool(ToolABC):
|
|
|
136
111
|
|
|
137
112
|
# FileTracker checks (only for editing existing files)
|
|
138
113
|
file_tracker = get_current_file_tracker()
|
|
139
|
-
if not
|
|
114
|
+
if not file_exists(file_path):
|
|
140
115
|
# We require reading before editing
|
|
141
116
|
return model.ToolResultItem(
|
|
142
117
|
status="error",
|
|
@@ -163,7 +138,7 @@ class EditTool(ToolABC):
|
|
|
163
138
|
|
|
164
139
|
# Edit existing file: validate and apply
|
|
165
140
|
try:
|
|
166
|
-
before = await asyncio.to_thread(
|
|
141
|
+
before = await asyncio.to_thread(read_text, file_path)
|
|
167
142
|
except FileNotFoundError:
|
|
168
143
|
return model.ToolResultItem(
|
|
169
144
|
status="error",
|
|
@@ -197,7 +172,7 @@ class EditTool(ToolABC):
|
|
|
197
172
|
|
|
198
173
|
# Write back
|
|
199
174
|
try:
|
|
200
|
-
await asyncio.to_thread(
|
|
175
|
+
await asyncio.to_thread(write_text, file_path, after)
|
|
201
176
|
except Exception as e: # pragma: no cover
|
|
202
177
|
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
203
178
|
|
|
@@ -212,7 +187,7 @@ class EditTool(ToolABC):
|
|
|
212
187
|
)
|
|
213
188
|
)
|
|
214
189
|
diff_text = "\n".join(diff_lines)
|
|
215
|
-
ui_extra = model.
|
|
190
|
+
ui_extra = model.DiffTextUIExtra(diff_text=diff_text)
|
|
216
191
|
|
|
217
192
|
# Update tracker with new mtime
|
|
218
193
|
if file_tracker is not None:
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
|
|
10
|
+
from klaude_code.core.tool.file._utils import file_exists, is_directory, read_text, write_text
|
|
10
11
|
from klaude_code.core.tool.file.edit_tool import EditTool
|
|
11
12
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
12
13
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
@@ -14,32 +15,6 @@ from klaude_code.core.tool.tool_registry import register
|
|
|
14
15
|
from klaude_code.protocol import llm_param, model, tools
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
def _is_directory(path: str) -> bool:
|
|
18
|
-
try:
|
|
19
|
-
return Path(path).is_dir()
|
|
20
|
-
except Exception:
|
|
21
|
-
return False
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _file_exists(path: str) -> bool:
|
|
25
|
-
try:
|
|
26
|
-
return Path(path).exists()
|
|
27
|
-
except Exception:
|
|
28
|
-
return False
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _read_text(path: str) -> str:
|
|
32
|
-
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
33
|
-
return f.read()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _write_text(path: str, content: str) -> None:
|
|
37
|
-
parent = Path(path).parent
|
|
38
|
-
parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
40
|
-
f.write(content)
|
|
41
|
-
|
|
42
|
-
|
|
43
18
|
@register(tools.MULTI_EDIT)
|
|
44
19
|
class MultiEditTool(ToolABC):
|
|
45
20
|
class MultiEditEditItem(BaseModel):
|
|
@@ -105,7 +80,7 @@ class MultiEditTool(ToolABC):
|
|
|
105
80
|
file_path = os.path.abspath(args.file_path)
|
|
106
81
|
|
|
107
82
|
# Directory error first
|
|
108
|
-
if
|
|
83
|
+
if is_directory(file_path):
|
|
109
84
|
return model.ToolResultItem(
|
|
110
85
|
status="error",
|
|
111
86
|
output="<tool_use_error>Illegal operation on a directory. multi_edit</tool_use_error>",
|
|
@@ -114,7 +89,7 @@ class MultiEditTool(ToolABC):
|
|
|
114
89
|
file_tracker = get_current_file_tracker()
|
|
115
90
|
|
|
116
91
|
# FileTracker check:
|
|
117
|
-
if
|
|
92
|
+
if file_exists(file_path):
|
|
118
93
|
if file_tracker is not None:
|
|
119
94
|
tracked = file_tracker.get(file_path)
|
|
120
95
|
if tracked is None:
|
|
@@ -142,8 +117,8 @@ class MultiEditTool(ToolABC):
|
|
|
142
117
|
)
|
|
143
118
|
|
|
144
119
|
# Load initial content (empty for new file case)
|
|
145
|
-
if
|
|
146
|
-
before = await asyncio.to_thread(
|
|
120
|
+
if file_exists(file_path):
|
|
121
|
+
before = await asyncio.to_thread(read_text, file_path)
|
|
147
122
|
else:
|
|
148
123
|
before = ""
|
|
149
124
|
|
|
@@ -168,7 +143,7 @@ class MultiEditTool(ToolABC):
|
|
|
168
143
|
|
|
169
144
|
# All edits valid; write to disk
|
|
170
145
|
try:
|
|
171
|
-
await asyncio.to_thread(
|
|
146
|
+
await asyncio.to_thread(write_text, file_path, staged)
|
|
172
147
|
except Exception as e: # pragma: no cover
|
|
173
148
|
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
174
149
|
|
|
@@ -183,7 +158,7 @@ class MultiEditTool(ToolABC):
|
|
|
183
158
|
)
|
|
184
159
|
)
|
|
185
160
|
diff_text = "\n".join(diff_lines)
|
|
186
|
-
ui_extra = model.
|
|
161
|
+
ui_extra = model.DiffTextUIExtra(diff_text=diff_text)
|
|
187
162
|
|
|
188
163
|
# Update tracker
|
|
189
164
|
if file_tracker is not None:
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
11
|
from klaude_code import const
|
|
12
|
+
from klaude_code.core.tool.file._utils import file_exists, is_directory
|
|
12
13
|
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
13
14
|
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
14
15
|
from klaude_code.core.tool.tool_registry import register
|
|
@@ -34,20 +35,6 @@ def _format_numbered_line(line_no: int, content: str) -> str:
|
|
|
34
35
|
return f"{line_no:>6}→{content}"
|
|
35
36
|
|
|
36
37
|
|
|
37
|
-
def _is_directory(path: str) -> bool:
|
|
38
|
-
try:
|
|
39
|
-
return Path(path).is_dir()
|
|
40
|
-
except Exception:
|
|
41
|
-
return False
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _file_exists(path: str) -> bool:
|
|
45
|
-
try:
|
|
46
|
-
return Path(path).exists()
|
|
47
|
-
except Exception:
|
|
48
|
-
return False
|
|
49
|
-
|
|
50
|
-
|
|
51
38
|
@dataclass
|
|
52
39
|
class ReadOptions:
|
|
53
40
|
file_path: str
|
|
@@ -101,7 +88,7 @@ def _read_segment(options: ReadOptions) -> ReadSegmentResult:
|
|
|
101
88
|
|
|
102
89
|
def _track_file_access(file_path: str) -> None:
|
|
103
90
|
file_tracker = get_current_file_tracker()
|
|
104
|
-
if file_tracker is None or not
|
|
91
|
+
if file_tracker is None or not file_exists(file_path) or is_directory(file_path):
|
|
105
92
|
return
|
|
106
93
|
try:
|
|
107
94
|
file_tracker[file_path] = Path(file_path).stat().st_mtime
|
|
@@ -188,12 +175,12 @@ class ReadTool(ToolABC):
|
|
|
188
175
|
char_per_line, line_cap, max_chars, max_kb = cls._effective_limits()
|
|
189
176
|
|
|
190
177
|
# Common file errors
|
|
191
|
-
if
|
|
178
|
+
if is_directory(file_path):
|
|
192
179
|
return model.ToolResultItem(
|
|
193
180
|
status="error",
|
|
194
181
|
output="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
|
|
195
182
|
)
|
|
196
|
-
if not
|
|
183
|
+
if not file_exists(file_path):
|
|
197
184
|
return model.ToolResultItem(
|
|
198
185
|
status="error",
|
|
199
186
|
output="<tool_use_error>File does not exist.</tool_use_error>",
|
|
@@ -222,7 +209,8 @@ class ReadTool(ToolABC):
|
|
|
222
209
|
# If file is too large and no pagination provided (only check if limits are enabled)
|
|
223
210
|
try:
|
|
224
211
|
size_bytes = Path(file_path).stat().st_size
|
|
225
|
-
except
|
|
212
|
+
except OSError:
|
|
213
|
+
# Best-effort size detection; on stat errors fall back to treating size as unknown.
|
|
226
214
|
size_bytes = 0
|
|
227
215
|
|
|
228
216
|
is_image_file = _is_supported_image_file(file_path)
|