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.
Files changed (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {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}")