klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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 (140) hide show
  1. klaude_code/cli/main.py +9 -4
  2. klaude_code/cli/runtime.py +42 -43
  3. klaude_code/command/__init__.py +7 -5
  4. klaude_code/command/clear_cmd.py +6 -29
  5. klaude_code/command/command_abc.py +44 -8
  6. klaude_code/command/diff_cmd.py +33 -27
  7. klaude_code/command/export_cmd.py +18 -26
  8. klaude_code/command/help_cmd.py +10 -8
  9. klaude_code/command/model_cmd.py +11 -40
  10. klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
  11. klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
  12. klaude_code/command/prompt-init.md +2 -5
  13. klaude_code/command/prompt_command.py +6 -6
  14. klaude_code/command/refresh_cmd.py +4 -5
  15. klaude_code/command/registry.py +16 -19
  16. klaude_code/command/terminal_setup_cmd.py +12 -11
  17. klaude_code/config/__init__.py +4 -0
  18. klaude_code/config/config.py +25 -26
  19. klaude_code/config/list_model.py +8 -3
  20. klaude_code/config/select_model.py +1 -1
  21. klaude_code/const/__init__.py +1 -1
  22. klaude_code/core/__init__.py +0 -3
  23. klaude_code/core/agent.py +25 -50
  24. klaude_code/core/executor.py +268 -101
  25. klaude_code/core/prompt.py +12 -12
  26. klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
  27. klaude_code/core/reminders.py +76 -95
  28. klaude_code/core/task.py +21 -14
  29. klaude_code/core/tool/__init__.py +45 -11
  30. klaude_code/core/tool/file/apply_patch.py +5 -1
  31. klaude_code/core/tool/file/apply_patch_tool.py +11 -13
  32. klaude_code/core/tool/file/edit_tool.py +27 -23
  33. klaude_code/core/tool/file/multi_edit_tool.py +15 -17
  34. klaude_code/core/tool/file/read_tool.py +41 -36
  35. klaude_code/core/tool/file/write_tool.py +13 -15
  36. klaude_code/core/tool/memory/memory_tool.py +85 -68
  37. klaude_code/core/tool/memory/skill_tool.py +10 -12
  38. klaude_code/core/tool/shell/bash_tool.py +24 -22
  39. klaude_code/core/tool/shell/command_safety.py +12 -1
  40. klaude_code/core/tool/sub_agent_tool.py +11 -12
  41. klaude_code/core/tool/todo/todo_write_tool.py +21 -28
  42. klaude_code/core/tool/todo/update_plan_tool.py +14 -24
  43. klaude_code/core/tool/tool_abc.py +3 -4
  44. klaude_code/core/tool/tool_context.py +7 -7
  45. klaude_code/core/tool/tool_registry.py +30 -47
  46. klaude_code/core/tool/tool_runner.py +35 -43
  47. klaude_code/core/tool/truncation.py +14 -20
  48. klaude_code/core/tool/web/mermaid_tool.py +12 -14
  49. klaude_code/core/tool/web/web_fetch_tool.py +15 -17
  50. klaude_code/core/turn.py +19 -7
  51. klaude_code/llm/__init__.py +3 -4
  52. klaude_code/llm/anthropic/client.py +30 -46
  53. klaude_code/llm/anthropic/input.py +4 -11
  54. klaude_code/llm/client.py +29 -8
  55. klaude_code/llm/input_common.py +66 -36
  56. klaude_code/llm/openai_compatible/client.py +42 -84
  57. klaude_code/llm/openai_compatible/input.py +11 -16
  58. klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
  59. klaude_code/llm/openrouter/client.py +40 -289
  60. klaude_code/llm/openrouter/input.py +13 -35
  61. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  62. klaude_code/llm/registry.py +5 -75
  63. klaude_code/llm/responses/client.py +34 -55
  64. klaude_code/llm/responses/input.py +24 -26
  65. klaude_code/llm/usage.py +109 -0
  66. klaude_code/protocol/__init__.py +4 -0
  67. klaude_code/protocol/events.py +3 -2
  68. klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
  69. klaude_code/protocol/model.py +49 -4
  70. klaude_code/protocol/op.py +18 -16
  71. klaude_code/protocol/op_handler.py +28 -0
  72. klaude_code/{core → protocol}/sub_agent.py +7 -0
  73. klaude_code/session/export.py +150 -70
  74. klaude_code/session/session.py +28 -14
  75. klaude_code/session/templates/export_session.html +180 -42
  76. klaude_code/trace/__init__.py +2 -2
  77. klaude_code/trace/log.py +11 -5
  78. klaude_code/ui/__init__.py +91 -8
  79. klaude_code/ui/core/__init__.py +1 -0
  80. klaude_code/ui/core/display.py +103 -0
  81. klaude_code/ui/core/input.py +71 -0
  82. klaude_code/ui/modes/__init__.py +1 -0
  83. klaude_code/ui/modes/debug/__init__.py +1 -0
  84. klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
  85. klaude_code/ui/modes/exec/__init__.py +1 -0
  86. klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
  87. klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
  88. klaude_code/ui/modes/repl/clipboard.py +152 -0
  89. klaude_code/ui/modes/repl/completers.py +429 -0
  90. klaude_code/ui/modes/repl/display.py +60 -0
  91. klaude_code/ui/modes/repl/event_handler.py +375 -0
  92. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  93. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  94. klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
  95. klaude_code/ui/renderers/assistant.py +21 -0
  96. klaude_code/ui/renderers/common.py +0 -16
  97. klaude_code/ui/renderers/developer.py +18 -18
  98. klaude_code/ui/renderers/diffs.py +36 -14
  99. klaude_code/ui/renderers/errors.py +1 -1
  100. klaude_code/ui/renderers/metadata.py +50 -27
  101. klaude_code/ui/renderers/sub_agent.py +43 -9
  102. klaude_code/ui/renderers/thinking.py +33 -1
  103. klaude_code/ui/renderers/tools.py +212 -20
  104. klaude_code/ui/renderers/user_input.py +19 -23
  105. klaude_code/ui/rich/__init__.py +1 -0
  106. klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
  107. klaude_code/ui/{renderers → rich}/status.py +29 -18
  108. klaude_code/ui/{base → rich}/theme.py +8 -2
  109. klaude_code/ui/terminal/__init__.py +1 -0
  110. klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
  111. klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
  112. klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
  113. klaude_code/ui/utils/__init__.py +1 -0
  114. klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
  115. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
  116. klaude_code-1.2.3.dist-info/RECORD +161 -0
  117. klaude_code/core/clipboard_manifest.py +0 -124
  118. klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
  119. klaude_code/ui/base/__init__.py +0 -1
  120. klaude_code/ui/base/display_abc.py +0 -36
  121. klaude_code/ui/base/input_abc.py +0 -20
  122. klaude_code/ui/repl/display.py +0 -36
  123. klaude_code/ui/repl/event_handler.py +0 -247
  124. klaude_code/ui/repl/input.py +0 -773
  125. klaude_code/ui/rich_ext/__init__.py +0 -1
  126. klaude_code-1.2.1.dist-info/RECORD +0 -151
  127. /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
  128. /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
  129. /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
  130. /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
  131. /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
  132. /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
  133. /klaude_code/ui/{base → core}/stage_manager.py +0 -0
  134. /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
  135. /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
  136. /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
  137. /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
  138. /klaude_code/ui/{base → utils}/debouncer.py +0 -0
  139. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
  140. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
@@ -13,9 +13,7 @@ from pydantic import BaseModel, Field
13
13
 
14
14
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
15
15
  from klaude_code.core.tool.tool_registry import register
16
- from klaude_code.protocol.llm_parameter import ToolSchema
17
- from klaude_code.protocol.model import ToolResultItem, ToolResultUIExtra, ToolResultUIExtraType
18
- from klaude_code.protocol.tools import MEMORY
16
+ from klaude_code.protocol import llm_param, model, tools
19
17
 
20
18
  MEMORY_VIRTUAL_ROOT = "/memories"
21
19
  MEMORY_DIR_NAME = ".claude/memories"
@@ -102,7 +100,7 @@ def _format_numbered_line(line_no: int, content: str) -> str:
102
100
  return f"{line_no:>6}|{content}"
103
101
 
104
102
 
105
- def _make_diff_ui_extra(before: str, after: str, path: str) -> ToolResultUIExtra:
103
+ def _make_diff_ui_extra(before: str, after: str, path: str) -> model.ToolResultUIExtra:
106
104
  diff_lines = list(
107
105
  difflib.unified_diff(
108
106
  before.splitlines(),
@@ -113,10 +111,10 @@ def _make_diff_ui_extra(before: str, after: str, path: str) -> ToolResultUIExtra
113
111
  )
114
112
  )
115
113
  diff_text = "\n".join(diff_lines)
116
- return ToolResultUIExtra(type=ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
114
+ return model.ToolResultUIExtra(type=model.ToolResultUIExtraType.DIFF_TEXT, diff_text=diff_text)
117
115
 
118
116
 
119
- @register(MEMORY)
117
+ @register(tools.MEMORY)
120
118
  class MemoryTool(ToolABC):
121
119
  class MemoryArguments(BaseModel):
122
120
  command: Literal["view", "create", "str_replace", "insert", "delete", "rename"]
@@ -136,9 +134,9 @@ class MemoryTool(ToolABC):
136
134
  new_path: str | None = Field(default=None)
137
135
 
138
136
  @classmethod
139
- def schema(cls) -> ToolSchema:
140
- return ToolSchema(
141
- name=MEMORY,
137
+ def schema(cls) -> llm_param.ToolSchema:
138
+ return llm_param.ToolSchema(
139
+ name=tools.MEMORY,
142
140
  type="function",
143
141
  description=load_desc(Path(__file__).parent / "memory_tool.md"),
144
142
  parameters={
@@ -146,7 +144,14 @@ class MemoryTool(ToolABC):
146
144
  "properties": {
147
145
  "command": {
148
146
  "type": "string",
149
- "enum": ["view", "create", "str_replace", "insert", "delete", "rename"],
147
+ "enum": [
148
+ "view",
149
+ "create",
150
+ "str_replace",
151
+ "insert",
152
+ "delete",
153
+ "rename",
154
+ ],
150
155
  "description": "The memory operation to perform",
151
156
  },
152
157
  "path": {
@@ -193,11 +198,11 @@ class MemoryTool(ToolABC):
193
198
  )
194
199
 
195
200
  @classmethod
196
- async def call(cls, arguments: str) -> ToolResultItem:
201
+ async def call(cls, arguments: str) -> model.ToolResultItem:
197
202
  try:
198
203
  args = cls.MemoryArguments.model_validate_json(arguments)
199
204
  except Exception as e:
200
- return ToolResultItem(status="error", output=f"Invalid arguments: {e}")
205
+ return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
201
206
 
202
207
  command = args.command
203
208
  if command == "view":
@@ -213,37 +218,40 @@ class MemoryTool(ToolABC):
213
218
  elif command == "rename":
214
219
  return await cls._rename(args)
215
220
  else:
216
- return ToolResultItem(status="error", output=f"Unknown command: {command}")
221
+ return model.ToolResultItem(status="error", output=f"Unknown command: {command}")
217
222
 
218
223
  @classmethod
219
- async def _view(cls, args: MemoryArguments) -> ToolResultItem:
224
+ async def _view(cls, args: MemoryArguments) -> model.ToolResultItem:
220
225
  if args.path is None:
221
- return ToolResultItem(status="error", output="path is required for view command")
226
+ return model.ToolResultItem(status="error", output="path is required for view command")
222
227
 
223
228
  actual_path, error = _validate_path(args.path)
224
229
  if error:
225
- return ToolResultItem(status="error", output=error)
230
+ return model.ToolResultItem(status="error", output=error)
226
231
  assert actual_path is not None
227
232
 
228
233
  # Ensure memories directory exists
229
234
  _ensure_memories_dir()
230
235
 
231
236
  if not actual_path.exists():
232
- return ToolResultItem(status="error", output=f"Path does not exist: {args.path}")
237
+ return model.ToolResultItem(status="error", output=f"Path does not exist: {args.path}")
233
238
 
234
239
  if actual_path.is_dir():
235
240
  # List directory contents
236
241
  try:
237
- entries = sorted(actual_path.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
242
+ entries = sorted(
243
+ actual_path.iterdir(),
244
+ key=lambda p: (not p.is_dir(), p.name.lower()),
245
+ )
238
246
  lines = [f"Directory: {args.path}"]
239
247
  for entry in entries:
240
248
  prefix = "/" if entry.is_dir() else ""
241
249
  lines.append(f"- {entry.name}{prefix}")
242
250
  if len(entries) == 0:
243
251
  lines.append("(empty directory)")
244
- return ToolResultItem(status="success", output="\n".join(lines))
252
+ return model.ToolResultItem(status="success", output="\n".join(lines))
245
253
  except Exception as e:
246
- return ToolResultItem(status="error", output=f"Failed to list directory: {e}")
254
+ return model.ToolResultItem(status="error", output=f"Failed to list directory: {e}")
247
255
  else:
248
256
  # Read file contents
249
257
  try:
@@ -259,7 +267,7 @@ class MemoryTool(ToolABC):
259
267
  end = min(total_lines, args.view_range[1])
260
268
 
261
269
  if start > total_lines:
262
- return ToolResultItem(
270
+ return model.ToolResultItem(
263
271
  status="success",
264
272
  output=f"File has {total_lines} lines, requested start line {start} is beyond end of file",
265
273
  )
@@ -269,25 +277,28 @@ class MemoryTool(ToolABC):
269
277
  output = "\n".join(numbered)
270
278
  if not output:
271
279
  output = "(empty file)"
272
- return ToolResultItem(status="success", output=output)
280
+ return model.ToolResultItem(status="success", output=output)
273
281
  except Exception as e:
274
- return ToolResultItem(status="error", output=f"Failed to read file: {e}")
282
+ return model.ToolResultItem(status="error", output=f"Failed to read file: {e}")
275
283
 
276
284
  @classmethod
277
- async def _create(cls, args: MemoryArguments) -> ToolResultItem:
285
+ async def _create(cls, args: MemoryArguments) -> model.ToolResultItem:
278
286
  if args.path is None:
279
- return ToolResultItem(status="error", output="path is required for create command")
287
+ return model.ToolResultItem(status="error", output="path is required for create command")
280
288
  if args.file_text is None:
281
- return ToolResultItem(status="error", output="file_text is required for create command")
289
+ return model.ToolResultItem(status="error", output="file_text is required for create command")
282
290
 
283
291
  actual_path, error = _validate_path(args.path)
284
292
  if error:
285
- return ToolResultItem(status="error", output=error)
293
+ return model.ToolResultItem(status="error", output=error)
286
294
  assert actual_path is not None
287
295
 
288
296
  # Cannot create the root directory itself
289
297
  if args.path == MEMORY_VIRTUAL_ROOT or args.path == MEMORY_VIRTUAL_ROOT + "/":
290
- return ToolResultItem(status="error", output="Cannot create the memories root directory as a file")
298
+ return model.ToolResultItem(
299
+ status="error",
300
+ output="Cannot create the memories root directory as a file",
301
+ )
291
302
 
292
303
  try:
293
304
  # Read existing content for diff (if file exists)
@@ -300,60 +311,64 @@ class MemoryTool(ToolABC):
300
311
  await asyncio.to_thread(actual_path.write_text, args.file_text, encoding="utf-8")
301
312
 
302
313
  ui_extra = _make_diff_ui_extra(before, args.file_text, args.path)
303
- return ToolResultItem(status="success", output=f"File created: {args.path}", ui_extra=ui_extra)
314
+ return model.ToolResultItem(status="success", output=f"File created: {args.path}", ui_extra=ui_extra)
304
315
  except Exception as e:
305
- return ToolResultItem(status="error", output=f"Failed to create file: {e}")
316
+ return model.ToolResultItem(status="error", output=f"Failed to create file: {e}")
306
317
 
307
318
  @classmethod
308
- async def _str_replace(cls, args: MemoryArguments) -> ToolResultItem:
319
+ async def _str_replace(cls, args: MemoryArguments) -> model.ToolResultItem:
309
320
  if args.path is None:
310
- return ToolResultItem(status="error", output="path is required for str_replace command")
321
+ return model.ToolResultItem(status="error", output="path is required for str_replace command")
311
322
  if args.old_str is None:
312
- return ToolResultItem(status="error", output="old_str is required for str_replace command")
323
+ return model.ToolResultItem(status="error", output="old_str is required for str_replace command")
313
324
  if args.new_str is None:
314
- return ToolResultItem(status="error", output="new_str is required for str_replace command")
325
+ return model.ToolResultItem(status="error", output="new_str is required for str_replace command")
315
326
 
316
327
  actual_path, error = _validate_path(args.path)
317
328
  if error:
318
- return ToolResultItem(status="error", output=error)
329
+ return model.ToolResultItem(status="error", output=error)
319
330
  assert actual_path is not None
320
331
 
321
332
  if not actual_path.exists():
322
- return ToolResultItem(status="error", output=f"File does not exist: {args.path}")
333
+ return model.ToolResultItem(status="error", output=f"File does not exist: {args.path}")
323
334
  if actual_path.is_dir():
324
- return ToolResultItem(status="error", output="Cannot perform str_replace on a directory")
335
+ return model.ToolResultItem(status="error", output="Cannot perform str_replace on a directory")
325
336
 
326
337
  try:
327
338
  before = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
328
339
  if args.old_str not in before:
329
- return ToolResultItem(status="error", output=f"String not found in file: {args.old_str}")
340
+ return model.ToolResultItem(status="error", output=f"String not found in file: {args.old_str}")
330
341
 
331
342
  after = before.replace(args.old_str, args.new_str, 1)
332
343
  await asyncio.to_thread(actual_path.write_text, after, encoding="utf-8")
333
344
 
334
345
  ui_extra = _make_diff_ui_extra(before, after, args.path)
335
- return ToolResultItem(status="success", output=f"Replaced text in {args.path}", ui_extra=ui_extra)
346
+ return model.ToolResultItem(
347
+ status="success",
348
+ output=f"Replaced text in {args.path}",
349
+ ui_extra=ui_extra,
350
+ )
336
351
  except Exception as e:
337
- return ToolResultItem(status="error", output=f"Failed to replace text: {e}")
352
+ return model.ToolResultItem(status="error", output=f"Failed to replace text: {e}")
338
353
 
339
354
  @classmethod
340
- async def _insert(cls, args: MemoryArguments) -> ToolResultItem:
355
+ async def _insert(cls, args: MemoryArguments) -> model.ToolResultItem:
341
356
  if args.path is None:
342
- return ToolResultItem(status="error", output="path is required for insert command")
357
+ return model.ToolResultItem(status="error", output="path is required for insert command")
343
358
  if args.insert_line is None:
344
- return ToolResultItem(status="error", output="insert_line is required for insert command")
359
+ return model.ToolResultItem(status="error", output="insert_line is required for insert command")
345
360
  if args.insert_text is None:
346
- return ToolResultItem(status="error", output="insert_text is required for insert command")
361
+ return model.ToolResultItem(status="error", output="insert_text is required for insert command")
347
362
 
348
363
  actual_path, error = _validate_path(args.path)
349
364
  if error:
350
- return ToolResultItem(status="error", output=error)
365
+ return model.ToolResultItem(status="error", output=error)
351
366
  assert actual_path is not None
352
367
 
353
368
  if not actual_path.exists():
354
- return ToolResultItem(status="error", output=f"File does not exist: {args.path}")
369
+ return model.ToolResultItem(status="error", output=f"File does not exist: {args.path}")
355
370
  if actual_path.is_dir():
356
- return ToolResultItem(status="error", output="Cannot insert into a directory")
371
+ return model.ToolResultItem(status="error", output="Cannot insert into a directory")
357
372
 
358
373
  try:
359
374
  before = await asyncio.to_thread(actual_path.read_text, encoding="utf-8")
@@ -377,69 +392,71 @@ class MemoryTool(ToolABC):
377
392
  await asyncio.to_thread(actual_path.write_text, after, encoding="utf-8")
378
393
 
379
394
  ui_extra = _make_diff_ui_extra(before, after, args.path)
380
- return ToolResultItem(
381
- status="success", output=f"Inserted text at line {args.insert_line} in {args.path}", ui_extra=ui_extra
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,
382
399
  )
383
400
  except Exception as e:
384
- return ToolResultItem(status="error", output=f"Failed to insert text: {e}")
401
+ return model.ToolResultItem(status="error", output=f"Failed to insert text: {e}")
385
402
 
386
403
  @classmethod
387
- async def _delete(cls, args: MemoryArguments) -> ToolResultItem:
404
+ async def _delete(cls, args: MemoryArguments) -> model.ToolResultItem:
388
405
  if args.path is None:
389
- return ToolResultItem(status="error", output="path is required for delete command")
406
+ return model.ToolResultItem(status="error", output="path is required for delete command")
390
407
 
391
408
  # Prevent deleting the root memories directory
392
409
  if args.path == MEMORY_VIRTUAL_ROOT or args.path == MEMORY_VIRTUAL_ROOT + "/":
393
- return ToolResultItem(status="error", output="Cannot delete the memories root directory")
410
+ return model.ToolResultItem(status="error", output="Cannot delete the memories root directory")
394
411
 
395
412
  actual_path, error = _validate_path(args.path)
396
413
  if error:
397
- return ToolResultItem(status="error", output=error)
414
+ return model.ToolResultItem(status="error", output=error)
398
415
  assert actual_path is not None
399
416
 
400
417
  if not actual_path.exists():
401
- return ToolResultItem(status="error", output=f"Path does not exist: {args.path}")
418
+ return model.ToolResultItem(status="error", output=f"Path does not exist: {args.path}")
402
419
 
403
420
  try:
404
421
  if actual_path.is_dir():
405
422
  await asyncio.to_thread(shutil.rmtree, actual_path)
406
- return ToolResultItem(status="success", output=f"Directory deleted: {args.path}")
423
+ return model.ToolResultItem(status="success", output=f"Directory deleted: {args.path}")
407
424
  else:
408
425
  await asyncio.to_thread(os.remove, actual_path)
409
- return ToolResultItem(status="success", output=f"File deleted: {args.path}")
426
+ return model.ToolResultItem(status="success", output=f"File deleted: {args.path}")
410
427
  except Exception as e:
411
- return ToolResultItem(status="error", output=f"Failed to delete: {e}")
428
+ return model.ToolResultItem(status="error", output=f"Failed to delete: {e}")
412
429
 
413
430
  @classmethod
414
- async def _rename(cls, args: MemoryArguments) -> ToolResultItem:
431
+ async def _rename(cls, args: MemoryArguments) -> model.ToolResultItem:
415
432
  if args.old_path is None:
416
- return ToolResultItem(status="error", output="old_path is required for rename command")
433
+ return model.ToolResultItem(status="error", output="old_path is required for rename command")
417
434
  if args.new_path is None:
418
- return ToolResultItem(status="error", output="new_path is required for rename command")
435
+ return model.ToolResultItem(status="error", output="new_path is required for rename command")
419
436
 
420
437
  # Prevent renaming the root memories directory
421
438
  if args.old_path == MEMORY_VIRTUAL_ROOT or args.old_path == MEMORY_VIRTUAL_ROOT + "/":
422
- return ToolResultItem(status="error", output="Cannot rename the memories root directory")
439
+ return model.ToolResultItem(status="error", output="Cannot rename the memories root directory")
423
440
 
424
441
  old_actual, error = _validate_path(args.old_path)
425
442
  if error:
426
- return ToolResultItem(status="error", output=f"Invalid old_path: {error}")
443
+ return model.ToolResultItem(status="error", output=f"Invalid old_path: {error}")
427
444
  assert old_actual is not None
428
445
 
429
446
  new_actual, error = _validate_path(args.new_path)
430
447
  if error:
431
- return ToolResultItem(status="error", output=f"Invalid new_path: {error}")
448
+ return model.ToolResultItem(status="error", output=f"Invalid new_path: {error}")
432
449
  assert new_actual is not None
433
450
 
434
451
  if not old_actual.exists():
435
- return ToolResultItem(status="error", output=f"Source path does not exist: {args.old_path}")
452
+ return model.ToolResultItem(status="error", output=f"Source path does not exist: {args.old_path}")
436
453
  if new_actual.exists():
437
- return ToolResultItem(status="error", output=f"Destination already exists: {args.new_path}")
454
+ return model.ToolResultItem(status="error", output=f"Destination already exists: {args.new_path}")
438
455
 
439
456
  try:
440
457
  # Ensure parent directory of destination exists
441
458
  new_actual.parent.mkdir(parents=True, exist_ok=True)
442
459
  await asyncio.to_thread(shutil.move, str(old_actual), str(new_actual))
443
- return ToolResultItem(status="success", output=f"Renamed {args.old_path} to {args.new_path}")
460
+ return model.ToolResultItem(status="success", output=f"Renamed {args.old_path} to {args.new_path}")
444
461
  except Exception as e:
445
- return ToolResultItem(status="error", output=f"Failed to rename: {e}")
462
+ return model.ToolResultItem(status="error", output=f"Failed to rename: {e}")
@@ -5,12 +5,10 @@ from pydantic import BaseModel
5
5
  from klaude_code.core.tool.memory.skill_loader import SkillLoader
6
6
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
7
7
  from klaude_code.core.tool.tool_registry import register
8
- from klaude_code.protocol.llm_parameter import ToolSchema
9
- from klaude_code.protocol.model import ToolResultItem
10
- from klaude_code.protocol.tools import SKILL
8
+ from klaude_code.protocol import llm_param, model, tools
11
9
 
12
10
 
13
- @register(SKILL)
11
+ @register(tools.SKILL)
14
12
  class SkillTool(ToolABC):
15
13
  """Tool to execute/load a skill within the main conversation"""
16
14
 
@@ -22,12 +20,12 @@ class SkillTool(ToolABC):
22
20
  cls._skill_loader = loader
23
21
 
24
22
  @classmethod
25
- def schema(cls) -> ToolSchema:
23
+ def schema(cls) -> llm_param.ToolSchema:
26
24
  """Generate schema with embedded available skills metadata"""
27
25
  skills_xml = cls._generate_skills_xml()
28
26
 
29
- return ToolSchema(
30
- name=SKILL,
27
+ return llm_param.ToolSchema(
28
+ name=tools.SKILL,
31
29
  type="function",
32
30
  description=load_desc(Path(__file__).parent / "skill_tool.md", {"skills_xml": skills_xml}),
33
31
  parameters={
@@ -61,18 +59,18 @@ class SkillTool(ToolABC):
61
59
  command: str
62
60
 
63
61
  @classmethod
64
- async def call(cls, arguments: str) -> ToolResultItem:
62
+ async def call(cls, arguments: str) -> model.ToolResultItem:
65
63
  """Load and return full skill content"""
66
64
  try:
67
65
  args = cls.SkillArguments.model_validate_json(arguments)
68
66
  except ValueError as e:
69
- return ToolResultItem(
67
+ return model.ToolResultItem(
70
68
  status="error",
71
69
  output=f"Invalid arguments: {e}",
72
70
  )
73
71
 
74
72
  if not cls._skill_loader:
75
- return ToolResultItem(
73
+ return model.ToolResultItem(
76
74
  status="error",
77
75
  output="Skill loader not initialized",
78
76
  )
@@ -81,7 +79,7 @@ class SkillTool(ToolABC):
81
79
 
82
80
  if not skill:
83
81
  available = ", ".join(cls._skill_loader.list_skills())
84
- return ToolResultItem(
82
+ return model.ToolResultItem(
85
83
  status="error",
86
84
  output=f"Skill '{args.command}' does not exist. Available skills: {available}",
87
85
  )
@@ -96,4 +94,4 @@ class SkillTool(ToolABC):
96
94
  Base directory for this skill: {base_dir}
97
95
 
98
96
  {skill.to_prompt()}"""
99
- return ToolResultItem(status="success", output=result)
97
+ return model.ToolResultItem(status="success", output=result)
@@ -1,24 +1,26 @@
1
1
  import asyncio
2
+ import re
2
3
  import subprocess
3
4
  from pathlib import Path
4
5
 
5
6
  from pydantic import BaseModel
6
7
 
7
- from klaude_code.const import BASH_DEFAULT_TIMEOUT_MS
8
+ from klaude_code import const
8
9
  from klaude_code.core.tool.shell.command_safety import is_safe_command, strip_bash_lc
9
10
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
10
11
  from klaude_code.core.tool.tool_registry import register
11
- from klaude_code.protocol.llm_parameter import ToolSchema
12
- from klaude_code.protocol.model import ToolResultItem
13
- from klaude_code.protocol.tools import BASH
12
+ from klaude_code.protocol import llm_param, model, tools
14
13
 
14
+ # Regex to strip ANSI escape sequences from command output
15
+ _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
15
16
 
16
- @register(BASH)
17
+
18
+ @register(tools.BASH)
17
19
  class BashTool(ToolABC):
18
20
  @classmethod
19
- def schema(cls) -> ToolSchema:
20
- return ToolSchema(
21
- name=BASH,
21
+ def schema(cls) -> llm_param.ToolSchema:
22
+ return llm_param.ToolSchema(
23
+ name=tools.BASH,
22
24
  type="function",
23
25
  description=load_desc(Path(__file__).parent / "bash_tool.md"),
24
26
  parameters={
@@ -30,8 +32,8 @@ class BashTool(ToolABC):
30
32
  },
31
33
  "timeout_ms": {
32
34
  "type": "integer",
33
- "description": f"The timeout for the command in milliseconds, default is {BASH_DEFAULT_TIMEOUT_MS}",
34
- "default": BASH_DEFAULT_TIMEOUT_MS,
35
+ "description": f"The timeout for the command in milliseconds, default is {const.BASH_DEFAULT_TIMEOUT_MS}",
36
+ "default": const.BASH_DEFAULT_TIMEOUT_MS,
35
37
  },
36
38
  },
37
39
  "required": ["command"],
@@ -40,27 +42,27 @@ class BashTool(ToolABC):
40
42
 
41
43
  class BashArguments(BaseModel):
42
44
  command: str
43
- timeout_ms: int = BASH_DEFAULT_TIMEOUT_MS
45
+ timeout_ms: int = const.BASH_DEFAULT_TIMEOUT_MS
44
46
 
45
47
  @classmethod
46
- async def call(cls, arguments: str) -> ToolResultItem:
48
+ async def call(cls, arguments: str) -> model.ToolResultItem:
47
49
  try:
48
50
  args = BashTool.BashArguments.model_validate_json(arguments)
49
51
  except ValueError as e:
50
- return ToolResultItem(
52
+ return model.ToolResultItem(
51
53
  status="error",
52
54
  output=f"Invalid arguments: {e}",
53
55
  )
54
56
  return await cls.call_with_args(args)
55
57
 
56
58
  @classmethod
57
- async def call_with_args(cls, args: BashArguments) -> ToolResultItem:
59
+ async def call_with_args(cls, args: BashArguments) -> model.ToolResultItem:
58
60
  command_str = strip_bash_lc(args.command)
59
61
 
60
62
  # Safety check: only execute commands proven as "known safe"
61
63
  result = is_safe_command(command_str)
62
64
  if not result.is_safe:
63
- return ToolResultItem(
65
+ return model.ToolResultItem(
64
66
  status="error",
65
67
  output=f"Command rejected: {result.error_msg}",
66
68
  )
@@ -80,8 +82,8 @@ class BashTool(ToolABC):
80
82
  check=False,
81
83
  )
82
84
 
83
- stdout = completed.stdout or ""
84
- stderr = completed.stderr or ""
85
+ stdout = _ANSI_ESCAPE_RE.sub("", completed.stdout or "")
86
+ stderr = _ANSI_ESCAPE_RE.sub("", completed.stderr or "")
85
87
  rc = completed.returncode
86
88
 
87
89
  if rc == 0:
@@ -89,7 +91,7 @@ class BashTool(ToolABC):
89
91
  # Include stderr if there is useful diagnostics despite success
90
92
  if stderr.strip():
91
93
  output = (output + ("\n" if output else "")) + f"[stderr]\n{stderr}"
92
- return ToolResultItem(
94
+ return model.ToolResultItem(
93
95
  status="success",
94
96
  output=output.strip(),
95
97
  )
@@ -101,23 +103,23 @@ class BashTool(ToolABC):
101
103
  combined += f"[stderr]\n{stderr}"
102
104
  if not combined:
103
105
  combined = f"Command exited with code {rc}"
104
- return ToolResultItem(
106
+ return model.ToolResultItem(
105
107
  status="error",
106
108
  output=combined.strip(),
107
109
  )
108
110
 
109
111
  except subprocess.TimeoutExpired:
110
- return ToolResultItem(
112
+ return model.ToolResultItem(
111
113
  status="error",
112
114
  output=f"Timeout after {args.timeout_ms} ms running: {command_str}",
113
115
  )
114
116
  except FileNotFoundError:
115
- return ToolResultItem(
117
+ return model.ToolResultItem(
116
118
  status="error",
117
119
  output="bash not found on system path",
118
120
  )
119
121
  except Exception as e: # safeguard against unexpected failures
120
- return ToolResultItem(
122
+ return model.ToolResultItem(
121
123
  status="error",
122
124
  output=f"Execution error: {e}",
123
125
  )
@@ -323,7 +323,18 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
323
323
  return SafetyCheckResult(True)
324
324
 
325
325
  # Build tools and linters - allow all subcommands
326
- if cmd0 in {"cargo", "uv", "go", "ruff", "pyright", "make", "isort", "npm", "pnpm", "bun"}:
326
+ if cmd0 in {
327
+ "cargo",
328
+ "uv",
329
+ "go",
330
+ "ruff",
331
+ "pyright",
332
+ "make",
333
+ "isort",
334
+ "npm",
335
+ "pnpm",
336
+ "bun",
337
+ }:
327
338
  return SafetyCheckResult(True)
328
339
 
329
340
  if cmd0 == "sed":
@@ -12,11 +12,10 @@ from typing import TYPE_CHECKING, ClassVar
12
12
 
13
13
  from klaude_code.core.tool.tool_abc import ToolABC
14
14
  from klaude_code.core.tool.tool_context import current_run_subtask_callback
15
- from klaude_code.protocol.llm_parameter import ToolSchema
16
- from klaude_code.protocol.model import SubAgentState, ToolResultItem, ToolResultUIExtra, ToolResultUIExtraType
15
+ from klaude_code.protocol import llm_param, model
17
16
 
18
17
  if TYPE_CHECKING:
19
- from klaude_code.core.sub_agent import SubAgentProfile
18
+ from klaude_code.protocol.sub_agent import SubAgentProfile
20
19
 
21
20
 
22
21
  class SubAgentTool(ToolABC):
@@ -38,9 +37,9 @@ class SubAgentTool(ToolABC):
38
37
  )
39
38
 
40
39
  @classmethod
41
- def schema(cls) -> ToolSchema:
40
+ def schema(cls) -> llm_param.ToolSchema:
42
41
  profile = cls._profile
43
- return ToolSchema(
42
+ return llm_param.ToolSchema(
44
43
  name=profile.name,
45
44
  type="function",
46
45
  description=profile.description,
@@ -48,17 +47,17 @@ class SubAgentTool(ToolABC):
48
47
  )
49
48
 
50
49
  @classmethod
51
- async def call(cls, arguments: str) -> ToolResultItem:
50
+ async def call(cls, arguments: str) -> model.ToolResultItem:
52
51
  profile = cls._profile
53
52
 
54
53
  try:
55
54
  args = json.loads(arguments)
56
55
  except json.JSONDecodeError as e:
57
- return ToolResultItem(status="error", output=f"Invalid JSON arguments: {e}")
56
+ return model.ToolResultItem(status="error", output=f"Invalid JSON arguments: {e}")
58
57
 
59
58
  runner = current_run_subtask_callback.get()
60
59
  if runner is None:
61
- return ToolResultItem(status="error", output="No subtask runner available in this context")
60
+ return model.ToolResultItem(status="error", output="No subtask runner available in this context")
62
61
 
63
62
  # Build the prompt using the profile's prompt builder
64
63
  prompt = profile.prompt_builder(args)
@@ -66,7 +65,7 @@ class SubAgentTool(ToolABC):
66
65
 
67
66
  try:
68
67
  result = await runner(
69
- SubAgentState(
68
+ model.SubAgentState(
70
69
  sub_agent_type=profile.name,
71
70
  sub_agent_desc=description,
72
71
  sub_agent_prompt=prompt,
@@ -75,10 +74,10 @@ class SubAgentTool(ToolABC):
75
74
  except asyncio.CancelledError:
76
75
  raise
77
76
  except Exception as e:
78
- return ToolResultItem(status="error", output=f"Failed to run subtask: {e}")
77
+ return model.ToolResultItem(status="error", output=f"Failed to run subtask: {e}")
79
78
 
80
- return ToolResultItem(
79
+ return model.ToolResultItem(
81
80
  status="success" if not result.error else "error",
82
81
  output=result.task_result or "",
83
- ui_extra=ToolResultUIExtra(type=ToolResultUIExtraType.SESSION_ID, session_id=result.session_id),
82
+ ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.SESSION_ID, session_id=result.session_id),
84
83
  )