klaude-code 1.2.6__py3-none-any.whl → 1.8.0__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/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import difflib
|
|
5
|
-
import os
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel, Field
|
|
9
|
-
|
|
10
|
-
from klaude_code.core.tool.file.edit_tool import EditTool
|
|
11
|
-
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
12
|
-
from klaude_code.core.tool.tool_context import get_current_file_tracker
|
|
13
|
-
from klaude_code.core.tool.tool_registry import register
|
|
14
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
@register(tools.MULTI_EDIT)
|
|
44
|
-
class MultiEditTool(ToolABC):
|
|
45
|
-
class MultiEditEditItem(BaseModel):
|
|
46
|
-
old_string: str
|
|
47
|
-
new_string: str
|
|
48
|
-
replace_all: bool = Field(default=False)
|
|
49
|
-
|
|
50
|
-
class MultiEditArguments(BaseModel):
|
|
51
|
-
file_path: str
|
|
52
|
-
edits: list[MultiEditTool.MultiEditEditItem]
|
|
53
|
-
|
|
54
|
-
@classmethod
|
|
55
|
-
def schema(cls) -> llm_param.ToolSchema:
|
|
56
|
-
return llm_param.ToolSchema(
|
|
57
|
-
name=tools.MULTI_EDIT,
|
|
58
|
-
type="function",
|
|
59
|
-
description=load_desc(Path(__file__).parent / "multi_edit_tool.md"),
|
|
60
|
-
parameters={
|
|
61
|
-
"type": "object",
|
|
62
|
-
"properties": {
|
|
63
|
-
"file_path": {
|
|
64
|
-
"type": "string",
|
|
65
|
-
"description": "The absolute path to the file to modify",
|
|
66
|
-
},
|
|
67
|
-
"edits": {
|
|
68
|
-
"type": "array",
|
|
69
|
-
"items": {
|
|
70
|
-
"type": "object",
|
|
71
|
-
"properties": {
|
|
72
|
-
"old_string": {
|
|
73
|
-
"type": "string",
|
|
74
|
-
"description": "The text to replace",
|
|
75
|
-
},
|
|
76
|
-
"new_string": {
|
|
77
|
-
"type": "string",
|
|
78
|
-
"description": "The text to replace it with",
|
|
79
|
-
},
|
|
80
|
-
"replace_all": {
|
|
81
|
-
"type": "boolean",
|
|
82
|
-
"default": False,
|
|
83
|
-
"description": "Replace all occurences of old_string (default false).",
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
"required": ["old_string", "new_string"],
|
|
87
|
-
"additionalProperties": False,
|
|
88
|
-
},
|
|
89
|
-
"minItems": 1,
|
|
90
|
-
"description": "Array of edit operations to perform sequentially on the file",
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
"required": ["file_path", "edits"],
|
|
94
|
-
"additionalProperties": False,
|
|
95
|
-
},
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
@classmethod
|
|
99
|
-
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
100
|
-
try:
|
|
101
|
-
args = MultiEditTool.MultiEditArguments.model_validate_json(arguments)
|
|
102
|
-
except Exception as e: # pragma: no cover - defensive
|
|
103
|
-
return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
|
|
104
|
-
|
|
105
|
-
file_path = os.path.abspath(args.file_path)
|
|
106
|
-
|
|
107
|
-
# Directory error first
|
|
108
|
-
if _is_directory(file_path):
|
|
109
|
-
return model.ToolResultItem(
|
|
110
|
-
status="error",
|
|
111
|
-
output="<tool_use_error>Illegal operation on a directory. multi_edit</tool_use_error>",
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
file_tracker = get_current_file_tracker()
|
|
115
|
-
|
|
116
|
-
# FileTracker check:
|
|
117
|
-
if _file_exists(file_path):
|
|
118
|
-
if file_tracker is not None:
|
|
119
|
-
tracked = file_tracker.get(file_path)
|
|
120
|
-
if tracked is None:
|
|
121
|
-
return model.ToolResultItem(
|
|
122
|
-
status="error",
|
|
123
|
-
output=("File has not been read yet. Read it first before writing to it."),
|
|
124
|
-
)
|
|
125
|
-
try:
|
|
126
|
-
current_mtime = Path(file_path).stat().st_mtime
|
|
127
|
-
except Exception:
|
|
128
|
-
current_mtime = tracked
|
|
129
|
-
if current_mtime != tracked:
|
|
130
|
-
return model.ToolResultItem(
|
|
131
|
-
status="error",
|
|
132
|
-
output=(
|
|
133
|
-
"File has been modified externally. Either by user or a linter. Read it first before writing to it."
|
|
134
|
-
),
|
|
135
|
-
)
|
|
136
|
-
else:
|
|
137
|
-
# Allow creation only if first edit is creating content (old_string == "")
|
|
138
|
-
if not args.edits or args.edits[0].old_string != "":
|
|
139
|
-
return model.ToolResultItem(
|
|
140
|
-
status="error",
|
|
141
|
-
output=("File has not been read yet. Read it first before writing to it."),
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
# Load initial content (empty for new file case)
|
|
145
|
-
if _file_exists(file_path):
|
|
146
|
-
before = await asyncio.to_thread(_read_text, file_path)
|
|
147
|
-
else:
|
|
148
|
-
before = ""
|
|
149
|
-
|
|
150
|
-
# Validate all edits atomically against staged content
|
|
151
|
-
staged = before
|
|
152
|
-
for edit in args.edits:
|
|
153
|
-
err = EditTool.valid(
|
|
154
|
-
content=staged,
|
|
155
|
-
old_string=edit.old_string,
|
|
156
|
-
new_string=edit.new_string,
|
|
157
|
-
replace_all=edit.replace_all,
|
|
158
|
-
)
|
|
159
|
-
if err is not None:
|
|
160
|
-
return model.ToolResultItem(status="error", output=err)
|
|
161
|
-
# Apply to staged content
|
|
162
|
-
staged = EditTool.execute(
|
|
163
|
-
content=staged,
|
|
164
|
-
old_string=edit.old_string,
|
|
165
|
-
new_string=edit.new_string,
|
|
166
|
-
replace_all=edit.replace_all,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
# All edits valid; write to disk
|
|
170
|
-
try:
|
|
171
|
-
await asyncio.to_thread(_write_text, file_path, staged)
|
|
172
|
-
except Exception as e: # pragma: no cover
|
|
173
|
-
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
174
|
-
|
|
175
|
-
# Prepare UI extra: unified diff
|
|
176
|
-
diff_lines = list(
|
|
177
|
-
difflib.unified_diff(
|
|
178
|
-
before.splitlines(),
|
|
179
|
-
staged.splitlines(),
|
|
180
|
-
fromfile=file_path,
|
|
181
|
-
tofile=file_path,
|
|
182
|
-
n=3,
|
|
183
|
-
)
|
|
184
|
-
)
|
|
185
|
-
diff_text = "\n".join(diff_lines)
|
|
186
|
-
ui_extra = model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
|
|
187
|
-
|
|
188
|
-
# Update tracker
|
|
189
|
-
if file_tracker is not None:
|
|
190
|
-
try:
|
|
191
|
-
file_tracker[file_path] = Path(file_path).stat().st_mtime
|
|
192
|
-
except Exception:
|
|
193
|
-
pass
|
|
194
|
-
|
|
195
|
-
# Build output message
|
|
196
|
-
lines = [f"Applied {len(args.edits)} edits to {file_path}:"]
|
|
197
|
-
for i, edit in enumerate(args.edits, start=1):
|
|
198
|
-
lines.append(f'{i}. Replaced "{edit.old_string}" with "{edit.new_string}"')
|
|
199
|
-
return model.ToolResultItem(status="success", output="\n".join(lines), ui_extra=ui_extra)
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
Stores and retrieves information across conversations through a memory file directory. Use this tool to persist knowledge, progress, and context that should survive between sessions.
|
|
2
|
-
|
|
3
|
-
The memory directory is located at `.claude/memories/` in the current project root (git repository root if present, otherwise the current working directory). Memories are scoped to the current project/directory and are not shared globally. All paths must start with `/memories` (e.g., `/memories/notes.txt`).
|
|
4
|
-
|
|
5
|
-
Commands:
|
|
6
|
-
- `view`: Show directory contents or file contents with optional line range
|
|
7
|
-
- `create`: Create or overwrite a file
|
|
8
|
-
- `str_replace`: Replace text in a file
|
|
9
|
-
- `insert`: Insert text at a specific line
|
|
10
|
-
- `delete`: Delete a file or directory
|
|
11
|
-
- `rename`: Rename or move a file/directory
|
|
12
|
-
|
|
13
|
-
Usage tips:
|
|
14
|
-
- Check your memory directory before starting tasks to recall previous context
|
|
15
|
-
- Record important decisions, progress, and learnings as you work
|
|
16
|
-
- Keep memory files organized and up-to-date; delete obsolete files
|
|
@@ -1,462 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import difflib
|
|
5
|
-
import os
|
|
6
|
-
import shutil
|
|
7
|
-
import subprocess
|
|
8
|
-
import urllib.parse
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Literal
|
|
11
|
-
|
|
12
|
-
from pydantic import BaseModel, Field
|
|
13
|
-
|
|
14
|
-
from klaude_code.core.tool.tool_abc import ToolABC, load_desc
|
|
15
|
-
from klaude_code.core.tool.tool_registry import register
|
|
16
|
-
from klaude_code.protocol import llm_param, model, tools
|
|
17
|
-
|
|
18
|
-
MEMORY_VIRTUAL_ROOT = "/memories"
|
|
19
|
-
MEMORY_DIR_NAME = ".claude/memories"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _get_git_root() -> Path | None:
|
|
23
|
-
"""Get the git repository root directory."""
|
|
24
|
-
try:
|
|
25
|
-
result = subprocess.run(
|
|
26
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
27
|
-
capture_output=True,
|
|
28
|
-
text=True,
|
|
29
|
-
timeout=5,
|
|
30
|
-
)
|
|
31
|
-
if result.returncode == 0:
|
|
32
|
-
return Path(result.stdout.strip())
|
|
33
|
-
except Exception:
|
|
34
|
-
pass
|
|
35
|
-
return None
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _get_memories_root() -> Path:
|
|
39
|
-
"""Get the actual memories directory path."""
|
|
40
|
-
git_root = _get_git_root()
|
|
41
|
-
if git_root is not None:
|
|
42
|
-
return git_root / MEMORY_DIR_NAME
|
|
43
|
-
return Path.cwd() / MEMORY_DIR_NAME
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _ensure_memories_dir() -> Path:
|
|
47
|
-
"""Ensure the memories directory exists and return its path."""
|
|
48
|
-
memories_root = _get_memories_root()
|
|
49
|
-
memories_root.mkdir(parents=True, exist_ok=True)
|
|
50
|
-
return memories_root
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _validate_path(virtual_path: str) -> tuple[Path | None, str | None]:
|
|
54
|
-
"""
|
|
55
|
-
Validate a virtual path and return the actual filesystem path.
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
(actual_path, None) on success
|
|
59
|
-
(None, error_message) on failure
|
|
60
|
-
"""
|
|
61
|
-
# Check for URL-encoded traversal attempts
|
|
62
|
-
decoded = urllib.parse.unquote(virtual_path)
|
|
63
|
-
if ".." in decoded or ".." in virtual_path:
|
|
64
|
-
return None, "Path traversal is not allowed"
|
|
65
|
-
|
|
66
|
-
# Must start with /memories
|
|
67
|
-
if not virtual_path.startswith(MEMORY_VIRTUAL_ROOT):
|
|
68
|
-
return None, f"Path must start with {MEMORY_VIRTUAL_ROOT}"
|
|
69
|
-
|
|
70
|
-
# Get relative path from /memories
|
|
71
|
-
if virtual_path == MEMORY_VIRTUAL_ROOT:
|
|
72
|
-
relative = ""
|
|
73
|
-
else:
|
|
74
|
-
relative = virtual_path[len(MEMORY_VIRTUAL_ROOT) :].lstrip("/")
|
|
75
|
-
|
|
76
|
-
memories_root = _get_memories_root()
|
|
77
|
-
if relative:
|
|
78
|
-
actual_path = memories_root / relative
|
|
79
|
-
else:
|
|
80
|
-
actual_path = memories_root
|
|
81
|
-
|
|
82
|
-
# Resolve to canonical path and verify it's still within memories
|
|
83
|
-
try:
|
|
84
|
-
resolved = actual_path.resolve()
|
|
85
|
-
memories_resolved = memories_root.resolve()
|
|
86
|
-
# Check if resolved path is within or equal to memories root
|
|
87
|
-
try:
|
|
88
|
-
resolved.relative_to(memories_resolved)
|
|
89
|
-
except ValueError:
|
|
90
|
-
# Also allow the exact memories root
|
|
91
|
-
if resolved != memories_resolved:
|
|
92
|
-
return None, "Path traversal is not allowed"
|
|
93
|
-
except Exception as e:
|
|
94
|
-
return None, f"Invalid path: {e}"
|
|
95
|
-
|
|
96
|
-
return actual_path, None
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def _format_numbered_line(line_no: int, content: str) -> str:
|
|
100
|
-
return f"{line_no:>6}|{content}"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _make_diff_ui_extra(before: str, after: str, path: str) -> model.ToolResultUIExtra:
|
|
104
|
-
diff_lines = list(
|
|
105
|
-
difflib.unified_diff(
|
|
106
|
-
before.splitlines(),
|
|
107
|
-
after.splitlines(),
|
|
108
|
-
fromfile=path,
|
|
109
|
-
tofile=path,
|
|
110
|
-
n=3,
|
|
111
|
-
)
|
|
112
|
-
)
|
|
113
|
-
diff_text = "\n".join(diff_lines)
|
|
114
|
-
return model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@register(tools.MEMORY)
|
|
118
|
-
class MemoryTool(ToolABC):
|
|
119
|
-
class MemoryArguments(BaseModel):
|
|
120
|
-
command: Literal["view", "create", "str_replace", "insert", "delete", "rename"]
|
|
121
|
-
path: str | None = Field(default=None)
|
|
122
|
-
# view command
|
|
123
|
-
view_range: list[int] | None = Field(default=None)
|
|
124
|
-
# create command
|
|
125
|
-
file_text: str | None = Field(default=None)
|
|
126
|
-
# str_replace command
|
|
127
|
-
old_str: str | None = Field(default=None)
|
|
128
|
-
new_str: str | None = Field(default=None)
|
|
129
|
-
# insert command
|
|
130
|
-
insert_line: int | None = Field(default=None)
|
|
131
|
-
insert_text: str | None = Field(default=None)
|
|
132
|
-
# rename command
|
|
133
|
-
old_path: str | None = Field(default=None)
|
|
134
|
-
new_path: str | None = Field(default=None)
|
|
135
|
-
|
|
136
|
-
@classmethod
|
|
137
|
-
def schema(cls) -> llm_param.ToolSchema:
|
|
138
|
-
return llm_param.ToolSchema(
|
|
139
|
-
name=tools.MEMORY,
|
|
140
|
-
type="function",
|
|
141
|
-
description=load_desc(Path(__file__).parent / "memory_tool.md"),
|
|
142
|
-
parameters={
|
|
143
|
-
"type": "object",
|
|
144
|
-
"properties": {
|
|
145
|
-
"command": {
|
|
146
|
-
"type": "string",
|
|
147
|
-
"enum": [
|
|
148
|
-
"view",
|
|
149
|
-
"create",
|
|
150
|
-
"str_replace",
|
|
151
|
-
"insert",
|
|
152
|
-
"delete",
|
|
153
|
-
"rename",
|
|
154
|
-
],
|
|
155
|
-
"description": "The memory operation to perform",
|
|
156
|
-
},
|
|
157
|
-
"path": {
|
|
158
|
-
"type": "string",
|
|
159
|
-
"description": "Path starting with /memories (for view, create, str_replace, insert, delete)",
|
|
160
|
-
},
|
|
161
|
-
"view_range": {
|
|
162
|
-
"type": "array",
|
|
163
|
-
"items": {"type": "integer"},
|
|
164
|
-
"description": "Optional [start, end] line range for view command (1-indexed)",
|
|
165
|
-
},
|
|
166
|
-
"file_text": {
|
|
167
|
-
"type": "string",
|
|
168
|
-
"description": "Content to write (for create command)",
|
|
169
|
-
},
|
|
170
|
-
"old_str": {
|
|
171
|
-
"type": "string",
|
|
172
|
-
"description": "Text to find (for str_replace command)",
|
|
173
|
-
},
|
|
174
|
-
"new_str": {
|
|
175
|
-
"type": "string",
|
|
176
|
-
"description": "Text to replace with (for str_replace command)",
|
|
177
|
-
},
|
|
178
|
-
"insert_line": {
|
|
179
|
-
"type": "integer",
|
|
180
|
-
"description": "Line number to insert at (for insert command, 1-indexed)",
|
|
181
|
-
},
|
|
182
|
-
"insert_text": {
|
|
183
|
-
"type": "string",
|
|
184
|
-
"description": "Text to insert (for insert command)",
|
|
185
|
-
},
|
|
186
|
-
"old_path": {
|
|
187
|
-
"type": "string",
|
|
188
|
-
"description": "Source path (for rename command)",
|
|
189
|
-
},
|
|
190
|
-
"new_path": {
|
|
191
|
-
"type": "string",
|
|
192
|
-
"description": "Destination path (for rename command)",
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
"required": ["command"],
|
|
196
|
-
"additionalProperties": False,
|
|
197
|
-
},
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
@classmethod
|
|
201
|
-
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
202
|
-
try:
|
|
203
|
-
args = cls.MemoryArguments.model_validate_json(arguments)
|
|
204
|
-
except Exception as e:
|
|
205
|
-
return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
|
|
206
|
-
|
|
207
|
-
command = args.command
|
|
208
|
-
if command == "view":
|
|
209
|
-
return await cls._view(args)
|
|
210
|
-
elif command == "create":
|
|
211
|
-
return await cls._create(args)
|
|
212
|
-
elif command == "str_replace":
|
|
213
|
-
return await cls._str_replace(args)
|
|
214
|
-
elif command == "insert":
|
|
215
|
-
return await cls._insert(args)
|
|
216
|
-
elif command == "delete":
|
|
217
|
-
return await cls._delete(args)
|
|
218
|
-
elif command == "rename":
|
|
219
|
-
return await cls._rename(args)
|
|
220
|
-
else:
|
|
221
|
-
return model.ToolResultItem(status="error", output=f"Unknown command: {command}")
|
|
222
|
-
|
|
223
|
-
@classmethod
|
|
224
|
-
async def _view(cls, args: MemoryArguments) -> model.ToolResultItem:
|
|
225
|
-
if args.path is None:
|
|
226
|
-
return model.ToolResultItem(status="error", output="path is required for view command")
|
|
227
|
-
|
|
228
|
-
actual_path, error = _validate_path(args.path)
|
|
229
|
-
if error:
|
|
230
|
-
return model.ToolResultItem(status="error", output=error)
|
|
231
|
-
assert actual_path is not None
|
|
232
|
-
|
|
233
|
-
# Ensure memories directory exists
|
|
234
|
-
_ensure_memories_dir()
|
|
235
|
-
|
|
236
|
-
if not actual_path.exists():
|
|
237
|
-
return model.ToolResultItem(status="error", output=f"Path does not exist: {args.path}")
|
|
238
|
-
|
|
239
|
-
if actual_path.is_dir():
|
|
240
|
-
# List directory contents
|
|
241
|
-
try:
|
|
242
|
-
entries = sorted(
|
|
243
|
-
actual_path.iterdir(),
|
|
244
|
-
key=lambda p: (not p.is_dir(), p.name.lower()),
|
|
245
|
-
)
|
|
246
|
-
lines = [f"Directory: {args.path}"]
|
|
247
|
-
for entry in entries:
|
|
248
|
-
prefix = "/" if entry.is_dir() else ""
|
|
249
|
-
lines.append(f"- {entry.name}{prefix}")
|
|
250
|
-
if len(entries) == 0:
|
|
251
|
-
lines.append("(empty directory)")
|
|
252
|
-
return model.ToolResultItem(status="success", output="\n".join(lines))
|
|
253
|
-
except Exception as e:
|
|
254
|
-
return model.ToolResultItem(status="error", output=f"Failed to list directory: {e}")
|
|
255
|
-
else:
|
|
256
|
-
# Read file contents
|
|
257
|
-
try:
|
|
258
|
-
content = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
|
|
259
|
-
lines = content.splitlines()
|
|
260
|
-
total_lines = len(lines)
|
|
261
|
-
|
|
262
|
-
# Apply view_range if specified
|
|
263
|
-
start = 1
|
|
264
|
-
end = total_lines
|
|
265
|
-
if args.view_range and len(args.view_range) >= 2:
|
|
266
|
-
start = max(1, args.view_range[0])
|
|
267
|
-
end = min(total_lines, args.view_range[1])
|
|
268
|
-
|
|
269
|
-
if start > total_lines:
|
|
270
|
-
return model.ToolResultItem(
|
|
271
|
-
status="success",
|
|
272
|
-
output=f"File has {total_lines} lines, requested start line {start} is beyond end of file",
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
selected = lines[start - 1 : end]
|
|
276
|
-
numbered = [_format_numbered_line(start + i, line) for i, line in enumerate(selected)]
|
|
277
|
-
output = "\n".join(numbered)
|
|
278
|
-
if not output:
|
|
279
|
-
output = "(empty file)"
|
|
280
|
-
return model.ToolResultItem(status="success", output=output)
|
|
281
|
-
except Exception as e:
|
|
282
|
-
return model.ToolResultItem(status="error", output=f"Failed to read file: {e}")
|
|
283
|
-
|
|
284
|
-
@classmethod
|
|
285
|
-
async def _create(cls, args: MemoryArguments) -> model.ToolResultItem:
|
|
286
|
-
if args.path is None:
|
|
287
|
-
return model.ToolResultItem(status="error", output="path is required for create command")
|
|
288
|
-
if args.file_text is None:
|
|
289
|
-
return model.ToolResultItem(status="error", output="file_text is required for create command")
|
|
290
|
-
|
|
291
|
-
actual_path, error = _validate_path(args.path)
|
|
292
|
-
if error:
|
|
293
|
-
return model.ToolResultItem(status="error", output=error)
|
|
294
|
-
assert actual_path is not None
|
|
295
|
-
|
|
296
|
-
# Cannot create the root directory itself
|
|
297
|
-
if args.path == MEMORY_VIRTUAL_ROOT or args.path == MEMORY_VIRTUAL_ROOT + "/":
|
|
298
|
-
return model.ToolResultItem(
|
|
299
|
-
status="error",
|
|
300
|
-
output="Cannot create the memories root directory as a file",
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
try:
|
|
304
|
-
# Read existing content for diff (if file exists)
|
|
305
|
-
before = ""
|
|
306
|
-
if actual_path.exists():
|
|
307
|
-
before = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
|
|
308
|
-
|
|
309
|
-
# Ensure parent directories exist
|
|
310
|
-
actual_path.parent.mkdir(parents=True, exist_ok=True)
|
|
311
|
-
await asyncio.to_thread(actual_path.write_text, args.file_text, encoding="utf-8")
|
|
312
|
-
|
|
313
|
-
ui_extra = _make_diff_ui_extra(before, args.file_text, args.path)
|
|
314
|
-
return model.ToolResultItem(status="success", output=f"File created: {args.path}", ui_extra=ui_extra)
|
|
315
|
-
except Exception as e:
|
|
316
|
-
return model.ToolResultItem(status="error", output=f"Failed to create file: {e}")
|
|
317
|
-
|
|
318
|
-
@classmethod
|
|
319
|
-
async def _str_replace(cls, args: MemoryArguments) -> model.ToolResultItem:
|
|
320
|
-
if args.path is None:
|
|
321
|
-
return model.ToolResultItem(status="error", output="path is required for str_replace command")
|
|
322
|
-
if args.old_str is None:
|
|
323
|
-
return model.ToolResultItem(status="error", output="old_str is required for str_replace command")
|
|
324
|
-
if args.new_str is None:
|
|
325
|
-
return model.ToolResultItem(status="error", output="new_str is required for str_replace command")
|
|
326
|
-
|
|
327
|
-
actual_path, error = _validate_path(args.path)
|
|
328
|
-
if error:
|
|
329
|
-
return model.ToolResultItem(status="error", output=error)
|
|
330
|
-
assert actual_path is not None
|
|
331
|
-
|
|
332
|
-
if not actual_path.exists():
|
|
333
|
-
return model.ToolResultItem(status="error", output=f"File does not exist: {args.path}")
|
|
334
|
-
if actual_path.is_dir():
|
|
335
|
-
return model.ToolResultItem(status="error", output="Cannot perform str_replace on a directory")
|
|
336
|
-
|
|
337
|
-
try:
|
|
338
|
-
before = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
|
|
339
|
-
if args.old_str not in before:
|
|
340
|
-
return model.ToolResultItem(status="error", output=f"String not found in file: {args.old_str}")
|
|
341
|
-
|
|
342
|
-
after = before.replace(args.old_str, args.new_str, 1)
|
|
343
|
-
await asyncio.to_thread(actual_path.write_text, after, encoding="utf-8")
|
|
344
|
-
|
|
345
|
-
ui_extra = _make_diff_ui_extra(before, after, args.path)
|
|
346
|
-
return model.ToolResultItem(
|
|
347
|
-
status="success",
|
|
348
|
-
output=f"Replaced text in {args.path}",
|
|
349
|
-
ui_extra=ui_extra,
|
|
350
|
-
)
|
|
351
|
-
except Exception as e:
|
|
352
|
-
return model.ToolResultItem(status="error", output=f"Failed to replace text: {e}")
|
|
353
|
-
|
|
354
|
-
@classmethod
|
|
355
|
-
async def _insert(cls, args: MemoryArguments) -> model.ToolResultItem:
|
|
356
|
-
if args.path is None:
|
|
357
|
-
return model.ToolResultItem(status="error", output="path is required for insert command")
|
|
358
|
-
if args.insert_line is None:
|
|
359
|
-
return model.ToolResultItem(status="error", output="insert_line is required for insert command")
|
|
360
|
-
if args.insert_text is None:
|
|
361
|
-
return model.ToolResultItem(status="error", output="insert_text is required for insert command")
|
|
362
|
-
|
|
363
|
-
actual_path, error = _validate_path(args.path)
|
|
364
|
-
if error:
|
|
365
|
-
return model.ToolResultItem(status="error", output=error)
|
|
366
|
-
assert actual_path is not None
|
|
367
|
-
|
|
368
|
-
if not actual_path.exists():
|
|
369
|
-
return model.ToolResultItem(status="error", output=f"File does not exist: {args.path}")
|
|
370
|
-
if actual_path.is_dir():
|
|
371
|
-
return model.ToolResultItem(status="error", output="Cannot insert into a directory")
|
|
372
|
-
|
|
373
|
-
try:
|
|
374
|
-
before = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
|
|
375
|
-
lines = before.splitlines(keepends=True)
|
|
376
|
-
|
|
377
|
-
# Handle empty file
|
|
378
|
-
if not lines:
|
|
379
|
-
lines = []
|
|
380
|
-
|
|
381
|
-
# Normalize insert_line (1-indexed)
|
|
382
|
-
insert_idx = max(0, args.insert_line - 1)
|
|
383
|
-
insert_idx = min(insert_idx, len(lines))
|
|
384
|
-
|
|
385
|
-
# Ensure insert_text ends with newline if inserting in middle
|
|
386
|
-
insert_text = args.insert_text
|
|
387
|
-
if insert_idx < len(lines) and not insert_text.endswith("\n"):
|
|
388
|
-
insert_text += "\n"
|
|
389
|
-
|
|
390
|
-
lines.insert(insert_idx, insert_text)
|
|
391
|
-
after = "".join(lines)
|
|
392
|
-
await asyncio.to_thread(actual_path.write_text, after, encoding="utf-8")
|
|
393
|
-
|
|
394
|
-
ui_extra = _make_diff_ui_extra(before, after, args.path)
|
|
395
|
-
return model.ToolResultItem(
|
|
396
|
-
status="success",
|
|
397
|
-
output=f"Inserted text at line {args.insert_line} in {args.path}",
|
|
398
|
-
ui_extra=ui_extra,
|
|
399
|
-
)
|
|
400
|
-
except Exception as e:
|
|
401
|
-
return model.ToolResultItem(status="error", output=f"Failed to insert text: {e}")
|
|
402
|
-
|
|
403
|
-
@classmethod
|
|
404
|
-
async def _delete(cls, args: MemoryArguments) -> model.ToolResultItem:
|
|
405
|
-
if args.path is None:
|
|
406
|
-
return model.ToolResultItem(status="error", output="path is required for delete command")
|
|
407
|
-
|
|
408
|
-
# Prevent deleting the root memories directory
|
|
409
|
-
if args.path == MEMORY_VIRTUAL_ROOT or args.path == MEMORY_VIRTUAL_ROOT + "/":
|
|
410
|
-
return model.ToolResultItem(status="error", output="Cannot delete the memories root directory")
|
|
411
|
-
|
|
412
|
-
actual_path, error = _validate_path(args.path)
|
|
413
|
-
if error:
|
|
414
|
-
return model.ToolResultItem(status="error", output=error)
|
|
415
|
-
assert actual_path is not None
|
|
416
|
-
|
|
417
|
-
if not actual_path.exists():
|
|
418
|
-
return model.ToolResultItem(status="error", output=f"Path does not exist: {args.path}")
|
|
419
|
-
|
|
420
|
-
try:
|
|
421
|
-
if actual_path.is_dir():
|
|
422
|
-
await asyncio.to_thread(shutil.rmtree, actual_path)
|
|
423
|
-
return model.ToolResultItem(status="success", output=f"Directory deleted: {args.path}")
|
|
424
|
-
else:
|
|
425
|
-
await asyncio.to_thread(os.remove, actual_path)
|
|
426
|
-
return model.ToolResultItem(status="success", output=f"File deleted: {args.path}")
|
|
427
|
-
except Exception as e:
|
|
428
|
-
return model.ToolResultItem(status="error", output=f"Failed to delete: {e}")
|
|
429
|
-
|
|
430
|
-
@classmethod
|
|
431
|
-
async def _rename(cls, args: MemoryArguments) -> model.ToolResultItem:
|
|
432
|
-
if args.old_path is None:
|
|
433
|
-
return model.ToolResultItem(status="error", output="old_path is required for rename command")
|
|
434
|
-
if args.new_path is None:
|
|
435
|
-
return model.ToolResultItem(status="error", output="new_path is required for rename command")
|
|
436
|
-
|
|
437
|
-
# Prevent renaming the root memories directory
|
|
438
|
-
if args.old_path == MEMORY_VIRTUAL_ROOT or args.old_path == MEMORY_VIRTUAL_ROOT + "/":
|
|
439
|
-
return model.ToolResultItem(status="error", output="Cannot rename the memories root directory")
|
|
440
|
-
|
|
441
|
-
old_actual, error = _validate_path(args.old_path)
|
|
442
|
-
if error:
|
|
443
|
-
return model.ToolResultItem(status="error", output=f"Invalid old_path: {error}")
|
|
444
|
-
assert old_actual is not None
|
|
445
|
-
|
|
446
|
-
new_actual, error = _validate_path(args.new_path)
|
|
447
|
-
if error:
|
|
448
|
-
return model.ToolResultItem(status="error", output=f"Invalid new_path: {error}")
|
|
449
|
-
assert new_actual is not None
|
|
450
|
-
|
|
451
|
-
if not old_actual.exists():
|
|
452
|
-
return model.ToolResultItem(status="error", output=f"Source path does not exist: {args.old_path}")
|
|
453
|
-
if new_actual.exists():
|
|
454
|
-
return model.ToolResultItem(status="error", output=f"Destination already exists: {args.new_path}")
|
|
455
|
-
|
|
456
|
-
try:
|
|
457
|
-
# Ensure parent directory of destination exists
|
|
458
|
-
new_actual.parent.mkdir(parents=True, exist_ok=True)
|
|
459
|
-
await asyncio.to_thread(shutil.move, str(old_actual), str(new_actual))
|
|
460
|
-
return model.ToolResultItem(status="success", output=f"Renamed {args.old_path} to {args.new_path}")
|
|
461
|
-
except Exception as e:
|
|
462
|
-
return model.ToolResultItem(status="error", output=f"Failed to rename: {e}")
|