klaude-code 1.2.6__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,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
|
|
6
|
-
from klaude_code.
|
|
7
|
-
from klaude_code.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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",
|
|
64
|
+
final_prompt = template_content.replace("$ARGUMENTS", user_input_text)
|
|
64
65
|
else:
|
|
65
66
|
final_prompt = template_content
|
|
66
|
-
if
|
|
67
|
-
final_prompt += f"\n\nAdditional Instructions:\n{
|
|
68
|
-
|
|
69
|
-
return CommandResult(
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
klaude_code/command/registry.py
CHANGED
|
@@ -1,24 +1,82 @@
|
|
|
1
1
|
from importlib.resources import files
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
|
-
from klaude_code.command.command_abc import
|
|
4
|
+
from klaude_code.command.command_abc import Agent, CommandResult
|
|
5
5
|
from klaude_code.command.prompt_command import PromptCommand
|
|
6
|
-
from klaude_code.
|
|
7
|
-
from klaude_code.
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
132
|
+
_ensure_commands_loaded()
|
|
133
|
+
return _resolve_command_key(name) is not None
|
|
45
134
|
|
|
46
135
|
|
|
47
|
-
async def dispatch_command(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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__}] {
|
|
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
|
-
|
|
104
|
-
|
|
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[
|
|
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
|
+
)
|