klaude-code 1.2.6__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/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TextIO, cast
|
|
8
|
+
|
|
9
|
+
from klaude_code.trace import DebugType, log_debug
|
|
10
|
+
|
|
11
|
+
ST = "\033\\"
|
|
12
|
+
BEL = "\a"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def resolve_stream(stream: TextIO | None) -> TextIO:
|
|
16
|
+
"""Use the original stdout when available to avoid interception by Rich wrappers."""
|
|
17
|
+
if stream is not None:
|
|
18
|
+
return stream
|
|
19
|
+
if hasattr(sys, "__stdout__") and sys.__stdout__ is not None:
|
|
20
|
+
return cast(TextIO, sys.__stdout__)
|
|
21
|
+
return sys.stdout
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NotificationType(Enum):
|
|
25
|
+
AGENT_TASK_COMPLETE = "agent_task_complete"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Notification:
|
|
30
|
+
type: NotificationType
|
|
31
|
+
title: str
|
|
32
|
+
body: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class TerminalNotifierConfig:
|
|
37
|
+
enabled: bool = True
|
|
38
|
+
use_bel: bool = False
|
|
39
|
+
stream: TextIO | None = None
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_env(cls) -> "TerminalNotifierConfig":
|
|
43
|
+
env = os.getenv("KLAUDE_NOTIFY", "").strip().lower()
|
|
44
|
+
if env in {"0", "off", "false", "disable", "disabled"}:
|
|
45
|
+
return cls(enabled=False)
|
|
46
|
+
return cls(enabled=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TerminalNotifier:
|
|
50
|
+
def __init__(self, config: TerminalNotifierConfig | None = None) -> None:
|
|
51
|
+
self.config = config or TerminalNotifierConfig.from_env()
|
|
52
|
+
|
|
53
|
+
def notify(self, notification: Notification) -> bool:
|
|
54
|
+
if not self.config.enabled:
|
|
55
|
+
log_debug(
|
|
56
|
+
"Terminal notifier skipped: disabled via config",
|
|
57
|
+
debug_type=DebugType.TERMINAL,
|
|
58
|
+
)
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
output = resolve_stream(self.config.stream)
|
|
62
|
+
if not self._supports_osc9(output):
|
|
63
|
+
log_debug(
|
|
64
|
+
"Terminal notifier skipped: OSC 9 unsupported or not a TTY",
|
|
65
|
+
debug_type=DebugType.TERMINAL,
|
|
66
|
+
)
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
payload = self._render_payload(notification)
|
|
70
|
+
return self._emit(payload, output)
|
|
71
|
+
|
|
72
|
+
def _render_payload(self, notification: Notification) -> str:
|
|
73
|
+
title = _compact(notification.title)
|
|
74
|
+
body = _compact(notification.body) if notification.body else None
|
|
75
|
+
if body:
|
|
76
|
+
return f"{title} - {body}"
|
|
77
|
+
return title
|
|
78
|
+
|
|
79
|
+
def _emit(self, payload: str, output: TextIO) -> bool:
|
|
80
|
+
terminator = BEL if self.config.use_bel else ST
|
|
81
|
+
seq = f"\033]9;{payload}{terminator}"
|
|
82
|
+
try:
|
|
83
|
+
output.write(seq)
|
|
84
|
+
output.flush()
|
|
85
|
+
log_debug("Terminal notifier sent OSC 9 payload", debug_type=DebugType.TERMINAL)
|
|
86
|
+
return True
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
log_debug(f"Terminal notifier send failed: {exc}", debug_type=DebugType.TERMINAL)
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _supports_osc9(stream: TextIO) -> bool:
|
|
93
|
+
if sys.platform == "win32":
|
|
94
|
+
return False
|
|
95
|
+
if not getattr(stream, "isatty", lambda: False)():
|
|
96
|
+
return False
|
|
97
|
+
term = os.getenv("TERM", "")
|
|
98
|
+
if term.lower() in {"", "dumb"}:
|
|
99
|
+
return False
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _compact(text: str, limit: int = 160) -> str:
|
|
104
|
+
squashed = " ".join(text.split())
|
|
105
|
+
if len(squashed) > limit:
|
|
106
|
+
return squashed[: limit - 3] + "…"
|
|
107
|
+
return squashed
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Use OSC 9;4;... to control progress bar in terminal like Ghostty
|
|
3
|
+
States:
|
|
4
|
+
0/hidden
|
|
5
|
+
1/normal
|
|
6
|
+
2/error
|
|
7
|
+
3/indeterminate
|
|
8
|
+
4/warning
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import TextIO, cast
|
|
16
|
+
|
|
17
|
+
is_ghostty = os.environ.get("TERM") == "xterm-ghostty" or "GHOSTTY_RESOURCES_DIR" in os.environ
|
|
18
|
+
|
|
19
|
+
ST = "\033\\" # ESC \
|
|
20
|
+
BEL = "\a" # Some terminals also accept BEL as terminator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OSC94States(Enum):
|
|
24
|
+
HIDDEN = 0
|
|
25
|
+
NORMAL = 1
|
|
26
|
+
ERROR = 2
|
|
27
|
+
INDETERMINATE = 3
|
|
28
|
+
WARNING = 4
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_stream(stream: TextIO | None) -> TextIO:
|
|
32
|
+
"""
|
|
33
|
+
Rich's status.start() (backed by Live) temporarily replaces sys.stdout/sys.stderr with a
|
|
34
|
+
Console._redirect_stdio wrapper. The wrapper strips control codes like OSC and BEL before
|
|
35
|
+
writing, so sequences such as \\x1b]9;4;3\\x1b\\ are truncated to ]9;4;3. Using sys.__stdout__ or
|
|
36
|
+
an explicit Console file handle bypasses the wrapper and preserves the full OSC payload.
|
|
37
|
+
"""
|
|
38
|
+
if stream is not None:
|
|
39
|
+
return stream
|
|
40
|
+
if hasattr(sys, "__stdout__") and sys.__stdout__ is not None:
|
|
41
|
+
return cast(TextIO, sys.__stdout__)
|
|
42
|
+
return sys.stdout
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def emit_osc94(
|
|
46
|
+
state: OSC94States,
|
|
47
|
+
progress: int | None = None,
|
|
48
|
+
*,
|
|
49
|
+
use_bel: bool = False,
|
|
50
|
+
stream: TextIO | None = None,
|
|
51
|
+
):
|
|
52
|
+
if not is_ghostty:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
seq = f"\033]9;4;{state.value}"
|
|
56
|
+
if state == OSC94States.NORMAL: # Normal progress needs percentage
|
|
57
|
+
if progress is None:
|
|
58
|
+
progress = 0
|
|
59
|
+
seq += f";{int(progress)}"
|
|
60
|
+
terminator = BEL if use_bel else ST
|
|
61
|
+
output = resolve_stream(stream)
|
|
62
|
+
output.write(seq + terminator)
|
|
63
|
+
output.flush()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
for i in range(101):
|
|
68
|
+
emit_osc94(OSC94States.NORMAL, i)
|
|
69
|
+
time.sleep(0.02)
|
|
70
|
+
|
|
71
|
+
# Clear progress bar
|
|
72
|
+
emit_osc94(OSC94States.HIDDEN)
|
|
73
|
+
|
|
74
|
+
print("Waiting...")
|
|
75
|
+
# Indeterminate
|
|
76
|
+
emit_osc94(OSC94States.INDETERMINATE)
|
|
77
|
+
|
|
78
|
+
time.sleep(2)
|
|
79
|
+
print("Error...")
|
|
80
|
+
# Error
|
|
81
|
+
emit_osc94(OSC94States.ERROR)
|
|
82
|
+
|
|
83
|
+
time.sleep(2)
|
|
84
|
+
print("Warning...")
|
|
85
|
+
# Warning
|
|
86
|
+
emit_osc94(OSC94States.WARNING)
|
|
87
|
+
time.sleep(2)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# UI utilities
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from klaude_code import const
|
|
6
|
+
|
|
7
|
+
LEADING_NEWLINES_REGEX = re.compile(r"^\n{2,}")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def remove_leading_newlines(text: str) -> str:
|
|
11
|
+
return text.lstrip("\n")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_number(tokens: int) -> str:
|
|
15
|
+
if tokens < 1000:
|
|
16
|
+
return f"{tokens}"
|
|
17
|
+
elif tokens < 1000000:
|
|
18
|
+
# 12.3k
|
|
19
|
+
k = tokens / 1000
|
|
20
|
+
if k == int(k):
|
|
21
|
+
return f"{int(k)}k"
|
|
22
|
+
else:
|
|
23
|
+
return f"{k:.1f}k"
|
|
24
|
+
else:
|
|
25
|
+
# 2M345k
|
|
26
|
+
m = tokens // 1000000
|
|
27
|
+
remaining = (tokens % 1000000) // 1000
|
|
28
|
+
if remaining == 0:
|
|
29
|
+
return f"{m}M"
|
|
30
|
+
else:
|
|
31
|
+
return f"{m}M{remaining}k"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_current_git_branch(path: Path | None = None) -> str | None:
|
|
35
|
+
"""Get current git branch name, return None if not in a git repository"""
|
|
36
|
+
if path is None:
|
|
37
|
+
path = Path.cwd()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
# Check if in git repository
|
|
41
|
+
git_dir = subprocess.run(
|
|
42
|
+
["git", "rev-parse", "--git-dir"],
|
|
43
|
+
cwd=path,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
timeout=2,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if git_dir.returncode != 0:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
# Get current branch name
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
["git", "branch", "--show-current"],
|
|
55
|
+
cwd=path,
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
timeout=2,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if result.returncode == 0:
|
|
62
|
+
branch = result.stdout.strip()
|
|
63
|
+
return branch if branch else None
|
|
64
|
+
|
|
65
|
+
# Fallback: get HEAD reference
|
|
66
|
+
head_file = subprocess.run(
|
|
67
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
68
|
+
cwd=path,
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
timeout=2,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if head_file.returncode == 0:
|
|
75
|
+
branch = head_file.stdout.strip()
|
|
76
|
+
return branch if branch and branch != "HEAD" else None
|
|
77
|
+
|
|
78
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def show_path_with_tilde(path: Path | None = None):
|
|
85
|
+
if path is None:
|
|
86
|
+
path = Path.cwd()
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
relative_path = path.relative_to(Path.home())
|
|
90
|
+
return f"~/{relative_path}"
|
|
91
|
+
except ValueError:
|
|
92
|
+
return str(path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def truncate_display(
|
|
96
|
+
text: str,
|
|
97
|
+
max_lines: int = const.TRUNCATE_DISPLAY_MAX_LINES,
|
|
98
|
+
max_line_length: int = const.TRUNCATE_DISPLAY_MAX_LINE_LENGTH,
|
|
99
|
+
) -> str:
|
|
100
|
+
lines = text.split("\n")
|
|
101
|
+
if len(lines) > max_lines:
|
|
102
|
+
lines = lines[:max_lines] + ["… (more " + str(len(lines) - max_lines) + " lines)"]
|
|
103
|
+
for i, line in enumerate(lines):
|
|
104
|
+
if len(line) > max_line_length:
|
|
105
|
+
lines[i] = (
|
|
106
|
+
line[:max_line_length] + "… (more " + str(len(line) - max_line_length) + " characters in this line)"
|
|
107
|
+
)
|
|
108
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Awaitable, Callable, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Debouncer:
|
|
6
|
+
"""Debouncing mechanism"""
|
|
7
|
+
|
|
8
|
+
def __init__(self, interval: float, callback: Callable[[], Awaitable[None]]):
|
|
9
|
+
"""
|
|
10
|
+
Initialize debouncer
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
interval: Debounce interval in seconds
|
|
14
|
+
callback: Async callback function to execute after debouncing
|
|
15
|
+
"""
|
|
16
|
+
self.interval = interval
|
|
17
|
+
self.callback = callback
|
|
18
|
+
self._task: Optional[asyncio.Task[None]] = None
|
|
19
|
+
|
|
20
|
+
def cancel(self) -> None:
|
|
21
|
+
"""Cancel current debounce task"""
|
|
22
|
+
if self._task is not None and not self._task.done():
|
|
23
|
+
self._task.cancel()
|
|
24
|
+
self._task = None
|
|
25
|
+
|
|
26
|
+
def schedule(self) -> None:
|
|
27
|
+
"""Schedule debounce task"""
|
|
28
|
+
self.cancel()
|
|
29
|
+
self._task = asyncio.create_task(self._debounced_execute())
|
|
30
|
+
|
|
31
|
+
async def _debounced_execute(self) -> None:
|
|
32
|
+
"""Execute debounced callback function"""
|
|
33
|
+
try:
|
|
34
|
+
await asyncio.sleep(self.interval)
|
|
35
|
+
await self.callback()
|
|
36
|
+
except asyncio.CancelledError:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
async def flush(self) -> None:
|
|
40
|
+
"""Immediately execute debounce task (without waiting)"""
|
|
41
|
+
self.cancel()
|
|
42
|
+
await self.callback()
|
klaude_code/version.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Version checking utilities for klaude-code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import urllib.request
|
|
11
|
+
from typing import NamedTuple
|
|
12
|
+
|
|
13
|
+
PACKAGE_NAME = "klaude-code"
|
|
14
|
+
PYPI_URL = f"https://pypi.org/pypi/{PACKAGE_NAME}/json"
|
|
15
|
+
CHECK_INTERVAL_SECONDS = 3600 # Check at most once per hour
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VersionInfo(NamedTuple):
|
|
19
|
+
"""Version check result."""
|
|
20
|
+
|
|
21
|
+
installed: str | None
|
|
22
|
+
latest: str | None
|
|
23
|
+
update_available: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Async check state
|
|
27
|
+
_cached_version_info: VersionInfo | None = None
|
|
28
|
+
_last_check_time: float = 0.0
|
|
29
|
+
_check_lock = threading.Lock()
|
|
30
|
+
_check_in_progress = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _has_uv() -> bool:
|
|
34
|
+
"""Check if uv command is available."""
|
|
35
|
+
return shutil.which("uv") is not None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_installed_version() -> str | None:
|
|
39
|
+
"""Get installed version of klaude-code via uv tool list."""
|
|
40
|
+
try:
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
["uv", "tool", "list"],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=5,
|
|
46
|
+
)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
return None
|
|
49
|
+
# Parse output like "klaude-code v0.1.0"
|
|
50
|
+
for line in result.stdout.splitlines():
|
|
51
|
+
if line.startswith(PACKAGE_NAME):
|
|
52
|
+
parts = line.split()
|
|
53
|
+
if len(parts) >= 2:
|
|
54
|
+
ver = parts[1]
|
|
55
|
+
# Remove 'v' prefix if present
|
|
56
|
+
if ver.startswith("v"):
|
|
57
|
+
ver = ver[1:]
|
|
58
|
+
return ver
|
|
59
|
+
return None
|
|
60
|
+
except Exception:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_latest_version() -> str | None:
|
|
65
|
+
"""Get latest version from PyPI."""
|
|
66
|
+
try:
|
|
67
|
+
with urllib.request.urlopen(PYPI_URL, timeout=5) as response:
|
|
68
|
+
data = json.loads(response.read().decode())
|
|
69
|
+
return data.get("info", {}).get("version")
|
|
70
|
+
except Exception:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_version(v: str) -> tuple[int, ...]:
|
|
75
|
+
"""Parse version string into comparable tuple of integers."""
|
|
76
|
+
parts: list[int] = []
|
|
77
|
+
for part in v.split("."):
|
|
78
|
+
# Extract leading digits
|
|
79
|
+
digits = ""
|
|
80
|
+
for c in part:
|
|
81
|
+
if c.isdigit():
|
|
82
|
+
digits += c
|
|
83
|
+
else:
|
|
84
|
+
break
|
|
85
|
+
if digits:
|
|
86
|
+
parts.append(int(digits))
|
|
87
|
+
return tuple(parts)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _compare_versions(installed: str, latest: str) -> bool:
|
|
91
|
+
"""Return True if latest is newer than installed."""
|
|
92
|
+
try:
|
|
93
|
+
installed_tuple = _parse_version(installed)
|
|
94
|
+
latest_tuple = _parse_version(latest)
|
|
95
|
+
return latest_tuple > installed_tuple
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _do_version_check() -> None:
|
|
101
|
+
"""Perform version check in background thread."""
|
|
102
|
+
global _cached_version_info, _last_check_time, _check_in_progress
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
installed = _get_installed_version()
|
|
106
|
+
latest = _get_latest_version()
|
|
107
|
+
|
|
108
|
+
update_available = False
|
|
109
|
+
if installed and latest:
|
|
110
|
+
update_available = _compare_versions(installed, latest)
|
|
111
|
+
|
|
112
|
+
with _check_lock:
|
|
113
|
+
_cached_version_info = VersionInfo(
|
|
114
|
+
installed=installed,
|
|
115
|
+
latest=latest,
|
|
116
|
+
update_available=update_available,
|
|
117
|
+
)
|
|
118
|
+
_last_check_time = time.time()
|
|
119
|
+
finally:
|
|
120
|
+
with _check_lock:
|
|
121
|
+
_check_in_progress = False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def check_for_updates() -> VersionInfo | None:
|
|
125
|
+
"""Check for updates to klaude-code asynchronously.
|
|
126
|
+
|
|
127
|
+
Returns cached VersionInfo immediately if available.
|
|
128
|
+
Triggers background check if cache is stale or missing.
|
|
129
|
+
Returns None if uv is not available or no cached result yet.
|
|
130
|
+
"""
|
|
131
|
+
global _check_in_progress
|
|
132
|
+
|
|
133
|
+
if not _has_uv():
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
now = time.time()
|
|
137
|
+
|
|
138
|
+
with _check_lock:
|
|
139
|
+
cache_valid = _cached_version_info is not None and (now - _last_check_time) < CHECK_INTERVAL_SECONDS
|
|
140
|
+
|
|
141
|
+
if cache_valid:
|
|
142
|
+
return _cached_version_info
|
|
143
|
+
|
|
144
|
+
# Start background check if not already in progress
|
|
145
|
+
if not _check_in_progress:
|
|
146
|
+
_check_in_progress = True
|
|
147
|
+
thread = threading.Thread(target=_do_version_check, daemon=True)
|
|
148
|
+
thread.start()
|
|
149
|
+
|
|
150
|
+
# Return cached result (may be stale) or None if no cache yet
|
|
151
|
+
return _cached_version_info
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_update_message() -> str | None:
|
|
155
|
+
"""Get update message if an update is available.
|
|
156
|
+
|
|
157
|
+
Returns immediately with cached result. Triggers async check if needed.
|
|
158
|
+
Returns None if no update, uv unavailable, or check not complete yet.
|
|
159
|
+
"""
|
|
160
|
+
info = check_for_updates()
|
|
161
|
+
if info is None or not info.update_available:
|
|
162
|
+
return None
|
|
163
|
+
return f"New version available: {info.latest}. Please run `uv tool upgrade {PACKAGE_NAME}` to upgrade."
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: klaude-code
|
|
3
|
+
Version: 1.2.6
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Dist: anthropic>=0.66.0
|
|
6
|
+
Requires-Dist: openai>=1.102.0
|
|
7
|
+
Requires-Dist: pillow>=12.0.0
|
|
8
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
9
|
+
Requires-Dist: pydantic>=2.11.7
|
|
10
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
11
|
+
Requires-Dist: questionary>=2.1.1
|
|
12
|
+
Requires-Dist: rich>=14.1.0
|
|
13
|
+
Requires-Dist: trafilatura>=2.0.0
|
|
14
|
+
Requires-Dist: typer>=0.17.3
|
|
15
|
+
Requires-Python: >=3.13
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# Minimal Code Agent CLI (Klaude Code)
|
|
19
|
+
|
|
20
|
+
An minimal and opinionated code agent with multi-model support.
|
|
21
|
+
|
|
22
|
+
## Key Features
|
|
23
|
+
- **Adaptive Tooling**: Model-aware toolsets (Claude Code tools for Sonnet, Codex `apply_patch` for GPT-5.1/Codex).
|
|
24
|
+
- **Multi-Provider Support**: Compatible with `anthropic-messages-api`,`openai-responses-api`, and `openai-compatible-api`(`openrouter-api`), featuring interleaved thinking, OpenRouter's provider sorting etc.
|
|
25
|
+
- **Skill System**: Extensible support for loading Claude Skills.
|
|
26
|
+
- **Session Management**: Robust context preservation with resumable sessions (`--continue`).
|
|
27
|
+
- **Simple TUI**: Clean interface offering full visibility into model responses, reasoning and actions.
|
|
28
|
+
- **Core Utilities**: Slash commands, sub-agents, image pasting, terminal notifications, file mentioning, and auto-theming.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install klaude-code
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
To update:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv tool upgrade klaude-code
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
### Interactive Mode
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
klaude [--model <name>] [--select-model]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Options:**
|
|
51
|
+
- `--version`/`-V`: Show version and exit.
|
|
52
|
+
- `--model`/`-m`: Select a model by logical name from config.
|
|
53
|
+
- `--select-model`/`-s`: Interactively choose a model at startup.
|
|
54
|
+
- `--continue`/`-c`: Resume the most recent session.
|
|
55
|
+
- `--resume`/`-r`: Select a session to resume for this project.
|
|
56
|
+
- `--vanilla`: Minimal mode with only basic tools (Bash, Read, Edit) and no system prompts.
|
|
57
|
+
|
|
58
|
+
**Debug Options:**
|
|
59
|
+
- `--debug`/`-d`: Enable debug mode with verbose logging and LLM trace.
|
|
60
|
+
- `--debug-filter`: Filter debug output by type (comma-separated).
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### Configuration
|
|
64
|
+
|
|
65
|
+
An example config will be created in `~/.klaude/config.yaml` when first run.
|
|
66
|
+
|
|
67
|
+
Open the configuration file in editor:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
klaude config
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
An minimal example config yaml using OpenRouter's API Key:
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
provider_list:
|
|
77
|
+
- provider_name: openrouter-work
|
|
78
|
+
protocol: openrouter # support <responses|openrouter|anthropic|openai>
|
|
79
|
+
api_key: <your-openrouter-api-key>
|
|
80
|
+
model_list:
|
|
81
|
+
- model_name: gpt-5.1-codex
|
|
82
|
+
provider: openrouter
|
|
83
|
+
model_params:
|
|
84
|
+
model: openai/gpt-5.1-codex
|
|
85
|
+
context_limit: 368000
|
|
86
|
+
thinking:
|
|
87
|
+
reasoning_effort: medium
|
|
88
|
+
- model_name: gpt-5.1-high
|
|
89
|
+
provider: openrouter
|
|
90
|
+
model_params:
|
|
91
|
+
model: openai/gpt-5.1
|
|
92
|
+
context_limit: 368000
|
|
93
|
+
thinking:
|
|
94
|
+
reasoning_effort: high
|
|
95
|
+
- model_name: sonnet
|
|
96
|
+
provider: openrouter
|
|
97
|
+
model_params:
|
|
98
|
+
model: anthropic/claude-4.5-sonnet
|
|
99
|
+
context_limit: 168000
|
|
100
|
+
provider_routing:
|
|
101
|
+
sort: throughput
|
|
102
|
+
- model_name: haiku
|
|
103
|
+
provider: openrouter
|
|
104
|
+
model_params:
|
|
105
|
+
model: anthropic/claude-haiku-4.5
|
|
106
|
+
context_limit: 168000
|
|
107
|
+
provider_routing:
|
|
108
|
+
sort: throughput
|
|
109
|
+
main_model: gpt-5.1-codex
|
|
110
|
+
subagent_models:
|
|
111
|
+
oracle: gpt-5.1-high
|
|
112
|
+
explore: haiku
|
|
113
|
+
task: sonnet
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
List configured providers and models:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
klaude list
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Session Management
|
|
123
|
+
|
|
124
|
+
Clean up sessions with few messages:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Remove sessions with fewer than 5 messages (default)
|
|
128
|
+
klaude session clean
|
|
129
|
+
|
|
130
|
+
# Remove sessions with fewer than 10 messages
|
|
131
|
+
klaude session clean --min 10
|
|
132
|
+
|
|
133
|
+
# Remove all sessions for the current project
|
|
134
|
+
klaude session clean-all
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Slash Commands
|
|
138
|
+
|
|
139
|
+
Inside the interactive session (`klaude`), use these commands to streamline your workflow:
|
|
140
|
+
|
|
141
|
+
- `/dev-doc [feature]` - Generate a comprehensive execution plan for a feature.
|
|
142
|
+
- `/export` - Export last assistant message to a temp Markdown file.
|
|
143
|
+
- `/init` - Bootstrap a new project structure or module.
|
|
144
|
+
- `/model` - Switch the active LLM during the session.
|
|
145
|
+
- `/clear` - Clear the current conversation context.
|
|
146
|
+
- `/diff` - Show local git diff changes.
|
|
147
|
+
- `/help` - List all available commands.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
### Input Shortcuts
|
|
151
|
+
|
|
152
|
+
| Key | Action |
|
|
153
|
+
| -------------------- | ------------------------------------------- |
|
|
154
|
+
| `Enter` | Submit input |
|
|
155
|
+
| `Shift+Enter` | Insert newline (requires `/terminal-setup`) |
|
|
156
|
+
| `Ctrl+J` | Insert newline |
|
|
157
|
+
| `Ctrl+V` | Paste image from clipboard |
|
|
158
|
+
| `Left/Right` | Move cursor (wraps across lines) |
|
|
159
|
+
| `Backspace` | Delete character or selected text |
|
|
160
|
+
| `c` (with selection) | Copy selected text to clipboard |
|
|
161
|
+
|
|
162
|
+
Mouse support is automatically enabled when input spans multiple lines.
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
### Non-Interactive Headless Mode (exec)
|
|
166
|
+
|
|
167
|
+
Execute a single command without starting the interactive REPL:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# Direct input
|
|
171
|
+
klaude exec "what is 2+2?"
|
|
172
|
+
|
|
173
|
+
# Pipe input
|
|
174
|
+
echo "hello world" | klaude exec
|
|
175
|
+
|
|
176
|
+
# With model selection
|
|
177
|
+
echo "generate quicksort in python" | klaude exec --model gpt-5.1
|
|
178
|
+
```
|