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,338 @@
|
|
|
1
|
+
"""Cost command for aggregating usage statistics across all sessions."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.box import Box
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from klaude_code.command.status_cmd import format_cost, format_tokens
|
|
14
|
+
from klaude_code.protocol import model
|
|
15
|
+
from klaude_code.session.codec import decode_jsonl_line
|
|
16
|
+
|
|
17
|
+
ASCII_HORIZONAL = Box(" -- \n \n -- \n \n -- \n -- \n \n -- \n")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ModelUsageStats:
|
|
22
|
+
"""Aggregated usage stats for a single model."""
|
|
23
|
+
|
|
24
|
+
model_name: str
|
|
25
|
+
input_tokens: int = 0
|
|
26
|
+
output_tokens: int = 0
|
|
27
|
+
cached_tokens: int = 0
|
|
28
|
+
cost_usd: float = 0.0
|
|
29
|
+
cost_cny: float = 0.0
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def total_tokens(self) -> int:
|
|
33
|
+
return self.input_tokens + self.output_tokens
|
|
34
|
+
|
|
35
|
+
def add_usage(self, usage: model.Usage) -> None:
|
|
36
|
+
self.input_tokens += usage.input_tokens
|
|
37
|
+
self.output_tokens += usage.output_tokens
|
|
38
|
+
self.cached_tokens += usage.cached_tokens
|
|
39
|
+
if usage.total_cost is not None:
|
|
40
|
+
if usage.currency == "CNY":
|
|
41
|
+
self.cost_cny += usage.total_cost
|
|
42
|
+
else:
|
|
43
|
+
self.cost_usd += usage.total_cost
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class DailyStats:
|
|
48
|
+
"""Aggregated stats for a single day."""
|
|
49
|
+
|
|
50
|
+
date: str
|
|
51
|
+
by_model: dict[str, ModelUsageStats] = field(default_factory=lambda: dict[str, ModelUsageStats]())
|
|
52
|
+
|
|
53
|
+
def add_task_metadata(self, meta: model.TaskMetadata, date_str: str) -> None:
|
|
54
|
+
"""Add a TaskMetadata to this day's stats."""
|
|
55
|
+
del date_str # unused, date is already set
|
|
56
|
+
if not meta.usage or not meta.model_name:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
model_key = meta.model_name
|
|
60
|
+
if model_key not in self.by_model:
|
|
61
|
+
self.by_model[model_key] = ModelUsageStats(model_name=model_key)
|
|
62
|
+
|
|
63
|
+
self.by_model[model_key].add_usage(meta.usage)
|
|
64
|
+
|
|
65
|
+
def get_subtotal(self) -> ModelUsageStats:
|
|
66
|
+
"""Get subtotal across all models for this day."""
|
|
67
|
+
subtotal = ModelUsageStats(model_name="(subtotal)")
|
|
68
|
+
for stats in self.by_model.values():
|
|
69
|
+
subtotal.input_tokens += stats.input_tokens
|
|
70
|
+
subtotal.output_tokens += stats.output_tokens
|
|
71
|
+
subtotal.cached_tokens += stats.cached_tokens
|
|
72
|
+
subtotal.cost_usd += stats.cost_usd
|
|
73
|
+
subtotal.cost_cny += stats.cost_cny
|
|
74
|
+
return subtotal
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def iter_all_sessions() -> list[tuple[str, Path]]:
|
|
78
|
+
"""Iterate over all sessions across all projects.
|
|
79
|
+
|
|
80
|
+
Returns list of (session_id, events_file_path) tuples.
|
|
81
|
+
"""
|
|
82
|
+
projects_dir = Path.home() / ".klaude" / "projects"
|
|
83
|
+
if not projects_dir.exists():
|
|
84
|
+
return []
|
|
85
|
+
|
|
86
|
+
sessions: list[tuple[str, Path]] = []
|
|
87
|
+
for project_dir in projects_dir.iterdir():
|
|
88
|
+
if not project_dir.is_dir():
|
|
89
|
+
continue
|
|
90
|
+
sessions_dir = project_dir / "sessions"
|
|
91
|
+
if not sessions_dir.exists():
|
|
92
|
+
continue
|
|
93
|
+
for session_dir in sessions_dir.iterdir():
|
|
94
|
+
if not session_dir.is_dir():
|
|
95
|
+
continue
|
|
96
|
+
events_file = session_dir / "events.jsonl"
|
|
97
|
+
meta_file = session_dir / "meta.json"
|
|
98
|
+
# Skip sub-agent sessions by checking meta.json
|
|
99
|
+
if meta_file.exists():
|
|
100
|
+
import json
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
|
104
|
+
if meta.get("sub_agent_state") is not None:
|
|
105
|
+
continue
|
|
106
|
+
except (json.JSONDecodeError, OSError):
|
|
107
|
+
pass
|
|
108
|
+
if events_file.exists():
|
|
109
|
+
sessions.append((session_dir.name, events_file))
|
|
110
|
+
|
|
111
|
+
return sessions
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def extract_task_metadata_from_events(events_path: Path) -> list[tuple[str, model.TaskMetadataItem]]:
|
|
115
|
+
"""Extract TaskMetadataItem entries from events.jsonl with their dates.
|
|
116
|
+
|
|
117
|
+
Returns list of (date_str, TaskMetadataItem) tuples.
|
|
118
|
+
"""
|
|
119
|
+
results: list[tuple[str, model.TaskMetadataItem]] = []
|
|
120
|
+
try:
|
|
121
|
+
content = events_path.read_text(encoding="utf-8")
|
|
122
|
+
except OSError:
|
|
123
|
+
return results
|
|
124
|
+
|
|
125
|
+
for line in content.splitlines():
|
|
126
|
+
item = decode_jsonl_line(line)
|
|
127
|
+
if isinstance(item, model.TaskMetadataItem):
|
|
128
|
+
date_str = item.created_at.strftime("%Y-%m-%d")
|
|
129
|
+
results.append((date_str, item))
|
|
130
|
+
|
|
131
|
+
return results
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def aggregate_all_sessions() -> dict[str, DailyStats]:
|
|
135
|
+
"""Aggregate usage stats from all sessions, grouped by date.
|
|
136
|
+
|
|
137
|
+
Returns dict mapping date string to DailyStats.
|
|
138
|
+
"""
|
|
139
|
+
daily_stats: dict[str, DailyStats] = defaultdict(lambda: DailyStats(date=""))
|
|
140
|
+
|
|
141
|
+
sessions = iter_all_sessions()
|
|
142
|
+
for _session_id, events_path in sessions:
|
|
143
|
+
metadata_items = extract_task_metadata_from_events(events_path)
|
|
144
|
+
for date_str, metadata_item in metadata_items:
|
|
145
|
+
if daily_stats[date_str].date == "":
|
|
146
|
+
daily_stats[date_str] = DailyStats(date=date_str)
|
|
147
|
+
|
|
148
|
+
# Process main agent metadata
|
|
149
|
+
daily_stats[date_str].add_task_metadata(metadata_item.main_agent, date_str)
|
|
150
|
+
|
|
151
|
+
# Process sub-agent metadata
|
|
152
|
+
for sub_meta in metadata_item.sub_agent_task_metadata:
|
|
153
|
+
daily_stats[date_str].add_task_metadata(sub_meta, date_str)
|
|
154
|
+
|
|
155
|
+
return dict(daily_stats)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def format_cost_dual(cost_usd: float, cost_cny: float) -> tuple[str, str]:
|
|
159
|
+
"""Format costs for both currencies."""
|
|
160
|
+
usd_str = format_cost(cost_usd if cost_usd > 0 else None, "USD")
|
|
161
|
+
cny_str = format_cost(cost_cny if cost_cny > 0 else None, "CNY")
|
|
162
|
+
return usd_str, cny_str
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def format_date_display(date_str: str) -> str:
|
|
166
|
+
"""Format date string YYYY-MM-DD to 'YYYY M-D' for table display."""
|
|
167
|
+
parts = date_str.split("-")
|
|
168
|
+
if len(parts) == 3:
|
|
169
|
+
month = int(parts[1])
|
|
170
|
+
day = int(parts[2])
|
|
171
|
+
return f"{parts[0]} {month}-{day}"
|
|
172
|
+
return date_str
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
|
|
176
|
+
"""Render the cost table using rich."""
|
|
177
|
+
table = Table(
|
|
178
|
+
title="Usage Statistics",
|
|
179
|
+
show_header=True,
|
|
180
|
+
header_style="bold",
|
|
181
|
+
padding=(0, 1, 0, 2),
|
|
182
|
+
box=ASCII_HORIZONAL,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
table.add_column("Date", style="cyan", no_wrap=True)
|
|
186
|
+
table.add_column("Model", no_wrap=True)
|
|
187
|
+
table.add_column("Input", justify="right", no_wrap=True)
|
|
188
|
+
table.add_column("Output", justify="right", no_wrap=True)
|
|
189
|
+
table.add_column("Cache", justify="right", no_wrap=True)
|
|
190
|
+
table.add_column("Total", justify="right", no_wrap=True)
|
|
191
|
+
table.add_column("USD", justify="right", no_wrap=True)
|
|
192
|
+
table.add_column("CNY", justify="right", no_wrap=True)
|
|
193
|
+
|
|
194
|
+
# Sort dates
|
|
195
|
+
sorted_dates = sorted(daily_stats.keys())
|
|
196
|
+
|
|
197
|
+
# Track global totals by model
|
|
198
|
+
global_by_model: dict[str, ModelUsageStats] = {}
|
|
199
|
+
|
|
200
|
+
def sort_by_cost(stats: ModelUsageStats) -> tuple[float, float]:
|
|
201
|
+
"""Sort key: USD desc, then CNY desc."""
|
|
202
|
+
return (-stats.cost_usd, -stats.cost_cny)
|
|
203
|
+
|
|
204
|
+
for date_str in sorted_dates:
|
|
205
|
+
day = daily_stats[date_str]
|
|
206
|
+
sorted_models = [s.model_name for s in sorted(day.by_model.values(), key=sort_by_cost)]
|
|
207
|
+
|
|
208
|
+
first_row = True
|
|
209
|
+
for model_name in sorted_models:
|
|
210
|
+
stats = day.by_model[model_name]
|
|
211
|
+
usd_str, cny_str = format_cost_dual(stats.cost_usd, stats.cost_cny)
|
|
212
|
+
|
|
213
|
+
# Accumulate to global totals
|
|
214
|
+
if model_name not in global_by_model:
|
|
215
|
+
global_by_model[model_name] = ModelUsageStats(model_name=model_name)
|
|
216
|
+
global_by_model[model_name].input_tokens += stats.input_tokens
|
|
217
|
+
global_by_model[model_name].output_tokens += stats.output_tokens
|
|
218
|
+
global_by_model[model_name].cached_tokens += stats.cached_tokens
|
|
219
|
+
global_by_model[model_name].cost_usd += stats.cost_usd
|
|
220
|
+
global_by_model[model_name].cost_cny += stats.cost_cny
|
|
221
|
+
|
|
222
|
+
table.add_row(
|
|
223
|
+
format_date_display(date_str) if first_row else "",
|
|
224
|
+
f"- {model_name}",
|
|
225
|
+
format_tokens(stats.input_tokens),
|
|
226
|
+
format_tokens(stats.output_tokens),
|
|
227
|
+
format_tokens(stats.cached_tokens),
|
|
228
|
+
format_tokens(stats.total_tokens),
|
|
229
|
+
usd_str,
|
|
230
|
+
cny_str,
|
|
231
|
+
)
|
|
232
|
+
first_row = False
|
|
233
|
+
|
|
234
|
+
# Add subtotal row for this day
|
|
235
|
+
subtotal = day.get_subtotal()
|
|
236
|
+
usd_str, cny_str = format_cost_dual(subtotal.cost_usd, subtotal.cost_cny)
|
|
237
|
+
table.add_row(
|
|
238
|
+
"",
|
|
239
|
+
"[cyan] (subtotal)[/cyan]",
|
|
240
|
+
f"[cyan]{format_tokens(subtotal.input_tokens)}[/cyan]",
|
|
241
|
+
f"[cyan]{format_tokens(subtotal.output_tokens)}[/cyan]",
|
|
242
|
+
f"[cyan]{format_tokens(subtotal.cached_tokens)}[/cyan]",
|
|
243
|
+
f"[cyan]{format_tokens(subtotal.total_tokens)}[/cyan]",
|
|
244
|
+
f"[cyan]{usd_str}[/cyan]",
|
|
245
|
+
f"[cyan]{cny_str}[/cyan]",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Add separator between days
|
|
249
|
+
if date_str != sorted_dates[-1]:
|
|
250
|
+
table.add_section()
|
|
251
|
+
|
|
252
|
+
# Add final section for totals
|
|
253
|
+
table.add_section()
|
|
254
|
+
|
|
255
|
+
# Build date range label for Total
|
|
256
|
+
if sorted_dates:
|
|
257
|
+
first_date = format_date_display(sorted_dates[0])
|
|
258
|
+
last_date = format_date_display(sorted_dates[-1])
|
|
259
|
+
if first_date == last_date:
|
|
260
|
+
total_label = f"[bold]Total[/bold]\n[dim]{first_date}[/dim]"
|
|
261
|
+
else:
|
|
262
|
+
total_label = f"[bold]Total[/bold]\n[dim]{first_date} ~[/dim]\n[dim]{last_date}[/dim]"
|
|
263
|
+
else:
|
|
264
|
+
total_label = "[bold]Total[/bold]"
|
|
265
|
+
|
|
266
|
+
# Add per-model totals
|
|
267
|
+
sorted_global_models = [s.model_name for s in sorted(global_by_model.values(), key=sort_by_cost)]
|
|
268
|
+
first_total_row = True
|
|
269
|
+
for model_name in sorted_global_models:
|
|
270
|
+
stats = global_by_model[model_name]
|
|
271
|
+
usd_str, cny_str = format_cost_dual(stats.cost_usd, stats.cost_cny)
|
|
272
|
+
table.add_row(
|
|
273
|
+
total_label if first_total_row else "",
|
|
274
|
+
f"- {model_name}",
|
|
275
|
+
format_tokens(stats.input_tokens),
|
|
276
|
+
format_tokens(stats.output_tokens),
|
|
277
|
+
format_tokens(stats.cached_tokens),
|
|
278
|
+
format_tokens(stats.total_tokens),
|
|
279
|
+
usd_str,
|
|
280
|
+
cny_str,
|
|
281
|
+
)
|
|
282
|
+
first_total_row = False
|
|
283
|
+
|
|
284
|
+
# Add grand total row
|
|
285
|
+
grand_total = ModelUsageStats(model_name="(total)")
|
|
286
|
+
for stats in global_by_model.values():
|
|
287
|
+
grand_total.input_tokens += stats.input_tokens
|
|
288
|
+
grand_total.output_tokens += stats.output_tokens
|
|
289
|
+
grand_total.cached_tokens += stats.cached_tokens
|
|
290
|
+
grand_total.cost_usd += stats.cost_usd
|
|
291
|
+
grand_total.cost_cny += stats.cost_cny
|
|
292
|
+
|
|
293
|
+
usd_str, cny_str = format_cost_dual(grand_total.cost_usd, grand_total.cost_cny)
|
|
294
|
+
table.add_row(
|
|
295
|
+
"",
|
|
296
|
+
"[bold] (total)[/bold]",
|
|
297
|
+
f"[bold]{format_tokens(grand_total.input_tokens)}[/bold]",
|
|
298
|
+
f"[bold]{format_tokens(grand_total.output_tokens)}[/bold]",
|
|
299
|
+
f"[bold]{format_tokens(grand_total.cached_tokens)}[/bold]",
|
|
300
|
+
f"[bold]{format_tokens(grand_total.total_tokens)}[/bold]",
|
|
301
|
+
f"[bold]{usd_str}[/bold]",
|
|
302
|
+
f"[bold]{cny_str}[/bold]",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return table
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def cost_command(
|
|
309
|
+
days: int | None = typer.Option(None, "--days", "-d", help="Limit to last N days"),
|
|
310
|
+
) -> None:
|
|
311
|
+
"""Display aggregated usage statistics across all sessions."""
|
|
312
|
+
daily_stats = aggregate_all_sessions()
|
|
313
|
+
|
|
314
|
+
if not daily_stats:
|
|
315
|
+
typer.echo("No usage data found.")
|
|
316
|
+
raise typer.Exit(0)
|
|
317
|
+
|
|
318
|
+
# Filter by days if specified
|
|
319
|
+
if days is not None:
|
|
320
|
+
cutoff = datetime.now().strftime("%Y-%m-%d")
|
|
321
|
+
from datetime import timedelta
|
|
322
|
+
|
|
323
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
|
324
|
+
cutoff = cutoff_date.strftime("%Y-%m-%d")
|
|
325
|
+
daily_stats = {k: v for k, v in daily_stats.items() if k >= cutoff}
|
|
326
|
+
|
|
327
|
+
if not daily_stats:
|
|
328
|
+
typer.echo(f"No usage data found in the last {days} days.")
|
|
329
|
+
raise typer.Exit(0)
|
|
330
|
+
|
|
331
|
+
table = render_cost_table(daily_stats)
|
|
332
|
+
console = Console()
|
|
333
|
+
console.print(table)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def register_cost_commands(app: typer.Typer) -> None:
|
|
337
|
+
"""Register cost command to the given Typer app."""
|
|
338
|
+
app.command("cost")(cost_command)
|
klaude_code/cli/debug.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Debug utilities for CLI."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from klaude_code.trace import DebugType, log
|
|
10
|
+
|
|
11
|
+
DEBUG_FILTER_HELP = "Comma-separated debug types: " + ", ".join(dt.value for dt in DebugType)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_debug_filters(raw: str | None) -> set[DebugType] | None:
|
|
15
|
+
"""Parse comma-separated debug filter string into a set of DebugType."""
|
|
16
|
+
if raw is None:
|
|
17
|
+
return None
|
|
18
|
+
filters: set[DebugType] = set()
|
|
19
|
+
for chunk in raw.split(","):
|
|
20
|
+
normalized = chunk.strip().lower().replace("-", "_")
|
|
21
|
+
if not normalized:
|
|
22
|
+
continue
|
|
23
|
+
try:
|
|
24
|
+
filters.add(DebugType(normalized))
|
|
25
|
+
except ValueError: # pragma: no cover - user input validation
|
|
26
|
+
valid_options = ", ".join(dt.value for dt in DebugType)
|
|
27
|
+
log(
|
|
28
|
+
(
|
|
29
|
+
f"Invalid debug filter '{normalized}'. Valid options: {valid_options}",
|
|
30
|
+
"red",
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
raise typer.Exit(2) from None
|
|
34
|
+
return filters or None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_debug_settings(flag: bool, raw_filters: str | None) -> tuple[bool, set[DebugType] | None]:
|
|
38
|
+
"""Resolve debug flag and filters into effective settings."""
|
|
39
|
+
filters = parse_debug_filters(raw_filters)
|
|
40
|
+
effective_flag = flag or (filters is not None)
|
|
41
|
+
return effective_flag, filters
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def open_log_file_in_editor(path: Path) -> None:
|
|
45
|
+
"""Open the given log file in a text editor without blocking the CLI."""
|
|
46
|
+
|
|
47
|
+
editor = ""
|
|
48
|
+
|
|
49
|
+
for cmd in ["code", "TextEdit", "notepad"]:
|
|
50
|
+
try:
|
|
51
|
+
subprocess.run(["which", cmd], check=True, capture_output=True)
|
|
52
|
+
editor = cmd
|
|
53
|
+
break
|
|
54
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if not editor:
|
|
58
|
+
if sys.platform == "darwin":
|
|
59
|
+
editor = "open"
|
|
60
|
+
elif sys.platform == "win32":
|
|
61
|
+
editor = "notepad"
|
|
62
|
+
else:
|
|
63
|
+
editor = "xdg-open"
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Detach stdin to prevent the editor from interfering with terminal input state.
|
|
67
|
+
# Without this, the spawned process inherits the parent's TTY and can disrupt
|
|
68
|
+
# prompt_toolkit's keyboard handling (e.g., history navigation with up/down keys).
|
|
69
|
+
subprocess.Popen(
|
|
70
|
+
[editor, str(path)],
|
|
71
|
+
stdin=subprocess.DEVNULL,
|
|
72
|
+
stdout=subprocess.DEVNULL,
|
|
73
|
+
stderr=subprocess.DEVNULL,
|
|
74
|
+
)
|
|
75
|
+
except FileNotFoundError:
|
|
76
|
+
log((f"Error: Editor '{editor}' not found", "red"))
|
|
77
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
78
|
+
log((f"Warning: failed to open log file in editor: {exc}", "yellow"))
|