klaude-code 1.6.0__py3-none-any.whl → 1.7.1__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 (34) hide show
  1. klaude_code/cli/list_model.py +55 -4
  2. klaude_code/cli/main.py +10 -0
  3. klaude_code/cli/runtime.py +2 -2
  4. klaude_code/cli/session_cmd.py +3 -2
  5. klaude_code/command/fork_session_cmd.py +7 -0
  6. klaude_code/config/assets/builtin_config.yaml +61 -2
  7. klaude_code/config/builtin_config.py +1 -0
  8. klaude_code/config/config.py +19 -0
  9. klaude_code/config/thinking.py +14 -0
  10. klaude_code/const.py +17 -2
  11. klaude_code/core/executor.py +16 -3
  12. klaude_code/core/task.py +5 -3
  13. klaude_code/core/tool/shell/command_safety.py +3 -5
  14. klaude_code/llm/anthropic/client.py +127 -114
  15. klaude_code/llm/bedrock/__init__.py +3 -0
  16. klaude_code/llm/bedrock/client.py +60 -0
  17. klaude_code/llm/google/__init__.py +3 -0
  18. klaude_code/llm/google/client.py +309 -0
  19. klaude_code/llm/google/input.py +215 -0
  20. klaude_code/llm/registry.py +10 -5
  21. klaude_code/protocol/events.py +1 -0
  22. klaude_code/protocol/llm_param.py +9 -0
  23. klaude_code/session/export.py +14 -2
  24. klaude_code/session/session.py +52 -3
  25. klaude_code/session/store.py +3 -0
  26. klaude_code/session/templates/export_session.html +210 -18
  27. klaude_code/ui/modes/repl/input_prompt_toolkit.py +6 -46
  28. klaude_code/ui/modes/repl/renderer.py +5 -1
  29. klaude_code/ui/renderers/developer.py +1 -1
  30. klaude_code/ui/renderers/sub_agent.py +1 -1
  31. {klaude_code-1.6.0.dist-info → klaude_code-1.7.1.dist-info}/METADATA +82 -10
  32. {klaude_code-1.6.0.dist-info → klaude_code-1.7.1.dist-info}/RECORD +34 -29
  33. {klaude_code-1.6.0.dist-info → klaude_code-1.7.1.dist-info}/WHEEL +0 -0
  34. {klaude_code-1.6.0.dist-info → klaude_code-1.7.1.dist-info}/entry_points.txt +0 -0
@@ -6,7 +6,7 @@ from rich.table import Table
6
6
  from rich.text import Text
7
7
 
8
8
  from klaude_code.config import Config
9
- from klaude_code.config.config import ModelConfig, ProviderConfig
9
+ from klaude_code.config.config import ModelConfig, ProviderConfig, parse_env_var_syntax
10
10
  from klaude_code.protocol.llm_param import LLMClientProtocol
11
11
  from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
12
12
  from klaude_code.ui.rich.theme import ThemeKey, get_theme
@@ -94,6 +94,29 @@ def format_api_key_display(provider: ProviderConfig) -> Text:
94
94
  return Text("N/A")
95
95
 
96
96
 
97
+ def format_env_var_display(value: str | None) -> Text:
98
+ """Format environment variable display with warning if not set."""
99
+ env_var, resolved = parse_env_var_syntax(value)
100
+
101
+ if env_var:
102
+ # Using ${ENV_VAR} syntax
103
+ if resolved:
104
+ return Text.assemble(
105
+ (f"${{{env_var}}} = ", "dim"),
106
+ (mask_api_key(resolved), ""),
107
+ )
108
+ else:
109
+ return Text.assemble(
110
+ (f"${{{env_var}}} ", ""),
111
+ ("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
112
+ )
113
+ elif value:
114
+ # Plain value
115
+ return Text(mask_api_key(value))
116
+ else:
117
+ return Text("N/A")
118
+
119
+
97
120
  def _get_model_params_display(model: ModelConfig) -> list[Text]:
98
121
  """Get display elements for model parameters."""
99
122
  params: list[Text] = []
@@ -162,15 +185,43 @@ def display_models_and_providers(config: Config):
162
185
  format_api_key_display(provider),
163
186
  )
164
187
 
188
+ # AWS Bedrock parameters
189
+ if provider.protocol == LLMClientProtocol.BEDROCK:
190
+ if provider.aws_access_key:
191
+ provider_info.add_row(
192
+ Text("AWS Key:", style=ThemeKey.CONFIG_PARAM_LABEL),
193
+ format_env_var_display(provider.aws_access_key),
194
+ )
195
+ if provider.aws_secret_key:
196
+ provider_info.add_row(
197
+ Text("AWS Secret:", style=ThemeKey.CONFIG_PARAM_LABEL),
198
+ format_env_var_display(provider.aws_secret_key),
199
+ )
200
+ if provider.aws_region:
201
+ provider_info.add_row(
202
+ Text("AWS Region:", style=ThemeKey.CONFIG_PARAM_LABEL),
203
+ format_env_var_display(provider.aws_region),
204
+ )
205
+ if provider.aws_session_token:
206
+ provider_info.add_row(
207
+ Text("AWS Token:", style=ThemeKey.CONFIG_PARAM_LABEL),
208
+ format_env_var_display(provider.aws_session_token),
209
+ )
210
+ if provider.aws_profile:
211
+ provider_info.add_row(
212
+ Text("AWS Profile:", style=ThemeKey.CONFIG_PARAM_LABEL),
213
+ format_env_var_display(provider.aws_profile),
214
+ )
215
+
165
216
  # Check if provider has valid API key
166
217
  provider_available = not provider.is_api_key_missing()
167
218
 
168
219
  # Models table for this provider
169
220
  models_table = Table.grid(padding=(0, 1), expand=True)
170
221
  models_table.add_column(width=2, no_wrap=True) # Status
171
- models_table.add_column(overflow="fold", ratio=1) # Name
172
- models_table.add_column(overflow="fold", ratio=2) # Model
173
- models_table.add_column(overflow="fold", ratio=3) # Params
222
+ models_table.add_column(overflow="fold", ratio=2) # Name
223
+ models_table.add_column(overflow="fold", ratio=3) # Model
224
+ models_table.add_column(overflow="fold", ratio=4) # Params
174
225
 
175
226
  # Add header
176
227
  models_table.add_row(
klaude_code/cli/main.py CHANGED
@@ -95,10 +95,20 @@ def read_input_content(cli_argument: str) -> str | None:
95
95
  return content
96
96
 
97
97
 
98
+ ENV_HELP = """\
99
+ Environment Variables:
100
+
101
+ KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)
102
+
103
+ KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)
104
+ """
105
+
98
106
  app = typer.Typer(
99
107
  add_completion=False,
100
108
  pretty_exceptions_enable=False,
101
109
  no_args_is_help=False,
110
+ rich_markup_mode="rich",
111
+ epilog=ENV_HELP,
102
112
  )
103
113
 
104
114
  # Register subcommands from modules
@@ -379,7 +379,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
379
379
  model_name=model_name,
380
380
  save_as_default=False,
381
381
  defer_thinking_selection=True,
382
- emit_welcome_event=False,
382
+ emit_welcome_event=True,
383
383
  emit_switch_message=False,
384
384
  )
385
385
  )
@@ -398,7 +398,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
398
398
  op.ChangeThinkingOperation(
399
399
  session_id=sid,
400
400
  thinking=thinking,
401
- emit_welcome_event=False,
401
+ emit_welcome_event=True,
402
402
  emit_switch_message=False,
403
403
  )
404
404
  )
@@ -22,8 +22,9 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
22
22
  log(f"Sessions to delete ({len(sessions)}):")
23
23
  for s in sessions:
24
24
  msg_count_display = "N/A" if s.messages_count == -1 else str(s.messages_count)
25
- first_msg = (s.first_user_message or "").strip().replace("\n", " ")[:50]
26
- if len(s.first_user_message or "") > 50:
25
+ first_msg_text = s.user_messages[0] if s.user_messages else ""
26
+ first_msg = first_msg_text.strip().replace("\n", " ")[:50]
27
+ if len(first_msg_text) > 50:
27
28
  first_msg += "..."
28
29
  log(f" {_fmt(s.updated_at)} {msg_count_display:>3} msgs {first_msg}")
29
30
 
@@ -7,6 +7,7 @@ from prompt_toolkit.styles import Style
7
7
 
8
8
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
9
9
  from klaude_code.protocol import commands, events, model
10
+ from klaude_code.ui.modes.repl.clipboard import copy_to_clipboard
10
11
  from klaude_code.ui.terminal.selector import SelectItem, select_one
11
12
 
12
13
  FORK_SELECT_STYLE = Style(
@@ -215,6 +216,9 @@ class ForkSessionCommand(CommandABC):
215
216
  new_session = agent.session.fork()
216
217
  await new_session.wait_for_flush()
217
218
 
219
+ resume_cmd = f"klaude --resume-by-id {new_session.id}"
220
+ copy_to_clipboard(resume_cmd)
221
+
218
222
  event = events.DeveloperMessageEvent(
219
223
  session_id=agent.session.id,
220
224
  item=model.DeveloperMessageItem(
@@ -247,6 +251,9 @@ class ForkSessionCommand(CommandABC):
247
251
  # Build result message
248
252
  fork_description = "entire conversation" if selected is None else f"up to message index {selected}"
249
253
 
254
+ resume_cmd = f"klaude --resume-by-id {new_session.id}"
255
+ copy_to_clipboard(resume_cmd)
256
+
250
257
  event = events.DeveloperMessageEvent(
251
258
  session_id=agent.session.id,
252
259
  item=model.DeveloperMessageItem(
@@ -7,7 +7,7 @@ provider_list:
7
7
  protocol: anthropic
8
8
  api_key: ${ANTHROPIC_API_KEY}
9
9
  model_list:
10
- - model_name: sonnet
10
+ - model_name: sonnet@ant
11
11
  model_params:
12
12
  model: claude-sonnet-4-5-20250929
13
13
  context_limit: 200000
@@ -18,7 +18,7 @@ provider_list:
18
18
  output: 15.0
19
19
  cache_read: 0.3
20
20
  cache_write: 3.75
21
- - model_name: opus
21
+ - model_name: opus@ant
22
22
  model_params:
23
23
  model: claude-opus-4-5-20251101
24
24
  context_limit: 200000
@@ -87,6 +87,30 @@ provider_list:
87
87
  input: 1.75
88
88
  output: 14.0
89
89
  cache_read: 0.17
90
+ - model_name: gpt-5.2-medium
91
+ model_params:
92
+ model: openai/gpt-5.2
93
+ max_tokens: 128000
94
+ context_limit: 400000
95
+ verbosity: high
96
+ thinking:
97
+ reasoning_effort: medium
98
+ cost:
99
+ input: 1.75
100
+ output: 14.0
101
+ cache_read: 0.17
102
+ - model_name: gpt-5.2-low
103
+ model_params:
104
+ model: openai/gpt-5.2
105
+ max_tokens: 128000
106
+ context_limit: 400000
107
+ verbosity: low
108
+ thinking:
109
+ reasoning_effort: low
110
+ cost:
111
+ input: 1.75
112
+ output: 14.0
113
+ cache_read: 0.17
90
114
  - model_name: gpt-5.2-fast
91
115
  model_params:
92
116
  model: openai/gpt-5.2
@@ -194,6 +218,41 @@ provider_list:
194
218
  output: 1.74
195
219
  cache_read: 0.04
196
220
 
221
+ - provider_name: google
222
+ protocol: google
223
+ api_key: ${GOOGLE_API_KEY}
224
+ model_list:
225
+ - model_name: gemini-pro@google
226
+ model_params:
227
+ model: gemini-3-pro-preview
228
+ context_limit: 1048576
229
+ cost:
230
+ input: 2.0
231
+ output: 12.0
232
+ cache_read: 0.2
233
+ - model_name: gemini-flash@google
234
+ model_params:
235
+ model: gemini-3-flash-preview
236
+ context_limit: 1048576
237
+ cost:
238
+ input: 0.5
239
+ output: 3.0
240
+ cache_read: 0.05
241
+ - provider_name: bedrock
242
+ protocol: bedrock
243
+ aws_access_key: ${AWS_ACCESS_KEY_ID}
244
+ aws_secret_key: ${AWS_SECRET_ACCESS_KEY}
245
+ aws_region: ${AWS_REGION}
246
+ model_list:
247
+ - model_name: sonnet@bedrock
248
+ model_params:
249
+ model: us.anthropic.claude-sonnet-4-5-20250929-v1:0
250
+ context_limit: 200000
251
+ cost:
252
+ input: 3.0
253
+ output: 15.0
254
+ cache_read: 0.3
255
+ cache_write: 3.75
197
256
  - provider_name: deepseek
198
257
  protocol: anthropic
199
258
  api_key: ${DEEPSEEK_API_KEY}
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
17
17
  # All supported API key environment variables
18
18
  SUPPORTED_API_KEY_ENVS = [
19
19
  "ANTHROPIC_API_KEY",
20
+ "GOOGLE_API_KEY",
20
21
  "OPENAI_API_KEY",
21
22
  "OPENROUTER_API_KEY",
22
23
  "DEEPSEEK_API_KEY",
@@ -77,6 +77,7 @@ class ProviderConfig(llm_param.LLMConfigProviderParameter):
77
77
  """Check if the API key is missing (either not set or env var not found).
78
78
 
79
79
  For codex protocol, checks OAuth login status instead of API key.
80
+ For bedrock protocol, checks AWS credentials instead of API key.
80
81
  """
81
82
  from klaude_code.protocol.llm_param import LLMClientProtocol
82
83
 
@@ -89,6 +90,19 @@ class ProviderConfig(llm_param.LLMConfigProviderParameter):
89
90
  # Consider available if logged in and token not expired
90
91
  return state is None or state.is_expired()
91
92
 
93
+ if self.protocol == LLMClientProtocol.BEDROCK:
94
+ # Bedrock uses AWS credentials, not API key. Region is always required.
95
+ _, resolved_profile = parse_env_var_syntax(self.aws_profile)
96
+ _, resolved_region = parse_env_var_syntax(self.aws_region)
97
+
98
+ # When using profile, we still need region to initialize the client.
99
+ if resolved_profile:
100
+ return resolved_region is None
101
+
102
+ _, resolved_access_key = parse_env_var_syntax(self.aws_access_key)
103
+ _, resolved_secret_key = parse_env_var_syntax(self.aws_secret_key)
104
+ return resolved_region is None or resolved_access_key is None or resolved_secret_key is None
105
+
92
106
  return self.get_resolved_api_key() is None
93
107
 
94
108
 
@@ -243,6 +257,11 @@ def get_example_config() -> UserConfig:
243
257
  model="model-id-from-provider",
244
258
  max_tokens=16000,
245
259
  context_limit=200000,
260
+ cost=llm_param.Cost(
261
+ input=1,
262
+ output=10,
263
+ cache_read=0.1,
264
+ ),
246
265
  ),
247
266
  ),
248
267
  ],
@@ -121,6 +121,13 @@ def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
121
121
  return f"enabled (budget_tokens={thinking.budget_tokens})"
122
122
  return "not set"
123
123
 
124
+ if protocol == llm_param.LLMClientProtocol.GOOGLE:
125
+ if thinking.type == "disabled":
126
+ return "off"
127
+ if thinking.type == "enabled":
128
+ return f"enabled (budget_tokens={thinking.budget_tokens})"
129
+ return "not set"
130
+
124
131
  return "unknown protocol"
125
132
 
126
133
 
@@ -230,6 +237,13 @@ def get_thinking_picker_data(config: llm_param.LLMConfigParameter) -> ThinkingPi
230
237
  current_value=_get_current_budget_value(thinking),
231
238
  )
232
239
 
240
+ if protocol == llm_param.LLMClientProtocol.GOOGLE:
241
+ return ThinkingPickerData(
242
+ options=_build_budget_options(),
243
+ message="Select thinking level:",
244
+ current_value=_get_current_budget_value(thinking),
245
+ )
246
+
233
247
  return None
234
248
 
235
249
 
klaude_code/const.py CHANGED
@@ -4,8 +4,21 @@ This module consolidates all magic numbers and configuration values
4
4
  that were previously scattered across the codebase.
5
5
  """
6
6
 
7
+ import os
7
8
  from pathlib import Path
8
9
 
10
+
11
+ def _get_int_env(name: str, default: int) -> int:
12
+ """Get an integer value from environment variable, or return default."""
13
+ val = os.environ.get(name)
14
+ if val is None:
15
+ return default
16
+ try:
17
+ return int(val)
18
+ except ValueError:
19
+ return default
20
+
21
+
9
22
  # =============================================================================
10
23
  # Agent Configuration
11
24
  # =============================================================================
@@ -47,10 +60,12 @@ TODO_REMINDER_TOOL_CALL_THRESHOLD = 10
47
60
  READ_CHAR_LIMIT_PER_LINE = 2000
48
61
 
49
62
  # Maximum number of lines to read from a file
50
- READ_GLOBAL_LINE_CAP = 2000
63
+ # Can be overridden via KLAUDE_READ_GLOBAL_LINE_CAP environment variable
64
+ READ_GLOBAL_LINE_CAP = _get_int_env("KLAUDE_READ_GLOBAL_LINE_CAP", 2000)
51
65
 
52
66
  # Maximum total characters to read (truncates beyond this limit)
53
- READ_MAX_CHARS = 50000
67
+ # Can be overridden via KLAUDE_READ_MAX_CHARS environment variable
68
+ READ_MAX_CHARS = _get_int_env("KLAUDE_READ_MAX_CHARS", 50000)
54
69
 
55
70
  # Maximum image file size in bytes (4MB)
56
71
  READ_MAX_IMAGE_BYTES = 4 * 1024 * 1024
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import asyncio
11
11
  import subprocess
12
+ import sys
12
13
  from collections.abc import Callable
13
14
  from dataclasses import dataclass
14
15
  from pathlib import Path
@@ -427,14 +428,26 @@ class ExecutorContext:
427
428
  return build_export_html(agent.session, system_prompt, tool_schemas, model_name)
428
429
 
429
430
  def _open_file(self, path: Path) -> None:
431
+ # Select platform-appropriate command
432
+ if sys.platform == "darwin":
433
+ cmd = "open"
434
+ elif sys.platform == "win32":
435
+ cmd = "start"
436
+ else:
437
+ cmd = "xdg-open"
438
+
430
439
  try:
431
440
  # Detach stdin to prevent interference with prompt_toolkit's terminal state
432
- subprocess.run(["open", str(path)], stdin=subprocess.DEVNULL, check=True)
441
+ if sys.platform == "win32":
442
+ # Windows 'start' requires shell=True
443
+ subprocess.run(f'start "" "{path}"', shell=True, stdin=subprocess.DEVNULL, check=True)
444
+ else:
445
+ subprocess.run([cmd, str(path)], stdin=subprocess.DEVNULL, check=True)
433
446
  except FileNotFoundError as exc: # pragma: no cover
434
- msg = "`open` command not found; please open the HTML manually."
447
+ msg = f"`{cmd}` command not found; please open the HTML manually."
435
448
  raise RuntimeError(msg) from exc
436
449
  except subprocess.CalledProcessError as exc: # pragma: no cover
437
- msg = f"Failed to open HTML with `open`: {exc}"
450
+ msg = f"Failed to open HTML with `{cmd}`: {exc}"
438
451
  raise RuntimeError(msg) from exc
439
452
 
440
453
  async def handle_interrupt(self, operation: op.InterruptOperation) -> None:
klaude_code/core/task.py CHANGED
@@ -220,7 +220,9 @@ class TaskExecutor:
220
220
  error_msg = f"Retrying {attempt + 1}/{const.MAX_FAILED_TURN_RETRIES} in {delay:.1f}s"
221
221
  if last_error_message:
222
222
  error_msg = f"{error_msg} - {last_error_message}"
223
- yield events.ErrorEvent(error_message=error_msg, can_retry=True)
223
+ yield events.ErrorEvent(
224
+ error_message=error_msg, can_retry=True, session_id=session_ctx.session_id
225
+ )
224
226
  await asyncio.sleep(delay)
225
227
  finally:
226
228
  self._current_turn = None
@@ -234,7 +236,7 @@ class TaskExecutor:
234
236
  final_error = f"Turn failed after {const.MAX_FAILED_TURN_RETRIES} retries."
235
237
  if last_error_message:
236
238
  final_error = f"{last_error_message}\n{final_error}"
237
- yield events.ErrorEvent(error_message=final_error, can_retry=False)
239
+ yield events.ErrorEvent(error_message=final_error, can_retry=False, session_id=session_ctx.session_id)
238
240
  return
239
241
 
240
242
  if turn is None or turn.task_finished:
@@ -244,7 +246,7 @@ class TaskExecutor:
244
246
  error_msg = "Sub-agent returned empty result, retrying..."
245
247
  else:
246
248
  error_msg = "Agent returned empty result, retrying..."
247
- yield events.ErrorEvent(error_message=error_msg, can_retry=True)
249
+ yield events.ErrorEvent(error_message=error_msg, can_retry=True, session_id=session_ctx.session_id)
248
250
  continue
249
251
  break
250
252
 
@@ -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)