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,18 +1,44 @@
1
1
  import asyncio
2
+ import contextlib
3
+ import os
2
4
  import re
5
+ import shlex
6
+ import signal
3
7
  import subprocess
4
8
  from pathlib import Path
9
+ from typing import Any
5
10
 
6
11
  from pydantic import BaseModel
7
12
 
8
13
  from klaude_code import const
9
14
  from klaude_code.core.tool.shell.command_safety import is_safe_command
10
15
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
16
+ from klaude_code.core.tool.tool_context import get_current_file_tracker
11
17
  from klaude_code.core.tool.tool_registry import register
12
18
  from klaude_code.protocol import llm_param, model, tools
13
19
 
14
- # Regex to strip ANSI escape sequences from command output
15
- _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
20
+ # Regex to strip ANSI and terminal control sequences from command output
21
+ #
22
+ # This is intentionally broader than just SGR color codes (e.g. "\x1b[31m").
23
+ # Many interactive or TUI-style programs emit additional escape sequences
24
+ # that move the cursor, clear the screen, or switch screen buffers
25
+ # (CSI/OSC/DCS/APC/PM, etc). If these reach the Rich console, they can
26
+ # corrupt the REPL layout. We therefore remove all of them before
27
+ # rendering the output.
28
+ _ANSI_ESCAPE_RE = re.compile(
29
+ r"""
30
+ \x1B
31
+ (?:
32
+ \[[0-?]*[ -/]*[@-~] | # CSI sequences
33
+ \][0-?]*.*?(?:\x07|\x1B\\) | # OSC sequences
34
+ P.*?(?:\x07|\x1B\\) | # DCS sequences
35
+ _.*?(?:\x07|\x1B\\) | # APC sequences
36
+ \^.*?(?:\x07|\x1B\\) | # PM sequences
37
+ [@-Z\\-_] # 2-char sequences
38
+ )
39
+ """,
40
+ re.VERBOSE | re.DOTALL,
41
+ )
16
42
 
17
43
 
18
44
  @register(tools.BASH)
@@ -67,31 +93,250 @@ class BashTool(ToolABC):
67
93
 
68
94
  # Run the command using bash -lc so shell semantics work (pipes, &&, etc.)
69
95
  # Capture stdout/stderr, respect timeout, and return a ToolMessage.
96
+ #
97
+ # Important: this tool is intentionally non-interactive.
98
+ # - Always detach stdin (DEVNULL) so interactive programs can't steal REPL input.
99
+ # - Always disable pagers/editors to avoid launching TUI subprocesses that can
100
+ # leave the terminal in a bad state.
70
101
  cmd = ["bash", "-lc", args.command]
71
102
  timeout_sec = max(0.0, args.timeout_ms / 1000.0)
72
103
 
104
+ env = os.environ.copy()
105
+ env.update(
106
+ {
107
+ # Avoid blocking on git/jj prompts.
108
+ "GIT_TERMINAL_PROMPT": "0",
109
+ # Avoid pagers.
110
+ "PAGER": "cat",
111
+ "GIT_PAGER": "cat",
112
+ # Avoid opening editors.
113
+ "EDITOR": "true",
114
+ "VISUAL": "true",
115
+ "GIT_EDITOR": "true",
116
+ "JJ_EDITOR": "true",
117
+ # Encourage non-interactive output.
118
+ "TERM": "dumb",
119
+ }
120
+ )
121
+
122
+ def _hash_file_content_sha256(file_path: str) -> str | None:
123
+ try:
124
+ suffix = Path(file_path).suffix.lower()
125
+ if suffix in {".png", ".jpg", ".jpeg", ".gif", ".webp"}:
126
+ import hashlib
127
+
128
+ with open(file_path, "rb") as f:
129
+ return hashlib.sha256(f.read()).hexdigest()
130
+
131
+ import hashlib
132
+
133
+ hasher = hashlib.sha256()
134
+ with open(file_path, encoding="utf-8", errors="replace") as f:
135
+ for line in f:
136
+ hasher.update(line.encode("utf-8"))
137
+ return hasher.hexdigest()
138
+ except (FileNotFoundError, IsADirectoryError, OSError, PermissionError, UnicodeDecodeError):
139
+ return None
140
+
141
+ def _resolve_in_dir(base_dir: str, path: str) -> str:
142
+ if os.path.isabs(path):
143
+ return os.path.abspath(path)
144
+ return os.path.abspath(os.path.join(base_dir, path))
145
+
146
+ def _track_files_read(file_paths: list[str], *, base_dir: str) -> None:
147
+ file_tracker = get_current_file_tracker()
148
+ if file_tracker is None:
149
+ return
150
+ for p in file_paths:
151
+ abs_path = _resolve_in_dir(base_dir, p)
152
+ if not os.path.exists(abs_path) or os.path.isdir(abs_path):
153
+ continue
154
+ sha = _hash_file_content_sha256(abs_path)
155
+ if sha is None:
156
+ continue
157
+ existing = file_tracker.get(abs_path)
158
+ is_mem = existing.is_memory if existing else False
159
+ with contextlib.suppress(Exception):
160
+ file_tracker[abs_path] = model.FileStatus(
161
+ mtime=Path(abs_path).stat().st_mtime,
162
+ content_sha256=sha,
163
+ is_memory=is_mem,
164
+ )
165
+
166
+ def _track_files_written(file_paths: list[str], *, base_dir: str) -> None:
167
+ # Same as read tracking, but intentionally kept separate for clarity.
168
+ _track_files_read(file_paths, base_dir=base_dir)
169
+
170
+ def _track_mv(src_paths: list[str], dest_path: str, *, base_dir: str) -> None:
171
+ file_tracker = get_current_file_tracker()
172
+ if file_tracker is None:
173
+ return
174
+
175
+ abs_dest = _resolve_in_dir(base_dir, dest_path)
176
+ dest_is_dir = os.path.isdir(abs_dest)
177
+
178
+ for src in src_paths:
179
+ abs_src = _resolve_in_dir(base_dir, src)
180
+ abs_new = os.path.join(abs_dest, os.path.basename(abs_src)) if dest_is_dir else abs_dest
181
+
182
+ # Remove old entry if present.
183
+ existing = file_tracker.pop(abs_src, None)
184
+ is_mem = existing.is_memory if existing else False
185
+
186
+ if not os.path.exists(abs_new) or os.path.isdir(abs_new):
187
+ continue
188
+
189
+ sha = _hash_file_content_sha256(abs_new)
190
+ if sha is None:
191
+ continue
192
+ with contextlib.suppress(Exception):
193
+ file_tracker[abs_new] = model.FileStatus(
194
+ mtime=Path(abs_new).stat().st_mtime,
195
+ content_sha256=sha,
196
+ is_memory=is_mem,
197
+ )
198
+
199
+ def _best_effort_update_file_tracker(command: str) -> None:
200
+ # Best-effort heuristics for common shell tools that access/modify files.
201
+ # We intentionally do not try to interpret complex shell scripts here.
202
+ try:
203
+ argv = shlex.split(command, posix=True)
204
+ except ValueError:
205
+ return
206
+ if not argv:
207
+ return
208
+
209
+ # Handle common patterns like: cd subdir && cat file
210
+ base_dir = os.getcwd()
211
+ while len(argv) >= 4 and argv[0] == "cd" and argv[2] == "&&":
212
+ dest = argv[1]
213
+ if dest != "-":
214
+ base_dir = _resolve_in_dir(base_dir, dest)
215
+ argv = argv[3:]
216
+ if not argv:
217
+ return
218
+
219
+ cmd0 = argv[0]
220
+ if cmd0 == "cat":
221
+ paths = [a for a in argv[1:] if a and not a.startswith("-") and a != "-"]
222
+ _track_files_read(paths, base_dir=base_dir)
223
+ return
224
+
225
+ if cmd0 == "sed":
226
+ # Support: sed [-i ...] 's/old/new/' file1 [file2 ...]
227
+ # and: sed -n 'Np' file
228
+ saw_script = False
229
+ file_paths: list[str] = []
230
+ for a in argv[1:]:
231
+ if not a:
232
+ continue
233
+ if a == "--":
234
+ continue
235
+ if a.startswith("-") and not saw_script:
236
+ continue
237
+ if not saw_script and (a.startswith("s/") or a.startswith("s|") or a.endswith("p")):
238
+ saw_script = True
239
+ continue
240
+ if saw_script and not a.startswith("-"):
241
+ file_paths.append(a)
242
+
243
+ if file_paths:
244
+ _track_files_written(file_paths, base_dir=base_dir)
245
+ return
246
+
247
+ if cmd0 == "mv":
248
+ # Support: mv [opts] src... dest
249
+ operands: list[str] = []
250
+ end_of_opts = False
251
+ for a in argv[1:]:
252
+ if not end_of_opts and a == "--":
253
+ end_of_opts = True
254
+ continue
255
+ if not end_of_opts and a.startswith("-"):
256
+ continue
257
+ operands.append(a)
258
+ if len(operands) < 2:
259
+ return
260
+ srcs = operands[:-1]
261
+ dest = operands[-1]
262
+ _track_mv(srcs, dest, base_dir=base_dir)
263
+ return
264
+
265
+ async def _terminate_process(proc: asyncio.subprocess.Process) -> None:
266
+ # Best-effort termination. Ensure we don't hang on cancellation.
267
+ if proc.returncode is not None:
268
+ return
269
+
270
+ try:
271
+ if os.name == "posix":
272
+ os.killpg(proc.pid, signal.SIGTERM)
273
+ else:
274
+ proc.terminate()
275
+ except ProcessLookupError:
276
+ return
277
+ except OSError:
278
+ # Fall back to kill below.
279
+ pass
280
+
281
+ with contextlib.suppress(Exception):
282
+ await asyncio.wait_for(proc.wait(), timeout=1.0)
283
+ return
284
+
285
+ # Escalate to hard kill if it didn't exit quickly.
286
+ with contextlib.suppress(Exception):
287
+ if os.name == "posix":
288
+ os.killpg(proc.pid, signal.SIGKILL)
289
+ else:
290
+ proc.kill()
291
+ with contextlib.suppress(Exception):
292
+ await asyncio.wait_for(proc.wait(), timeout=1.0)
293
+
73
294
  try:
74
- completed = await asyncio.to_thread(
75
- subprocess.run,
76
- cmd,
77
- capture_output=True,
78
- text=True,
79
- timeout=timeout_sec,
80
- check=False,
81
- )
295
+ # Create a dedicated process group so we can terminate the whole tree.
296
+ # (macOS/Linux support start_new_session; Windows does not.)
297
+ kwargs: dict[str, Any] = {
298
+ "stdin": asyncio.subprocess.DEVNULL,
299
+ "stdout": asyncio.subprocess.PIPE,
300
+ "stderr": asyncio.subprocess.PIPE,
301
+ "env": env,
302
+ }
303
+ if os.name == "posix":
304
+ kwargs["start_new_session"] = True
305
+ elif os.name == "nt": # pragma: no cover
306
+ kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
82
307
 
83
- stdout = _ANSI_ESCAPE_RE.sub("", completed.stdout or "")
84
- stderr = _ANSI_ESCAPE_RE.sub("", completed.stderr or "")
85
- rc = completed.returncode
308
+ proc = await asyncio.create_subprocess_exec(*cmd, **kwargs)
309
+ try:
310
+ stdout_b, stderr_b = await asyncio.wait_for(proc.communicate(), timeout=timeout_sec)
311
+ except TimeoutError:
312
+ with contextlib.suppress(Exception):
313
+ await _terminate_process(proc)
314
+ return model.ToolResultItem(
315
+ status="error",
316
+ output=f"Timeout after {args.timeout_ms} ms running: {args.command}",
317
+ )
318
+ except asyncio.CancelledError:
319
+ # Ensure subprocess is stopped and propagate cancellation.
320
+ with contextlib.suppress(Exception):
321
+ await asyncio.shield(_terminate_process(proc))
322
+ raise
323
+
324
+ stdout = _ANSI_ESCAPE_RE.sub("", (stdout_b or b"").decode(errors="replace"))
325
+ stderr = _ANSI_ESCAPE_RE.sub("", (stderr_b or b"").decode(errors="replace"))
326
+ rc = proc.returncode
86
327
 
87
328
  if rc == 0:
88
329
  output = stdout if stdout else ""
89
330
  # Include stderr if there is useful diagnostics despite success
90
331
  if stderr.strip():
91
332
  output = (output + ("\n" if output else "")) + f"[stderr]\n{stderr}"
333
+
334
+ _best_effort_update_file_tracker(args.command)
92
335
  return model.ToolResultItem(
93
336
  status="success",
94
- output=output.strip(),
337
+ # Preserve leading whitespace for tools like `nl -ba`.
338
+ # Only trim trailing newlines to avoid adding an extra blank line in the UI.
339
+ output=output.rstrip("\n"),
95
340
  )
96
341
  else:
97
342
  combined = ""
@@ -103,20 +348,18 @@ class BashTool(ToolABC):
103
348
  combined = f"Command exited with code {rc}"
104
349
  return model.ToolResultItem(
105
350
  status="error",
106
- output=combined.strip(),
351
+ # Preserve leading whitespace; only trim trailing newlines.
352
+ output=combined.rstrip("\n"),
107
353
  )
108
-
109
- except subprocess.TimeoutExpired:
110
- return model.ToolResultItem(
111
- status="error",
112
- output=f"Timeout after {args.timeout_ms} ms running: {args.command}",
113
- )
114
354
  except FileNotFoundError:
115
355
  return model.ToolResultItem(
116
356
  status="error",
117
357
  output="bash not found on system path",
118
358
  )
119
- except Exception as e: # safeguard against unexpected failures
359
+ except asyncio.CancelledError:
360
+ # Propagate cooperative cancellation so outer layers can handle interrupts correctly.
361
+ raise
362
+ except OSError as e: # safeguard: catch remaining OS-level errors (permissions, resources, etc.)
120
363
  return model.ToolResultItem(
121
364
  status="error",
122
365
  output=f"Execution error: {e}",
@@ -275,12 +275,10 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
275
275
  "tag",
276
276
  "clone",
277
277
  "worktree",
278
+ "push",
279
+ "pull",
280
+ "remote",
278
281
  }
279
- # Block remote operations
280
- blocked_git_cmds = {"push", "pull", "remote"}
281
-
282
- if sub in blocked_git_cmds:
283
- return SafetyCheckResult(False, f"git: Remote operation '{sub}' not allowed")
284
282
  if sub not in allowed_git_cmds:
285
283
  return SafetyCheckResult(False, f"git: Subcommand '{sub}' not in allow list")
286
284
  return SafetyCheckResult(True)
@@ -293,7 +291,6 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
293
291
  "ruff",
294
292
  "pyright",
295
293
  "make",
296
- "isort",
297
294
  "npm",
298
295
  "pnpm",
299
296
  "bun",
@@ -1,27 +1,22 @@
1
+ """SkillTool - Tool for agent to activate and load skills."""
2
+
1
3
  from pathlib import Path
2
4
 
3
5
  from pydantic import BaseModel
4
6
 
5
- from klaude_code.core.tool.memory.skill_loader import SkillLoader
6
7
  from klaude_code.core.tool.tool_abc import ToolABC, load_desc
7
8
  from klaude_code.core.tool.tool_registry import register
8
9
  from klaude_code.protocol import llm_param, model, tools
10
+ from klaude_code.skill import get_available_skills, get_skill, list_skill_names
9
11
 
10
12
 
11
13
  @register(tools.SKILL)
12
14
  class SkillTool(ToolABC):
13
- """Tool to execute/load a skill within the main conversation"""
14
-
15
- _skill_loader: SkillLoader | None = None
16
-
17
- @classmethod
18
- def set_skill_loader(cls, loader: SkillLoader) -> None:
19
- """Set the skill loader instance"""
20
- cls._skill_loader = loader
15
+ """Tool to execute/load a skill within the main conversation."""
21
16
 
22
17
  @classmethod
23
18
  def schema(cls) -> llm_param.ToolSchema:
24
- """Generate schema with embedded available skills metadata"""
19
+ """Generate schema with embedded available skills metadata."""
25
20
  skills_xml = cls._generate_skills_xml()
26
21
 
27
22
  return llm_param.ToolSchema(
@@ -42,16 +37,17 @@ class SkillTool(ToolABC):
42
37
 
43
38
  @classmethod
44
39
  def _generate_skills_xml(cls) -> str:
45
- """Generate XML format skills metadata"""
46
- if not cls._skill_loader:
40
+ """Generate XML format skills metadata."""
41
+ skills = get_available_skills()
42
+ if not skills:
47
43
  return ""
48
44
 
49
45
  xml_parts: list[str] = []
50
- for skill in cls._skill_loader.loaded_skills.values():
46
+ for name, description, location in skills:
51
47
  xml_parts.append(f"""<skill>
52
- <name>{skill.name}</name>
53
- <description>{skill.description}</description>
54
- <location>{skill.location}</location>
48
+ <name>{name}</name>
49
+ <description>{description}</description>
50
+ <location>{location}</location>
55
51
  </skill>""")
56
52
  return "\n".join(xml_parts)
57
53
 
@@ -60,7 +56,7 @@ class SkillTool(ToolABC):
60
56
 
61
57
  @classmethod
62
58
  async def call(cls, arguments: str) -> model.ToolResultItem:
63
- """Load and return full skill content"""
59
+ """Load and return full skill content."""
64
60
  try:
65
61
  args = cls.SkillArguments.model_validate_json(arguments)
66
62
  except ValueError as e:
@@ -69,16 +65,10 @@ class SkillTool(ToolABC):
69
65
  output=f"Invalid arguments: {e}",
70
66
  )
71
67
 
72
- if not cls._skill_loader:
73
- return model.ToolResultItem(
74
- status="error",
75
- output="Skill loader not initialized",
76
- )
77
-
78
- skill = cls._skill_loader.get_skill(args.command)
68
+ skill = get_skill(args.command)
79
69
 
80
70
  if not skill:
81
- available = ", ".join(cls._skill_loader.list_skills())
71
+ available = ", ".join(list_skill_names())
82
72
  return model.ToolResultItem(
83
73
  status="error",
84
74
  output=f"Skill '{args.command}' does not exist. Available skills: {available}",
@@ -88,7 +78,7 @@ class SkillTool(ToolABC):
88
78
  base_dir = str(skill.skill_path.parent) if skill.skill_path else "unknown"
89
79
 
90
80
  # Return with loading message format
91
- result = f"""<command-message>The "{skill.name}" skill is running</command-message>
81
+ result = f"""<command-message>The "{skill.name}" skill is activated</command-message>
92
82
  <command-name>{skill.name}</command-name>
93
83
 
94
84
  Base directory for this skill: {base_dir}
@@ -10,7 +10,7 @@ import asyncio
10
10
  import json
11
11
  from typing import TYPE_CHECKING, ClassVar
12
12
 
13
- from klaude_code.core.tool.tool_abc import ToolABC
13
+ from klaude_code.core.tool.tool_abc import ToolABC, ToolConcurrencyPolicy, ToolMetadata
14
14
  from klaude_code.core.tool.tool_context import current_run_subtask_callback
15
15
  from klaude_code.protocol import llm_param, model
16
16
 
@@ -36,6 +36,10 @@ class SubAgentTool(ToolABC):
36
36
  {"_profile": profile},
37
37
  )
38
38
 
39
+ @classmethod
40
+ def metadata(cls) -> ToolMetadata:
41
+ return ToolMetadata(concurrency_policy=ToolConcurrencyPolicy.CONCURRENT, has_side_effects=True)
42
+
39
43
  @classmethod
40
44
  def schema(cls) -> llm_param.ToolSchema:
41
45
  profile = cls._profile
@@ -63,12 +67,18 @@ class SubAgentTool(ToolABC):
63
67
  prompt = profile.prompt_builder(args)
64
68
  description = args.get("description", "")
65
69
 
70
+ # Extract output_schema if configured
71
+ output_schema = None
72
+ if profile.output_schema_arg:
73
+ output_schema = args.get(profile.output_schema_arg)
74
+
66
75
  try:
67
76
  result = await runner(
68
77
  model.SubAgentState(
69
78
  sub_agent_type=profile.name,
70
79
  sub_agent_desc=description,
71
80
  sub_agent_prompt=prompt,
81
+ output_schema=output_schema,
72
82
  )
73
83
  )
74
84
  except asyncio.CancelledError:
@@ -79,5 +89,6 @@ class SubAgentTool(ToolABC):
79
89
  return model.ToolResultItem(
80
90
  status="success" if not result.error else "error",
81
91
  output=result.task_result or "",
82
- ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.SESSION_ID, session_id=result.session_id),
92
+ ui_extra=model.SessionIdUIExtra(session_id=result.session_id),
93
+ task_metadata=result.task_metadata,
83
94
  )
@@ -22,161 +22,4 @@ Skip using this tool when:
22
22
 
23
23
  NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
24
24
 
25
- #### Examples of When to Use the Todo List
26
-
27
- <example>
28
- User: I want to add a dark mode toggle to the application settings. Make sure you run the tests and build when you're done!
29
- Assistant: I'll help add a dark mode toggle to your application settings. Let me create a todo list to track this implementation.
30
- *Creates todo list with the following items:*
31
- 1. Creating dark mode toggle component in Settings page
32
- 2. Adding dark mode state management (context/store)
33
- 3. Implementing CSS-in-JS styles for dark theme
34
- 4. Updating existing components to support theme switching
35
- 5. Running tests and build process, addressing any failures or errors that occur
36
- *Begins working on the first task*
37
-
38
- <reasoning>
39
- The assistant used the todo list because:
40
- 1. Adding dark mode is a multi-step feature requiring UI, state management, and styling changes
41
- 2. The user explicitly requested tests and build be run afterward
42
- 3. The assistant inferred that tests and build need to pass by adding "Ensure tests and build succeed" as the final task
43
- </reasoning>
44
- </example>
45
-
46
- <example>
47
- User: Help me rename the function getCwd to getCurrentWorkingDirectory across my project
48
- Assistant: Let me first search through your codebase to find all occurrences of 'getCwd'.
49
- *Uses grep or search tools to locate all instances of getCwd in the codebase*
50
- Assistant: I've found 15 instances of 'getCwd' across 8 different files. Let me create a todo list to track these changes.
51
- *Creates todo list with specific items for each file that needs updating*
52
-
53
- <reasoning>
54
- The assistant used the todo list because:
55
- 1. First, the assistant searched to understand the scope of the task
56
- 2. Upon finding multiple occurrences across different files, it determined this was a complex task with multiple steps
57
- 3. The todo list helps ensure every instance is tracked and updated systematically
58
- 4. This approach prevents missing any occurrences and maintains code consistency
59
- </reasoning>
60
- </example>
61
-
62
-
63
- <example>
64
- User: I need to implement these features for my e-commerce site: user registration, product catalog, shopping cart, and checkout flow.
65
- Assistant: I'll help implement these features. First, let's add all the features to the todo list.
66
- *Creates a todo list breaking down each feature into specific tasks based on the project architecture*
67
- Assistant: Let's start with user registration. This will involve creating database models, API endpoints, and frontend forms that integrate with your existing authentication system.
68
-
69
- <reasoning>
70
- The assistant used the todo list because:
71
- 1. The user provided multiple complex features to implement in a comma separated list
72
- 2. The todo list helps organize these large features into manageable tasks
73
- 3. This approach allows for tracking progress across the entire implementation
74
- </reasoning>
75
- </example>
76
-
77
- <example>
78
- User: Can you help optimize my React application? It's rendering slowly and has performance issues.</user>
79
- Assistant: I'll help optimize your React application. First, let me examine your codebase to identify potential performance bottlenecks.
80
- *Reviews component structure, render patterns, state management, and data fetching*
81
- Assistant: After analyzing your codebase, I've identified several performance issues. Let me create a todo list to track our optimization efforts.
82
- *Creates todo list with items like: 1) Implementing memoization for expensive calculations in ProductList, 2) Adding virtualization for long lists in Dashboard, 3) Optimizing image loading in Gallery component, 4) Fixing state update loops in ShoppingCart, 5) Reviewing bundle size and implementing code splitting*
83
- Let's start by implementing memoization for the expensive calculations in your ProductList component.</assistant>
84
-
85
- <reasoning>
86
- The assistant used the todo list because:
87
- 1. First, the assistant examined the codebase to identify specific performance issues
88
- 2. Based on this analysis, it identified multiple optimization opportunities
89
- 3. Performance optimization is a non-trivial task requiring multiple steps
90
- 4. The todo list helps methodically track improvements across different components
91
- 5. This systematic approach ensures all performance bottlenecks are addressed
92
- </reasoning>
93
- </example>
94
-
95
- #### Examples of When NOT to Use the Todo List
96
-
97
- <example>
98
- User: How do I print 'Hello World' in Python?
99
- Assistant: In Python, you can print "Hello World" with this simple code:
100
-
101
- python
102
- print("Hello World")
103
-
104
- This will output the text "Hello World" to the console when executed.</assistant>
105
-
106
- <reasoning>
107
- The assistant did not use the todo list because this is a single, trivial task that can be completed in one step. There's no need to track multiple tasks or steps for such a straightforward request.
108
- </reasoning>
109
- </example>
110
-
111
- <example>
112
- User: What does the git status command do?
113
- Assistant: The git status command shows the current state of your working directory and staging area. It displays which changes have been staged, which haven't, and which files aren't being tracked by Git. This is a fundamental command for understanding the current state of your repository before making commits.
114
-
115
- <reasoning>
116
- The assistant did not use the todo list because this is an informational request with no actual coding task to complete. The user is simply asking for an explanation, not for the assistant to perform multiple steps or tasks.
117
- </reasoning>
118
- </example>
119
-
120
- <example>
121
- User: Can you add a comment to the calculateTotal function to explain what it does?
122
- Assistant: Sure, let me add a comment to the calculateTotal function to explain what it does.
123
- * Uses the Edit tool to add a comment to the calculateTotal function *
124
-
125
- <reasoning>
126
- The assistant did not use the todo list because this is a single, straightforward task confined to one location in the code. Adding a comment doesn't require tracking multiple steps or systematic organization.
127
- </reasoning>
128
- </example>
129
-
130
- <example>
131
- User: Run npm install for me and tell me what happens.
132
- Assistant: I'll run the npm install command for you.
133
-
134
- *Executes: npm install*
135
-
136
- The command completed successfully. Here's the output:
137
- [Output of npm install command]
138
-
139
- All dependencies have been installed according to your package.json file.
140
-
141
- <reasoning>
142
- The assistant did not use the todo list because this is a single command execution with immediate results. There are no multiple steps to track or organize, making the todo list unnecessary for this straightforward task.
143
- </reasoning>
144
- </example>
145
-
146
- #### Task States and Management
147
-
148
- 1. **Task States**: Use these states to track progress:
149
- - pending: Task not yet started
150
- - in_progress: Currently working on (limit to ONE task at a time)
151
- - completed: Task finished successfully
152
-
153
- **IMPORTANT**: Task descriptions must have two forms:
154
- - content: The imperative form describing what needs to be done (e.g., "Run tests", "Build the project")
155
- - activeForm: The present continuous form shown during execution (e.g., "Running tests", "Building the project")
156
-
157
- 2. **Task Management**:
158
- - Update task status in real-time as you work
159
- - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
160
- - Exactly ONE task must be in_progress at any time (not less, not more)
161
- - Complete current tasks before starting new ones
162
- - Remove tasks that are no longer relevant from the list entirely
163
-
164
- 3. **Task Completion Requirements**:
165
- - ONLY mark a task as completed when you have FULLY accomplished it
166
- - If you encounter errors, blockers, or cannot finish, keep the task as in_progress
167
- - When blocked, create a new task describing what needs to be resolved
168
- - Never mark a task as completed if:
169
- - Tests are failing
170
- - Implementation is partial
171
- - You encountered unresolved errors
172
- - You couldn't find necessary files or dependencies
173
-
174
- 4. **Task Breakdown**:
175
- - Create specific, actionable items
176
- - Break complex tasks into smaller, manageable steps
177
- - Use clear, descriptive task names
178
- - Always provide both forms:
179
- - content: "Fix authentication bug"
180
- - activeForm: "Fixing authentication bug"
181
-
182
25
  When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
@@ -116,6 +116,6 @@ Your todo list has changed. DO NOT mention this explicitly to the user. Here are
116
116
  return model.ToolResultItem(
117
117
  status="success",
118
118
  output=response,
119
- ui_extra=model.ToolResultUIExtra(type=model.ToolResultUIExtraType.TODO_LIST, todo_list=ui_extra),
119
+ ui_extra=model.TodoListUIExtra(todo_list=ui_extra),
120
120
  side_effects=[model.ToolSideEffect.TODO_CHANGE],
121
121
  )