klaude-code 1.2.12__py3-none-any.whl → 1.2.14__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 (84) hide show
  1. klaude_code/auth/codex/oauth.py +3 -3
  2. klaude_code/cli/auth_cmd.py +73 -0
  3. klaude_code/cli/config_cmd.py +88 -0
  4. klaude_code/cli/debug.py +72 -0
  5. klaude_code/cli/main.py +31 -142
  6. klaude_code/cli/runtime.py +19 -58
  7. klaude_code/cli/session_cmd.py +9 -9
  8. klaude_code/command/__init__.py +6 -6
  9. klaude_code/command/export_cmd.py +3 -3
  10. klaude_code/command/model_cmd.py +1 -1
  11. klaude_code/command/registry.py +1 -1
  12. klaude_code/command/terminal_setup_cmd.py +2 -2
  13. klaude_code/command/thinking_cmd.py +8 -6
  14. klaude_code/config/__init__.py +1 -5
  15. klaude_code/config/config.py +31 -4
  16. klaude_code/config/list_model.py +1 -1
  17. klaude_code/const/__init__.py +8 -3
  18. klaude_code/core/agent.py +14 -62
  19. klaude_code/core/executor.py +11 -10
  20. klaude_code/core/manager/agent_manager.py +4 -4
  21. klaude_code/core/manager/llm_clients.py +10 -49
  22. klaude_code/core/manager/llm_clients_builder.py +8 -21
  23. klaude_code/core/manager/sub_agent_manager.py +3 -3
  24. klaude_code/core/prompt.py +12 -7
  25. klaude_code/core/reminders.py +1 -1
  26. klaude_code/core/task.py +2 -2
  27. klaude_code/core/tool/__init__.py +16 -25
  28. klaude_code/core/tool/file/_utils.py +1 -1
  29. klaude_code/core/tool/file/apply_patch.py +17 -25
  30. klaude_code/core/tool/file/apply_patch_tool.py +4 -7
  31. klaude_code/core/tool/file/edit_tool.py +4 -11
  32. klaude_code/core/tool/file/multi_edit_tool.py +2 -3
  33. klaude_code/core/tool/file/read_tool.py +3 -4
  34. klaude_code/core/tool/file/write_tool.py +2 -3
  35. klaude_code/core/tool/memory/memory_tool.py +2 -8
  36. klaude_code/core/tool/memory/skill_loader.py +3 -2
  37. klaude_code/core/tool/shell/command_safety.py +0 -1
  38. klaude_code/core/tool/tool_context.py +1 -3
  39. klaude_code/core/tool/tool_registry.py +2 -1
  40. klaude_code/core/tool/tool_runner.py +1 -1
  41. klaude_code/core/tool/truncation.py +2 -5
  42. klaude_code/core/turn.py +9 -3
  43. klaude_code/llm/anthropic/client.py +6 -2
  44. klaude_code/llm/client.py +5 -1
  45. klaude_code/llm/codex/client.py +2 -2
  46. klaude_code/llm/input_common.py +2 -2
  47. klaude_code/llm/openai_compatible/client.py +11 -8
  48. klaude_code/llm/openai_compatible/stream_processor.py +2 -1
  49. klaude_code/llm/openrouter/client.py +22 -9
  50. klaude_code/llm/openrouter/reasoning_handler.py +19 -132
  51. klaude_code/llm/registry.py +6 -5
  52. klaude_code/llm/responses/client.py +10 -5
  53. klaude_code/protocol/events.py +9 -2
  54. klaude_code/protocol/model.py +7 -1
  55. klaude_code/protocol/sub_agent.py +2 -2
  56. klaude_code/session/export.py +58 -0
  57. klaude_code/session/selector.py +2 -2
  58. klaude_code/session/session.py +37 -7
  59. klaude_code/session/templates/export_session.html +46 -0
  60. klaude_code/trace/__init__.py +2 -2
  61. klaude_code/trace/log.py +144 -5
  62. klaude_code/ui/__init__.py +4 -9
  63. klaude_code/ui/core/stage_manager.py +7 -4
  64. klaude_code/ui/modes/debug/display.py +2 -1
  65. klaude_code/ui/modes/repl/__init__.py +1 -1
  66. klaude_code/ui/modes/repl/completers.py +6 -7
  67. klaude_code/ui/modes/repl/display.py +3 -4
  68. klaude_code/ui/modes/repl/event_handler.py +63 -5
  69. klaude_code/ui/modes/repl/key_bindings.py +2 -3
  70. klaude_code/ui/modes/repl/renderer.py +52 -62
  71. klaude_code/ui/renderers/diffs.py +1 -4
  72. klaude_code/ui/renderers/tools.py +4 -0
  73. klaude_code/ui/rich/markdown.py +3 -3
  74. klaude_code/ui/rich/searchable_text.py +6 -6
  75. klaude_code/ui/rich/status.py +3 -4
  76. klaude_code/ui/rich/theme.py +2 -5
  77. klaude_code/ui/terminal/control.py +7 -16
  78. klaude_code/ui/terminal/notifier.py +2 -4
  79. klaude_code/ui/utils/common.py +1 -1
  80. klaude_code/ui/utils/debouncer.py +2 -2
  81. {klaude_code-1.2.12.dist-info → klaude_code-1.2.14.dist-info}/METADATA +1 -1
  82. {klaude_code-1.2.12.dist-info → klaude_code-1.2.14.dist-info}/RECORD +84 -81
  83. {klaude_code-1.2.12.dist-info → klaude_code-1.2.14.dist-info}/WHEEL +0 -0
  84. {klaude_code-1.2.12.dist-info → klaude_code-1.2.14.dist-info}/entry_points.txt +0 -0
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Iterator
3
4
  from contextlib import contextmanager
4
5
  from dataclasses import dataclass
5
- from typing import Any, Iterator
6
+ from typing import Any
6
7
 
7
8
  from rich import box
8
9
  from rich.box import Box
@@ -98,48 +99,21 @@ class REPLRenderer:
98
99
  def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
99
100
  if self.current_sub_agent_color:
100
101
  if objects:
101
- self.console.print(Quote(*objects, style=self.current_sub_agent_color))
102
+ content = objects[0] if len(objects) == 1 else objects
103
+ self.console.print(Quote(content, style=self.current_sub_agent_color))
102
104
  return
103
105
  self.console.print(*objects, style=style, end=end)
104
106
 
105
107
  def display_tool_call(self, e: events.ToolCallEvent) -> None:
106
- # Handle sub-agent tool calls in replay mode
107
108
  if r_tools.is_sub_agent_tool(e.tool_name):
108
- if e.is_replay:
109
- state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
110
- if state is not None:
111
- sub_agent_default_style = (
112
- self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
113
- )
114
- self.print(
115
- Quote(
116
- r_sub_agent.render_sub_agent_call(state, sub_agent_default_style),
117
- style=sub_agent_default_style,
118
- )
119
- )
120
109
  return
121
-
122
110
  renderable = r_tools.render_tool_call(e)
123
111
  if renderable is not None:
124
112
  self.print(renderable)
125
113
 
126
114
  def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
127
- # Handle sub-agent tool results in replay mode
128
115
  if r_tools.is_sub_agent_tool(e.tool_name):
129
- if e.is_replay:
130
- sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
131
- self.print(
132
- Quote(
133
- r_sub_agent.render_sub_agent_result(
134
- e.result,
135
- code_theme=self.themes.code_theme,
136
- style=sub_agent_default_style,
137
- ),
138
- style=sub_agent_default_style,
139
- )
140
- )
141
116
  return
142
-
143
117
  renderable = r_tools.render_tool_result(e)
144
118
  if renderable is not None:
145
119
  self.print(renderable)
@@ -159,39 +133,55 @@ class REPLRenderer:
159
133
  async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
160
134
  tool_call_dict: dict[str, events.ToolCallEvent] = {}
161
135
  for event in history_events.events:
162
- match event:
163
- case events.TurnStartEvent():
164
- self.print()
165
- case events.AssistantMessageEvent() as assistant_event:
166
- renderable = r_assistant.render_assistant_message(
167
- assistant_event.content, code_theme=self.themes.code_theme
168
- )
169
- if renderable is not None:
170
- self.print(renderable)
136
+ event_session_id = getattr(event, "session_id", history_events.session_id)
137
+ is_sub_agent = self.is_sub_agent_session(event_session_id)
138
+
139
+ with self.session_print_context(event_session_id):
140
+ match event:
141
+ case events.TaskStartEvent() as e:
142
+ self.display_task_start(e)
143
+ case events.TurnStartEvent():
144
+ self.print()
145
+ case events.AssistantMessageEvent() as e:
146
+ if is_sub_agent:
147
+ continue
148
+ renderable = r_assistant.render_assistant_message(
149
+ e.content, code_theme=self.themes.code_theme
150
+ )
151
+ if renderable is not None:
152
+ self.print(renderable)
153
+ self.print()
154
+ case events.ThinkingEvent() as e:
155
+ if is_sub_agent:
156
+ continue
157
+ self.display_thinking(e.content)
158
+ case events.DeveloperMessageEvent() as e:
159
+ self.display_developer_message(e)
160
+ self.display_command_output(e)
161
+ case events.UserMessageEvent() as e:
162
+ if is_sub_agent:
163
+ continue
164
+ self.print(r_user_input.render_user_input(e.content))
165
+ case events.ToolCallEvent() as e:
166
+ tool_call_dict[e.tool_call_id] = e
167
+ case events.ToolResultEvent() as e:
168
+ tool_call_event = tool_call_dict.get(e.tool_call_id)
169
+ if tool_call_event is not None:
170
+ self.display_tool_call(tool_call_event)
171
+ tool_call_dict.pop(e.tool_call_id, None)
172
+ if is_sub_agent:
173
+ continue
174
+ self.display_tool_call_result(e)
175
+ case events.TaskMetadataEvent() as e:
176
+ self.print(r_metadata.render_task_metadata(e))
177
+ self.print()
178
+ case events.InterruptEvent():
171
179
  self.print()
172
- case events.ThinkingEvent() as thinking_event:
173
- self.display_thinking(thinking_event.content)
174
- case events.DeveloperMessageEvent() as developer_event:
175
- self.display_developer_message(developer_event)
176
- self.display_command_output(developer_event)
177
- case events.UserMessageEvent() as user_event:
178
- self.print(r_user_input.render_user_input(user_event.content))
179
- case events.ToolCallEvent() as tool_call_event:
180
- tool_call_dict[tool_call_event.tool_call_id] = tool_call_event
181
- case events.ToolResultEvent() as tool_result_event:
182
- tool_call_event = tool_call_dict.get(tool_result_event.tool_call_id)
183
- if tool_call_event is not None:
184
- self.display_tool_call(tool_call_event)
185
- tool_call_dict.pop(tool_result_event.tool_call_id, None)
186
- self.display_tool_call_result(tool_result_event)
187
- case events.TaskMetadataEvent() as metadata_event:
188
- self.print(r_metadata.render_task_metadata(metadata_event))
189
- self.print()
190
- case events.InterruptEvent():
191
- self.print()
192
- self.print(r_user_input.render_interrupt())
193
- case events.ErrorEvent() as e:
194
- self.display_error(e)
180
+ self.print(r_user_input.render_interrupt())
181
+ case events.ErrorEvent() as e:
182
+ self.display_error(e)
183
+ case events.TaskFinishEvent() as e:
184
+ self.display_task_finish(e)
195
185
 
196
186
  def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
197
187
  if not r_developer.need_render_developer_message(e):
@@ -73,10 +73,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
73
73
  if line.startswith("--- "):
74
74
  raw = line[4:].strip()
75
75
  if raw != "/dev/null":
76
- if raw.startswith(("a/", "b/")):
77
- from_file_name = raw[2:]
78
- else:
79
- from_file_name = raw
76
+ from_file_name = raw[2:] if raw.startswith(("a/", "b/")) else raw
80
77
  continue
81
78
 
82
79
  # Parse file name from diff headers
@@ -239,6 +239,10 @@ def render_todo(tr: events.ToolResultEvent) -> RenderableType:
239
239
  mark = "✔"
240
240
  mark_style = ThemeKey.TODO_NEW_COMPLETED_MARK if is_new_completed else ThemeKey.TODO_COMPLETED_MARK
241
241
  text_style = ThemeKey.TODO_NEW_COMPLETED if is_new_completed else ThemeKey.TODO_COMPLETED
242
+ case _:
243
+ mark = "?"
244
+ mark_style = ThemeKey.TODO_PENDING_MARK
245
+ text_style = ThemeKey.TODO_PENDING
242
246
  text = Text(todo.content)
243
247
  text.stylize(text_style)
244
248
  todo_grid.add_row(Text(mark, style=mark_style), text)
@@ -1,6 +1,7 @@
1
1
  # copy from https://github.com/Aider-AI/aider/blob/main/aider/mdstream.py
2
2
  from __future__ import annotations
3
3
 
4
+ import contextlib
4
5
  import io
5
6
  import time
6
7
  from typing import Any, ClassVar
@@ -183,10 +184,9 @@ class MarkdownStream:
183
184
  def __del__(self) -> None:
184
185
  """Destructor to ensure Live display is properly cleaned up."""
185
186
  if self.live:
186
- try:
187
+ # Ignore any errors during cleanup
188
+ with contextlib.suppress(Exception):
187
189
  self.live.stop()
188
- except Exception:
189
- pass # Ignore any errors during cleanup
190
190
 
191
191
  def update(self, text: str, final: bool = False) -> None:
192
192
  """Update the displayed markdown content.
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Iterable, List, Sequence, Tuple
3
+ from collections.abc import Iterable, Sequence
4
4
 
5
5
 
6
6
  class SearchableFormattedText:
@@ -16,8 +16,8 @@ class SearchableFormattedText:
16
16
  concatenating the text parts of the fragments.
17
17
  """
18
18
 
19
- def __init__(self, fragments: Sequence[Tuple[str, str]], plain: str | None = None):
20
- self._fragments: List[Tuple[str, str]] = list(fragments)
19
+ def __init__(self, fragments: Sequence[tuple[str, str]], plain: str | None = None):
20
+ self._fragments: list[tuple[str, str]] = list(fragments)
21
21
  if plain is None:
22
22
  plain = "".join(text for _, text in self._fragments)
23
23
  self._plain = plain
@@ -25,7 +25,7 @@ class SearchableFormattedText:
25
25
  # Recognized by prompt_toolkit's to_formatted_text(value)
26
26
  def __pt_formatted_text__(
27
27
  self,
28
- ) -> Iterable[Tuple[str, str]]: # pragma: no cover - passthrough
28
+ ) -> Iterable[tuple[str, str]]: # pragma: no cover - passthrough
29
29
  return self._fragments
30
30
 
31
31
  # Provide a human-readable representation.
@@ -45,7 +45,7 @@ class SearchableFormattedText:
45
45
  return self._plain
46
46
 
47
47
 
48
- class SearchableFormattedList(list[Tuple[str, str]]):
48
+ class SearchableFormattedList(list[tuple[str, str]]):
49
49
  """
50
50
  List variant compatible with questionary's expected ``Choice.title`` type.
51
51
 
@@ -54,7 +54,7 @@ class SearchableFormattedList(list[Tuple[str, str]]):
54
54
  - Provides ``.lower()``/``.upper()`` returning the plain text for search filtering.
55
55
  """
56
56
 
57
- def __init__(self, fragments: Sequence[Tuple[str, str]], plain: str | None = None):
57
+ def __init__(self, fragments: Sequence[tuple[str, str]], plain: str | None = None):
58
58
  super().__init__(fragments)
59
59
  if plain is None:
60
60
  plain = "".join(text for _, text in fragments)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import math
4
5
  import time
5
6
 
@@ -233,8 +234,6 @@ class BreathingSpinner(RichSpinner):
233
234
 
234
235
  # Monkey-patch Rich's Status module to use the breathing spinner implementation
235
236
  # for the configured spinner name, while preserving default behavior elsewhere.
236
- try:
237
+ # Best-effort patch; if it fails we silently fall back to default spinner.
238
+ with contextlib.suppress(Exception):
237
239
  rich_status.Spinner = BreathingSpinner # type: ignore[assignment]
238
- except Exception:
239
- # Best-effort patch; if it fails we silently fall back to default spinner.
240
- pass
@@ -43,7 +43,7 @@ LIGHT_PALETTE = Palette(
43
43
  diff_add="#2e5a32 on #e8f5e9",
44
44
  diff_remove="#5a2e32 on #ffebee",
45
45
  code_theme="ansi_light",
46
- text_background="#f0f0f0",
46
+ text_background="#e0e0e0",
47
47
  )
48
48
 
49
49
  DARK_PALETTE = Palette(
@@ -153,10 +153,7 @@ class Themes:
153
153
 
154
154
 
155
155
  def get_theme(theme: str | None = None) -> Themes:
156
- if theme == "light":
157
- palette = LIGHT_PALETTE
158
- else:
159
- palette = DARK_PALETTE
156
+ palette = LIGHT_PALETTE if theme == "light" else DARK_PALETTE
160
157
  return Themes(
161
158
  app_theme=Theme(
162
159
  styles={
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import contextlib
2
3
  import os
3
4
  import select
4
5
  import signal
@@ -75,19 +76,15 @@ def start_esc_interrupt_monitor(
75
76
  r2, _, _ = select.select([sys.stdin], [], [], 0.0)
76
77
 
77
78
  if seq == "":
78
- try:
79
+ # Best-effort only; failures here should not crash the UI.
80
+ with contextlib.suppress(Exception):
79
81
  asyncio.run_coroutine_threadsafe(on_interrupt(), loop)
80
- except Exception:
81
- # Best-effort only; failures here should not crash the UI.
82
- pass
83
82
  stop.set()
84
83
  except Exception as exc: # pragma: no cover - environment dependent
85
84
  log((f"esc monitor error: {exc}", "r red"))
86
85
  finally:
87
- try:
86
+ with contextlib.suppress(Exception):
88
87
  termios.tcsetattr(fd, termios.TCSADRAIN, old)
89
- except Exception:
90
- pass
91
88
 
92
89
  esc_task: asyncio.Task[None] = asyncio.create_task(asyncio.to_thread(_esc_monitor, stop_event))
93
90
  return stop_event, esc_task
@@ -119,18 +116,14 @@ def install_sigint_double_press_exit(
119
116
  now = time.monotonic()
120
117
  if now - last_sigint_time <= window_seconds:
121
118
  # Second press within window: hide progress UI and exit.
122
- try:
119
+ with contextlib.suppress(Exception):
123
120
  hide_progress()
124
- except Exception:
125
- pass
126
121
  raise KeyboardInterrupt
127
122
 
128
123
  # First press: remember timestamp and show toast.
129
124
  last_sigint_time = now
130
- try:
125
+ with contextlib.suppress(Exception):
131
126
  show_toast()
132
- except Exception:
133
- pass
134
127
 
135
128
  try:
136
129
  signal.signal(signal.SIGINT, _handler)
@@ -139,9 +132,7 @@ def install_sigint_double_press_exit(
139
132
  return lambda: None
140
133
 
141
134
  def restore() -> None:
142
- try:
135
+ with contextlib.suppress(Exception):
143
136
  signal.signal(signal.SIGINT, original_handler)
144
- except Exception:
145
- pass
146
137
 
147
138
  return restore
@@ -39,7 +39,7 @@ class TerminalNotifierConfig:
39
39
  stream: TextIO | None = None
40
40
 
41
41
  @classmethod
42
- def from_env(cls) -> "TerminalNotifierConfig":
42
+ def from_env(cls) -> TerminalNotifierConfig:
43
43
  env = os.getenv("KLAUDE_NOTIFY", "").strip().lower()
44
44
  if env in {"0", "off", "false", "disable", "disabled"}:
45
45
  return cls(enabled=False)
@@ -95,9 +95,7 @@ class TerminalNotifier:
95
95
  if not getattr(stream, "isatty", lambda: False)():
96
96
  return False
97
97
  term = os.getenv("TERM", "")
98
- if term.lower() in {"", "dumb"}:
99
- return False
100
- return True
98
+ return term.lower() not in {"", "dumb"}
101
99
 
102
100
 
103
101
  def _compact(text: str, limit: int = 160) -> str:
@@ -99,7 +99,7 @@ def truncate_display(
99
99
  ) -> str:
100
100
  lines = text.split("\n")
101
101
  if len(lines) > max_lines:
102
- lines = lines[:max_lines] + ["… (more " + str(len(lines) - max_lines) + " lines)"]
102
+ lines = [*lines[:max_lines], "… (more " + str(len(lines) - max_lines) + " lines)"]
103
103
  for i, line in enumerate(lines):
104
104
  if len(line) > max_line_length:
105
105
  lines[i] = (
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from typing import Awaitable, Callable, Optional
2
+ from collections.abc import Awaitable, Callable
3
3
 
4
4
 
5
5
  class Debouncer:
@@ -15,7 +15,7 @@ class Debouncer:
15
15
  """
16
16
  self.interval = interval
17
17
  self.callback = callback
18
- self._task: Optional[asyncio.Task[None]] = None
18
+ self._task: asyncio.Task[None] | None = None
19
19
 
20
20
  def cancel(self) -> None:
21
21
  """Cancel current debounce task"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.2.12
3
+ Version: 1.2.14
4
4
  Summary: Add your description here
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: openai>=1.102.0