klaude-code 2.1.1__py3-none-any.whl → 2.2.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 (38) hide show
  1. klaude_code/app/__init__.py +1 -2
  2. klaude_code/app/runtime.py +26 -41
  3. klaude_code/cli/main.py +19 -152
  4. klaude_code/config/assets/builtin_config.yaml +13 -0
  5. klaude_code/const.py +1 -1
  6. klaude_code/core/agent_profile.py +28 -0
  7. klaude_code/core/manager/llm_clients_builder.py +1 -1
  8. klaude_code/core/prompts/prompt-nano-banana.md +1 -0
  9. klaude_code/core/tool/shell/command_safety.py +4 -189
  10. klaude_code/core/turn.py +1 -1
  11. klaude_code/llm/anthropic/client.py +1 -1
  12. klaude_code/llm/google/client.py +1 -1
  13. klaude_code/llm/openai_compatible/stream.py +1 -1
  14. klaude_code/llm/responses/client.py +1 -1
  15. klaude_code/protocol/message.py +2 -2
  16. klaude_code/tui/command/__init__.py +4 -4
  17. klaude_code/tui/command/clear_cmd.py +1 -1
  18. klaude_code/tui/command/command_abc.py +1 -2
  19. klaude_code/tui/command/copy_cmd.py +1 -2
  20. klaude_code/tui/command/fork_session_cmd.py +4 -4
  21. klaude_code/tui/command/refresh_cmd.py +1 -2
  22. klaude_code/tui/command/resume_cmd.py +3 -4
  23. klaude_code/tui/command/status_cmd.py +1 -1
  24. klaude_code/tui/components/developer.py +11 -11
  25. klaude_code/tui/components/metadata.py +1 -1
  26. klaude_code/tui/components/rich/theme.py +2 -2
  27. klaude_code/tui/components/user_input.py +9 -21
  28. klaude_code/tui/runner.py +2 -2
  29. klaude_code/tui/terminal/selector.py +3 -15
  30. klaude_code/ui/__init__.py +0 -24
  31. klaude_code/ui/common.py +3 -2
  32. klaude_code/ui/core/display.py +2 -2
  33. {klaude_code-2.1.1.dist-info → klaude_code-2.2.0.dist-info}/METADATA +16 -81
  34. {klaude_code-2.1.1.dist-info → klaude_code-2.2.0.dist-info}/RECORD +36 -37
  35. klaude_code/tui/command/prompt-commit.md +0 -82
  36. klaude_code/ui/exec_mode.py +0 -60
  37. {klaude_code-2.1.1.dist-info → klaude_code-2.2.0.dist-info}/WHEEL +0 -0
  38. {klaude_code-2.1.1.dist-info → klaude_code-2.2.0.dist-info}/entry_points.txt +0 -0
@@ -4,9 +4,8 @@ This package coordinates core execution (Executor) with frontend displays.
4
4
  Terminal-specific rendering and input handling live in `klaude_code.tui`.
5
5
  """
6
6
 
7
- from .runtime import AppInitConfig, run_exec
7
+ from .runtime import AppInitConfig
8
8
 
9
9
  __all__ = [
10
10
  "AppInitConfig",
11
- "run_exec",
12
11
  ]
@@ -9,12 +9,15 @@ import typer
9
9
  from klaude_code import ui
10
10
  from klaude_code.config import Config, load_config
11
11
  from klaude_code.core.agent import Agent
12
- from klaude_code.core.agent_profile import DefaultModelProfileProvider, VanillaModelProfileProvider
12
+ from klaude_code.core.agent_profile import (
13
+ DefaultModelProfileProvider,
14
+ NanoBananaModelProfileProvider,
15
+ VanillaModelProfileProvider,
16
+ )
13
17
  from klaude_code.core.executor import Executor
14
18
  from klaude_code.core.manager import build_llm_clients
15
19
  from klaude_code.log import DebugType, log, set_debug_logging
16
20
  from klaude_code.protocol import events, op
17
- from klaude_code.protocol.message import UserInputPayload
18
21
  from klaude_code.session.session import Session, close_default_store
19
22
 
20
23
 
@@ -25,8 +28,8 @@ class AppInitConfig:
25
28
  model: str | None
26
29
  debug: bool
27
30
  vanilla: bool
31
+ banana: bool
28
32
  debug_filters: set[DebugType] | None = None
29
- stream_json: bool = False
30
33
 
31
34
 
32
35
  @dataclass
@@ -52,6 +55,20 @@ async def initialize_app_components(
52
55
 
53
56
  config = load_config()
54
57
 
58
+ if init_config.banana:
59
+ # Banana mode is strict: it requires the built-in Nano Banana image model to be available.
60
+ required_model = "nano-banana-pro@or"
61
+ available = {m.model_name for m in config.iter_model_entries(only_available=True)}
62
+ if required_model not in available:
63
+ log(
64
+ (
65
+ f"Error: --banana requires model '{required_model}', but it is not available in the current environment",
66
+ "red",
67
+ )
68
+ )
69
+ log(("Hint: set OPENROUTER_API_KEY (Nano Banana Pro is configured via OpenRouter by default)", "yellow"))
70
+ raise typer.Exit(2)
71
+
55
72
  try:
56
73
  llm_clients = build_llm_clients(
57
74
  config,
@@ -70,7 +87,12 @@ async def initialize_app_components(
70
87
  log((f"Error: failed to load the default model configuration: {exc}", "red"))
71
88
  raise typer.Exit(2) from None
72
89
 
73
- model_profile_provider = VanillaModelProfileProvider() if init_config.vanilla else DefaultModelProfileProvider()
90
+ if init_config.banana:
91
+ model_profile_provider = NanoBananaModelProfileProvider()
92
+ elif init_config.vanilla:
93
+ model_profile_provider = VanillaModelProfileProvider()
94
+ else:
95
+ model_profile_provider = DefaultModelProfileProvider()
74
96
 
75
97
  event_queue: asyncio.Queue[events.Event] = asyncio.Queue()
76
98
 
@@ -176,40 +198,3 @@ async def handle_keyboard_interrupt(executor: Executor) -> None:
176
198
  log(("Resume with:", "dim"), (f"klaude --resume-by-id {session_id}", "green"))
177
199
  with contextlib.suppress(Exception):
178
200
  await executor.submit(op.InterruptOperation(target_session_id=None))
179
-
180
-
181
- async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
182
- """Run a single task non-interactively (exec mode)."""
183
- from klaude_code.ui.terminal.title import update_terminal_title
184
-
185
- display = ui.create_exec_display(debug=init_config.debug, stream_json=init_config.stream_json)
186
- components = await initialize_app_components(
187
- init_config=init_config,
188
- display=display,
189
- on_model_change=update_terminal_title,
190
- )
191
-
192
- try:
193
- session_id = await initialize_session(components.executor, components.event_queue)
194
- backfill_session_model_config(
195
- components.executor.context.current_agent,
196
- init_config.model,
197
- components.config.main_model,
198
- is_new_session=True,
199
- )
200
-
201
- if session_id is None:
202
- raise RuntimeError("No active session")
203
-
204
- op_id = await components.executor.submit(
205
- op.RunAgentOperation(
206
- session_id=session_id,
207
- input=UserInputPayload(text=input_content),
208
- )
209
- )
210
- await components.executor.wait_for(op_id)
211
- await components.event_queue.join()
212
- except KeyboardInterrupt:
213
- await handle_keyboard_interrupt(components.executor)
214
- finally:
215
- await cleanup_app_components(components)
klaude_code/cli/main.py CHANGED
@@ -13,44 +13,6 @@ from klaude_code.session import Session
13
13
  from klaude_code.tui.command.resume_cmd import select_session_sync
14
14
  from klaude_code.ui.terminal.title import update_terminal_title
15
15
 
16
-
17
- def read_input_content(cli_argument: str) -> str | None:
18
- """Read and merge input from stdin and CLI argument.
19
-
20
- Args:
21
- cli_argument: The input content passed as CLI argument.
22
-
23
- Returns:
24
- The merged input content, or None if no input was provided.
25
- """
26
- from klaude_code.log import log
27
-
28
- parts: list[str] = []
29
-
30
- # Handle stdin input
31
- if not sys.stdin.isatty():
32
- try:
33
- stdin = sys.stdin.read().rstrip("\n")
34
- if stdin:
35
- parts.append(stdin)
36
- except (OSError, ValueError) as e:
37
- # Expected I/O-related errors when reading from stdin (e.g. broken pipe, closed stream).
38
- log((f"Error reading from stdin: {e}", "red"))
39
- except Exception as e:
40
- # Unexpected errors are still reported but kept from crashing the CLI.
41
- log((f"Unexpected error reading from stdin: {e}", "red"))
42
-
43
- if cli_argument:
44
- parts.append(cli_argument)
45
-
46
- content = "\n".join(parts)
47
- if len(content) == 0:
48
- log(("Error: No input content provided", "red"))
49
- return None
50
-
51
- return content
52
-
53
-
54
16
  ENV_HELP = """\
55
17
  Environment Variables:
56
18
 
@@ -76,101 +38,6 @@ register_cost_commands(app)
76
38
  register_self_update_commands(app)
77
39
 
78
40
 
79
- @app.command("exec")
80
- def exec_command(
81
- input_content: str = typer.Argument("", help="Input message to execute"),
82
- model: str | None = typer.Option(
83
- None,
84
- "--model",
85
- "-m",
86
- help="Override model config name (uses main model by default)",
87
- rich_help_panel="LLM",
88
- ),
89
- select_model: bool = typer.Option(
90
- False,
91
- "--select-model",
92
- "-s",
93
- help="Interactively choose a model at startup",
94
- rich_help_panel="LLM",
95
- ),
96
- debug: bool = typer.Option(
97
- False,
98
- "--debug",
99
- "-d",
100
- help="Enable debug mode",
101
- rich_help_panel="Debug",
102
- ),
103
- debug_filter: str | None = typer.Option(
104
- None,
105
- "--debug-filter",
106
- help=DEBUG_FILTER_HELP,
107
- rich_help_panel="Debug",
108
- ),
109
- vanilla: bool = typer.Option(
110
- False,
111
- "--vanilla",
112
- help="Vanilla mode exposes the model's raw API behavior: it provides only minimal tools (Bash, Read & Edit) and omits system prompts and reminders.",
113
- ),
114
- stream_json: bool = typer.Option(
115
- False,
116
- "--stream-json",
117
- help="Stream all events as JSON lines to stdout.",
118
- ),
119
- ) -> None:
120
- """Execute non-interactively with provided input."""
121
- update_terminal_title()
122
-
123
- merged_input = read_input_content(input_content)
124
- if merged_input is None:
125
- raise typer.Exit(1)
126
-
127
- from klaude_code.app.runtime import AppInitConfig, run_exec
128
- from klaude_code.config import load_config
129
- from klaude_code.tui.command.model_select import select_model_interactive
130
-
131
- chosen_model = model
132
- if model or select_model:
133
- chosen_model = select_model_interactive(preferred=model)
134
- if chosen_model is None:
135
- raise typer.Exit(1)
136
- else:
137
- # Check if main_model is configured; if not, trigger interactive selection
138
- config = load_config()
139
- if config.main_model is None:
140
- chosen_model = select_model_interactive()
141
- if chosen_model is None:
142
- raise typer.Exit(1)
143
- # Save the selection as default
144
- config.main_model = chosen_model
145
- from klaude_code.config.config import config_path
146
- from klaude_code.log import log
147
-
148
- asyncio.run(config.save())
149
- log(f"Saved main_model={chosen_model} to {config_path}", style="cyan")
150
-
151
- debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
152
-
153
- init_config = AppInitConfig(
154
- model=chosen_model,
155
- debug=debug_enabled,
156
- vanilla=vanilla,
157
- debug_filters=debug_filters,
158
- stream_json=stream_json,
159
- )
160
-
161
- if log_path:
162
- from klaude_code.log import log
163
-
164
- log(f"Debug log: {log_path}", style="dim")
165
-
166
- asyncio.run(
167
- run_exec(
168
- init_config=init_config,
169
- input_content=merged_input,
170
- )
171
- )
172
-
173
-
174
41
  @app.callback(invoke_without_command=True)
175
42
  def main_callback(
176
43
  ctx: typer.Context,
@@ -220,13 +87,23 @@ def main_callback(
220
87
  vanilla: bool = typer.Option(
221
88
  False,
222
89
  "--vanilla",
223
- help="Vanilla mode exposes the model's raw API behavior: it provides only minimal tools (Bash, Read & Edit) and omits system prompts and reminders.",
90
+ help="Vanilla mode exposes the model's raw API behavior: it provides only minimal tools (Bash, Read, Write & Edit) and omits system prompts and reminders.",
91
+ ),
92
+ banana: bool = typer.Option(
93
+ False,
94
+ "--banana",
95
+ help="Image generation mode with Nano Banana",
96
+ rich_help_panel="LLM",
224
97
  ),
225
98
  ) -> None:
226
99
  # Only run interactive mode when no subcommand is invoked
227
100
  if ctx.invoked_subcommand is None:
228
101
  from klaude_code.log import log
229
102
 
103
+ if vanilla and banana:
104
+ log(("Error: --banana cannot be combined with --vanilla", "red"))
105
+ raise typer.Exit(2)
106
+
230
107
  resume_by_id_value = resume_by_id.strip() if resume_by_id is not None else None
231
108
  if resume_by_id_value == "":
232
109
  log(("Error: --resume-by-id cannot be empty", "red"))
@@ -241,24 +118,10 @@ def main_callback(
241
118
  log(("Hint: run `klaude --resume` to select an existing session", "yellow"))
242
119
  raise typer.Exit(2)
243
120
 
244
- # In non-interactive environments, default to exec-mode behavior.
245
- # This allows: echo "…" | klaude
246
121
  if not sys.stdin.isatty() or not sys.stdout.isatty():
247
- if continue_ or resume or resume_by_id is not None:
248
- log(("Error: --continue/--resume options require a TTY", "red"))
249
- log(("Hint: use `klaude exec` for non-interactive usage", "yellow"))
250
- raise typer.Exit(2)
251
-
252
- exec_command(
253
- input_content="",
254
- model=model,
255
- select_model=select_model,
256
- debug=debug,
257
- debug_filter=debug_filter,
258
- vanilla=vanilla,
259
- stream_json=False,
260
- )
261
- return
122
+ log(("Error: interactive mode requires a TTY", "red"))
123
+ log(("Hint: run klaude from an interactive terminal", "yellow"))
124
+ raise typer.Exit(2)
262
125
 
263
126
  from klaude_code.app.runtime import AppInitConfig
264
127
  from klaude_code.tui.command.model_select import select_model_interactive
@@ -267,7 +130,10 @@ def main_callback(
267
130
  update_terminal_title()
268
131
 
269
132
  chosen_model = model
270
- if model or select_model:
133
+ if banana:
134
+ # Banana mode always uses the built-in Nano Banana Pro image model.
135
+ chosen_model = "nano-banana-pro@or"
136
+ elif model or select_model:
271
137
  chosen_model = select_model_interactive(preferred=model)
272
138
  if chosen_model is None:
273
139
  return
@@ -340,6 +206,7 @@ def main_callback(
340
206
  model=chosen_model,
341
207
  debug=debug_enabled,
342
208
  vanilla=vanilla,
209
+ banana=banana,
343
210
  debug_filters=debug_filters,
344
211
  )
345
212
 
@@ -250,6 +250,18 @@ provider_list:
250
250
  input: 0.5
251
251
  output: 3.0
252
252
  cache_read: 0.05
253
+ - model_name: nano-banana-pro@google
254
+ model_params:
255
+ model: gemini-3-pro-image-preview
256
+ context_limit: 1048576
257
+ modalities:
258
+ - image
259
+ - text
260
+ cost:
261
+ input: 2
262
+ output: 12
263
+ cache_read: 0.2
264
+ image: 120
253
265
 
254
266
  - provider_name: bedrock
255
267
  protocol: bedrock
@@ -266,6 +278,7 @@ provider_list:
266
278
  output: 15.0
267
279
  cache_read: 0.3
268
280
  cache_write: 3.75
281
+
269
282
  - provider_name: deepseek
270
283
  protocol: anthropic
271
284
  api_key: ${DEEPSEEK_API_KEY}
klaude_code/const.py CHANGED
@@ -155,7 +155,7 @@ MARKDOWN_RIGHT_MARGIN = 2 # Right margin (columns) for markdown rendering
155
155
  STATUS_HINT_TEXT = " (esc to interrupt)" # Status hint text shown after spinner
156
156
 
157
157
  # Spinner status texts
158
- STATUS_WAITING_TEXT = "Awaiting …"
158
+ STATUS_WAITING_TEXT = "Connecting …"
159
159
  STATUS_THINKING_TEXT = "Reasoning …"
160
160
  STATUS_COMPOSING_TEXT = "Generating"
161
161
 
@@ -58,6 +58,9 @@ PROMPT_FILES: dict[str, str] = {
58
58
  }
59
59
 
60
60
 
61
+ NANO_BANANA_SYSTEM_PROMPT_PATH = "prompts/prompt-nano-banana.md"
62
+
63
+
61
64
  STRUCTURED_OUTPUT_PROMPT = """\
62
65
 
63
66
  # Structured Output
@@ -289,3 +292,28 @@ class VanillaModelProfileProvider(ModelProfileProvider):
289
292
  if output_schema:
290
293
  return with_structured_output(profile, output_schema)
291
294
  return profile
295
+
296
+
297
+ class NanoBananaModelProfileProvider(ModelProfileProvider):
298
+ """Provider for the Nano Banana image generation model.
299
+
300
+ This mode uses a dedicated system prompt and strips all tools/reminders.
301
+ """
302
+
303
+ def build_profile(
304
+ self,
305
+ llm_client: LLMClientABC,
306
+ sub_agent_type: tools.SubAgentType | None = None,
307
+ *,
308
+ output_schema: dict[str, Any] | None = None,
309
+ ) -> AgentProfile:
310
+ del sub_agent_type
311
+ profile = AgentProfile(
312
+ llm_client=llm_client,
313
+ system_prompt=_load_prompt_by_path(NANO_BANANA_SYSTEM_PROMPT_PATH),
314
+ tools=[],
315
+ reminders=[],
316
+ )
317
+ if output_schema:
318
+ return with_structured_output(profile, output_schema)
319
+ return profile
@@ -21,7 +21,7 @@ def build_llm_clients(
21
21
  # Resolve main agent LLM config
22
22
  model_name = model_override or config.main_model
23
23
  if model_name is None:
24
- raise ValueError("No model specified. Use --model or --select-model to specify a model.")
24
+ raise ValueError("No model specified. Set main_model in the config or pass --model.")
25
25
  llm_config = config.get_model_config(model_name)
26
26
 
27
27
  log_debug(
@@ -0,0 +1 @@
1
+ You're a helpful art assistant
@@ -1,5 +1,4 @@
1
1
  import os
2
- import re
3
2
  import shlex
4
3
 
5
4
 
@@ -11,76 +10,6 @@ class SafetyCheckResult:
11
10
  self.error_msg = error_msg
12
11
 
13
12
 
14
- def _is_valid_sed_n_arg(s: str | None) -> bool:
15
- if not s:
16
- return False
17
- # Matches: Np or M,Np where M,N are positive integers
18
- return bool(re.fullmatch(r"\d+(,\d+)?p", s))
19
-
20
-
21
- def _is_safe_awk_program(program: str) -> SafetyCheckResult:
22
- lowered = program.lower()
23
-
24
- if "`" in program:
25
- return SafetyCheckResult(False, "awk: backticks not allowed in program")
26
- if "$(" in program:
27
- return SafetyCheckResult(False, "awk: command substitution not allowed in program")
28
- if "|&" in program:
29
- return SafetyCheckResult(False, "awk: background pipeline not allowed in program")
30
-
31
- if "system(" in lowered:
32
- return SafetyCheckResult(False, "awk: system() call not allowed in program")
33
-
34
- if re.search(r"(?<![|&>])\bprint\s*\|", program, re.IGNORECASE):
35
- return SafetyCheckResult(False, "awk: piping output to external command not allowed")
36
- if re.search(r"\bprintf\s*\|", program, re.IGNORECASE):
37
- return SafetyCheckResult(False, "awk: piping output to external command not allowed")
38
-
39
- return SafetyCheckResult(True)
40
-
41
-
42
- def _is_safe_awk_argv(argv: list[str]) -> SafetyCheckResult:
43
- if len(argv) < 2:
44
- return SafetyCheckResult(False, "awk: Missing program")
45
-
46
- program: str | None = None
47
-
48
- i = 1
49
- while i < len(argv):
50
- arg = argv[i]
51
-
52
- if arg in {"-f", "--file", "--source"} or arg.startswith("-f"):
53
- return SafetyCheckResult(False, "awk: -f/--file not allowed")
54
-
55
- if arg in {"-e", "--exec"}:
56
- if i + 1 >= len(argv):
57
- return SafetyCheckResult(False, "awk: Missing program for -e")
58
- script = argv[i + 1]
59
- program_check = _is_safe_awk_program(script)
60
- if not program_check.is_safe:
61
- return program_check
62
- if program is None:
63
- program = script
64
- i += 2
65
- continue
66
-
67
- if arg.startswith("-"):
68
- i += 1
69
- continue
70
-
71
- if program is None:
72
- program_check = _is_safe_awk_program(arg)
73
- if not program_check.is_safe:
74
- return program_check
75
- program = arg
76
- i += 1
77
-
78
- if program is None:
79
- return SafetyCheckResult(False, "awk: Missing program")
80
-
81
- return SafetyCheckResult(True)
82
-
83
-
84
13
  def _is_safe_rm_argv(argv: list[str]) -> SafetyCheckResult:
85
14
  """Check safety of rm command arguments."""
86
15
  # Enforce strict safety rules for rm operands
@@ -217,112 +146,12 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
217
146
 
218
147
  cmd0 = argv[0]
219
148
 
220
- # if _has_shell_redirection(argv):
221
- # return SafetyCheckResult(False, "Shell redirection and pipelines are not allowed in single commands")
222
-
223
- # Special handling for rm to prevent dangerous operations
224
149
  if cmd0 == "rm":
225
150
  return _is_safe_rm_argv(argv)
226
151
 
227
- # Special handling for trash to prevent dangerous operations
228
152
  if cmd0 == "trash":
229
153
  return _is_safe_trash_argv(argv)
230
154
 
231
- if cmd0 == "find":
232
- unsafe_opts = {
233
- "-exec": "command execution",
234
- "-execdir": "command execution",
235
- "-ok": "interactive command execution",
236
- "-okdir": "interactive command execution",
237
- "-delete": "file deletion",
238
- "-fls": "file output",
239
- "-fprint": "file output",
240
- "-fprint0": "file output",
241
- "-fprintf": "formatted file output",
242
- }
243
- for arg in argv[1:]:
244
- if arg in unsafe_opts:
245
- return SafetyCheckResult(False, f"find: {unsafe_opts[arg]} option '{arg}' not allowed")
246
- return SafetyCheckResult(True)
247
-
248
- if cmd0 == "git":
249
- sub = argv[1] if len(argv) > 1 else None
250
- if not sub:
251
- return SafetyCheckResult(False, "git: Missing subcommand")
252
-
253
- # Allow most local git operations, but block remote operations
254
- allowed_git_cmds = {
255
- "add",
256
- "branch",
257
- "checkout",
258
- "commit",
259
- "config",
260
- "diff",
261
- "fetch",
262
- "init",
263
- "log",
264
- "merge",
265
- "mv",
266
- "rebase",
267
- "reset",
268
- "restore",
269
- "revert",
270
- "rm",
271
- "show",
272
- "stash",
273
- "status",
274
- "switch",
275
- "tag",
276
- "clone",
277
- "worktree",
278
- "push",
279
- "pull",
280
- "remote",
281
- }
282
- if sub not in allowed_git_cmds:
283
- return SafetyCheckResult(False, f"git: Subcommand '{sub}' not in allow list")
284
- return SafetyCheckResult(True)
285
-
286
- # Build tools and linters - allow all subcommands
287
- if cmd0 in {
288
- "cargo",
289
- "uv",
290
- "go",
291
- "ruff",
292
- "pyright",
293
- "make",
294
- "npm",
295
- "pnpm",
296
- "bun",
297
- }:
298
- return SafetyCheckResult(True)
299
-
300
- if cmd0 == "sed":
301
- # Allow sed -n patterns (line printing)
302
- if len(argv) >= 3 and argv[1] == "-n" and _is_valid_sed_n_arg(argv[2]):
303
- return SafetyCheckResult(True)
304
- # Allow simple text replacement: sed 's/old/new/g' file
305
- # or sed -i 's/old/new/g' file for in-place editing
306
- if len(argv) >= 3:
307
- # Find the sed script argument (usually starts with 's/')
308
- for arg in argv[1:]:
309
- if arg.startswith("s/") or arg.startswith("s|"):
310
- # Basic safety check: no command execution in replacement
311
- if ";" in arg:
312
- return SafetyCheckResult(False, f"sed: Command separator ';' not allowed in '{arg}'")
313
- if "`" in arg:
314
- return SafetyCheckResult(False, f"sed: Backticks not allowed in '{arg}'")
315
- if "$(" in arg:
316
- return SafetyCheckResult(False, f"sed: Command substitution not allowed in '{arg}'")
317
- return SafetyCheckResult(True)
318
- return SafetyCheckResult(
319
- False,
320
- "sed: Only text replacement (s/old/new/) or line printing (-n 'Np') is allowed",
321
- )
322
-
323
- if cmd0 == "awk":
324
- return _is_safe_awk_argv(argv)
325
-
326
155
  # Default allow when command is not explicitly restricted
327
156
  return SafetyCheckResult(True)
328
157
 
@@ -330,30 +159,16 @@ def _is_safe_argv(argv: list[str]) -> SafetyCheckResult:
330
159
  def is_safe_command(command: str) -> SafetyCheckResult:
331
160
  """Determine if a command is safe enough to run.
332
161
 
333
- The check is intentionally lightweight: it blocks only a small set of
334
- obviously dangerous patterns (rm/trash/git remotes, unsafe sed/awk,
335
- find -exec/-delete, etc.) and otherwise lets the real shell surface
336
- syntax errors (for example, unmatched quotes in complex multiline
337
- scripts).
162
+ Only rm and trash commands are checked for safety. All other commands
163
+ are allowed by default.
338
164
  """
339
-
340
- # Try to parse into an argv-style list first. If this fails (e.g. due
341
- # to unterminated quotes in a complex heredoc), treat the command as
342
- # safe here and let bash itself perform syntax checking instead of
343
- # blocking execution pre-emptively.
344
165
  try:
345
166
  argv = shlex.split(command, posix=True)
346
167
  except ValueError:
347
- # If we cannot reliably parse the command (e.g. due to unterminated
348
- # quotes in a complex heredoc), treat it as safe here and let the
349
- # real shell surface any syntax errors instead of blocking execution
350
- # pre-emptively.
168
+ # If we cannot reliably parse the command, treat it as safe here
169
+ # and let the real shell surface any syntax errors
351
170
  return SafetyCheckResult(True)
352
171
 
353
- # All further safety checks are done directly on the parsed argv via
354
- # _is_safe_argv. We intentionally avoid trying to re-interpret complex
355
- # shell sequences here and rely on the real shell to handle syntax.
356
-
357
172
  if not argv:
358
173
  return SafetyCheckResult(False, "Empty command")
359
174
 
klaude_code/core/turn.py CHANGED
@@ -348,7 +348,7 @@ class TurnExecutor:
348
348
  style="red",
349
349
  debug_type=DebugType.RESPONSE,
350
350
  )
351
- case message.ToolCallStartItem() as msg:
351
+ case message.ToolCallStartDelta() as msg:
352
352
  if thinking_active:
353
353
  thinking_active = False
354
354
  yield events.ThinkingEndEvent(
@@ -169,7 +169,7 @@ async def parse_anthropic_stream(
169
169
  match event.content_block:
170
170
  case BetaToolUseBlock() as block:
171
171
  metadata_tracker.record_token()
172
- yield message.ToolCallStartItem(
172
+ yield message.ToolCallStartDelta(
173
173
  response_id=response_id,
174
174
  call_id=block.id,
175
175
  name=block.name,
@@ -242,7 +242,7 @@ async def parse_google_stream(
242
242
 
243
243
  if call_id not in started_tool_items:
244
244
  started_tool_items.add(call_id)
245
- yield message.ToolCallStartItem(response_id=response_id, call_id=call_id, name=name)
245
+ yield message.ToolCallStartDelta(response_id=response_id, call_id=call_id, name=name)
246
246
 
247
247
  args_obj = getattr(function_call, "args", None)
248
248
  if args_obj is not None: