klaude-code 2.5.3__py3-none-any.whl → 2.7.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 (60) hide show
  1. klaude_code/app/runtime.py +1 -1
  2. klaude_code/auth/__init__.py +10 -0
  3. klaude_code/auth/env.py +81 -0
  4. klaude_code/cli/auth_cmd.py +87 -8
  5. klaude_code/cli/config_cmd.py +5 -5
  6. klaude_code/cli/cost_cmd.py +159 -60
  7. klaude_code/cli/main.py +146 -65
  8. klaude_code/cli/self_update.py +7 -7
  9. klaude_code/config/builtin_config.py +23 -9
  10. klaude_code/config/config.py +19 -9
  11. klaude_code/const.py +10 -1
  12. klaude_code/core/reminders.py +4 -5
  13. klaude_code/core/turn.py +8 -9
  14. klaude_code/llm/google/client.py +12 -0
  15. klaude_code/llm/openai_compatible/stream.py +5 -1
  16. klaude_code/llm/openrouter/client.py +1 -0
  17. klaude_code/protocol/commands.py +0 -1
  18. klaude_code/protocol/events.py +214 -0
  19. klaude_code/protocol/sub_agent/image_gen.py +0 -4
  20. klaude_code/session/session.py +51 -18
  21. klaude_code/skill/loader.py +12 -13
  22. klaude_code/skill/manager.py +3 -3
  23. klaude_code/tui/command/__init__.py +1 -4
  24. klaude_code/tui/command/copy_cmd.py +1 -1
  25. klaude_code/tui/command/fork_session_cmd.py +4 -4
  26. klaude_code/tui/commands.py +0 -5
  27. klaude_code/tui/components/command_output.py +1 -1
  28. klaude_code/tui/components/metadata.py +4 -5
  29. klaude_code/tui/components/rich/markdown.py +60 -0
  30. klaude_code/tui/components/rich/theme.py +8 -0
  31. klaude_code/tui/components/sub_agent.py +6 -0
  32. klaude_code/tui/components/user_input.py +38 -27
  33. klaude_code/tui/display.py +11 -1
  34. klaude_code/tui/input/AGENTS.md +44 -0
  35. klaude_code/tui/input/completers.py +21 -21
  36. klaude_code/tui/input/drag_drop.py +197 -0
  37. klaude_code/tui/input/images.py +227 -0
  38. klaude_code/tui/input/key_bindings.py +173 -19
  39. klaude_code/tui/input/paste.py +71 -0
  40. klaude_code/tui/input/prompt_toolkit.py +13 -3
  41. klaude_code/tui/machine.py +90 -56
  42. klaude_code/tui/renderer.py +1 -62
  43. klaude_code/tui/runner.py +1 -1
  44. klaude_code/tui/terminal/image.py +40 -9
  45. klaude_code/tui/terminal/selector.py +52 -2
  46. {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/METADATA +32 -40
  47. {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/RECORD +49 -54
  48. klaude_code/cli/session_cmd.py +0 -87
  49. klaude_code/protocol/events/__init__.py +0 -63
  50. klaude_code/protocol/events/base.py +0 -18
  51. klaude_code/protocol/events/chat.py +0 -30
  52. klaude_code/protocol/events/lifecycle.py +0 -23
  53. klaude_code/protocol/events/metadata.py +0 -16
  54. klaude_code/protocol/events/streaming.py +0 -43
  55. klaude_code/protocol/events/system.py +0 -56
  56. klaude_code/protocol/events/tools.py +0 -27
  57. klaude_code/tui/command/terminal_setup_cmd.py +0 -248
  58. klaude_code/tui/input/clipboard.py +0 -152
  59. {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/WHEEL +0 -0
  60. {klaude_code-2.5.3.dist-info → klaude_code-2.7.0.dist-info}/entry_points.txt +0 -0
klaude_code/cli/main.py CHANGED
@@ -1,114 +1,186 @@
1
1
  import asyncio
2
2
  import sys
3
+ from collections.abc import Sequence
4
+ from typing import Any
3
5
 
4
6
  import typer
7
+ from typer.core import TyperGroup
5
8
 
6
9
  from klaude_code.cli.auth_cmd import register_auth_commands
7
10
  from klaude_code.cli.config_cmd import register_config_commands
8
11
  from klaude_code.cli.cost_cmd import register_cost_commands
9
12
  from klaude_code.cli.debug import DEBUG_FILTER_HELP, prepare_debug_logging
10
- from klaude_code.cli.self_update import register_self_update_commands, version_option_callback
11
- from klaude_code.cli.session_cmd import register_session_commands
13
+ from klaude_code.cli.self_update import register_self_upgrade_commands, version_option_callback
12
14
  from klaude_code.session import Session
13
15
  from klaude_code.tui.command.resume_cmd import select_session_sync
14
16
  from klaude_code.ui.terminal.title import update_terminal_title
15
17
 
16
- ENV_HELP_LINES = [
17
- "Environment Variables:",
18
- "",
19
- "Provider API keys (built-in config):",
20
- " ANTHROPIC_API_KEY Anthropic API key",
21
- " OPENAI_API_KEY OpenAI API key",
22
- " OPENROUTER_API_KEY OpenRouter API key",
23
- " GOOGLE_API_KEY Google API key (Gemini)",
24
- " DEEPSEEK_API_KEY DeepSeek API key",
25
- " MOONSHOT_API_KEY Moonshot API key (Kimi)",
26
- "",
27
- "AWS credentials (Bedrock):",
28
- " AWS_ACCESS_KEY_ID AWS access key id",
29
- " AWS_SECRET_ACCESS_KEY AWS secret access key",
30
- " AWS_REGION AWS region",
31
- "",
32
- "Tool limits (Read):",
33
- " KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)",
34
- " KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)",
35
- " KLAUDE_READ_MAX_IMAGE_BYTES Max image bytes to read (default: 4MB)",
36
- " KLAUDE_IMAGE_OUTPUT_MAX_BYTES Max decoded image bytes (default: 64MB)",
37
- "",
38
- "Notifications / testing:",
39
- " KLAUDE_NOTIFY Set to 0/off/false/disable(d) to disable task notifications",
40
- " KLAUDE_TEST_SIGNAL In tmux, emit `tmux wait-for -S <channel>` on task completion",
41
- " TMUX Auto-detected; required for KLAUDE_TEST_SIGNAL",
42
- "",
43
- "Editor / terminal integration:",
44
- " EDITOR Preferred editor for `klaude config`",
45
- " TERM Terminal identification (auto-detected)",
46
- " TERM_PROGRAM Terminal identification (auto-detected)",
47
- " WT_SESSION Terminal hint (auto-detected)",
48
- " VTE_VERSION Terminal hint (auto-detected)",
49
- " GHOSTTY_RESOURCES_DIR Ghostty detection (auto-detected)",
50
- "",
51
- "Compatibility:",
52
- " ANTHROPIC_AUTH_TOKEN Reserved by anthropic SDK; temporarily unset during client init",
53
- ]
54
-
55
- ENV_HELP = "\n\n".join(ENV_HELP_LINES)
18
+
19
+ def _build_env_help() -> str:
20
+ from klaude_code.config.builtin_config import SUPPORTED_API_KEYS
21
+
22
+ lines = [
23
+ "Environment Variables:",
24
+ "",
25
+ "Provider API keys (built-in config):",
26
+ ]
27
+ # Calculate max env_var length for alignment
28
+ max_len = max(len(k.env_var) for k in SUPPORTED_API_KEYS)
29
+ for k in SUPPORTED_API_KEYS:
30
+ lines.append(f" {k.env_var:<{max_len}} {k.description}")
31
+ lines.extend(
32
+ [
33
+ "",
34
+ "Tool limits (Read):",
35
+ " KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)",
36
+ " KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)",
37
+ " KLAUDE_READ_MAX_IMAGE_BYTES Max image bytes to read (default: 4MB)",
38
+ " KLAUDE_IMAGE_OUTPUT_MAX_BYTES Max decoded image bytes (default: 64MB)",
39
+ ]
40
+ )
41
+ return "\n\n".join(lines)
42
+
43
+
44
+ ENV_HELP = _build_env_help()
45
+
46
+
47
+ def _looks_like_flag(token: str) -> bool:
48
+ return token.startswith("-") and token != "-"
49
+
50
+
51
+ def _preprocess_cli_args(args: list[str]) -> list[str]:
52
+ """Rewrite CLI args to support optional values for selected options.
53
+
54
+ Supported rewrites:
55
+ - --model / -m with no value -> --model-select
56
+ - --resume / -r with value -> --resume-by-id <value>
57
+ """
58
+
59
+ rewritten: list[str] = []
60
+ i = 0
61
+ while i < len(args):
62
+ token = args[i]
63
+
64
+ if token in {"--model", "-m"}:
65
+ next_token = args[i + 1] if i + 1 < len(args) else None
66
+ if next_token is None or next_token == "--" or _looks_like_flag(next_token):
67
+ rewritten.append("--model-select")
68
+ i += 1
69
+ continue
70
+ rewritten.append(token)
71
+ i += 1
72
+ continue
73
+
74
+ if token.startswith("--model="):
75
+ value = token.split("=", 1)[1]
76
+ if value == "":
77
+ rewritten.append("--model-select")
78
+ else:
79
+ rewritten.append(token)
80
+ i += 1
81
+ continue
82
+
83
+ if token in {"--resume", "-r"}:
84
+ next_token = args[i + 1] if i + 1 < len(args) else None
85
+ if next_token is not None and next_token != "--" and not _looks_like_flag(next_token):
86
+ rewritten.extend(["--resume-by-id", next_token])
87
+ i += 2
88
+ continue
89
+ rewritten.append(token)
90
+ i += 1
91
+ continue
92
+
93
+ if token.startswith("--resume="):
94
+ value = token.split("=", 1)[1]
95
+ rewritten.extend(["--resume-by-id", value])
96
+ i += 1
97
+ continue
98
+
99
+ rewritten.append(token)
100
+ i += 1
101
+
102
+ return rewritten
103
+
104
+
105
+ class _PreprocessingTyperGroup(TyperGroup):
106
+ def main(
107
+ self,
108
+ args: Sequence[str] | None = None,
109
+ prog_name: str | None = None,
110
+ complete_var: str | None = None,
111
+ standalone_mode: bool = True,
112
+ windows_expand_args: bool = True,
113
+ **extra: Any,
114
+ ) -> Any:
115
+ click_args = _preprocess_cli_args(list(args) if args is not None else sys.argv[1:])
116
+ return super().main(
117
+ args=click_args,
118
+ prog_name=prog_name,
119
+ complete_var=complete_var,
120
+ standalone_mode=standalone_mode,
121
+ windows_expand_args=windows_expand_args,
122
+ **extra,
123
+ )
124
+
56
125
 
57
126
  app = typer.Typer(
127
+ cls=_PreprocessingTyperGroup,
58
128
  add_completion=False,
59
129
  pretty_exceptions_enable=False,
60
130
  no_args_is_help=False,
61
131
  rich_markup_mode="rich",
62
132
  epilog=ENV_HELP,
133
+ context_settings={"help_option_names": ["-h", "--help"]},
63
134
  )
64
135
 
65
136
  # Register subcommands from modules
66
- register_session_commands(app)
67
137
  register_auth_commands(app)
68
138
  register_config_commands(app)
69
139
  register_cost_commands(app)
140
+ register_self_upgrade_commands(app)
70
141
 
71
- register_self_update_commands(app)
142
+
143
+ @app.command("help", hidden=True)
144
+ def help_command(ctx: typer.Context) -> None:
145
+ """Show help message."""
146
+ print(ctx.parent.get_help() if ctx.parent else ctx.get_help())
72
147
 
73
148
 
74
149
  @app.callback(invoke_without_command=True)
75
150
  def main_callback(
76
151
  ctx: typer.Context,
77
- version: bool = typer.Option(
78
- False,
79
- "--version",
80
- "-V",
81
- "-v",
82
- help="Show version and exit",
83
- callback=version_option_callback,
84
- is_eager=True,
85
- ),
86
152
  model: str | None = typer.Option(
87
153
  None,
88
154
  "--model",
89
155
  "-m",
90
- help="Override model config name (uses main model by default)",
156
+ help="Select model by name; use --model with no value to choose interactively",
91
157
  rich_help_panel="LLM",
92
158
  ),
93
- continue_: bool = typer.Option(False, "--continue", "-c", help="Continue from latest session"),
94
- resume: bool = typer.Option(False, "--resume", "-r", help="Select a session to resume for this project"),
159
+ continue_: bool = typer.Option(False, "--continue", "-c", help="Resume latest session"),
160
+ resume: bool = typer.Option(
161
+ False,
162
+ "--resume",
163
+ "-r",
164
+ help="Resume a session; use --resume <id> to resume directly, or --resume to pick interactively",
165
+ ),
95
166
  resume_by_id: str | None = typer.Option(
96
167
  None,
97
168
  "--resume-by-id",
98
- help="Resume a session by its ID (must exist)",
169
+ help="Resume session by ID",
170
+ hidden=True,
99
171
  ),
100
172
  select_model: bool = typer.Option(
101
173
  False,
102
- "--select-model",
103
- "-s",
104
- help="Interactively choose a model at startup",
174
+ "--model-select",
175
+ help="Choose model interactively (same as --model with no value)",
176
+ hidden=True,
105
177
  rich_help_panel="LLM",
106
178
  ),
107
179
  debug: bool = typer.Option(
108
180
  False,
109
181
  "--debug",
110
182
  "-d",
111
- help="Enable debug mode",
183
+ help="Enable debug logging",
112
184
  rich_help_panel="Debug",
113
185
  ),
114
186
  debug_filter: str | None = typer.Option(
@@ -120,14 +192,23 @@ def main_callback(
120
192
  vanilla: bool = typer.Option(
121
193
  False,
122
194
  "--vanilla",
123
- 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.",
195
+ help="Minimal mode: basic tools only, no system prompts",
124
196
  ),
125
197
  banana: bool = typer.Option(
126
198
  False,
127
199
  "--banana",
128
- help="Image generation mode with Nano Banana",
200
+ help="Image generation mode (alias for --model banana)",
129
201
  rich_help_panel="LLM",
130
202
  ),
203
+ version: bool = typer.Option(
204
+ False,
205
+ "--version",
206
+ "-V",
207
+ "-v",
208
+ help="Show version and exit",
209
+ callback=version_option_callback,
210
+ is_eager=True,
211
+ ),
131
212
  ) -> None:
132
213
  # Only run interactive mode when no subcommand is invoked
133
214
  if ctx.invoked_subcommand is None:
@@ -139,11 +220,11 @@ def main_callback(
139
220
 
140
221
  resume_by_id_value = resume_by_id.strip() if resume_by_id is not None else None
141
222
  if resume_by_id_value == "":
142
- log(("Error: --resume-by-id cannot be empty", "red"))
223
+ log(("Error: --resume <id> cannot be empty", "red"))
143
224
  raise typer.Exit(2)
144
225
 
145
226
  if resume_by_id_value is not None and (resume or continue_):
146
- log(("Error: --resume-by-id cannot be combined with --resume/--continue", "red"))
227
+ log(("Error: --resume <id> cannot be combined with --continue or interactive --resume", "red"))
147
228
  raise typer.Exit(2)
148
229
 
149
230
  if resume_by_id_value is not None and not Session.exists(resume_by_id_value):
@@ -35,14 +35,14 @@ def version_command() -> None:
35
35
  _print_version()
36
36
 
37
37
 
38
- def update_command(
38
+ def upgrade_command(
39
39
  check: bool = typer.Option(
40
40
  False,
41
41
  "--check",
42
- help="Check for updates and exit without upgrading",
42
+ help="Check only, don't upgrade",
43
43
  ),
44
44
  ) -> None:
45
- """Upgrade klaude-code when installed via `uv tool`."""
45
+ """Upgrade to latest version"""
46
46
 
47
47
  info = check_for_updates_blocking()
48
48
 
@@ -79,9 +79,9 @@ def update_command(
79
79
  log("Update complete. Please re-run `klaude` to use the new version.")
80
80
 
81
81
 
82
- def register_self_update_commands(app: typer.Typer) -> None:
82
+ def register_self_upgrade_commands(app: typer.Typer) -> None:
83
83
  """Register self-update and version subcommands to the given Typer app."""
84
84
 
85
- app.command("update")(update_command)
86
- app.command("upgrade", help="Alias for `klaude update`.")(update_command)
87
- app.command("version", help="Alias for `klaude --version`.")(version_command)
85
+ app.command("upgrade")(upgrade_command)
86
+ app.command("update", hidden=True)(upgrade_command)
87
+ app.command("version", hidden=True)(version_command)
@@ -5,6 +5,7 @@ environment variables (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) without
5
5
  manually configuring providers.
6
6
  """
7
7
 
8
+ from dataclasses import dataclass
8
9
  from functools import lru_cache
9
10
  from importlib import resources
10
11
  from typing import TYPE_CHECKING, Any
@@ -14,15 +15,28 @@ import yaml
14
15
  if TYPE_CHECKING:
15
16
  from klaude_code.config.config import ProviderConfig
16
17
 
17
- # All supported API key environment variables
18
- SUPPORTED_API_KEY_ENVS = [
19
- "ANTHROPIC_API_KEY",
20
- "GOOGLE_API_KEY",
21
- "OPENAI_API_KEY",
22
- "OPENROUTER_API_KEY",
23
- "DEEPSEEK_API_KEY",
24
- "MOONSHOT_API_KEY",
25
- ]
18
+
19
+ @dataclass(frozen=True)
20
+ class ApiKeyInfo:
21
+ """Information about a supported API key."""
22
+
23
+ env_var: str
24
+ name: str
25
+ description: str
26
+
27
+
28
+ # All supported API keys with their metadata
29
+ SUPPORTED_API_KEYS: tuple[ApiKeyInfo, ...] = (
30
+ ApiKeyInfo("ANTHROPIC_API_KEY", "Anthropic", "Anthropic API key"),
31
+ ApiKeyInfo("OPENAI_API_KEY", "OpenAI", "OpenAI API key"),
32
+ ApiKeyInfo("OPENROUTER_API_KEY", "OpenRouter", "OpenRouter API key"),
33
+ ApiKeyInfo("GOOGLE_API_KEY", "Google Gemini", "Google API key (Gemini)"),
34
+ ApiKeyInfo("DEEPSEEK_API_KEY", "DeepSeek", "DeepSeek API key"),
35
+ ApiKeyInfo("MOONSHOT_API_KEY", "Moonshot Kimi", "Moonshot API key (Kimi)"),
36
+ )
37
+
38
+ # For backwards compatibility
39
+ SUPPORTED_API_KEY_ENVS = [k.env_var for k in SUPPORTED_API_KEYS]
26
40
 
27
41
 
28
42
  @lru_cache(maxsize=1)
@@ -8,8 +8,9 @@ from typing import Any, cast
8
8
  import yaml
9
9
  from pydantic import BaseModel, Field, ValidationError, model_validator
10
10
 
11
+ from klaude_code.auth.env import get_auth_env
11
12
  from klaude_code.config.builtin_config import (
12
- SUPPORTED_API_KEY_ENVS,
13
+ SUPPORTED_API_KEYS,
13
14
  get_builtin_provider_configs,
14
15
  get_builtin_sub_agent_models,
15
16
  )
@@ -26,7 +27,8 @@ def parse_env_var_syntax(value: str | None) -> tuple[str | None, str | None]:
26
27
 
27
28
  Returns:
28
29
  A tuple of (env_var_name, resolved_value).
29
- - If value uses ${ENV_VAR} syntax: (env_var_name, os.environ.get(env_var_name))
30
+ - If value uses ${ENV_VAR} syntax: (env_var_name, resolved_value)
31
+ Priority: os.environ > klaude-auth.json env section
30
32
  - If value is a plain string: (None, value)
31
33
  - If value is None: (None, None)
32
34
  """
@@ -36,7 +38,9 @@ def parse_env_var_syntax(value: str | None) -> tuple[str | None, str | None]:
36
38
  match = _ENV_VAR_PATTERN.match(value)
37
39
  if match:
38
40
  env_var_name = match.group(1)
39
- return env_var_name, os.environ.get(env_var_name)
41
+ # Priority: real env var > auth.json env section
42
+ resolved = os.environ.get(env_var_name) or get_auth_env(env_var_name)
43
+ return env_var_name, resolved
40
44
 
41
45
  return None, value
42
46
 
@@ -613,17 +617,23 @@ def load_config() -> Config:
613
617
 
614
618
  def print_no_available_models_hint() -> None:
615
619
  """Print helpful message when no models are available due to missing API keys."""
616
- log("No available models. Please set one of the following environment variables:", style="yellow")
620
+ log("No available models. Configure an API key using one of these methods:", style="yellow")
617
621
  log("")
618
- for env_var in SUPPORTED_API_KEY_ENVS:
619
- current_value = os.environ.get(env_var)
622
+ log("Option 1: Use klaude auth login", style="bold")
623
+ # Use first word of name for brevity
624
+ names = [k.name.split()[0].lower() for k in SUPPORTED_API_KEYS]
625
+ log(f" klaude auth login <provider> (providers: {', '.join(names)})", style="dim")
626
+ log("")
627
+ log("Option 2: Set environment variables", style="bold")
628
+ max_len = max(len(k.env_var) for k in SUPPORTED_API_KEYS)
629
+ for key_info in SUPPORTED_API_KEYS:
630
+ current_value = os.environ.get(key_info.env_var) or get_auth_env(key_info.env_var)
620
631
  if current_value:
621
- log(f" {env_var} = (set)", style="green")
632
+ log(f" {key_info.env_var:<{max_len}} (set)", style="green")
622
633
  else:
623
- log(f" export {env_var}=<your-api-key>", style="dim")
634
+ log(f" {key_info.env_var:<{max_len}} {key_info.description}", style="dim")
624
635
  log("")
625
636
  log(f"Or add custom providers in: {config_path}", style="dim")
626
- log(f"See example config: {example_config_path}", style="dim")
627
637
 
628
638
 
629
639
  # Expose cache control for tests and callers that need to invalidate the cache.
klaude_code/const.py CHANGED
@@ -7,6 +7,8 @@ that were previously scattered across the codebase.
7
7
  from __future__ import annotations
8
8
 
9
9
  import os
10
+ import sys
11
+ import tempfile
10
12
  from dataclasses import dataclass
11
13
  from pathlib import Path
12
14
 
@@ -22,6 +24,13 @@ def _get_int_env(name: str, default: int) -> int:
22
24
  return default
23
25
 
24
26
 
27
+ def get_system_temp() -> str:
28
+ """Return system-level temp directory: /tmp on Unix, system temp on Windows."""
29
+ if sys.platform == "win32":
30
+ return tempfile.gettempdir()
31
+ return "/tmp"
32
+
33
+
25
34
  # =============================================================================
26
35
  # Agent / LLM Configuration
27
36
  # =============================================================================
@@ -115,7 +124,7 @@ TOOL_OUTPUT_DISPLAY_TAIL = 10000 # Characters to show from the end of truncated
115
124
  TOOL_OUTPUT_MAX_LINES = 2000 # Maximum lines for tool output before truncation
116
125
  TOOL_OUTPUT_DISPLAY_HEAD_LINES = 1000 # Lines to show from the beginning of truncated output
117
126
  TOOL_OUTPUT_DISPLAY_TAIL_LINES = 1000 # Lines to show from the end of truncated output
118
- TOOL_OUTPUT_TRUNCATION_DIR = "/tmp" # Directory for saving full truncated output
127
+ TOOL_OUTPUT_TRUNCATION_DIR = get_system_temp() # Directory for saving full truncated output
119
128
 
120
129
 
121
130
  # =============================================================================
@@ -17,8 +17,8 @@ from klaude_code.skill import get_skill
17
17
  # Match @ preceded by whitespace, start of line, or → (ReadTool line number arrow)
18
18
  AT_FILE_PATTERN = re.compile(r'(?:(?<!\S)|(?<=\u2192))@("(?P<quoted>[^\"]+)"|(?P<plain>\S+))')
19
19
 
20
- # Match $skill or ¥skill at the beginning of the first line
21
- SKILL_PATTERN = re.compile(r"^[$¥](?P<skill>\S+)")
20
+ # Match $skill or ¥skill inline (at start of line or after whitespace)
21
+ SKILL_PATTERN = re.compile(r"(?:^|\s)[$¥](?P<skill>\S+)")
22
22
 
23
23
 
24
24
  def get_last_new_user_input(session: Session) -> str | None:
@@ -79,14 +79,13 @@ def get_at_patterns_with_source(session: Session) -> list[AtPatternSource]:
79
79
 
80
80
 
81
81
  def get_skill_from_user_input(session: Session) -> str | None:
82
- """Get $skill reference from the first line of last user input."""
82
+ """Get $skill reference from last user input (first match wins)."""
83
83
  for item in reversed(session.conversation_history):
84
84
  if isinstance(item, message.ToolResultMessage):
85
85
  return None
86
86
  if isinstance(item, message.UserMessage):
87
87
  content = message.join_text_parts(item.parts)
88
- first_line = content.split("\n", 1)[0]
89
- m = SKILL_PATTERN.match(first_line)
88
+ m = SKILL_PATTERN.search(content)
90
89
  if m:
91
90
  return m.group("skill")
92
91
  return None
klaude_code/core/turn.py CHANGED
@@ -25,10 +25,12 @@ from klaude_code.log import DebugType, log_debug
25
25
  from klaude_code.protocol import events, llm_param, message, model, tools
26
26
 
27
27
  # Protocols that support prefill (continuing from partial assistant message)
28
- _PREFILL_SUPPORTED_PROTOCOLS = frozenset({
29
- "anthropic",
30
- "claude_oauth",
31
- })
28
+ _PREFILL_SUPPORTED_PROTOCOLS = frozenset(
29
+ {
30
+ "anthropic",
31
+ "claude_oauth",
32
+ }
33
+ )
32
34
 
33
35
 
34
36
  class TurnError(Exception):
@@ -183,6 +185,7 @@ class TurnExecutor:
183
185
 
184
186
  if self._turn_result.stream_error is not None:
185
187
  # Save accumulated content for potential prefill on retry (only for supported protocols)
188
+ session_ctx.append_history([self._turn_result.stream_error])
186
189
  protocol = ctx.llm_client.get_llm_config().protocol
187
190
  supports_prefill = protocol.value in _PREFILL_SUPPORTED_PROTOCOLS
188
191
  if (
@@ -193,8 +196,7 @@ class TurnExecutor:
193
196
  ):
194
197
  session_ctx.append_history([self._turn_result.assistant_message])
195
198
  # Add continuation prompt to avoid Anthropic thinking block requirement
196
- session_ctx.append_history([message.UserMessage(parts=[message.TextPart(text="continue")])])
197
- session_ctx.append_history([self._turn_result.stream_error])
199
+ session_ctx.append_history([message.UserMessage(parts=[message.TextPart(text="<system>continue</system>")])])
198
200
  yield events.TurnEndEvent(session_id=session_ctx.session_id)
199
201
  raise TurnError(self._turn_result.stream_error.error)
200
202
 
@@ -249,9 +251,6 @@ class TurnExecutor:
249
251
  image_size = generation.get("image_size")
250
252
  if image_size in SUPPORTED_IMAGE_SIZES:
251
253
  image_config.image_size = image_size
252
- extra = generation.get("extra")
253
- if isinstance(extra, dict) and extra:
254
- image_config.extra = extra
255
254
  if image_config.model_dump(exclude_none=True):
256
255
  call_param.image_config = image_config
257
256
 
@@ -25,6 +25,9 @@ from google.genai.types import (
25
25
  ThinkingLevel,
26
26
  ToolConfig,
27
27
  )
28
+ from google.genai.types import (
29
+ ImageConfig as GoogleImageConfig,
30
+ )
28
31
 
29
32
  from klaude_code.llm.client import LLMClientABC, LLMStreamABC
30
33
  from klaude_code.llm.google.input import convert_history_to_contents, convert_tool_schema
@@ -91,6 +94,14 @@ def _build_config(param: llm_param.LLMCallParameter) -> GenerateContentConfig:
91
94
  if param.thinking.reasoning_effort:
92
95
  thinking_config.thinking_level = convert_gemini_thinking_level(param.thinking.reasoning_effort)
93
96
 
97
+ # ImageGen per-call overrides
98
+ image_config: GoogleImageConfig | None = None
99
+ if param.image_config is not None:
100
+ image_config = GoogleImageConfig(
101
+ aspect_ratio=param.image_config.aspect_ratio,
102
+ image_size=param.image_config.image_size,
103
+ )
104
+
94
105
  return GenerateContentConfig(
95
106
  system_instruction=param.system,
96
107
  temperature=param.temperature,
@@ -98,6 +109,7 @@ def _build_config(param: llm_param.LLMCallParameter) -> GenerateContentConfig:
98
109
  tools=cast(Any, tool_list) if tool_list else None,
99
110
  tool_config=tool_config,
100
111
  thinking_config=thinking_config,
112
+ image_config=image_config,
101
113
  )
102
114
 
103
115
 
@@ -199,6 +199,7 @@ async def parse_chat_completions_stream(
199
199
  metadata_tracker: MetadataTracker,
200
200
  reasoning_handler: ReasoningHandlerABC,
201
201
  on_event: Callable[[object], None] | None = None,
202
+ provider_prefix: str = "",
202
203
  ) -> AsyncGenerator[message.LLMStreamItem]:
203
204
  """Parse OpenAI Chat Completions stream into stream items.
204
205
 
@@ -235,7 +236,7 @@ async def parse_chat_completions_stream(
235
236
  if event_model := getattr(event, "model", None):
236
237
  metadata_tracker.set_model_name(str(event_model))
237
238
  if provider := getattr(event, "provider", None):
238
- metadata_tracker.set_provider(str(provider))
239
+ metadata_tracker.set_provider(f"{provider_prefix}{provider}")
239
240
 
240
241
  choices = cast(Any, getattr(event, "choices", None))
241
242
  if not choices:
@@ -364,12 +365,14 @@ class OpenAILLMStream(LLMStreamABC):
364
365
  metadata_tracker: MetadataTracker,
365
366
  reasoning_handler: ReasoningHandlerABC,
366
367
  on_event: Callable[[object], None] | None = None,
368
+ provider_prefix: str = "",
367
369
  ) -> None:
368
370
  self._stream = stream
369
371
  self._param = param
370
372
  self._metadata_tracker = metadata_tracker
371
373
  self._reasoning_handler = reasoning_handler
372
374
  self._on_event = on_event
375
+ self._provider_prefix = provider_prefix
373
376
  self._state = StreamStateManager(
374
377
  param_model=str(param.model_id),
375
378
  )
@@ -386,6 +389,7 @@ class OpenAILLMStream(LLMStreamABC):
386
389
  metadata_tracker=self._metadata_tracker,
387
390
  reasoning_handler=self._reasoning_handler,
388
391
  on_event=self._on_event,
392
+ provider_prefix=self._provider_prefix,
389
393
  ):
390
394
  if isinstance(item, message.AssistantMessage):
391
395
  self._completed = True
@@ -145,4 +145,5 @@ class OpenRouterClient(LLMClientABC):
145
145
  metadata_tracker=metadata_tracker,
146
146
  reasoning_handler=reasoning_handler,
147
147
  on_event=on_event,
148
+ provider_prefix="openrouter/",
148
149
  )
@@ -20,7 +20,6 @@ class CommandName(str, Enum):
20
20
  COMPACT = "compact"
21
21
  REFRESH_TERMINAL = "refresh-terminal"
22
22
  CLEAR = "clear"
23
- TERMINAL_SETUP = "terminal-setup"
24
23
  EXPORT = "export"
25
24
  EXPORT_ONLINE = "export-online"
26
25
  STATUS = "status"