klaude-code 2.6.0__py3-none-any.whl → 2.8.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 (82) hide show
  1. klaude_code/app/runtime.py +1 -1
  2. klaude_code/auth/AGENTS.md +325 -0
  3. klaude_code/auth/__init__.py +17 -1
  4. klaude_code/auth/antigravity/__init__.py +20 -0
  5. klaude_code/auth/antigravity/exceptions.py +17 -0
  6. klaude_code/auth/antigravity/oauth.py +320 -0
  7. klaude_code/auth/antigravity/pkce.py +25 -0
  8. klaude_code/auth/antigravity/token_manager.py +45 -0
  9. klaude_code/auth/base.py +4 -0
  10. klaude_code/auth/claude/oauth.py +29 -9
  11. klaude_code/auth/codex/exceptions.py +4 -0
  12. klaude_code/auth/env.py +19 -15
  13. klaude_code/cli/auth_cmd.py +54 -4
  14. klaude_code/cli/cost_cmd.py +83 -160
  15. klaude_code/cli/list_model.py +50 -0
  16. klaude_code/cli/main.py +99 -9
  17. klaude_code/config/assets/builtin_config.yaml +108 -0
  18. klaude_code/config/builtin_config.py +5 -11
  19. klaude_code/config/config.py +24 -10
  20. klaude_code/const.py +11 -1
  21. klaude_code/core/agent.py +5 -1
  22. klaude_code/core/agent_profile.py +28 -32
  23. klaude_code/core/compaction/AGENTS.md +112 -0
  24. klaude_code/core/compaction/__init__.py +11 -0
  25. klaude_code/core/compaction/compaction.py +707 -0
  26. klaude_code/core/compaction/overflow.py +30 -0
  27. klaude_code/core/compaction/prompts.py +97 -0
  28. klaude_code/core/executor.py +103 -2
  29. klaude_code/core/manager/llm_clients.py +5 -0
  30. klaude_code/core/manager/llm_clients_builder.py +14 -2
  31. klaude_code/core/prompts/prompt-antigravity.md +80 -0
  32. klaude_code/core/prompts/prompt-codex-gpt-5-2.md +335 -0
  33. klaude_code/core/reminders.py +11 -7
  34. klaude_code/core/task.py +126 -0
  35. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  36. klaude_code/core/turn.py +3 -1
  37. klaude_code/llm/antigravity/__init__.py +3 -0
  38. klaude_code/llm/antigravity/client.py +558 -0
  39. klaude_code/llm/antigravity/input.py +261 -0
  40. klaude_code/llm/registry.py +1 -0
  41. klaude_code/protocol/commands.py +0 -1
  42. klaude_code/protocol/events.py +18 -0
  43. klaude_code/protocol/llm_param.py +1 -0
  44. klaude_code/protocol/message.py +23 -1
  45. klaude_code/protocol/op.py +15 -1
  46. klaude_code/protocol/op_handler.py +5 -0
  47. klaude_code/session/session.py +36 -0
  48. klaude_code/skill/assets/create-plan/SKILL.md +6 -6
  49. klaude_code/skill/loader.py +12 -13
  50. klaude_code/skill/manager.py +3 -3
  51. klaude_code/tui/command/__init__.py +4 -4
  52. klaude_code/tui/command/compact_cmd.py +32 -0
  53. klaude_code/tui/command/copy_cmd.py +1 -1
  54. klaude_code/tui/command/fork_session_cmd.py +114 -18
  55. klaude_code/tui/command/model_picker.py +5 -1
  56. klaude_code/tui/command/thinking_cmd.py +1 -1
  57. klaude_code/tui/commands.py +6 -0
  58. klaude_code/tui/components/command_output.py +1 -1
  59. klaude_code/tui/components/rich/markdown.py +117 -1
  60. klaude_code/tui/components/rich/theme.py +18 -2
  61. klaude_code/tui/components/tools.py +39 -25
  62. klaude_code/tui/components/user_input.py +39 -28
  63. klaude_code/tui/input/AGENTS.md +44 -0
  64. klaude_code/tui/input/__init__.py +5 -2
  65. klaude_code/tui/input/completers.py +10 -14
  66. klaude_code/tui/input/drag_drop.py +146 -0
  67. klaude_code/tui/input/images.py +227 -0
  68. klaude_code/tui/input/key_bindings.py +183 -19
  69. klaude_code/tui/input/paste.py +71 -0
  70. klaude_code/tui/input/prompt_toolkit.py +32 -9
  71. klaude_code/tui/machine.py +26 -1
  72. klaude_code/tui/renderer.py +67 -4
  73. klaude_code/tui/runner.py +19 -3
  74. klaude_code/tui/terminal/image.py +103 -10
  75. klaude_code/tui/terminal/selector.py +81 -7
  76. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/METADATA +10 -10
  77. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/RECORD +79 -61
  78. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
  79. klaude_code/tui/command/terminal_setup_cmd.py +0 -248
  80. klaude_code/tui/input/clipboard.py +0 -152
  81. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/WHEEL +0 -0
  82. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/entry_points.txt +0 -0
klaude_code/tui/runner.py CHANGED
@@ -16,8 +16,9 @@ from klaude_code.app.runtime import (
16
16
  )
17
17
  from klaude_code.config import load_config
18
18
  from klaude_code.const import SIGINT_DOUBLE_PRESS_EXIT_TEXT
19
+ from klaude_code.core.compaction import should_compact_threshold
19
20
  from klaude_code.core.executor import Executor
20
- from klaude_code.log import log
21
+ from klaude_code.log import get_current_log_file, log
21
22
  from klaude_code.protocol import events, llm_param, op
22
23
  from klaude_code.protocol.message import UserInputPayload
23
24
  from klaude_code.session.session import Session
@@ -80,6 +81,19 @@ async def submit_user_input_payload(
80
81
  for evt in cmd_result.events:
81
82
  await executor.context.emit_event(evt)
82
83
 
84
+ if run_ops and should_compact_threshold(
85
+ session=agent.session,
86
+ config=None,
87
+ llm_config=agent.profile.llm_client.get_llm_config(),
88
+ ):
89
+ await executor.submit_and_wait(
90
+ op.CompactSessionOperation(
91
+ session_id=agent.session.id,
92
+ reason="threshold",
93
+ will_retry=False,
94
+ )
95
+ )
96
+
83
97
  submitted_ids: list[str] = []
84
98
  for operation_item in operations:
85
99
  submitted_ids.append(await executor.submit(operation_item))
@@ -124,7 +138,9 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
124
138
 
125
139
  def _status_provider() -> REPLStatusSnapshot:
126
140
  update_message = get_update_message()
127
- return build_repl_status_snapshot(update_message)
141
+ debug_log = get_current_log_file()
142
+ debug_log_path = str(debug_log) if debug_log else None
143
+ return build_repl_status_snapshot(update_message, debug_log_path=debug_log_path)
128
144
 
129
145
  def _stop_rich_bottom_ui() -> None:
130
146
  active_display = components.display
@@ -312,4 +328,4 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
312
328
  active_session_id = components.executor.context.current_session_id()
313
329
  if active_session_id and Session.exists(active_session_id):
314
330
  log(f"Session ID: {active_session_id}")
315
- log(f"Resume with: klaude --resume-by-id {active_session_id}")
331
+ log(f"Resume with: klaude --resume {active_session_id}")
@@ -1,34 +1,127 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import base64
4
+ import shutil
5
+ import struct
6
+ import subprocess
3
7
  import sys
8
+ import tempfile
4
9
  from pathlib import Path
5
10
  from typing import IO
6
11
 
12
+ # Kitty graphics protocol chunk size (4096 is the recommended max)
13
+ _CHUNK_SIZE = 4096
7
14
 
8
- def print_kitty_image(file_path: str | Path, *, height: int | None = None, file: IO[str] | None = None) -> None:
15
+ # Max columns for non-wide images
16
+ _MAX_COLS = 120
17
+
18
+ # Image formats that need conversion to PNG
19
+ _NEEDS_CONVERSION = {".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tiff", ".tif"}
20
+
21
+
22
+ def _convert_to_png(path: Path) -> bytes | None:
23
+ """Convert image to PNG using sips (macOS) or convert (ImageMagick)."""
24
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=True) as tmp:
25
+ tmp_path = tmp.name
26
+ # Try sips first (macOS built-in)
27
+ result = subprocess.run(
28
+ ["sips", "-s", "format", "png", str(path), "--out", tmp_path],
29
+ capture_output=True,
30
+ )
31
+ if result.returncode == 0:
32
+ return Path(tmp_path).read_bytes()
33
+ # Fallback to ImageMagick convert
34
+ result = subprocess.run(
35
+ ["convert", str(path), tmp_path],
36
+ capture_output=True,
37
+ )
38
+ if result.returncode == 0:
39
+ return Path(tmp_path).read_bytes()
40
+ return None
41
+
42
+
43
+ def _get_png_dimensions(data: bytes) -> tuple[int, int] | None:
44
+ """Extract width and height from PNG file header."""
45
+ # PNG: 8-byte signature + IHDR chunk (4 len + 4 type + 4 width + 4 height)
46
+ if len(data) < 24 or data[:8] != b"\x89PNG\r\n\x1a\n":
47
+ return None
48
+ width, height = struct.unpack(">II", data[16:24])
49
+ return width, height
50
+
51
+
52
+ def print_kitty_image(file_path: str | Path, *, file: IO[str] | None = None) -> None:
9
53
  """Print an image to the terminal using Kitty graphics protocol.
10
54
 
11
55
  This intentionally bypasses Rich rendering to avoid interleaving Live refreshes
12
- with raw escape sequences.
13
- """
56
+ with raw escape sequences. Image size adapts based on aspect ratio:
57
+ - Landscape images: fill terminal width
58
+ - Portrait images: limit height to avoid oversized display
14
59
 
60
+ Args:
61
+ file_path: Path to the image file (PNG recommended).
62
+ file: Output file stream. Defaults to stdout.
63
+ """
15
64
  path = Path(file_path) if isinstance(file_path, str) else file_path
16
65
  if not path.exists():
17
66
  print(f"Image not found: {path}", file=file or sys.stdout, flush=True)
18
67
  return
19
68
 
20
69
  try:
21
- from term_image.image import KittyImage # type: ignore[import-untyped]
22
-
23
- KittyImage.forced_support = True # type: ignore[reportUnknownMemberType]
24
- img = KittyImage.from_file(path) # type: ignore[reportUnknownMemberType]
25
- if height is not None:
26
- img.height = height # type: ignore[reportUnknownMemberType]
70
+ # Convert non-PNG formats to PNG for Kitty graphics protocol compatibility
71
+ if path.suffix.lower() in _NEEDS_CONVERSION:
72
+ data = _convert_to_png(path)
73
+ if data is None:
74
+ print(f"Saved image: {path}", file=file or sys.stdout, flush=True)
75
+ return
76
+ else:
77
+ data = path.read_bytes()
27
78
 
79
+ encoded = base64.standard_b64encode(data).decode("ascii")
28
80
  out = file or sys.stdout
81
+
82
+ term_size = shutil.get_terminal_size()
83
+ dimensions = _get_png_dimensions(data)
84
+
85
+ # Determine sizing strategy based on aspect ratio
86
+ if dimensions is not None:
87
+ img_width, img_height = dimensions
88
+ if img_width > 2 * img_height:
89
+ # Wide landscape (width > 2x height): fill terminal width
90
+ size_param = f"c={term_size.columns}"
91
+ else:
92
+ # Other images: limit width to 80% of terminal
93
+ size_param = f"c={min(_MAX_COLS, term_size.columns * 4 // 5)}"
94
+ else:
95
+ # Fallback: limit width to 80% of terminal
96
+ size_param = f"c={min(_MAX_COLS, term_size.columns * 4 // 5)}"
29
97
  print("", file=out)
30
- print(str(img), file=out)
98
+ _write_kitty_graphics(out, encoded, size_param=size_param)
31
99
  print("", file=out)
32
100
  out.flush()
33
101
  except Exception:
34
102
  print(f"Saved image: {path}", file=file or sys.stdout, flush=True)
103
+
104
+
105
+ def _write_kitty_graphics(out: IO[str], encoded_data: str, *, size_param: str) -> None:
106
+ """Write Kitty graphics protocol escape sequences.
107
+
108
+ Protocol format: ESC _ G <control>;<payload> ESC \\
109
+ - a=T: direct transmission (data in payload)
110
+ - f=100: PNG format (auto-detected by Kitty)
111
+ - c=N: display width in columns
112
+ - r=N: display height in rows
113
+ - m=1: more data follows, m=0: last chunk
114
+ """
115
+ total_len = len(encoded_data)
116
+
117
+ for i in range(0, total_len, _CHUNK_SIZE):
118
+ chunk = encoded_data[i : i + _CHUNK_SIZE]
119
+ is_last = i + _CHUNK_SIZE >= total_len
120
+
121
+ if i == 0:
122
+ # First chunk: include control parameters
123
+ ctrl = f"a=T,f=100,{size_param},m={0 if is_last else 1}"
124
+ out.write(f"\033_G{ctrl};{chunk}\033\\")
125
+ else:
126
+ # Subsequent chunks: only m parameter needed
127
+ out.write(f"\033_Gm={0 if is_last else 1};{chunk}\033\\")
@@ -81,13 +81,13 @@ def build_model_select_items(models: list[Any]) -> list[SelectItem[str]]:
81
81
 
82
82
  items: list[SelectItem[str]] = []
83
83
  model_idx = 0
84
- separator_base_len = 40
84
+ separator_base_len = 80
85
85
  for provider, provider_models in grouped.items():
86
86
  provider_text = provider.lower()
87
87
  count_text = f"({len(provider_models)})"
88
88
  header_len = len(provider_text) + 1 + len(count_text)
89
89
  separator_len = separator_base_len + max_header_len - header_len
90
- separator = "" * separator_len
90
+ separator = "-" * separator_len
91
91
  items.append(
92
92
  SelectItem(
93
93
  title=[
@@ -389,7 +389,7 @@ def _build_search_container(
389
389
  frame: bool = True,
390
390
  ) -> tuple[Window, Container]:
391
391
  """Build the search input container with placeholder."""
392
- placeholder_text = f"{search_placeholder} · ↑↓ to select · esc to quit"
392
+ placeholder_text = f"{search_placeholder} · ↑↓ to select · enter/tab to confirm · esc to quit"
393
393
 
394
394
  search_prefix_window = Window(
395
395
  FormattedTextControl([("class:search_prefix", "/ ")]),
@@ -522,6 +522,23 @@ def select_one[T](
522
522
  return
523
523
  event.app.exit(result=value)
524
524
 
525
+ @kb.add(Keys.Tab, eager=True)
526
+ def _(event: KeyPressEvent) -> None:
527
+ """Accept the currently pointed item."""
528
+ indices, _ = _filter_items(items, get_filter_text())
529
+ if not indices:
530
+ event.app.exit(result=None)
531
+ return
532
+
533
+ nonlocal pointed_at
534
+ pointed_at = _coerce_pointed_at_to_selectable(items, indices, pointed_at)
535
+ idx = indices[pointed_at % len(indices)]
536
+ value = items[idx].value
537
+ if value is None:
538
+ event.app.exit(result=None)
539
+ return
540
+ event.app.exit(result=value)
541
+
525
542
  @kb.add(Keys.Escape, eager=True)
526
543
  def _(event: KeyPressEvent) -> None:
527
544
  nonlocal pointed_at
@@ -557,7 +574,10 @@ def select_one[T](
557
574
  )
558
575
  list_window = Window(
559
576
  FormattedTextControl(get_choices_tokens),
560
- scroll_offsets=ScrollOffsets(top=0, bottom=2),
577
+ # Keep 1 line of context above the cursor so non-selectable header rows
578
+ # (e.g. provider group labels) remain visible when wrapping back to the
579
+ # first selectable item in a scrolled list.
580
+ scroll_offsets=ScrollOffsets(top=1, bottom=2),
561
581
  allow_scroll_beyond_bottom=True,
562
582
  dont_extend_height=Always(),
563
583
  always_hide_cursor=Always(),
@@ -702,6 +722,28 @@ class SelectOverlay[T]:
702
722
  if hasattr(result, "__await__"):
703
723
  event.app.create_background_task(cast(Coroutine[Any, Any, None], result))
704
724
 
725
+ @kb.add(Keys.Tab, filter=is_open_filter, eager=True)
726
+ def _(event: KeyPressEvent) -> None:
727
+ indices, _ = self._get_visible_indices()
728
+ if not indices:
729
+ self.close()
730
+ return
731
+
732
+ self._pointed_at = _coerce_pointed_at_to_selectable(self._items, indices, self._pointed_at)
733
+ idx = indices[self._pointed_at % len(indices)]
734
+ value = self._items[idx].value
735
+ if value is None:
736
+ self.close()
737
+ return
738
+ self.close()
739
+
740
+ if self._on_select is None:
741
+ return
742
+
743
+ result = self._on_select(value)
744
+ if hasattr(result, "__await__"):
745
+ event.app.create_background_task(cast(Coroutine[Any, Any, None], result))
746
+
705
747
  @kb.add(Keys.Escape, filter=is_open_filter, eager=True)
706
748
  def _(event: KeyPressEvent) -> None:
707
749
  if self._use_search_filter and self._search_buffer is not None and self._search_buffer.text:
@@ -757,11 +799,43 @@ class SelectOverlay[T]:
757
799
  dont_extend_height=Always(),
758
800
  always_hide_cursor=Always(),
759
801
  )
802
+
803
+ def get_list_height() -> int:
804
+ # Dynamic height: min of configured height and available terminal space
805
+ # Overhead: header(1) + spacer(1) + search(1) + frame borders(2) + prompt area(3)
806
+ overhead = 8
807
+ try:
808
+ terminal_height = get_app().output.get_size().rows
809
+ available = max(3, terminal_height - overhead)
810
+ cap = min(self._list_height, available)
811
+ except Exception:
812
+ cap = self._list_height
813
+
814
+ # Shrink list height when content is shorter than the configured cap.
815
+ # This is especially helpful for small pickers (e.g. thinking level)
816
+ # where a fixed list_height would otherwise render extra blank rows.
817
+ indices, _ = self._get_visible_indices()
818
+ if not indices:
819
+ return max(1, cap)
820
+
821
+ visible_lines = 0
822
+ for idx in indices:
823
+ item = self._items[idx]
824
+ newlines = sum(text.count("\n") for _style, text in item.title)
825
+ visible_lines += max(1, newlines)
826
+ if visible_lines >= cap:
827
+ break
828
+
829
+ return max(1, min(cap, visible_lines))
830
+
760
831
  list_window = Window(
761
832
  FormattedTextControl(get_choices_tokens),
762
- height=self._list_height,
763
- scroll_offsets=ScrollOffsets(top=0, bottom=2),
764
- allow_scroll_beyond_bottom=True,
833
+ height=get_list_height,
834
+ # See select_one(): keep header rows visible when wrapping.
835
+ # For embedded overlays, avoid reserving extra blank lines near the
836
+ # bottom when the list height is tight (e.g. short pickers).
837
+ scroll_offsets=ScrollOffsets(top=1, bottom=0),
838
+ allow_scroll_beyond_bottom=False,
765
839
  dont_extend_height=Always(),
766
840
  always_hide_cursor=Always(),
767
841
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 2.6.0
3
+ Version: 2.8.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -9,15 +9,13 @@ Requires-Dist: diff-match-patch>=20241021
9
9
  Requires-Dist: google-genai>=1.56.0
10
10
  Requires-Dist: markdown-it-py>=4.0.0
11
11
  Requires-Dist: openai>=1.102.0
12
- Requires-Dist: pillow>=9.1,<11.0
13
12
  Requires-Dist: prompt-toolkit>=3.0.52
14
13
  Requires-Dist: pydantic>=2.11.7
15
14
  Requires-Dist: pyyaml>=6.0.2
16
15
  Requires-Dist: rich>=14.1.0
17
- Requires-Dist: term-image>=0.7.2
18
16
  Requires-Dist: trafilatura>=2.0.0
19
17
  Requires-Dist: typer>=0.17.3
20
- Requires-Python: >=3.13, <3.14
18
+ Requires-Python: >=3.13
21
19
  Description-Content-Type: text/markdown
22
20
 
23
21
  # Klaude Code
@@ -61,20 +59,22 @@ klaude upgrade
61
59
  ## Usage
62
60
 
63
61
  ```bash
64
- klaude [--model <name>] [--select-model]
62
+ klaude [--model [<name>]] [--continue] [--resume [<id>]]
65
63
  ```
66
64
 
67
65
  **Options:**
68
- - `--model`/`-m`: Preferred model name (exact match picks immediately; otherwise opens the interactive selector filtered by this value).
69
- - `--select-model`/`-s`: Open the interactive model selector at startup (shows all models unless `--model` is also provided).
66
+ - `--model`/`-m`: Choose a model.
67
+ - `--model` (no value): opens the interactive selector.
68
+ - `--model <value>`: resolves `<value>` to a single model; if it can't, it opens the interactive selector filtered by `<value>`.
70
69
  - `--continue`/`-c`: Resume the most recent session.
71
- - `--resume`/`-r`: Select a session to resume for this project.
72
- - `--resume-by-id <id>`: Resume a session by its ID directly.
70
+ - `--resume`/`-r`: Resume a session.
71
+ - `--resume` (no value): select a session to resume for this project.
72
+ - `--resume <id>`: resume a session by its ID directly.
73
73
  - `--vanilla`: Minimal mode with only basic tools (Bash, Read, Edit, Write) and no system prompts.
74
74
 
75
75
  **Model selection behavior:**
76
76
  - Default: uses `main_model` from config.
77
- - `--select-model`: always prompts you to pick.
77
+ - `--model` (no value): always prompts you to pick.
78
78
  - `--model <value>`: tries to resolve `<value>` to a single model; if it can't, it prompts with a filtered list (and falls back to showing all models if there are no matches).
79
79
 
80
80
  **Debug Options:**