klaude-code 1.2.9__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/cli/main.py +12 -1
- klaude_code/cli/runtime.py +7 -11
- klaude_code/command/__init__.py +68 -23
- 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_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 +5 -2
- klaude_code/command/status_cmd.py +8 -4
- klaude_code/command/terminal_setup_cmd.py +7 -4
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/agent.py +55 -9
- 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 +1 -1
- klaude_code/core/reminders.py +0 -3
- klaude_code/core/task.py +2 -2
- klaude_code/core/tool/file/_utils.py +30 -0
- klaude_code/core/tool/file/edit_tool.py +5 -30
- klaude_code/core/tool/file/multi_edit_tool.py +6 -31
- klaude_code/core/tool/file/read_tool.py +6 -18
- klaude_code/core/tool/file/write_tool.py +5 -30
- klaude_code/core/tool/memory/__init__.py +5 -0
- klaude_code/core/tool/memory/skill_loader.py +2 -1
- klaude_code/core/tool/memory/skill_tool.py +13 -0
- klaude_code/llm/__init__.py +2 -12
- klaude_code/llm/anthropic/client.py +2 -1
- klaude_code/llm/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +3 -2
- klaude_code/llm/openrouter/client.py +3 -3
- klaude_code/llm/registry.py +33 -7
- klaude_code/llm/responses/client.py +2 -1
- klaude_code/llm/responses/input.py +1 -1
- klaude_code/llm/usage.py +17 -8
- klaude_code/protocol/model.py +12 -7
- klaude_code/protocol/op.py +1 -0
- klaude_code/session/export.py +5 -5
- klaude_code/session/session.py +15 -5
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/renderers/metadata.py +1 -1
- klaude_code/ui/terminal/control.py +2 -2
- klaude_code/version.py +3 -3
- {klaude_code-1.2.9.dist-info → klaude_code-1.2.10.dist-info}/METADATA +1 -1
- {klaude_code-1.2.9.dist-info → klaude_code-1.2.10.dist-info}/RECORD +55 -54
- {klaude_code-1.2.9.dist-info → klaude_code-1.2.10.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.9.dist-info → klaude_code-1.2.10.dist-info}/entry_points.txt +0 -0
klaude_code/core/agent.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from collections.abc import AsyncGenerator, Iterable
|
|
3
|
+
from collections.abc import AsyncGenerator, Callable, Iterable
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Protocol
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol
|
|
6
6
|
|
|
7
7
|
from klaude_code.core.prompt import get_system_prompt as load_system_prompt
|
|
8
8
|
from klaude_code.core.reminders import Reminder, load_agent_reminders
|
|
@@ -14,21 +14,38 @@ from klaude_code.protocol.model import UserInputPayload
|
|
|
14
14
|
from klaude_code.session import Session
|
|
15
15
|
from klaude_code.trace import DebugType, log_debug
|
|
16
16
|
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from klaude_code.core.manager.llm_clients import LLMClients
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
@dataclass(frozen=True)
|
|
19
22
|
class AgentProfile:
|
|
20
23
|
"""Encapsulates the active LLM client plus prompts/tools/reminders."""
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
llm_client_factory: Callable[[], LLMClientABC]
|
|
23
26
|
system_prompt: str | None
|
|
24
27
|
tools: list[llm_param.ToolSchema]
|
|
25
28
|
reminders: list[Reminder]
|
|
26
29
|
|
|
30
|
+
_llm_client: LLMClientABC | None = None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def llm_client(self) -> LLMClientABC:
|
|
34
|
+
if self._llm_client is None:
|
|
35
|
+
object.__setattr__(self, "_llm_client", self.llm_client_factory())
|
|
36
|
+
return self._llm_client # type: ignore[return-value]
|
|
37
|
+
|
|
27
38
|
|
|
28
39
|
class ModelProfileProvider(Protocol):
|
|
29
40
|
"""Strategy interface for constructing agent profiles."""
|
|
30
41
|
|
|
31
42
|
def build_profile(
|
|
43
|
+
self,
|
|
44
|
+
llm_clients: LLMClients,
|
|
45
|
+
sub_agent_type: tools.SubAgentType | None = None,
|
|
46
|
+
) -> AgentProfile: ...
|
|
47
|
+
|
|
48
|
+
def build_profile_eager(
|
|
32
49
|
self,
|
|
33
50
|
llm_client: LLMClientABC,
|
|
34
51
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
@@ -39,13 +56,26 @@ class DefaultModelProfileProvider(ModelProfileProvider):
|
|
|
39
56
|
"""Default provider backed by global prompts/tool/reminder registries."""
|
|
40
57
|
|
|
41
58
|
def build_profile(
|
|
59
|
+
self,
|
|
60
|
+
llm_clients: LLMClients,
|
|
61
|
+
sub_agent_type: tools.SubAgentType | None = None,
|
|
62
|
+
) -> AgentProfile:
|
|
63
|
+
model_name = llm_clients.main_model_name
|
|
64
|
+
return AgentProfile(
|
|
65
|
+
llm_client_factory=lambda: llm_clients.main,
|
|
66
|
+
system_prompt=load_system_prompt(model_name, sub_agent_type),
|
|
67
|
+
tools=load_agent_tools(model_name, sub_agent_type),
|
|
68
|
+
reminders=load_agent_reminders(model_name, sub_agent_type),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def build_profile_eager(
|
|
42
72
|
self,
|
|
43
73
|
llm_client: LLMClientABC,
|
|
44
74
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
45
75
|
) -> AgentProfile:
|
|
46
76
|
model_name = llm_client.model_name
|
|
47
77
|
return AgentProfile(
|
|
48
|
-
|
|
78
|
+
llm_client_factory=lambda: llm_client,
|
|
49
79
|
system_prompt=load_system_prompt(model_name, sub_agent_type),
|
|
50
80
|
tools=load_agent_tools(model_name, sub_agent_type),
|
|
51
81
|
reminders=load_agent_reminders(model_name, sub_agent_type),
|
|
@@ -56,13 +86,26 @@ class VanillaModelProfileProvider(ModelProfileProvider):
|
|
|
56
86
|
"""Provider that strips prompts, reminders, and tools for vanilla mode."""
|
|
57
87
|
|
|
58
88
|
def build_profile(
|
|
89
|
+
self,
|
|
90
|
+
llm_clients: LLMClients,
|
|
91
|
+
sub_agent_type: tools.SubAgentType | None = None,
|
|
92
|
+
) -> AgentProfile:
|
|
93
|
+
model_name = llm_clients.main_model_name
|
|
94
|
+
return AgentProfile(
|
|
95
|
+
llm_client_factory=lambda: llm_clients.main,
|
|
96
|
+
system_prompt=None,
|
|
97
|
+
tools=load_agent_tools(model_name, vanilla=True),
|
|
98
|
+
reminders=load_agent_reminders(model_name, vanilla=True),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def build_profile_eager(
|
|
59
102
|
self,
|
|
60
103
|
llm_client: LLMClientABC,
|
|
61
104
|
sub_agent_type: tools.SubAgentType | None = None,
|
|
62
105
|
) -> AgentProfile:
|
|
63
106
|
model_name = llm_client.model_name
|
|
64
107
|
return AgentProfile(
|
|
65
|
-
|
|
108
|
+
llm_client_factory=lambda: llm_client,
|
|
66
109
|
system_prompt=None,
|
|
67
110
|
tools=load_agent_tools(model_name, vanilla=True),
|
|
68
111
|
reminders=load_agent_reminders(model_name, vanilla=True),
|
|
@@ -74,12 +117,13 @@ class Agent:
|
|
|
74
117
|
self,
|
|
75
118
|
session: Session,
|
|
76
119
|
profile: AgentProfile,
|
|
120
|
+
model_name: str | None = None,
|
|
77
121
|
):
|
|
78
122
|
self.session: Session = session
|
|
79
123
|
self.profile: AgentProfile = profile
|
|
80
124
|
self._current_task: TaskExecutor | None = None
|
|
81
|
-
if not self.session.model_name:
|
|
82
|
-
self.session.model_name =
|
|
125
|
+
if not self.session.model_name and model_name:
|
|
126
|
+
self.session.model_name = model_name
|
|
83
127
|
|
|
84
128
|
def cancel(self) -> Iterable[events.Event]:
|
|
85
129
|
"""Handle agent cancellation and persist an interrupt marker and tool cancellations.
|
|
@@ -148,11 +192,13 @@ class Agent:
|
|
|
148
192
|
self.session.append_history([item])
|
|
149
193
|
yield events.DeveloperMessageEvent(session_id=self.session.id, item=item)
|
|
150
194
|
|
|
151
|
-
def set_model_profile(self, profile: AgentProfile) -> None:
|
|
195
|
+
def set_model_profile(self, profile: AgentProfile, model_name: str | None = None) -> None:
|
|
152
196
|
"""Apply a fully constructed profile to the agent."""
|
|
153
197
|
|
|
154
198
|
self.profile = profile
|
|
155
|
-
if
|
|
199
|
+
if model_name:
|
|
200
|
+
self.session.model_name = model_name
|
|
201
|
+
elif not self.session.model_name:
|
|
156
202
|
self.session.model_name = profile.llm_client.model_name
|
|
157
203
|
|
|
158
204
|
def get_llm_client(self) -> LLMClientABC:
|
klaude_code/core/executor.py
CHANGED
|
@@ -117,7 +117,7 @@ class ExecutorContext:
|
|
|
117
117
|
if operation.session_id is None:
|
|
118
118
|
raise ValueError("session_id cannot be None")
|
|
119
119
|
|
|
120
|
-
await self.agent_manager.ensure_agent(operation.session_id)
|
|
120
|
+
await self.agent_manager.ensure_agent(operation.session_id, is_new_session=operation.is_new_session)
|
|
121
121
|
|
|
122
122
|
async def handle_user_input(self, operation: op.UserInputOperation) -> None:
|
|
123
123
|
"""Handle a user input operation by running it through an agent."""
|
|
@@ -482,4 +482,4 @@ class Executor:
|
|
|
482
482
|
|
|
483
483
|
# Static type check: ExecutorContext must satisfy OperationHandler protocol.
|
|
484
484
|
# If this line causes a type error, ExecutorContext is missing required methods.
|
|
485
|
-
_: type[OperationHandler] = ExecutorContext
|
|
485
|
+
_: type[OperationHandler] = ExecutorContext
|
|
@@ -38,16 +38,15 @@ class AgentManager:
|
|
|
38
38
|
|
|
39
39
|
await self._event_queue.put(event)
|
|
40
40
|
|
|
41
|
-
async def ensure_agent(self, session_id: str) -> Agent:
|
|
41
|
+
async def ensure_agent(self, session_id: str, *, is_new_session: bool = False) -> Agent:
|
|
42
42
|
"""Return an existing agent for the session or create a new one."""
|
|
43
|
-
|
|
44
43
|
agent = self._active_agents.get(session_id)
|
|
45
44
|
if agent is not None:
|
|
46
45
|
return agent
|
|
47
46
|
|
|
48
|
-
session = Session.load(session_id)
|
|
49
|
-
profile = self._model_profile_provider.build_profile(self._llm_clients
|
|
50
|
-
agent = Agent(session=session, profile=profile)
|
|
47
|
+
session = Session.load(session_id, skip_if_missing=is_new_session)
|
|
48
|
+
profile = self._model_profile_provider.build_profile(self._llm_clients)
|
|
49
|
+
agent = Agent(session=session, profile=profile, model_name=self._llm_clients.main_model_name)
|
|
51
50
|
|
|
52
51
|
async for evt in agent.replay_history():
|
|
53
52
|
await self.emit_event(evt)
|
|
@@ -55,7 +54,7 @@ class AgentManager:
|
|
|
55
54
|
await self.emit_event(
|
|
56
55
|
events.WelcomeEvent(
|
|
57
56
|
work_dir=str(session.work_dir),
|
|
58
|
-
llm_config=self._llm_clients.
|
|
57
|
+
llm_config=self._llm_clients.get_llm_config(),
|
|
59
58
|
)
|
|
60
59
|
)
|
|
61
60
|
|
|
@@ -76,7 +75,7 @@ class AgentManager:
|
|
|
76
75
|
|
|
77
76
|
llm_config = config.get_model_config(model_name)
|
|
78
77
|
llm_client = create_llm_client(llm_config)
|
|
79
|
-
agent.set_model_profile(self._model_profile_provider.
|
|
78
|
+
agent.set_model_profile(self._model_profile_provider.build_profile_eager(llm_client), model_name=model_name)
|
|
80
79
|
|
|
81
80
|
developer_item = model.DeveloperMessageItem(
|
|
82
81
|
content=f"switched to model: {model_name}",
|
|
@@ -2,41 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from dataclasses import field as dataclass_field
|
|
5
|
+
from collections.abc import Callable
|
|
7
6
|
|
|
8
7
|
from klaude_code.llm.client import LLMClientABC
|
|
8
|
+
from klaude_code.protocol import llm_param
|
|
9
9
|
from klaude_code.protocol.tools import SubAgentType
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
"""
|
|
12
|
+
class LLMClients:
|
|
13
|
+
"""Container for LLM clients used by main agent and sub-agents."""
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
main_factory: Callable[[], LLMClientABC],
|
|
18
|
+
main_model_name: str,
|
|
19
|
+
main_llm_config: llm_param.LLMConfigParameter,
|
|
20
|
+
) -> None:
|
|
21
|
+
self._main_factory: Callable[[], LLMClientABC] | None = main_factory
|
|
22
|
+
self._main_client: LLMClientABC | None = None
|
|
23
|
+
self._main_model_name: str = main_model_name
|
|
24
|
+
self._main_llm_config: llm_param.LLMConfigParameter = main_llm_config
|
|
25
|
+
self._sub_clients: dict[SubAgentType, LLMClientABC] = {}
|
|
26
|
+
self._sub_factories: dict[SubAgentType, Callable[[], LLMClientABC]] = {}
|
|
18
27
|
|
|
19
|
-
|
|
28
|
+
@property
|
|
29
|
+
def main_model_name(self) -> str:
|
|
30
|
+
return self._main_model_name
|
|
20
31
|
|
|
32
|
+
def get_llm_config(self) -> llm_param.LLMConfigParameter:
|
|
33
|
+
return self._main_llm_config
|
|
21
34
|
|
|
22
|
-
@
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
@property
|
|
36
|
+
def main(self) -> LLMClientABC:
|
|
37
|
+
if self._main_client is None:
|
|
38
|
+
if self._main_factory is None:
|
|
39
|
+
raise RuntimeError("Main client factory not set")
|
|
40
|
+
self._main_client = self._main_factory()
|
|
41
|
+
self._main_factory = None
|
|
42
|
+
return self._main_client
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
def register_sub_client_factory(
|
|
45
|
+
self,
|
|
46
|
+
sub_agent_type: SubAgentType,
|
|
47
|
+
factory: Callable[[], LLMClientABC],
|
|
48
|
+
) -> None:
|
|
49
|
+
self._sub_factories[sub_agent_type] = factory
|
|
28
50
|
|
|
29
51
|
def get_client(self, sub_agent_type: SubAgentType | None = None) -> LLMClientABC:
|
|
30
|
-
"""Return client for a sub-agent type or the main client.
|
|
52
|
+
"""Return client for a sub-agent type or the main client."""
|
|
31
53
|
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
if sub_agent_type is None:
|
|
55
|
+
return self.main
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"""
|
|
57
|
+
existing = self._sub_clients.get(sub_agent_type)
|
|
58
|
+
if existing is not None:
|
|
59
|
+
return existing
|
|
39
60
|
|
|
40
|
-
|
|
61
|
+
factory = self._sub_factories.get(sub_agent_type)
|
|
62
|
+
if factory is None:
|
|
41
63
|
return self.main
|
|
42
|
-
|
|
64
|
+
|
|
65
|
+
client = factory()
|
|
66
|
+
self._sub_clients[sub_agent_type] = client
|
|
67
|
+
return client
|
|
@@ -32,18 +32,30 @@ def build_llm_clients(
|
|
|
32
32
|
debug_type=DebugType.LLM_CONFIG,
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
main_model_name = str(llm_config.model)
|
|
36
|
+
|
|
37
|
+
def _main_factory() -> LLMClientABC:
|
|
38
|
+
return create_llm_client(llm_config)
|
|
39
|
+
|
|
40
|
+
clients = LLMClients(
|
|
41
|
+
main_factory=_main_factory,
|
|
42
|
+
main_model_name=main_model_name,
|
|
43
|
+
main_llm_config=llm_config,
|
|
44
|
+
)
|
|
37
45
|
|
|
38
|
-
# Initialize sub-agent clients
|
|
39
46
|
for sub_agent_type in enabled_sub_agents or []:
|
|
40
47
|
model_name = config.subagent_models.get(sub_agent_type)
|
|
41
48
|
if not model_name:
|
|
42
49
|
continue
|
|
50
|
+
|
|
43
51
|
profile = get_sub_agent_profile(sub_agent_type)
|
|
44
|
-
if not profile.enabled_for_model(
|
|
52
|
+
if not profile.enabled_for_model(main_model_name):
|
|
45
53
|
continue
|
|
46
|
-
sub_llm_config = config.get_model_config(model_name)
|
|
47
|
-
sub_clients[sub_agent_type] = create_llm_client(sub_llm_config)
|
|
48
54
|
|
|
49
|
-
|
|
55
|
+
def _factory(model_name_for_factory: str = model_name) -> LLMClientABC:
|
|
56
|
+
sub_llm_config = config.get_model_config(model_name_for_factory)
|
|
57
|
+
return create_llm_client(sub_llm_config)
|
|
58
|
+
|
|
59
|
+
clients.register_sub_client_factory(sub_agent_type, _factory)
|
|
60
|
+
|
|
61
|
+
return clients
|
|
@@ -43,7 +43,7 @@ class SubAgentManager:
|
|
|
43
43
|
child_session = Session(work_dir=parent_session.work_dir)
|
|
44
44
|
child_session.sub_agent_state = state
|
|
45
45
|
|
|
46
|
-
child_profile = self._model_profile_provider.
|
|
46
|
+
child_profile = self._model_profile_provider.build_profile_eager(
|
|
47
47
|
self._llm_clients.get_client(state.sub_agent_type),
|
|
48
48
|
state.sub_agent_type,
|
|
49
49
|
)
|
klaude_code/core/reminders.py
CHANGED
|
@@ -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
|
@@ -45,8 +45,8 @@ class MetadataAccumulator:
|
|
|
45
45
|
acc_usage.output_tokens += usage.output_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
50
|
if usage.context_limit is not None:
|
|
51
51
|
acc_usage.context_limit = usage.context_limit
|
|
52
52
|
|
|
@@ -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)
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -7,38 +7,13 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from pydantic import BaseModel
|
|
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 _write_text(path: str, content: str) -> None:
|
|
31
|
-
parent = Path(path).parent
|
|
32
|
-
parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
34
|
-
f.write(content)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _read_text(path: str) -> str:
|
|
38
|
-
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
39
|
-
return f.read()
|
|
40
|
-
|
|
41
|
-
|
|
42
17
|
class WriteArguments(BaseModel):
|
|
43
18
|
file_path: str
|
|
44
19
|
content: str
|
|
@@ -78,14 +53,14 @@ class WriteTool(ToolABC):
|
|
|
78
53
|
|
|
79
54
|
file_path = os.path.abspath(args.file_path)
|
|
80
55
|
|
|
81
|
-
if
|
|
56
|
+
if is_directory(file_path):
|
|
82
57
|
return model.ToolResultItem(
|
|
83
58
|
status="error",
|
|
84
59
|
output="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
|
|
85
60
|
)
|
|
86
61
|
|
|
87
62
|
file_tracker = get_current_file_tracker()
|
|
88
|
-
exists =
|
|
63
|
+
exists = file_exists(file_path)
|
|
89
64
|
|
|
90
65
|
if exists:
|
|
91
66
|
tracked_mtime: float | None = None
|
|
@@ -113,12 +88,12 @@ class WriteTool(ToolABC):
|
|
|
113
88
|
before = ""
|
|
114
89
|
if exists:
|
|
115
90
|
try:
|
|
116
|
-
before = await asyncio.to_thread(
|
|
91
|
+
before = await asyncio.to_thread(read_text, file_path)
|
|
117
92
|
except Exception:
|
|
118
93
|
before = ""
|
|
119
94
|
|
|
120
95
|
try:
|
|
121
|
-
await asyncio.to_thread(
|
|
96
|
+
await asyncio.to_thread(write_text, file_path, args.content)
|
|
122
97
|
except Exception as e: # pragma: no cover
|
|
123
98
|
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
124
99
|
|