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.
Files changed (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. 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
+ ```