klaude-code 1.2.6__py3-none-any.whl → 1.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.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""System skills management - install built-in skills to user directory.
|
|
2
|
+
|
|
3
|
+
This module handles extracting bundled skills from the package to ~/.klaude/skills/.system/
|
|
4
|
+
on application startup. It uses a fingerprint mechanism to avoid unnecessary re-extraction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import shutil
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from importlib import resources
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from klaude_code.trace import log_debug
|
|
15
|
+
|
|
16
|
+
# Marker file name for tracking installed skills version
|
|
17
|
+
SYSTEM_SKILLS_MARKER_FILENAME = ".klaude-system-skills.marker"
|
|
18
|
+
|
|
19
|
+
# Salt for fingerprint calculation (increment to force re-extraction)
|
|
20
|
+
SYSTEM_SKILLS_MARKER_SALT = "v1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_system_skills_dir() -> Path:
|
|
24
|
+
"""Get the system skills installation directory.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Path to ~/.klaude/skills/.system/
|
|
28
|
+
"""
|
|
29
|
+
return Path.home() / ".klaude" / "skills" / ".system"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _calculate_fingerprint(assets_dir: Path) -> str:
|
|
33
|
+
"""Calculate a fingerprint hash for the embedded skills assets.
|
|
34
|
+
|
|
35
|
+
The fingerprint is based on all file paths and their contents.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
assets_dir: Path to the assets directory
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Hex string of the hash
|
|
42
|
+
"""
|
|
43
|
+
hasher = hashlib.sha256()
|
|
44
|
+
hasher.update(SYSTEM_SKILLS_MARKER_SALT.encode())
|
|
45
|
+
|
|
46
|
+
if not assets_dir.exists():
|
|
47
|
+
return hasher.hexdigest()
|
|
48
|
+
|
|
49
|
+
# Sort entries for consistent ordering
|
|
50
|
+
for entry in sorted(assets_dir.rglob("*")):
|
|
51
|
+
if entry.is_file():
|
|
52
|
+
# Hash the relative path
|
|
53
|
+
rel_path = entry.relative_to(assets_dir)
|
|
54
|
+
hasher.update(str(rel_path).encode())
|
|
55
|
+
# Hash the file contents
|
|
56
|
+
hasher.update(entry.read_bytes())
|
|
57
|
+
|
|
58
|
+
return hasher.hexdigest()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _read_marker(marker_path: Path) -> str | None:
|
|
62
|
+
"""Read the fingerprint from the marker file.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
marker_path: Path to the marker file
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The stored fingerprint, or None if the file doesn't exist or is invalid
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
if marker_path.exists():
|
|
72
|
+
return marker_path.read_text(encoding="utf-8").strip()
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _write_marker(marker_path: Path, fingerprint: str) -> None:
|
|
79
|
+
"""Write the fingerprint to the marker file.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
marker_path: Path to the marker file
|
|
83
|
+
fingerprint: The fingerprint to store
|
|
84
|
+
"""
|
|
85
|
+
marker_path.write_text(f"{fingerprint}\n", encoding="utf-8")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@contextmanager
|
|
89
|
+
def _with_embedded_assets_dir() -> Iterator[Path | None]:
|
|
90
|
+
"""Resolve the embedded assets directory as a real filesystem path.
|
|
91
|
+
|
|
92
|
+
Uses `importlib.resources.as_file()` so it works for both normal installs
|
|
93
|
+
and zipimport-style environments.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
assets_ref = resources.files("klaude_code.skill").joinpath("assets")
|
|
97
|
+
with resources.as_file(assets_ref) as assets_path:
|
|
98
|
+
p = Path(assets_path)
|
|
99
|
+
yield p if p.exists() else None
|
|
100
|
+
return
|
|
101
|
+
except (TypeError, AttributeError, ImportError, FileNotFoundError, OSError):
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
module_dir = Path(__file__).parent
|
|
106
|
+
assets_path = module_dir / "assets"
|
|
107
|
+
yield assets_path if assets_path.exists() else None
|
|
108
|
+
except (TypeError, NameError, OSError):
|
|
109
|
+
yield None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def install_system_skills() -> bool:
|
|
113
|
+
"""Install system skills from the embedded assets to the user directory.
|
|
114
|
+
|
|
115
|
+
This function:
|
|
116
|
+
1. Calculates a fingerprint of the embedded assets
|
|
117
|
+
2. Checks if the installed skills match (via marker file)
|
|
118
|
+
3. If they don't match, clears and re-extracts the skills
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if skills were installed/updated, False if already up-to-date
|
|
122
|
+
"""
|
|
123
|
+
dest_dir = get_system_skills_dir()
|
|
124
|
+
marker_path = dest_dir / SYSTEM_SKILLS_MARKER_FILENAME
|
|
125
|
+
|
|
126
|
+
with _with_embedded_assets_dir() as assets_path:
|
|
127
|
+
if assets_path is None or not assets_path.exists():
|
|
128
|
+
log_debug("No embedded system skills found")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Calculate fingerprint of embedded assets
|
|
132
|
+
expected_fingerprint = _calculate_fingerprint(assets_path)
|
|
133
|
+
|
|
134
|
+
# Check if already installed with matching fingerprint
|
|
135
|
+
current_fingerprint = _read_marker(marker_path)
|
|
136
|
+
if current_fingerprint == expected_fingerprint and dest_dir.exists():
|
|
137
|
+
log_debug("System skills already up-to-date")
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
log_debug(f"Installing system skills to {dest_dir}")
|
|
141
|
+
|
|
142
|
+
# Clear existing installation
|
|
143
|
+
if dest_dir.exists():
|
|
144
|
+
try:
|
|
145
|
+
shutil.rmtree(dest_dir)
|
|
146
|
+
except OSError as e:
|
|
147
|
+
log_debug(f"Failed to clear existing system skills: {e}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
# Create destination directory
|
|
151
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
|
|
153
|
+
# Copy all skill directories from assets
|
|
154
|
+
try:
|
|
155
|
+
for item in assets_path.iterdir():
|
|
156
|
+
if item.is_dir() and not item.name.startswith("."):
|
|
157
|
+
dest_skill_dir = dest_dir / item.name
|
|
158
|
+
shutil.copytree(item, dest_skill_dir)
|
|
159
|
+
log_debug(f"Installed system skill: {item.name}")
|
|
160
|
+
except OSError as e:
|
|
161
|
+
log_debug(f"Failed to copy system skills: {e}")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
# Write marker file
|
|
165
|
+
try:
|
|
166
|
+
_write_marker(marker_path, expected_fingerprint)
|
|
167
|
+
except OSError as e:
|
|
168
|
+
log_debug(f"Failed to write marker file: {e}")
|
|
169
|
+
# Installation succeeded, just marker failed
|
|
170
|
+
|
|
171
|
+
log_debug("System skills installation complete")
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def get_installed_system_skills() -> list[Path]:
|
|
176
|
+
"""Get list of installed system skill directories.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of paths to installed skill directories
|
|
180
|
+
"""
|
|
181
|
+
dest_dir = get_system_skills_dir()
|
|
182
|
+
if not dest_dir.exists():
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
skills: list[Path] = []
|
|
186
|
+
for item in dest_dir.iterdir():
|
|
187
|
+
if item.is_dir() and not item.name.startswith("."):
|
|
188
|
+
skill_file = item / "SKILL.md"
|
|
189
|
+
if skill_file.exists():
|
|
190
|
+
skills.append(item)
|
|
191
|
+
|
|
192
|
+
return skills
|
klaude_code/trace/__init__.py
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
-
from .log import
|
|
1
|
+
from .log import (
|
|
2
|
+
DebugType,
|
|
3
|
+
get_current_log_file,
|
|
4
|
+
is_debug_enabled,
|
|
5
|
+
log,
|
|
6
|
+
log_debug,
|
|
7
|
+
logger,
|
|
8
|
+
prepare_debug_log_file,
|
|
9
|
+
set_debug_logging,
|
|
10
|
+
)
|
|
2
11
|
|
|
3
|
-
__all__ = [
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DebugType",
|
|
14
|
+
"get_current_log_file",
|
|
15
|
+
"is_debug_enabled",
|
|
16
|
+
"log",
|
|
17
|
+
"log_debug",
|
|
18
|
+
"logger",
|
|
19
|
+
"prepare_debug_log_file",
|
|
20
|
+
"set_debug_logging",
|
|
21
|
+
]
|
klaude_code/trace/log.py
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import gzip
|
|
1
2
|
import logging
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from datetime import datetime, timedelta
|
|
2
8
|
from enum import Enum
|
|
3
9
|
from logging.handlers import RotatingFileHandler
|
|
4
|
-
from
|
|
10
|
+
from pathlib import Path
|
|
5
11
|
|
|
6
12
|
from rich.console import Console
|
|
7
13
|
from rich.logging import RichHandler
|
|
@@ -49,6 +55,26 @@ _file_handler: RotatingFileHandler | None = None
|
|
|
49
55
|
_console_handler: RichHandler | None = None
|
|
50
56
|
_debug_filter: DebugTypeFilter | None = None
|
|
51
57
|
_debug_enabled = False
|
|
58
|
+
_current_log_file: Path | None = None
|
|
59
|
+
|
|
60
|
+
LOG_RETENTION_DAYS = 3
|
|
61
|
+
LOG_MAX_TOTAL_BYTES = 200 * 1024 * 1024
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GzipRotatingFileHandler(RotatingFileHandler):
|
|
65
|
+
"""Rotating file handler that gzips rolled files."""
|
|
66
|
+
|
|
67
|
+
def rotation_filename(self, default_name: str) -> str:
|
|
68
|
+
"""Append .gz to rotation targets."""
|
|
69
|
+
|
|
70
|
+
return f"{default_name}.gz"
|
|
71
|
+
|
|
72
|
+
def rotate(self, source: str, dest: str) -> None:
|
|
73
|
+
"""Compress the rotated file and remove the original."""
|
|
74
|
+
|
|
75
|
+
with open(source, "rb") as source_file, gzip.open(dest, "wb") as dest_file:
|
|
76
|
+
shutil.copyfileobj(source_file, dest_file)
|
|
77
|
+
Path(source).unlink(missing_ok=True)
|
|
52
78
|
|
|
53
79
|
|
|
54
80
|
def set_debug_logging(
|
|
@@ -66,7 +92,7 @@ def set_debug_logging(
|
|
|
66
92
|
log_file: Path to the log file (default: debug.log)
|
|
67
93
|
filters: Set of DebugType to include; None means all types
|
|
68
94
|
"""
|
|
69
|
-
global _file_handler, _console_handler, _debug_filter, _debug_enabled
|
|
95
|
+
global _file_handler, _console_handler, _debug_filter, _debug_enabled, _current_log_file
|
|
70
96
|
|
|
71
97
|
_debug_enabled = enabled
|
|
72
98
|
|
|
@@ -80,6 +106,7 @@ def set_debug_logging(
|
|
|
80
106
|
_console_handler = None
|
|
81
107
|
|
|
82
108
|
if not enabled:
|
|
109
|
+
_current_log_file = None
|
|
83
110
|
return
|
|
84
111
|
|
|
85
112
|
# Create filter
|
|
@@ -87,10 +114,19 @@ def set_debug_logging(
|
|
|
87
114
|
|
|
88
115
|
# Determine output mode
|
|
89
116
|
use_file = write_to_file if write_to_file is not None else True
|
|
90
|
-
file_path = log_file if log_file is not None else const.DEFAULT_DEBUG_LOG_FILE
|
|
91
|
-
|
|
92
117
|
if use_file:
|
|
93
|
-
|
|
118
|
+
if _current_log_file is None:
|
|
119
|
+
_current_log_file = _resolve_log_file(log_file)
|
|
120
|
+
file_path = _current_log_file
|
|
121
|
+
else:
|
|
122
|
+
_current_log_file = None
|
|
123
|
+
file_path = None
|
|
124
|
+
|
|
125
|
+
if use_file and file_path is not None:
|
|
126
|
+
_prune_old_logs(const.DEFAULT_DEBUG_LOG_DIR, LOG_RETENTION_DAYS, LOG_MAX_TOTAL_BYTES)
|
|
127
|
+
|
|
128
|
+
if use_file and file_path is not None:
|
|
129
|
+
_file_handler = GzipRotatingFileHandler(
|
|
94
130
|
file_path,
|
|
95
131
|
maxBytes=const.LOG_MAX_BYTES,
|
|
96
132
|
backupCount=const.LOG_BACKUP_COUNT,
|
|
@@ -166,3 +202,112 @@ def _build_message(objects: Iterable[str | tuple[str, str]]) -> str:
|
|
|
166
202
|
def is_debug_enabled() -> bool:
|
|
167
203
|
"""Check if debug logging is currently enabled."""
|
|
168
204
|
return _debug_enabled
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def prepare_debug_log_file(log_file: str | os.PathLike[str] | None = None) -> Path:
|
|
208
|
+
"""Prepare and remember the log file path for this session."""
|
|
209
|
+
|
|
210
|
+
global _current_log_file
|
|
211
|
+
_current_log_file = _resolve_log_file(log_file)
|
|
212
|
+
return _current_log_file
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_current_log_file() -> Path | None:
|
|
216
|
+
"""Return the currently active log file path, if any."""
|
|
217
|
+
|
|
218
|
+
return _current_log_file
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _resolve_log_file(log_file: str | os.PathLike[str] | None) -> Path:
|
|
222
|
+
"""Resolve the log file path and ensure directories exist."""
|
|
223
|
+
|
|
224
|
+
if log_file:
|
|
225
|
+
path = Path(log_file).expanduser()
|
|
226
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
return path
|
|
228
|
+
else:
|
|
229
|
+
path = _build_default_log_file_path()
|
|
230
|
+
|
|
231
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
path.touch(exist_ok=True)
|
|
233
|
+
_refresh_latest_symlink(path)
|
|
234
|
+
return path
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _build_default_log_file_path() -> Path:
|
|
238
|
+
"""Build a per-session log path under the default log directory."""
|
|
239
|
+
|
|
240
|
+
now = datetime.now()
|
|
241
|
+
session_dir = const.DEFAULT_DEBUG_LOG_DIR / now.strftime("%Y-%m-%d")
|
|
242
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
filename = f"{now.strftime('%H%M%S')}-{os.getpid()}.log"
|
|
244
|
+
return session_dir / filename
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _refresh_latest_symlink(target: Path) -> None:
|
|
248
|
+
"""Point the debug.log symlink at the latest session file."""
|
|
249
|
+
|
|
250
|
+
latest = const.DEFAULT_DEBUG_LOG_FILE
|
|
251
|
+
try:
|
|
252
|
+
latest.unlink(missing_ok=True)
|
|
253
|
+
latest.symlink_to(target)
|
|
254
|
+
except OSError:
|
|
255
|
+
# Non-blocking best-effort; logging should still proceed
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _prune_old_logs(log_root: Path, keep_days: int, max_total_bytes: int) -> None:
|
|
260
|
+
"""Remove logs older than keep_days or when exceeding max_total_bytes."""
|
|
261
|
+
|
|
262
|
+
if not log_root.exists():
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
cutoff = datetime.now() - timedelta(days=keep_days)
|
|
266
|
+
files: list[Path] = [p for p in log_root.rglob("*") if p.is_file() and not p.is_symlink()]
|
|
267
|
+
|
|
268
|
+
# Remove by age
|
|
269
|
+
for path in files:
|
|
270
|
+
try:
|
|
271
|
+
mtime = datetime.fromtimestamp(path.stat().st_mtime)
|
|
272
|
+
except OSError:
|
|
273
|
+
continue
|
|
274
|
+
if mtime < cutoff:
|
|
275
|
+
_trash_path(path)
|
|
276
|
+
|
|
277
|
+
# Recompute remaining files and sizes
|
|
278
|
+
remaining: list[tuple[Path, float, int]] = []
|
|
279
|
+
total_size = 0
|
|
280
|
+
for path in log_root.rglob("*"):
|
|
281
|
+
if not path.is_file() or path.is_symlink():
|
|
282
|
+
continue
|
|
283
|
+
try:
|
|
284
|
+
stat = path.stat()
|
|
285
|
+
except OSError:
|
|
286
|
+
continue
|
|
287
|
+
remaining.append((path, stat.st_mtime, stat.st_size))
|
|
288
|
+
total_size += stat.st_size
|
|
289
|
+
|
|
290
|
+
if total_size <= max_total_bytes:
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
remaining.sort(key=lambda item: item[1])
|
|
294
|
+
for path, _, size in remaining:
|
|
295
|
+
_trash_path(path)
|
|
296
|
+
total_size -= size
|
|
297
|
+
if total_size <= max_total_bytes:
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _trash_path(path: Path) -> None:
|
|
302
|
+
"""Send a path to trash, falling back to unlink if trash is unavailable."""
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
subprocess.run(
|
|
306
|
+
["trash", str(path)],
|
|
307
|
+
stdin=subprocess.DEVNULL,
|
|
308
|
+
stdout=subprocess.DEVNULL,
|
|
309
|
+
stderr=subprocess.DEVNULL,
|
|
310
|
+
check=False,
|
|
311
|
+
)
|
|
312
|
+
except FileNotFoundError:
|
|
313
|
+
path.unlink(missing_ok=True)
|
klaude_code/ui/__init__.py
CHANGED
|
@@ -73,19 +73,14 @@ def create_exec_display(debug: bool = False, stream_json: bool = False) -> Displ
|
|
|
73
73
|
|
|
74
74
|
|
|
75
75
|
__all__ = [
|
|
76
|
-
|
|
76
|
+
"DebugEventDisplay",
|
|
77
77
|
"DisplayABC",
|
|
78
|
+
"ExecDisplay",
|
|
78
79
|
"InputProviderABC",
|
|
79
|
-
|
|
80
|
+
"PromptToolkitInput",
|
|
80
81
|
"REPLDisplay",
|
|
81
|
-
"ExecDisplay",
|
|
82
82
|
"StreamJsonDisplay",
|
|
83
|
-
"
|
|
84
|
-
# Input implementations
|
|
85
|
-
"PromptToolkitInput",
|
|
86
|
-
# Factory functions
|
|
83
|
+
"TerminalNotifier",
|
|
87
84
|
"create_default_display",
|
|
88
85
|
"create_exec_display",
|
|
89
|
-
# Supporting types
|
|
90
|
-
"TerminalNotifier",
|
|
91
86
|
]
|
klaude_code/ui/core/input.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
3
4
|
from enum import Enum
|
|
4
|
-
from typing import Awaitable, Callable
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Stage(Enum):
|
|
@@ -19,11 +19,11 @@ class StageManager:
|
|
|
19
19
|
self,
|
|
20
20
|
*,
|
|
21
21
|
finish_assistant: Callable[[], Awaitable[None]],
|
|
22
|
-
|
|
22
|
+
finish_thinking: Callable[[], Awaitable[None]],
|
|
23
23
|
):
|
|
24
24
|
self._stage = Stage.WAITING
|
|
25
25
|
self._finish_assistant = finish_assistant
|
|
26
|
-
self.
|
|
26
|
+
self._finish_thinking = finish_thinking
|
|
27
27
|
|
|
28
28
|
@property
|
|
29
29
|
def current_stage(self) -> Stage:
|
|
@@ -39,7 +39,6 @@ class StageManager:
|
|
|
39
39
|
if self._stage == Stage.THINKING:
|
|
40
40
|
return
|
|
41
41
|
await self.transition_to(Stage.THINKING)
|
|
42
|
-
self._on_enter_thinking()
|
|
43
42
|
|
|
44
43
|
async def finish_assistant(self) -> None:
|
|
45
44
|
if self._stage != Stage.ASSISTANT:
|
|
@@ -49,7 +48,8 @@ class StageManager:
|
|
|
49
48
|
self._stage = Stage.WAITING
|
|
50
49
|
|
|
51
50
|
async def _leave_current_stage(self) -> None:
|
|
52
|
-
if self._stage == Stage.
|
|
51
|
+
if self._stage == Stage.THINKING:
|
|
52
|
+
await self._finish_thinking()
|
|
53
|
+
elif self._stage == Stage.ASSISTANT:
|
|
53
54
|
await self.finish_assistant()
|
|
54
|
-
|
|
55
|
-
self._stage = Stage.WAITING
|
|
55
|
+
self._stage = Stage.WAITING
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from typing import override
|
|
2
3
|
|
|
3
4
|
from klaude_code import const
|
|
@@ -10,7 +11,7 @@ class DebugEventDisplay(DisplayABC):
|
|
|
10
11
|
def __init__(
|
|
11
12
|
self,
|
|
12
13
|
wrapped_display: DisplayABC | None = None,
|
|
13
|
-
log_file: str = const.DEFAULT_DEBUG_LOG_FILE,
|
|
14
|
+
log_file: str | os.PathLike[str] = const.DEFAULT_DEBUG_LOG_FILE,
|
|
14
15
|
):
|
|
15
16
|
self.wrapped_display = wrapped_display
|
|
16
17
|
self.log_file = log_file
|
|
@@ -1,51 +1,6 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
from klaude_code.protocol import model
|
|
6
1
|
from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
|
|
7
2
|
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from klaude_code.core.agent import Agent
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def build_repl_status_snapshot(agent: "Agent | None", update_message: str | None) -> REPLStatusSnapshot:
|
|
13
|
-
"""Build a status snapshot for the REPL bottom toolbar.
|
|
14
|
-
|
|
15
|
-
Aggregates model name, context usage, and basic call counts from the
|
|
16
|
-
provided agent's session history.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
model_name = ""
|
|
20
|
-
context_usage_percent: float | None = None
|
|
21
|
-
llm_calls = 0
|
|
22
|
-
tool_calls = 0
|
|
23
|
-
|
|
24
|
-
if agent is not None:
|
|
25
|
-
profile = agent.profile
|
|
26
|
-
if profile is not None:
|
|
27
|
-
model_name = profile.llm_client.model_name or ""
|
|
28
|
-
else:
|
|
29
|
-
model_name = "N/A"
|
|
30
|
-
|
|
31
|
-
history = agent.session.conversation_history
|
|
32
|
-
for item in history:
|
|
33
|
-
if isinstance(item, model.AssistantMessageItem):
|
|
34
|
-
llm_calls += 1
|
|
35
|
-
elif isinstance(item, model.ToolCallItem):
|
|
36
|
-
tool_calls += 1
|
|
37
|
-
|
|
38
|
-
for item in reversed(history):
|
|
39
|
-
if isinstance(item, model.ResponseMetadataItem):
|
|
40
|
-
usage = item.usage
|
|
41
|
-
if usage is not None and hasattr(usage, "context_usage_percent"):
|
|
42
|
-
context_usage_percent = usage.context_usage_percent
|
|
43
|
-
break
|
|
44
3
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
llm_calls=llm_calls,
|
|
49
|
-
tool_calls=tool_calls,
|
|
50
|
-
update_message=update_message,
|
|
51
|
-
)
|
|
4
|
+
def build_repl_status_snapshot(update_message: str | None) -> REPLStatusSnapshot:
|
|
5
|
+
"""Build a status snapshot for the REPL bottom toolbar."""
|
|
6
|
+
return REPLStatusSnapshot(update_message=update_message)
|
|
@@ -40,19 +40,19 @@ class ClipboardCaptureState:
|
|
|
40
40
|
"""Capture image from clipboard, save to disk, and return a tag like [Image #N]."""
|
|
41
41
|
try:
|
|
42
42
|
clipboard_data = ImageGrab.grabclipboard()
|
|
43
|
-
except
|
|
43
|
+
except OSError:
|
|
44
44
|
return None
|
|
45
45
|
if not isinstance(clipboard_data, Image.Image):
|
|
46
46
|
return None
|
|
47
47
|
try:
|
|
48
48
|
self._images_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
-
except
|
|
49
|
+
except OSError:
|
|
50
50
|
return None
|
|
51
51
|
filename = f"clipboard_{uuid.uuid4().hex[:8]}.png"
|
|
52
52
|
path = self._images_dir / filename
|
|
53
53
|
try:
|
|
54
54
|
clipboard_data.save(path, "PNG")
|
|
55
|
-
except
|
|
55
|
+
except OSError:
|
|
56
56
|
return None
|
|
57
57
|
tag = f"[Image #{self._counter}]"
|
|
58
58
|
self._counter += 1
|
|
@@ -123,7 +123,7 @@ def _encode_image_file(file_path: str) -> ImageURLPart | None:
|
|
|
123
123
|
# Clipboard images are always saved as PNG
|
|
124
124
|
data_url = f"data:image/png;base64,{encoded}"
|
|
125
125
|
return ImageURLPart(image_url=ImageURLPart.ImageURL(url=data_url, id=None))
|
|
126
|
-
except
|
|
126
|
+
except OSError:
|
|
127
127
|
return None
|
|
128
128
|
|
|
129
129
|
|
|
@@ -148,5 +148,5 @@ def copy_to_clipboard(text: str) -> None:
|
|
|
148
148
|
input=text.encode("utf-8"),
|
|
149
149
|
check=True,
|
|
150
150
|
)
|
|
151
|
-
except
|
|
151
|
+
except (OSError, subprocess.SubprocessError):
|
|
152
152
|
pass
|