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
@@ -1,248 +0,0 @@
1
- import os
2
- import subprocess
3
- from pathlib import Path
4
-
5
- from klaude_code.protocol import commands, events, message
6
-
7
- from .command_abc import Agent, CommandABC, CommandResult
8
-
9
-
10
- class TerminalSetupCommand(CommandABC):
11
- """Setup shift+enter newline functionality in terminal"""
12
-
13
- @property
14
- def name(self) -> commands.CommandName:
15
- return commands.CommandName.TERMINAL_SETUP
16
-
17
- @property
18
- def summary(self) -> str:
19
- return "Install shift+enter key binding for newlines"
20
-
21
- @property
22
- def is_interactive(self) -> bool:
23
- return False
24
-
25
- async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
26
- del user_input # unused
27
- term_program = os.environ.get("TERM_PROGRAM", "").lower()
28
-
29
- try:
30
- if term_program == "ghostty":
31
- message = self._setup_ghostty()
32
- elif term_program == "iterm.app":
33
- message = self._setup_iterm()
34
- elif term_program == "vscode":
35
- # VS Code family terminals (VS Code, Windsurf, Cursor) all report TERM_PROGRAM=vscode
36
- message = self._setup_vscode_family()
37
- else:
38
- # Provide generic manual configuration guide for unknown or unsupported terminals
39
- message = self._setup_generic(term_program)
40
-
41
- return self._create_success_result(agent, message)
42
-
43
- except Exception as e:
44
- return self._create_error_result(agent, f"Error configuring terminal: {e!s}")
45
-
46
- def _setup_ghostty(self) -> str:
47
- """Configure shift+enter newline for Ghostty terminal"""
48
- config_dir = Path.home() / ".config" / "ghostty"
49
- config_file = config_dir / "config"
50
-
51
- keybind_line = 'keybind="shift+enter=text:\\n"'
52
-
53
- # Ensure config directory exists
54
- config_dir.mkdir(parents=True, exist_ok=True)
55
-
56
- # Check if configuration already exists in config file
57
- if config_file.exists():
58
- content = config_file.read_text()
59
- if keybind_line in content or 'keybind="shift+enter=' in content:
60
- return "Ghostty terminal shift+enter newline configuration already exists"
61
-
62
- # Add configuration
63
- with config_file.open("a", encoding="utf-8") as f:
64
- if config_file.exists() and not config_file.read_text().endswith("\n"):
65
- f.write("\n")
66
- f.write(f"{keybind_line}\n")
67
-
68
- return f"Added shift+enter newline configuration for Ghostty terminal to {config_file}"
69
-
70
- def _setup_iterm(self) -> str:
71
- """Configure shift+enter newline for iTerm terminal using defaults command"""
72
- try:
73
- # First check if iTerm preferences exist
74
- prefs_path = Path.home() / "Library" / "Preferences" / "com.googlecode.iterm2.plist"
75
- if not prefs_path.exists():
76
- return "iTerm preferences file not found. Please open iTerm first to create initial preferences."
77
-
78
- # Check if the key binding already exists
79
- check_cmd = ["defaults", "read", "com.googlecode.iterm2", "New Bookmarks"]
80
-
81
- try:
82
- result = subprocess.run(check_cmd, capture_output=True, text=True, check=True)
83
- # If we can read bookmarks, iTerm is properly configured
84
- except subprocess.CalledProcessError:
85
- return "Unable to read iTerm configuration. Please ensure iTerm is properly installed and has been opened at least once."
86
-
87
- # Add to the default profile's keyboard map
88
- add_keymap_cmd = [
89
- "defaults",
90
- "write",
91
- "com.googlecode.iterm2",
92
- "GlobalKeyMap",
93
- "-dict-add",
94
- # Do not include quotes when passing args as a list (no shell)
95
- "0x0d-0x20000",
96
- # Pass Property List dict directly; \n should be literal backslash-n so iTerm parses newline
97
- '{Action=12;Text="\\\\n";}',
98
- ]
99
- # Execute without shell so arguments are passed correctly
100
- result = subprocess.run(add_keymap_cmd, capture_output=True, text=True)
101
- print(result.stdout, result.stderr)
102
- if result.returncode == 0:
103
- return "Successfully configured Shift+Enter for newline in iTerm. Please restart iTerm for changes to take effect."
104
- else:
105
- # Fallback to manual instructions if defaults command fails
106
- return (
107
- "Automatic configuration failed. Please manually configure:\n"
108
- "1. Open iTerm -> Preferences (⌘,)\n"
109
- "2. Go to Profiles -> Keys -> Key Mappings\n"
110
- "3. Click '+' to add: Shift+Enter -> Send Text -> \\n"
111
- )
112
-
113
- except Exception as e:
114
- raise Exception(f"Error configuring iTerm: {e!s}") from e
115
-
116
- def _setup_vscode_family(self) -> str:
117
- """Configure shift+enter newline for VS Code family terminals (VS Code, Windsurf, Cursor).
118
-
119
- These editors share TERM_PROGRAM=vscode and use keybindings.json under their respective
120
- Application Support folders. We ensure the required keybinding exists; if not, we append it.
121
- """
122
- base_dir = Path.home() / "Library" / "Application Support"
123
- targets = [
124
- ("VS Code", base_dir / "Code" / "User" / "keybindings.json"),
125
- ("Windsurf", base_dir / "Windsurf" / "User" / "keybindings.json"),
126
- ("Cursor", base_dir / "Cursor" / "User" / "keybindings.json"),
127
- ]
128
-
129
- mapping_block = r""" {
130
- "key": "shift+enter",
131
- "command": "workbench.action.terminal.sendSequence",
132
- "args": {
133
- "text": "\\\r\n"
134
- },
135
- "when": "terminalFocus"
136
- }"""
137
-
138
- results: list[str] = []
139
-
140
- for name, file_path in targets:
141
- try:
142
- _, msg = self._ensure_vscode_keybinding(file_path, mapping_block)
143
- results.append(f"{name}: {msg}")
144
- except Exception as e: # pragma: no cover - protect against any unexpected FS issue
145
- results.append(f"{name}: failed to update keybindings ({e})")
146
-
147
- return "\n".join(results)
148
-
149
- def _ensure_vscode_keybinding(self, path: Path, mapping_block: str) -> tuple[bool, str]:
150
- """Ensure the VS Code-style keybinding exists in the given keybindings.json file.
151
-
152
- Returns (added, message).
153
- - added=True if we created or modified the file to include the mapping
154
- - added=False if mapping already present or file couldn't be safely modified
155
- """
156
- path.parent.mkdir(parents=True, exist_ok=True)
157
-
158
- # If file does not exist, create with the mapping in an array
159
- if not path.exists():
160
- content = "[\n " + mapping_block + "\n]\n"
161
- path.write_text(content, encoding="utf-8")
162
- return True, f"created {path} with Shift+Enter mapping"
163
-
164
- # Read existing content
165
- raw = path.read_text(encoding="utf-8")
166
- text = raw
167
-
168
- # Quick detection: if both key and command exist together anywhere, assume configured
169
- if '"key": "shift+enter"' in text and "workbench.action.terminal.sendSequence" in text:
170
- return False, "already configured"
171
-
172
- stripped = text.strip()
173
- # If file is empty, write a fresh array
174
- if stripped == "":
175
- content = "[\n " + mapping_block + "\n]\n"
176
- path.write_text(content, encoding="utf-8")
177
- return True, "initialized empty keybindings.json with mapping"
178
-
179
- # If the content contains a top-level array (allowing header comments), append before the final ]
180
- open_idx = text.find("[")
181
- close_idx = text.rfind("]")
182
- if open_idx != -1 and close_idx != -1 and open_idx < close_idx:
183
- before = text[:close_idx].rstrip()
184
- after = text[close_idx:]
185
-
186
- # Heuristic: treat as non-empty if there's an object marker between [ and ]
187
- inner = text[open_idx + 1 : close_idx]
188
- has_item = "{" in inner
189
-
190
- # Construct new content by adding optional comma, newline, then our block
191
- new_content = before + ("," if has_item else "") + "\n" + mapping_block + "\n" + after
192
-
193
- path.write_text(new_content, encoding="utf-8")
194
- return True, "appended mapping"
195
-
196
- # Not an array – avoid modifying to prevent corrupting user config
197
- return (
198
- False,
199
- "unsupported keybindings.json format (not an array); please add mapping manually",
200
- )
201
-
202
- def _setup_generic(self, term_program: str) -> str:
203
- """Provide generic manual configuration guide for unknown or unsupported terminals"""
204
- if term_program:
205
- intro = f"Terminal type '{term_program}' is not specifically supported, but you can manually configure shift+enter newline functionality."
206
- else:
207
- intro = "Unable to detect terminal type, but you can manually configure shift+enter newline functionality."
208
-
209
- message = (
210
- f"{intro}\n\n"
211
- "General steps to configure shift+enter for newline:\n"
212
- "1. Open your terminal's preferences/settings\n"
213
- "2. Look for 'Key Bindings', 'Key Mappings', or 'Keyboard' section\n"
214
- "3. Add a new key binding:\n"
215
- " - Key combination: Shift+Enter\n"
216
- " - Action: Send text or Insert text\n"
217
- " - Text to send: \\n (literal newline character)\n"
218
- "4. Save the configuration\n\n"
219
- "Note: The exact steps may vary depending on your terminal application. "
220
- "Currently supported terminals with automatic configuration: Ghostty, iTerm.app, VS Code family (VS Code, Windsurf, Cursor)"
221
- )
222
-
223
- return message
224
-
225
- def _create_success_result(self, agent: "Agent", msg: str) -> CommandResult:
226
- """Create success result"""
227
- return CommandResult(
228
- events=[
229
- events.CommandOutputEvent(
230
- session_id=agent.session.id,
231
- command_name=self.name,
232
- content=msg,
233
- )
234
- ]
235
- )
236
-
237
- def _create_error_result(self, agent: "Agent", msg: str) -> CommandResult:
238
- """Create error result"""
239
- return CommandResult(
240
- events=[
241
- events.CommandOutputEvent(
242
- session_id=agent.session.id,
243
- command_name=self.name,
244
- content=msg,
245
- is_error=True,
246
- )
247
- ]
248
- )
@@ -1,152 +0,0 @@
1
- """Clipboard and image handling for REPL input.
2
-
3
- This module provides:
4
- - ClipboardCaptureState: Captures clipboard images and maps tags to file paths
5
- - capture_clipboard_tag(): Capture clipboard image and return tag
6
- - extract_images_from_text(): Parse tags and return ImageURLPart list
7
- - copy_to_clipboard(): Copy text to system clipboard
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import re
13
- import shutil
14
- import subprocess
15
- import sys
16
- import uuid
17
- from base64 import b64encode
18
- from pathlib import Path
19
-
20
- from PIL import Image, ImageGrab
21
-
22
- from klaude_code.protocol.message import ImageURLPart
23
-
24
- # Directory for storing clipboard images
25
- CLIPBOARD_IMAGES_DIR = Path.home() / ".klaude" / "clipboard" / "images"
26
-
27
- # Pattern to match [Image #N] tags in user input
28
- _IMAGE_TAG_RE = re.compile(r"\[Image #(\d+)\]")
29
-
30
-
31
- class ClipboardCaptureState:
32
- """Captures clipboard images and maps tags to file paths in memory."""
33
-
34
- def __init__(self, images_dir: Path | None = None):
35
- self._images_dir = images_dir or CLIPBOARD_IMAGES_DIR
36
- self._pending: dict[str, str] = {} # tag -> path mapping
37
- self._counter = 1
38
-
39
- def capture_from_clipboard(self) -> str | None:
40
- """Capture image from clipboard, save to disk, and return a tag like [Image #N]."""
41
- try:
42
- clipboard_data = ImageGrab.grabclipboard()
43
- except OSError:
44
- return None
45
- if not isinstance(clipboard_data, Image.Image):
46
- return None
47
- try:
48
- self._images_dir.mkdir(parents=True, exist_ok=True)
49
- except OSError:
50
- return None
51
- filename = f"clipboard_{uuid.uuid4().hex[:8]}.png"
52
- path = self._images_dir / filename
53
- try:
54
- clipboard_data.save(path, "PNG")
55
- except OSError:
56
- return None
57
- tag = f"[Image #{self._counter}]"
58
- self._counter += 1
59
- self._pending[tag] = str(path)
60
- return tag
61
-
62
- def get_pending_images(self) -> dict[str, str]:
63
- """Return the current tag-to-path mapping for pending images."""
64
- return dict(self._pending)
65
-
66
- def flush(self) -> dict[str, str]:
67
- """Flush pending images and return tag-to-path mapping, then reset state."""
68
- result = dict(self._pending)
69
- self._pending = {}
70
- self._counter = 1
71
- return result
72
-
73
-
74
- # Module-level singleton instance
75
- clipboard_state = ClipboardCaptureState()
76
-
77
-
78
- def capture_clipboard_tag() -> str | None:
79
- """Capture image from clipboard and return tag like [Image #N].
80
-
81
- Uses the module-level clipboard_state singleton. Returns None if no image
82
- is available in the clipboard or capture fails.
83
- """
84
- return clipboard_state.capture_from_clipboard()
85
-
86
-
87
- def extract_images_from_text(text: str) -> list[ImageURLPart]:
88
- """Extract images from pending clipboard state based on tags in text.
89
-
90
- Parses [Image #N] tags in the text, looks up corresponding image paths
91
- in the clipboard state, and creates ImageURLPart objects from them.
92
- Flushes the clipboard state after extraction.
93
- """
94
- pending_images = clipboard_state.flush()
95
- if not pending_images:
96
- return []
97
-
98
- # Find all [Image #N] tags in text
99
- found_tags = set(_IMAGE_TAG_RE.findall(text))
100
- if not found_tags:
101
- return []
102
-
103
- images: list[ImageURLPart] = []
104
- for tag, path in pending_images.items():
105
- # Extract the number from the tag and check if it's referenced
106
- match = _IMAGE_TAG_RE.match(tag)
107
- if match and match.group(1) in found_tags:
108
- image_part = _encode_image_file(path)
109
- if image_part:
110
- images.append(image_part)
111
-
112
- return images
113
-
114
-
115
- def _encode_image_file(file_path: str) -> ImageURLPart | None:
116
- """Encode an image file as base64 data URL and create ImageURLPart."""
117
- try:
118
- path = Path(file_path)
119
- if not path.exists():
120
- return None
121
- with open(path, "rb") as f:
122
- encoded = b64encode(f.read()).decode("ascii")
123
- # Clipboard images are always saved as PNG
124
- data_url = f"data:image/png;base64,{encoded}"
125
- return ImageURLPart(url=data_url, id=None)
126
- except OSError:
127
- return None
128
-
129
-
130
- def copy_to_clipboard(text: str) -> None:
131
- """Copy text to system clipboard using platform-specific commands."""
132
- try:
133
- if sys.platform == "darwin":
134
- subprocess.run(["pbcopy"], input=text.encode("utf-8"), check=True)
135
- elif sys.platform == "win32":
136
- subprocess.run(["clip"], input=text.encode("utf-16"), check=True)
137
- else:
138
- # Linux: try xclip first, then xsel
139
- if shutil.which("xclip"):
140
- subprocess.run(
141
- ["xclip", "-selection", "clipboard"],
142
- input=text.encode("utf-8"),
143
- check=True,
144
- )
145
- elif shutil.which("xsel"):
146
- subprocess.run(
147
- ["xsel", "--clipboard", "--input"],
148
- input=text.encode("utf-8"),
149
- check=True,
150
- )
151
- except (OSError, subprocess.SubprocessError):
152
- pass