klaude-code 1.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,199 @@
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)
@@ -0,0 +1,14 @@
1
+ Reads a file from the local filesystem. You can access any file directly by using this tool.
2
+ Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
3
+
4
+ Usage:
5
+ - The file_path parameter must be an absolute path, not a relative path
6
+ - By default, it reads up to 2000 lines starting from the beginning of the file
7
+ - This tool allows you to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as you are a multimodal LLM.
8
+ - You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
9
+ - Any lines longer than 2000 characters will be truncated
10
+ - Results are returned using cat -n format, with line numbers starting at 1
11
+ - This tool can only read files, not directories. To read a directory, use an ls command via the Bash tool.
12
+ - You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
13
+ - If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
14
+ - This tool does NOT support reading PDF files. Use a Python script with `pdfplumber` (for text/tables) or `pypdf` (for basic operations) to extract content from PDFs.
@@ -0,0 +1,326 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ from base64 import b64encode
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from klaude_code import const
12
+ from klaude_code.core.tool.tool_abc import ToolABC, load_desc
13
+ from klaude_code.core.tool.tool_context import get_current_file_tracker
14
+ from klaude_code.core.tool.tool_registry import register
15
+ from klaude_code.protocol import llm_param, model, tools
16
+
17
+ SYSTEM_REMINDER_MALICIOUS = (
18
+ "<system-reminder>\n"
19
+ "Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.\n"
20
+ "</system-reminder>"
21
+ )
22
+
23
+ _IMAGE_MIME_TYPES: dict[str, str] = {
24
+ ".png": "image/png",
25
+ ".jpg": "image/jpeg",
26
+ ".jpeg": "image/jpeg",
27
+ ".gif": "image/gif",
28
+ ".webp": "image/webp",
29
+ }
30
+
31
+
32
+ def _format_numbered_line(line_no: int, content: str) -> str:
33
+ # 6-width right-aligned line number followed by a right arrow
34
+ return f"{line_no:>6}→{content}"
35
+
36
+
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
+ @dataclass
52
+ class ReadOptions:
53
+ file_path: str
54
+ offset: int
55
+ limit: int | None
56
+ char_limit_per_line: int | None = const.READ_CHAR_LIMIT_PER_LINE
57
+ global_line_cap: int | None = const.READ_GLOBAL_LINE_CAP
58
+
59
+
60
+ @dataclass
61
+ class ReadSegmentResult:
62
+ total_lines: int
63
+ selected_lines: list[tuple[int, str]]
64
+ selected_chars_count: int
65
+ remaining_selected_beyond_cap: int
66
+
67
+
68
+ def _read_segment(options: ReadOptions) -> ReadSegmentResult:
69
+ total_lines = 0
70
+ selected_lines_count = 0
71
+ remaining_selected_beyond_cap = 0
72
+ selected_lines: list[tuple[int, str]] = []
73
+ selected_chars = 0
74
+ with open(options.file_path, "r", encoding="utf-8", errors="replace") as f:
75
+ for line_no, raw_line in enumerate(f, start=1):
76
+ total_lines = line_no
77
+ within = line_no >= options.offset and (options.limit is None or selected_lines_count < options.limit)
78
+ if not within:
79
+ continue
80
+ selected_lines_count += 1
81
+ content = raw_line.rstrip("\n")
82
+ original_len = len(content)
83
+ if options.char_limit_per_line is not None and original_len > options.char_limit_per_line:
84
+ truncated_chars = original_len - options.char_limit_per_line
85
+ content = (
86
+ content[: options.char_limit_per_line]
87
+ + f" ... (more {truncated_chars} characters in this line are truncated)"
88
+ )
89
+ selected_chars += len(content) + 1
90
+ if options.global_line_cap is None or len(selected_lines) < options.global_line_cap:
91
+ selected_lines.append((line_no, content))
92
+ else:
93
+ remaining_selected_beyond_cap += 1
94
+ return ReadSegmentResult(
95
+ total_lines=total_lines,
96
+ selected_lines=selected_lines,
97
+ selected_chars_count=selected_chars,
98
+ remaining_selected_beyond_cap=remaining_selected_beyond_cap,
99
+ )
100
+
101
+
102
+ def _track_file_access(file_path: str) -> None:
103
+ file_tracker = get_current_file_tracker()
104
+ if file_tracker is None or not _file_exists(file_path) or _is_directory(file_path):
105
+ return
106
+ try:
107
+ file_tracker[file_path] = Path(file_path).stat().st_mtime
108
+ except Exception:
109
+ pass
110
+
111
+
112
+ def _is_supported_image_file(file_path: str) -> bool:
113
+ return Path(file_path).suffix.lower() in _IMAGE_MIME_TYPES
114
+
115
+
116
+ def _image_mime_type(file_path: str) -> str:
117
+ suffix = Path(file_path).suffix.lower()
118
+ mime_type = _IMAGE_MIME_TYPES.get(suffix)
119
+ if mime_type is None:
120
+ raise ValueError(f"Unsupported image file extension: {suffix}")
121
+ return mime_type
122
+
123
+
124
+ def _encode_image_to_data_url(file_path: str, mime_type: str) -> str:
125
+ with open(file_path, "rb") as image_file:
126
+ encoded = b64encode(image_file.read()).decode("ascii")
127
+ return f"data:{mime_type};base64,{encoded}"
128
+
129
+
130
+ @register(tools.READ)
131
+ class ReadTool(ToolABC):
132
+ class ReadArguments(BaseModel):
133
+ file_path: str
134
+ offset: int | None = Field(default=None)
135
+ limit: int | None = Field(default=None)
136
+
137
+ @classmethod
138
+ def schema(cls) -> llm_param.ToolSchema:
139
+ return llm_param.ToolSchema(
140
+ name=tools.READ,
141
+ type="function",
142
+ description=load_desc(Path(__file__).parent / "read_tool.md"),
143
+ parameters={
144
+ "type": "object",
145
+ "properties": {
146
+ "file_path": {
147
+ "type": "string",
148
+ "description": "The absolute path to the file to read",
149
+ },
150
+ "offset": {
151
+ "type": "number",
152
+ "description": "The line number to start reading from. Only provide if the file is too large to read at once",
153
+ },
154
+ "limit": {
155
+ "type": "number",
156
+ "description": "The number of lines to read. Only provide if the file is too large to read at once.",
157
+ },
158
+ },
159
+ "required": ["file_path"],
160
+ "additionalProperties": False,
161
+ },
162
+ )
163
+
164
+ @classmethod
165
+ async def call(cls, arguments: str) -> model.ToolResultItem:
166
+ try:
167
+ args = ReadTool.ReadArguments.model_validate_json(arguments)
168
+ except Exception as e: # pragma: no cover - defensive
169
+ return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
170
+ return await cls.call_with_args(args)
171
+
172
+ @classmethod
173
+ def _effective_limits(cls) -> tuple[int | None, int | None, int | None, int | None]:
174
+ """Return effective limits based on current policy: char_per_line, global_line_cap, max_chars, max_kb"""
175
+ return (
176
+ const.READ_CHAR_LIMIT_PER_LINE,
177
+ const.READ_GLOBAL_LINE_CAP,
178
+ const.READ_MAX_CHARS,
179
+ const.READ_MAX_KB,
180
+ )
181
+
182
+ @classmethod
183
+ async def call_with_args(cls, args: ReadTool.ReadArguments) -> model.ToolResultItem:
184
+ # Accept relative path by resolving to absolute (schema encourages absolute)
185
+ file_path = os.path.abspath(args.file_path)
186
+
187
+ # Get effective limits based on policy
188
+ char_per_line, line_cap, max_chars, max_kb = cls._effective_limits()
189
+
190
+ # Common file errors
191
+ if _is_directory(file_path):
192
+ return model.ToolResultItem(
193
+ status="error",
194
+ output="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
195
+ )
196
+ if not _file_exists(file_path):
197
+ return model.ToolResultItem(
198
+ status="error",
199
+ output="<tool_use_error>File does not exist.</tool_use_error>",
200
+ )
201
+
202
+ # Check for PDF files
203
+ if Path(file_path).suffix.lower() == ".pdf":
204
+ return model.ToolResultItem(
205
+ status="error",
206
+ output=(
207
+ "<tool_use_error>PDF files are not supported by this tool. "
208
+ "Please use a Python script with `pdfplumber` to extract text/tables:\n\n"
209
+ "```python\n"
210
+ "# /// script\n"
211
+ '# dependencies = ["pdfplumber"]\n'
212
+ "# ///\n"
213
+ "import pdfplumber\n\n"
214
+ "with pdfplumber.open('file.pdf') as pdf:\n"
215
+ " for page in pdf.pages:\n"
216
+ " print(page.extract_text())\n"
217
+ "```\n"
218
+ "</tool_use_error>"
219
+ ),
220
+ )
221
+
222
+ # If file is too large and no pagination provided (only check if limits are enabled)
223
+ try:
224
+ size_bytes = Path(file_path).stat().st_size
225
+ except Exception:
226
+ size_bytes = 0
227
+
228
+ is_image_file = _is_supported_image_file(file_path)
229
+ if is_image_file:
230
+ if size_bytes > const.READ_MAX_IMAGE_BYTES:
231
+ size_mb = size_bytes / (1024 * 1024)
232
+ return model.ToolResultItem(
233
+ status="error",
234
+ output=(
235
+ f"<tool_use_error>Image size ({size_mb:.2f}MB) exceeds maximum supported size (4.00MB) for inline transfer.</tool_use_error>"
236
+ ),
237
+ )
238
+ try:
239
+ mime_type = _image_mime_type(file_path)
240
+ data_url = _encode_image_to_data_url(file_path, mime_type)
241
+ except Exception as exc:
242
+ return model.ToolResultItem(
243
+ status="error",
244
+ output=f"<tool_use_error>Failed to read image file: {exc}</tool_use_error>",
245
+ )
246
+
247
+ _track_file_access(file_path)
248
+ size_kb = size_bytes / 1024.0 if size_bytes else 0.0
249
+ output_text = f"[image] {Path(file_path).name} ({size_kb:.1f}KB)"
250
+ image_part = model.ImageURLPart(image_url=model.ImageURLPart.ImageURL(url=data_url, id=None))
251
+ return model.ToolResultItem(status="success", output=output_text, images=[image_part])
252
+
253
+ if (
254
+ not is_image_file
255
+ and max_kb is not None
256
+ and args.offset is None
257
+ and args.limit is None
258
+ and size_bytes > max_kb * 1024
259
+ ):
260
+ size_kb = size_bytes / 1024.0
261
+ return model.ToolResultItem(
262
+ status="error",
263
+ output=(
264
+ f"File content ({size_kb:.1f}KB) exceeds maximum allowed size ({max_kb}KB). Please use offset and limit parameters to read specific portions of the file, or use the `rg` command to search for specific content."
265
+ ),
266
+ )
267
+
268
+ offset = 1 if args.offset is None or args.offset < 1 else int(args.offset)
269
+ limit = None if args.limit is None else int(args.limit)
270
+ if limit is not None and limit < 0:
271
+ limit = 0
272
+
273
+ # Stream file line-by-line and build response
274
+ read_result: ReadSegmentResult | None = None
275
+
276
+ try:
277
+ read_result = await asyncio.to_thread(
278
+ _read_segment,
279
+ ReadOptions(
280
+ file_path=file_path,
281
+ offset=offset,
282
+ limit=limit,
283
+ char_limit_per_line=char_per_line,
284
+ global_line_cap=line_cap,
285
+ ),
286
+ )
287
+
288
+ except FileNotFoundError:
289
+ return model.ToolResultItem(
290
+ status="error",
291
+ output="<tool_use_error>File does not exist.</tool_use_error>",
292
+ )
293
+ except IsADirectoryError:
294
+ return model.ToolResultItem(
295
+ status="error",
296
+ output="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
297
+ )
298
+
299
+ # If offset beyond total lines, emit system reminder warning
300
+ if offset > max(read_result.total_lines, 0):
301
+ warn = f"<system-reminder>Warning: the file exists but is shorter than the provided offset ({offset}). The file has {read_result.total_lines} lines.</system-reminder>"
302
+ # Update FileTracker (we still consider it as a read attempt)
303
+ _track_file_access(file_path)
304
+ return model.ToolResultItem(status="success", output=warn)
305
+
306
+ # After limit/offset, if total selected chars exceed limit, error (only check if limits are enabled)
307
+ if max_chars is not None and read_result.selected_chars_count > max_chars:
308
+ return model.ToolResultItem(
309
+ status="error",
310
+ output=(
311
+ f"File content ({read_result.selected_chars_count} chars) exceeds maximum allowed tokens ({max_chars}). Please use offset and limit parameters to read specific portions of the file, or use the `rg` command to search for specific content."
312
+ ),
313
+ )
314
+
315
+ # Build display with numbering and reminders
316
+ lines_out: list[str] = [_format_numbered_line(no, content) for no, content in read_result.selected_lines]
317
+ if read_result.remaining_selected_beyond_cap > 0:
318
+ lines_out.append(f"... (more {read_result.remaining_selected_beyond_cap} lines are truncated)")
319
+ read_result_str = "\n".join(lines_out)
320
+ # if read_result_str:
321
+ # read_result_str += "\n\n" + SYSTEM_REMINDER_MALICIOUS
322
+
323
+ # Update FileTracker with last modified time
324
+ _track_file_access(file_path)
325
+
326
+ return model.ToolResultItem(status="success", output=read_result_str)
@@ -0,0 +1,8 @@
1
+ Writes a file to the local filesystem.
2
+
3
+ Usage:
4
+ This tool will overwrite the existing file if there is one at the provided path.
5
+ If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
6
+ ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
7
+ NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
8
+ Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.