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.
- klaude_code/app/__init__.py +1 -2
- klaude_code/app/runtime.py +26 -41
- klaude_code/cli/main.py +19 -152
- klaude_code/config/assets/builtin_config.yaml +13 -0
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +28 -0
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/prompts/prompt-nano-banana.md +1 -0
- klaude_code/core/tool/shell/command_safety.py +4 -189
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/stream.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/message.py +2 -2
- klaude_code/tui/command/__init__.py +4 -4
- klaude_code/tui/command/clear_cmd.py +1 -1
- klaude_code/tui/command/command_abc.py +1 -2
- klaude_code/tui/command/copy_cmd.py +1 -2
- klaude_code/tui/command/fork_session_cmd.py +4 -4
- klaude_code/tui/command/refresh_cmd.py +1 -2
- klaude_code/tui/command/resume_cmd.py +3 -4
- klaude_code/tui/command/status_cmd.py +1 -1
- klaude_code/tui/components/developer.py +11 -11
- klaude_code/tui/components/metadata.py +1 -1
- klaude_code/tui/components/rich/theme.py +2 -2
- klaude_code/tui/components/user_input.py +9 -21
- klaude_code/tui/runner.py +2 -2
- klaude_code/tui/terminal/selector.py +3 -15
- klaude_code/ui/__init__.py +0 -24
- klaude_code/ui/common.py +3 -2
- klaude_code/ui/core/display.py +2 -2
- {klaude_code-2.1.1.dist-info → klaude_code-2.2.0.dist-info}/METADATA +16 -81
- {klaude_code-2.1.1.dist-info → klaude_code-2.2.0.dist-info}/RECORD +36 -37
- klaude_code/tui/command/prompt-commit.md +0 -82
- klaude_code/ui/exec_mode.py +0 -60
- {klaude_code-2.1.1.dist-info → klaude_code-2.2.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.1.1.dist-info → klaude_code-2.2.0.dist-info}/entry_points.txt +0 -0
klaude_code/app/__init__.py
CHANGED
|
@@ -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
|
|
7
|
+
from .runtime import AppInitConfig
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"AppInitConfig",
|
|
11
|
-
"run_exec",
|
|
12
11
|
]
|
klaude_code/app/runtime.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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 = "
|
|
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.
|
|
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
|
-
|
|
334
|
-
|
|
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
|
|
348
|
-
#
|
|
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.
|
|
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.
|
|
172
|
+
yield message.ToolCallStartDelta(
|
|
173
173
|
response_id=response_id,
|
|
174
174
|
call_id=block.id,
|
|
175
175
|
name=block.name,
|
klaude_code/llm/google/client.py
CHANGED
|
@@ -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.
|
|
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:
|