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.
Files changed (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,84 @@
1
+ """Interactive model selection for CLI."""
2
+
3
+ import sys
4
+
5
+ from klaude_code.config.config import load_config
6
+ from klaude_code.config.select_model import match_model_from_config
7
+ from klaude_code.trace import log
8
+
9
+
10
+ def select_model_interactive(preferred: str | None = None) -> str | None:
11
+ """Interactive single-choice model selector.
12
+
13
+ This function combines matching logic with interactive UI selection.
14
+ For CLI usage.
15
+
16
+ If preferred is provided:
17
+ - Exact match: return immediately
18
+ - Single partial match (case-insensitive): return immediately
19
+ - Otherwise: fall through to interactive selection
20
+ """
21
+ result = match_model_from_config(preferred)
22
+
23
+ if result.error_message:
24
+ return None
25
+
26
+ if result.matched_model:
27
+ return result.matched_model
28
+
29
+ # Non-interactive environments (CI/pipes) should never enter an interactive prompt.
30
+ # If we couldn't resolve to a single model deterministically above, fail with a clear hint.
31
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
32
+ log(("Error: cannot use interactive model selection without a TTY", "red"))
33
+ log(("Hint: pass --model <config-name> or set main_model in ~/.klaude/klaude-config.yaml", "yellow"))
34
+ if preferred:
35
+ log((f"Hint: '{preferred}' did not resolve to a single configured model", "yellow"))
36
+ return None
37
+
38
+ # Interactive selection
39
+ from prompt_toolkit.styles import Style
40
+
41
+ from klaude_code.ui.terminal.selector import build_model_select_items, select_one
42
+
43
+ config = load_config()
44
+ names = [m.model_name for m in result.filtered_models]
45
+
46
+ try:
47
+ items = build_model_select_items(result.filtered_models)
48
+
49
+ message = f"Select a model (filtered by '{result.filter_hint}'):" if result.filter_hint else "Select a model:"
50
+ selected = select_one(
51
+ message=message,
52
+ items=items,
53
+ pointer="->",
54
+ use_search_filter=True,
55
+ initial_value=config.main_model,
56
+ style=Style(
57
+ [
58
+ ("pointer", "ansigreen"),
59
+ ("highlighted", "ansigreen"),
60
+ ("msg", ""),
61
+ ("meta", "fg:ansibrightblack"),
62
+ ("text", "ansibrightblack"),
63
+ ("question", "bold"),
64
+ ("search_prefix", "ansibrightblack"),
65
+ # search filter colors at the bottom
66
+ ("search_success", "noinherit fg:ansigreen"),
67
+ ("search_none", "noinherit fg:ansired"),
68
+ ]
69
+ ),
70
+ )
71
+ if isinstance(selected, str) and selected in names:
72
+ return selected
73
+ except KeyboardInterrupt:
74
+ return None
75
+ except Exception as e:
76
+ log((f"Failed to use prompt_toolkit for model selection: {e}", "yellow"))
77
+ # Never return an unvalidated model name here.
78
+ # If we can't interactively select, fall back to a known configured model.
79
+ if isinstance(preferred, str) and preferred in names:
80
+ return preferred
81
+ if config.main_model and config.main_model in names:
82
+ return config.main_model
83
+
84
+ return None
@@ -0,0 +1,32 @@
1
+ ---
2
+ description: Add description for current jj change
3
+ ---
4
+
5
+ Run `jj status` and `jj diff --git` to see the current changes and add a description for the it.
6
+
7
+ In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:<example>
8
+ jj describe -m "$(cat <<'EOF'
9
+ Commit message here.
10
+ EOF
11
+ )"
12
+ </example>
13
+
14
+ Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
15
+ ```
16
+ <type>(<scope>): <description>
17
+ ```
18
+
19
+ Types:
20
+ - `feat`: New feature
21
+ - `fix`: Bug fix
22
+ - `docs`: Documentation changes
23
+ - `style`: Code style changes (formatting, no logic change)
24
+ - `refactor`: Code refactoring (no feature or fix)
25
+ - `test`: Adding or updating tests
26
+ - `chore`: Build process, dependencies, or tooling changes
27
+
28
+ Examples:
29
+ - `feat(cli): add --verbose flag for debug output`
30
+ - `fix(llm): handle API timeout errors gracefully`
31
+ - `docs(readme): update installation instructions`
32
+ - `refactor(core): simplify session state management`
@@ -2,9 +2,9 @@ from importlib.resources import files
2
2
 
3
3
  import yaml
4
4
 
5
- from klaude_code.command.command_abc import CommandABC, CommandResult, InputAction
6
- from klaude_code.core.agent import Agent
7
- from klaude_code.protocol import commands
5
+ from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
+ from klaude_code.protocol import commands, model, op
7
+ from klaude_code.trace import log_debug
8
8
 
9
9
 
10
10
  class PromptCommand(CommandABC):
@@ -41,7 +41,8 @@ class PromptCommand(CommandABC):
41
41
 
42
42
  self._metadata = {}
43
43
  self._content = raw_text
44
- except Exception:
44
+ except (OSError, yaml.YAMLError) as e:
45
+ log_debug(f"Failed to load prompt template {self.template_name}: {e}")
45
46
  self._metadata = {"description": "Error loading template"}
46
47
  self._content = f"Error loading template: {self.template_name}"
47
48
 
@@ -54,16 +55,23 @@ class PromptCommand(CommandABC):
54
55
  def support_addition_params(self) -> bool:
55
56
  return True
56
57
 
57
- async def run(self, raw: str, agent: Agent) -> CommandResult:
58
+ async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
58
59
  self._ensure_loaded()
59
60
  template_content = self._content or ""
60
- user_input = raw.strip() or "<none>"
61
+ user_input_text = user_input.text.strip() or "<none>"
61
62
 
62
63
  if "$ARGUMENTS" in template_content:
63
- final_prompt = template_content.replace("$ARGUMENTS", user_input)
64
+ final_prompt = template_content.replace("$ARGUMENTS", user_input_text)
64
65
  else:
65
66
  final_prompt = template_content
66
- if user_input:
67
- final_prompt += f"\n\nAdditional Instructions:\n{user_input}"
68
-
69
- return CommandResult(actions=[InputAction.run_agent(final_prompt)])
67
+ if user_input_text:
68
+ final_prompt += f"\n\nAdditional Instructions:\n{user_input_text}"
69
+
70
+ return CommandResult(
71
+ operations=[
72
+ op.RunAgentOperation(
73
+ session_id=agent.session.id,
74
+ input=model.UserInputPayload(text=final_prompt, images=user_input.images),
75
+ )
76
+ ]
77
+ )
@@ -1,10 +1,7 @@
1
- from klaude_code.command.command_abc import CommandABC, CommandResult
2
- from klaude_code.command.registry import register_command
3
- from klaude_code.core.agent import Agent
4
- from klaude_code.protocol import commands, events
1
+ from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
2
+ from klaude_code.protocol import commands, events, model
5
3
 
6
4
 
7
- @register_command
8
5
  class RefreshTerminalCommand(CommandABC):
9
6
  """Refresh terminal display"""
10
7
 
@@ -20,12 +17,13 @@ class RefreshTerminalCommand(CommandABC):
20
17
  def is_interactive(self) -> bool:
21
18
  return True
22
19
 
23
- async def run(self, raw: str, agent: Agent) -> CommandResult:
20
+ async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
21
+ del user_input # unused
24
22
  import os
25
23
 
26
24
  os.system("cls" if os.name == "nt" else "clear")
27
25
 
28
- result = CommandResult(
26
+ return CommandResult(
29
27
  events=[
30
28
  events.WelcomeEvent(
31
29
  work_dir=str(agent.session.work_dir),
@@ -37,7 +35,7 @@ class RefreshTerminalCommand(CommandABC):
37
35
  updated_at=agent.session.updated_at,
38
36
  is_load=False,
39
37
  ),
40
- ]
38
+ ],
39
+ persist_user_input=False,
40
+ persist_events=False,
41
41
  )
42
-
43
- return result
@@ -1,24 +1,82 @@
1
1
  from importlib.resources import files
2
- from typing import TYPE_CHECKING, TypeVar
2
+ from typing import TYPE_CHECKING
3
3
 
4
- from klaude_code.command.command_abc import CommandResult, InputAction
4
+ from klaude_code.command.command_abc import Agent, CommandResult
5
5
  from klaude_code.command.prompt_command import PromptCommand
6
- from klaude_code.core.agent import Agent
7
- from klaude_code.protocol import commands, events, model
6
+ from klaude_code.protocol import commands, events, model, op
7
+ from klaude_code.trace import log_debug
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from .command_abc import CommandABC
11
11
 
12
12
  _COMMANDS: dict[commands.CommandName | str, "CommandABC"] = {}
13
13
 
14
- T = TypeVar("T", bound="CommandABC")
15
14
 
15
+ def _command_key_to_str(key: commands.CommandName | str) -> str:
16
+ if isinstance(key, commands.CommandName):
17
+ return key.value
18
+ return key
16
19
 
17
- def register_command(cls: type[T]) -> type[T]:
18
- """Decorator to register a command class in the global registry."""
19
- instance = cls()
20
- _COMMANDS[instance.name] = instance
21
- return cls
20
+
21
+ def _resolve_command_key(command_name_raw: str) -> commands.CommandName | str | None:
22
+ """Resolve raw command token to a registered command key.
23
+
24
+ Resolution order:
25
+ 1) Exact match
26
+ 2) Enum conversion (for standard commands)
27
+ 3) Prefix match (supports abbreviations like `exp` -> `export`)
28
+
29
+ Prefix match rules:
30
+ - If there's exactly one prefix match, use it.
31
+ - If multiple matches exist and one command name is a prefix of all others,
32
+ treat it as the base command and use it (e.g. `export` over `export-online`).
33
+ - Otherwise, consider it ambiguous and return None.
34
+ """
35
+
36
+ if not command_name_raw:
37
+ return None
38
+
39
+ # Exact string match (works for both Enum and str keys because CommandName is a str Enum)
40
+ if command_name_raw in _COMMANDS:
41
+ return command_name_raw
42
+
43
+ # Enum conversion for standard commands
44
+ try:
45
+ enum_key = commands.CommandName(command_name_raw)
46
+ except ValueError:
47
+ enum_key = None
48
+ else:
49
+ if enum_key in _COMMANDS:
50
+ return enum_key
51
+
52
+ # Prefix match across all registered names
53
+ matching_keys: list[commands.CommandName | str] = []
54
+ matching_names: list[str] = []
55
+ for key in _COMMANDS:
56
+ key_str = _command_key_to_str(key)
57
+ if key_str.startswith(command_name_raw):
58
+ matching_keys.append(key)
59
+ matching_names.append(key_str)
60
+
61
+ if len(matching_keys) == 1:
62
+ return matching_keys[0]
63
+
64
+ if len(matching_keys) > 1:
65
+ # Prefer the base command when one is a prefix of all other matches.
66
+ base_matches = [
67
+ key
68
+ for key, key_name in zip(matching_keys, matching_names, strict=True)
69
+ if all(other.startswith(key_name) for other in matching_names if other != key_name)
70
+ ]
71
+ if len(base_matches) == 1:
72
+ return base_matches[0]
73
+
74
+ return None
75
+
76
+
77
+ def register(cmd: "CommandABC") -> None:
78
+ """Register a command instance. Order of registration determines display order."""
79
+ _COMMANDS[cmd.name] = cmd
22
80
 
23
81
 
24
82
  def load_prompt_commands():
@@ -30,52 +88,95 @@ def load_prompt_commands():
30
88
  if (name.startswith("prompt_") or name.startswith("prompt-")) and name.endswith(".md"):
31
89
  cmd = PromptCommand(name)
32
90
  _COMMANDS[cmd.name] = cmd
33
- except Exception:
34
- # If resource loading fails, just ignore
35
- pass
91
+ except OSError as e:
92
+ log_debug(f"Failed to load prompt commands: {e}")
93
+
94
+
95
+ def _ensure_commands_loaded() -> None:
96
+ """Ensure all commands are loaded (lazy initialization)."""
97
+ from klaude_code.command import ensure_commands_loaded
98
+
99
+ ensure_commands_loaded()
36
100
 
37
101
 
38
102
  def get_commands() -> dict[commands.CommandName | str, "CommandABC"]:
39
103
  """Get all registered commands."""
104
+ _ensure_commands_loaded()
40
105
  return _COMMANDS.copy()
41
106
 
42
107
 
108
+ def get_command_info_list() -> list[commands.CommandInfo]:
109
+ """Get lightweight command metadata for UI purposes.
110
+
111
+ Returns CommandInfo list in registration order (display order).
112
+ """
113
+ _ensure_commands_loaded()
114
+ return [
115
+ commands.CommandInfo(
116
+ name=_command_key_to_str(cmd.name),
117
+ summary=cmd.summary,
118
+ support_addition_params=cmd.support_addition_params,
119
+ placeholder=cmd.placeholder,
120
+ )
121
+ for cmd in _COMMANDS.values()
122
+ ]
123
+
124
+
125
+ def get_command_names() -> frozenset[str]:
126
+ """Get all registered command names as a frozen set for fast lookup."""
127
+ _ensure_commands_loaded()
128
+ return frozenset(_command_key_to_str(key) for key in _COMMANDS)
129
+
130
+
43
131
  def is_slash_command_name(name: str) -> bool:
44
- return name in _COMMANDS
132
+ _ensure_commands_loaded()
133
+ return _resolve_command_key(name) is not None
45
134
 
46
135
 
47
- async def dispatch_command(raw: str, agent: Agent) -> CommandResult:
136
+ async def dispatch_command(user_input: model.UserInputPayload, agent: Agent, *, submission_id: str) -> CommandResult:
137
+ _ensure_commands_loaded()
48
138
  # Detect command name
139
+ raw = user_input.text
49
140
  if not raw.startswith("/"):
50
- return CommandResult(actions=[InputAction.run_agent(raw)])
141
+ return CommandResult(
142
+ operations=[
143
+ op.RunAgentOperation(
144
+ id=submission_id,
145
+ session_id=agent.session.id,
146
+ input=user_input,
147
+ )
148
+ ]
149
+ )
51
150
 
52
151
  splits = raw.split(" ", maxsplit=1)
53
152
  command_name_raw = splits[0][1:]
54
153
  rest = " ".join(splits[1:]) if len(splits) > 1 else ""
55
154
 
56
- # Try to match against registered commands (both Enum and string keys)
57
- command_key = None
58
-
59
- # First try exact string match
60
- if command_name_raw in _COMMANDS:
61
- command_key = command_name_raw
62
- else:
63
- # Then try Enum conversion for standard commands
64
- try:
65
- enum_key = commands.CommandName(command_name_raw)
66
- if enum_key in _COMMANDS:
67
- command_key = enum_key
68
- except ValueError:
69
- pass
70
-
155
+ command_key = _resolve_command_key(command_name_raw)
71
156
  if command_key is None:
72
- return CommandResult(actions=[InputAction.run_agent(raw)])
157
+ return CommandResult(
158
+ operations=[
159
+ op.RunAgentOperation(
160
+ id=submission_id,
161
+ session_id=agent.session.id,
162
+ input=user_input,
163
+ )
164
+ ]
165
+ )
73
166
 
74
167
  command = _COMMANDS[command_key]
75
168
  command_identifier: commands.CommandName | str = command.name
76
169
 
77
170
  try:
78
- return await command.run(rest, agent)
171
+ user_input_for_command = model.UserInputPayload(text=rest, images=user_input.images)
172
+ result = await command.run(agent, user_input_for_command)
173
+ ops = list(result.operations or [])
174
+ for operation in ops:
175
+ if isinstance(operation, op.RunAgentOperation):
176
+ operation.id = submission_id
177
+ if ops:
178
+ result.operations = ops
179
+ return result
79
180
  except Exception as e:
80
181
  command_output = (
81
182
  model.CommandOutput(command_name=command_identifier, is_error=True)
@@ -87,7 +188,7 @@ async def dispatch_command(raw: str, agent: Agent) -> CommandResult:
87
188
  events.DeveloperMessageEvent(
88
189
  session_id=agent.session.id,
89
190
  item=model.DeveloperMessageItem(
90
- content=f"Command {command_identifier} error: [{e.__class__.__name__}] {str(e)}",
191
+ content=f"Command {command_identifier} error: [{e.__class__.__name__}] {e!s}",
91
192
  command_output=command_output,
92
193
  ),
93
194
  )
@@ -96,15 +197,13 @@ async def dispatch_command(raw: str, agent: Agent) -> CommandResult:
96
197
 
97
198
 
98
199
  def has_interactive_command(raw: str) -> bool:
200
+ _ensure_commands_loaded()
99
201
  if not raw.startswith("/"):
100
202
  return False
101
203
  splits = raw.split(" ", maxsplit=1)
102
204
  command_name_raw = splits[0][1:]
103
- try:
104
- command_name = commands.CommandName(command_name_raw)
105
- except ValueError:
106
- return False
107
- if command_name not in _COMMANDS:
205
+ command_key = _resolve_command_key(command_name_raw)
206
+ if command_key is None:
108
207
  return False
109
- command = _COMMANDS[command_name]
208
+ command = _COMMANDS[command_key]
110
209
  return command.is_interactive
@@ -0,0 +1,84 @@
1
+ from pathlib import Path
2
+
3
+ from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
4
+ from klaude_code.protocol import commands, events, model
5
+
6
+
7
+ def _read_changelog() -> str:
8
+ """Read CHANGELOG.md from project root."""
9
+ changelog_path = Path(__file__).parent.parent.parent.parent / "CHANGELOG.md"
10
+ if not changelog_path.exists():
11
+ return "CHANGELOG.md not found"
12
+ return changelog_path.read_text(encoding="utf-8")
13
+
14
+
15
+ def _extract_releases(changelog: str, count: int = 1) -> str:
16
+ """Extract release sections from changelog in reverse order (oldest first).
17
+
18
+ Args:
19
+ changelog: The full changelog content.
20
+ count: Number of releases to extract (default 1).
21
+
22
+ Returns:
23
+ The content of the specified number of releases, with newest at bottom.
24
+ """
25
+ lines = changelog.split("\n")
26
+ releases: list[list[str]] = []
27
+ current_release: list[str] = []
28
+ version_count = 0
29
+
30
+ for line in lines:
31
+ # Skip [Unreleased] section header
32
+ if line.startswith("## [Unreleased]"):
33
+ continue
34
+
35
+ # Check for version header (e.g., ## [1.2.8] - 2025-12-01)
36
+ if line.startswith("## [") and "]" in line:
37
+ if current_release:
38
+ releases.append(current_release)
39
+ version_count += 1
40
+ if version_count > count:
41
+ break
42
+ current_release = [line]
43
+ continue
44
+
45
+ if version_count > 0:
46
+ current_release.append(line)
47
+
48
+ # Append the last release if exists
49
+ if current_release and version_count <= count:
50
+ releases.append(current_release)
51
+
52
+ if not releases:
53
+ return "No release notes found"
54
+
55
+ # Reverse to show oldest first, newest last
56
+ releases.reverse()
57
+ return "\n".join("\n".join(release) for release in releases).strip()
58
+
59
+
60
+ class ReleaseNotesCommand(CommandABC):
61
+ """Display the latest release notes from CHANGELOG.md."""
62
+
63
+ @property
64
+ def name(self) -> commands.CommandName:
65
+ return commands.CommandName.RELEASE_NOTES
66
+
67
+ @property
68
+ def summary(self) -> str:
69
+ return "Show the latest release notes"
70
+
71
+ async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
72
+ del user_input # unused
73
+ changelog = _read_changelog()
74
+ content = _extract_releases(changelog, count=10)
75
+
76
+ event = events.DeveloperMessageEvent(
77
+ session_id=agent.session.id,
78
+ item=model.DeveloperMessageItem(
79
+ content=content,
80
+ command_output=model.CommandOutput(command_name=self.name),
81
+ ),
82
+ )
83
+
84
+ return CommandResult(events=[event])
@@ -0,0 +1,111 @@
1
+ import asyncio
2
+
3
+ from prompt_toolkit.styles import Style
4
+
5
+ from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
+ from klaude_code.protocol import commands, events, model, op
7
+ from klaude_code.session.selector import build_session_select_options, format_user_messages_display
8
+ from klaude_code.trace import log
9
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
10
+
11
+ SESSION_SELECT_STYLE = Style(
12
+ [
13
+ ("msg", "fg:ansibrightblack"),
14
+ ("meta", ""),
15
+ ("pointer", "bold fg:ansigreen"),
16
+ ("highlighted", "fg:ansigreen"),
17
+ ("search_prefix", "fg:ansibrightblack"),
18
+ ("search_success", "noinherit fg:ansigreen"),
19
+ ("search_none", "noinherit fg:ansired"),
20
+ ("question", "bold"),
21
+ ("text", ""),
22
+ ]
23
+ )
24
+
25
+
26
+ def select_session_sync() -> str | None:
27
+ """Interactive session selection (sync version for asyncio.to_thread)."""
28
+ options = build_session_select_options()
29
+ if not options:
30
+ log("No sessions found for this project.")
31
+ return None
32
+
33
+ items: list[SelectItem[str]] = []
34
+ for idx, opt in enumerate(options, 1):
35
+ display_msgs = format_user_messages_display(opt.user_messages)
36
+ title: list[tuple[str, str]] = []
37
+ title.append(("fg:ansibrightblack", f"{idx:2}. "))
38
+ title.append(
39
+ ("class:meta", f"{opt.relative_time} · {opt.messages_count} · {opt.model_name} · {opt.session_id}\n")
40
+ )
41
+ for msg in display_msgs:
42
+ if msg == "⋮":
43
+ title.append(("class:msg", f" {msg}\n"))
44
+ else:
45
+ title.append(("class:msg", f" > {msg}\n"))
46
+ title.append(("", "\n"))
47
+
48
+ search_text = " ".join(opt.user_messages) + f" {opt.model_name} {opt.session_id}"
49
+ items.append(
50
+ SelectItem(
51
+ title=title,
52
+ value=opt.session_id,
53
+ search_text=search_text,
54
+ )
55
+ )
56
+
57
+ try:
58
+ return select_one(
59
+ message="Select a session to resume:",
60
+ items=items,
61
+ pointer="→",
62
+ style=SESSION_SELECT_STYLE,
63
+ )
64
+ except KeyboardInterrupt:
65
+ return None
66
+
67
+
68
+ class ResumeCommand(CommandABC):
69
+ """Resume a previous session."""
70
+
71
+ @property
72
+ def name(self) -> commands.CommandName:
73
+ return commands.CommandName.RESUME
74
+
75
+ @property
76
+ def summary(self) -> str:
77
+ return "Resume a previous session"
78
+
79
+ @property
80
+ def is_interactive(self) -> bool:
81
+ return True
82
+
83
+ async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
84
+ del user_input # unused
85
+
86
+ if agent.session.messages_count > 0:
87
+ event = events.DeveloperMessageEvent(
88
+ session_id=agent.session.id,
89
+ item=model.DeveloperMessageItem(
90
+ content="Cannot resume: current session already has messages. Use `klaude -r` to start a new instance with session selection.",
91
+ command_output=model.CommandOutput(command_name=self.name, is_error=True),
92
+ ),
93
+ )
94
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
95
+
96
+ selected_session_id = await asyncio.to_thread(select_session_sync)
97
+ if selected_session_id is None:
98
+ event = events.DeveloperMessageEvent(
99
+ session_id=agent.session.id,
100
+ item=model.DeveloperMessageItem(
101
+ content="(no session selected)",
102
+ command_output=model.CommandOutput(command_name=self.name),
103
+ ),
104
+ )
105
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
106
+
107
+ return CommandResult(
108
+ operations=[op.ResumeSessionOperation(target_session_id=selected_session_id)],
109
+ persist_user_input=False,
110
+ persist_events=False,
111
+ )