kimi-cli 0.44__py3-none-any.whl → 0.78__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.
Potentially problematic release.
This version of kimi-cli might be problematic. Click here for more details.
- kimi_cli/CHANGELOG.md +349 -40
- kimi_cli/__init__.py +6 -0
- kimi_cli/acp/AGENTS.md +91 -0
- kimi_cli/acp/__init__.py +13 -0
- kimi_cli/acp/convert.py +111 -0
- kimi_cli/acp/kaos.py +270 -0
- kimi_cli/acp/mcp.py +46 -0
- kimi_cli/acp/server.py +335 -0
- kimi_cli/acp/session.py +445 -0
- kimi_cli/acp/tools.py +158 -0
- kimi_cli/acp/types.py +13 -0
- kimi_cli/agents/default/agent.yaml +4 -4
- kimi_cli/agents/default/sub.yaml +2 -1
- kimi_cli/agents/default/system.md +79 -21
- kimi_cli/agents/okabe/agent.yaml +17 -0
- kimi_cli/agentspec.py +53 -25
- kimi_cli/app.py +180 -52
- kimi_cli/cli/__init__.py +595 -0
- kimi_cli/cli/__main__.py +8 -0
- kimi_cli/cli/info.py +63 -0
- kimi_cli/cli/mcp.py +349 -0
- kimi_cli/config.py +153 -17
- kimi_cli/constant.py +3 -0
- kimi_cli/exception.py +23 -2
- kimi_cli/flow/__init__.py +117 -0
- kimi_cli/flow/d2.py +376 -0
- kimi_cli/flow/mermaid.py +218 -0
- kimi_cli/llm.py +129 -23
- kimi_cli/metadata.py +32 -7
- kimi_cli/platforms.py +262 -0
- kimi_cli/prompts/__init__.py +2 -0
- kimi_cli/prompts/compact.md +4 -5
- kimi_cli/session.py +223 -31
- kimi_cli/share.py +2 -0
- kimi_cli/skill.py +145 -0
- kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
- kimi_cli/skills/skill-creator/SKILL.md +351 -0
- kimi_cli/soul/__init__.py +51 -20
- kimi_cli/soul/agent.py +213 -85
- kimi_cli/soul/approval.py +86 -17
- kimi_cli/soul/compaction.py +64 -53
- kimi_cli/soul/context.py +38 -5
- kimi_cli/soul/denwarenji.py +2 -0
- kimi_cli/soul/kimisoul.py +442 -60
- kimi_cli/soul/message.py +54 -54
- kimi_cli/soul/slash.py +72 -0
- kimi_cli/soul/toolset.py +387 -6
- kimi_cli/toad.py +74 -0
- kimi_cli/tools/AGENTS.md +5 -0
- kimi_cli/tools/__init__.py +42 -34
- kimi_cli/tools/display.py +25 -0
- kimi_cli/tools/dmail/__init__.py +10 -10
- kimi_cli/tools/dmail/dmail.md +11 -9
- kimi_cli/tools/file/__init__.py +1 -3
- kimi_cli/tools/file/glob.py +20 -23
- kimi_cli/tools/file/grep.md +1 -1
- kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
- kimi_cli/tools/file/read.md +24 -6
- kimi_cli/tools/file/read.py +134 -50
- kimi_cli/tools/file/replace.md +1 -1
- kimi_cli/tools/file/replace.py +36 -29
- kimi_cli/tools/file/utils.py +282 -0
- kimi_cli/tools/file/write.py +43 -22
- kimi_cli/tools/multiagent/__init__.py +7 -0
- kimi_cli/tools/multiagent/create.md +11 -0
- kimi_cli/tools/multiagent/create.py +50 -0
- kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
- kimi_cli/tools/shell/__init__.py +120 -0
- kimi_cli/tools/{bash → shell}/bash.md +1 -2
- kimi_cli/tools/shell/powershell.md +25 -0
- kimi_cli/tools/test.py +4 -4
- kimi_cli/tools/think/__init__.py +2 -2
- kimi_cli/tools/todo/__init__.py +14 -8
- kimi_cli/tools/utils.py +64 -24
- kimi_cli/tools/web/fetch.py +68 -13
- kimi_cli/tools/web/search.py +10 -12
- kimi_cli/ui/acp/__init__.py +65 -412
- kimi_cli/ui/print/__init__.py +37 -49
- kimi_cli/ui/print/visualize.py +179 -0
- kimi_cli/ui/shell/__init__.py +141 -84
- kimi_cli/ui/shell/console.py +2 -0
- kimi_cli/ui/shell/debug.py +28 -23
- kimi_cli/ui/shell/keyboard.py +5 -1
- kimi_cli/ui/shell/prompt.py +220 -194
- kimi_cli/ui/shell/replay.py +111 -46
- kimi_cli/ui/shell/setup.py +89 -82
- kimi_cli/ui/shell/slash.py +422 -0
- kimi_cli/ui/shell/update.py +4 -2
- kimi_cli/ui/shell/usage.py +271 -0
- kimi_cli/ui/shell/visualize.py +574 -72
- kimi_cli/ui/wire/__init__.py +267 -0
- kimi_cli/ui/wire/jsonrpc.py +142 -0
- kimi_cli/ui/wire/protocol.py +1 -0
- kimi_cli/utils/__init__.py +0 -0
- kimi_cli/utils/aiohttp.py +2 -0
- kimi_cli/utils/aioqueue.py +72 -0
- kimi_cli/utils/broadcast.py +37 -0
- kimi_cli/utils/changelog.py +12 -7
- kimi_cli/utils/clipboard.py +12 -0
- kimi_cli/utils/datetime.py +37 -0
- kimi_cli/utils/environment.py +58 -0
- kimi_cli/utils/envvar.py +12 -0
- kimi_cli/utils/frontmatter.py +44 -0
- kimi_cli/utils/logging.py +7 -6
- kimi_cli/utils/message.py +9 -14
- kimi_cli/utils/path.py +99 -9
- kimi_cli/utils/pyinstaller.py +6 -0
- kimi_cli/utils/rich/__init__.py +33 -0
- kimi_cli/utils/rich/columns.py +99 -0
- kimi_cli/utils/rich/markdown.py +961 -0
- kimi_cli/utils/rich/markdown_sample.md +108 -0
- kimi_cli/utils/rich/markdown_sample_short.md +2 -0
- kimi_cli/utils/signals.py +2 -0
- kimi_cli/utils/slashcmd.py +124 -0
- kimi_cli/utils/string.py +2 -0
- kimi_cli/utils/term.py +168 -0
- kimi_cli/utils/typing.py +20 -0
- kimi_cli/wire/__init__.py +98 -29
- kimi_cli/wire/serde.py +45 -0
- kimi_cli/wire/types.py +299 -0
- kimi_cli-0.78.dist-info/METADATA +200 -0
- kimi_cli-0.78.dist-info/RECORD +135 -0
- kimi_cli-0.78.dist-info/entry_points.txt +4 -0
- kimi_cli/cli.py +0 -250
- kimi_cli/soul/runtime.py +0 -96
- kimi_cli/tools/bash/__init__.py +0 -99
- kimi_cli/tools/file/patch.md +0 -8
- kimi_cli/tools/file/patch.py +0 -143
- kimi_cli/tools/mcp.py +0 -85
- kimi_cli/ui/shell/liveview.py +0 -386
- kimi_cli/ui/shell/metacmd.py +0 -262
- kimi_cli/wire/message.py +0 -91
- kimi_cli-0.44.dist-info/METADATA +0 -188
- kimi_cli-0.44.dist-info/RECORD +0 -89
- kimi_cli-0.44.dist-info/entry_points.txt +0 -3
- /kimi_cli/tools/{task → multiagent}/task.md +0 -0
- {kimi_cli-0.44.dist-info → kimi_cli-0.78.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""This file is pure vibe-coded. If any bugs are found, let's just rewrite it..."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
from rich.console import Group, RenderableType
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.progress_bar import ProgressBar
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from kimi_cli.config import LLMProvider
|
|
17
|
+
from kimi_cli.soul.kimisoul import KimiSoul
|
|
18
|
+
from kimi_cli.ui.shell.console import console
|
|
19
|
+
from kimi_cli.ui.shell.slash import registry
|
|
20
|
+
from kimi_cli.utils.aiohttp import new_client_session
|
|
21
|
+
from kimi_cli.utils.datetime import format_duration
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from kimi_cli.ui.shell import Shell
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True, frozen=True)
|
|
28
|
+
class UsageRow:
|
|
29
|
+
label: str
|
|
30
|
+
used: int
|
|
31
|
+
limit: int
|
|
32
|
+
reset_hint: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@registry.command
|
|
36
|
+
async def usage(app: Shell, args: str):
|
|
37
|
+
"""Display API usage and quota information"""
|
|
38
|
+
assert isinstance(app.soul, KimiSoul)
|
|
39
|
+
if app.soul.runtime.llm is None:
|
|
40
|
+
console.print("[red]LLM not set. Please run /setup first.[/red]")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
provider = app.soul.runtime.llm.provider_config
|
|
44
|
+
if provider is None:
|
|
45
|
+
console.print("[red]LLM provider configuration not found.[/red]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
usage_url = _usage_url(provider)
|
|
49
|
+
if usage_url is None:
|
|
50
|
+
console.print("[yellow]Usage is available on Kimi Code platform only.[/yellow]")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
with console.status("[cyan]Fetching usage...[/cyan]"):
|
|
54
|
+
try:
|
|
55
|
+
payload = await _fetch_usage(usage_url, provider.api_key.get_secret_value())
|
|
56
|
+
except aiohttp.ClientResponseError as e:
|
|
57
|
+
message = "Failed to fetch usage."
|
|
58
|
+
if e.status == 401:
|
|
59
|
+
message = "Authorization failed. Please check your API key."
|
|
60
|
+
elif e.status == 404:
|
|
61
|
+
message = "Usage endpoint not available. Try Kimi For Coding."
|
|
62
|
+
console.print(f"[red]{message}[/red]")
|
|
63
|
+
return
|
|
64
|
+
except aiohttp.ClientError as e:
|
|
65
|
+
console.print(f"[red]Failed to fetch usage: {e}[/red]")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
summary, limits = _parse_usage_payload(payload)
|
|
69
|
+
if summary is None and not limits:
|
|
70
|
+
console.print("[yellow]No usage data available.[/yellow]")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
console.print(_build_usage_panel(summary, limits))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _usage_url(provider: LLMProvider) -> str | None:
|
|
77
|
+
base_url = (provider.base_url or "").rstrip("/")
|
|
78
|
+
coding_base_url = "https://api.kimi.com/coding/v1"
|
|
79
|
+
if base_url != coding_base_url:
|
|
80
|
+
return None
|
|
81
|
+
return f"{base_url}/usages"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def _fetch_usage(url: str, api_key: str) -> Mapping[str, Any]:
|
|
85
|
+
async with (
|
|
86
|
+
new_client_session() as session,
|
|
87
|
+
session.get(
|
|
88
|
+
url,
|
|
89
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
90
|
+
raise_for_status=True,
|
|
91
|
+
) as resp,
|
|
92
|
+
):
|
|
93
|
+
return await resp.json()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _parse_usage_payload(
|
|
97
|
+
payload: Mapping[str, Any],
|
|
98
|
+
) -> tuple[UsageRow | None, list[UsageRow]]:
|
|
99
|
+
summary: UsageRow | None = None
|
|
100
|
+
limits: list[UsageRow] = []
|
|
101
|
+
|
|
102
|
+
usage = payload.get("usage")
|
|
103
|
+
if isinstance(usage, Mapping):
|
|
104
|
+
usage_map: Mapping[str, Any] = cast(Mapping[str, Any], usage)
|
|
105
|
+
summary = _to_usage_row(usage_map, default_label="Total quota")
|
|
106
|
+
|
|
107
|
+
raw_limits_obj = payload.get("limits")
|
|
108
|
+
if isinstance(raw_limits_obj, Sequence):
|
|
109
|
+
limits_seq: Sequence[Any] = cast(Sequence[Any], raw_limits_obj)
|
|
110
|
+
for idx, item in enumerate(limits_seq):
|
|
111
|
+
if not isinstance(item, Mapping):
|
|
112
|
+
continue
|
|
113
|
+
item_map: Mapping[str, Any] = cast(Mapping[str, Any], item)
|
|
114
|
+
detail_raw = item_map.get("detail")
|
|
115
|
+
detail: Mapping[str, Any] = (
|
|
116
|
+
cast(Mapping[str, Any], detail_raw) if isinstance(detail_raw, Mapping) else item_map
|
|
117
|
+
)
|
|
118
|
+
# window may contain duration/timeUnit
|
|
119
|
+
window_raw = item_map.get("window")
|
|
120
|
+
window: Mapping[str, Any] = (
|
|
121
|
+
cast(Mapping[str, Any], window_raw) if isinstance(window_raw, Mapping) else {}
|
|
122
|
+
)
|
|
123
|
+
label = _limit_label(item_map, detail, window, idx)
|
|
124
|
+
row = _to_usage_row(detail, default_label=label)
|
|
125
|
+
if row:
|
|
126
|
+
limits.append(row)
|
|
127
|
+
|
|
128
|
+
return summary, limits
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _to_usage_row(data: Mapping[str, Any], *, default_label: str) -> UsageRow | None:
|
|
132
|
+
limit = _to_int(data.get("limit"))
|
|
133
|
+
# Support both "used" and "remaining" (used = limit - remaining)
|
|
134
|
+
used = _to_int(data.get("used"))
|
|
135
|
+
if used is None:
|
|
136
|
+
remaining = _to_int(data.get("remaining"))
|
|
137
|
+
if remaining is not None and limit is not None:
|
|
138
|
+
used = limit - remaining
|
|
139
|
+
if used is None and limit is None:
|
|
140
|
+
return None
|
|
141
|
+
return UsageRow(
|
|
142
|
+
label=str(data.get("name") or data.get("title") or default_label),
|
|
143
|
+
used=used or 0,
|
|
144
|
+
limit=limit or 0,
|
|
145
|
+
reset_hint=_reset_hint(data),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _limit_label(
|
|
150
|
+
item: Mapping[str, Any],
|
|
151
|
+
detail: Mapping[str, Any],
|
|
152
|
+
window: Mapping[str, Any],
|
|
153
|
+
idx: int,
|
|
154
|
+
) -> str:
|
|
155
|
+
# Try to extract a human-readable label
|
|
156
|
+
for key in ("name", "title", "scope"):
|
|
157
|
+
if val := (item.get(key) or detail.get(key)):
|
|
158
|
+
return str(val)
|
|
159
|
+
|
|
160
|
+
# Convert duration to readable format (e.g., 300 minutes -> "5h quota")
|
|
161
|
+
# Check window first, then item, then detail
|
|
162
|
+
duration = _to_int(window.get("duration") or item.get("duration") or detail.get("duration"))
|
|
163
|
+
time_unit = window.get("timeUnit") or item.get("timeUnit") or detail.get("timeUnit") or ""
|
|
164
|
+
if duration:
|
|
165
|
+
if "MINUTE" in time_unit:
|
|
166
|
+
if duration >= 60 and duration % 60 == 0:
|
|
167
|
+
return f"{duration // 60}h limit"
|
|
168
|
+
return f"{duration}m limit"
|
|
169
|
+
if "HOUR" in time_unit:
|
|
170
|
+
return f"{duration}h limit"
|
|
171
|
+
if "DAY" in time_unit:
|
|
172
|
+
return f"{duration}d limit"
|
|
173
|
+
return f"{duration}s limit"
|
|
174
|
+
|
|
175
|
+
return f"Limit #{idx + 1}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _reset_hint(data: Mapping[str, Any]) -> str | None:
|
|
179
|
+
for key in ("reset_at", "resetAt", "reset_time", "resetTime"):
|
|
180
|
+
if val := data.get(key):
|
|
181
|
+
return _format_reset_time(str(val))
|
|
182
|
+
|
|
183
|
+
for key in ("reset_in", "resetIn", "ttl", "window"):
|
|
184
|
+
seconds = _to_int(data.get(key))
|
|
185
|
+
if seconds:
|
|
186
|
+
return f"resets in {format_duration(seconds)}"
|
|
187
|
+
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _format_reset_time(val: str) -> str:
|
|
192
|
+
"""Format ISO timestamp to a readable duration."""
|
|
193
|
+
from datetime import UTC, datetime
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
# Parse ISO format like "2025-12-23T05:24:18.443553353Z"
|
|
197
|
+
# Truncate nanoseconds to microseconds for Python compatibility
|
|
198
|
+
if "." in val and val.endswith("Z"):
|
|
199
|
+
base, frac = val[:-1].split(".")
|
|
200
|
+
frac = frac[:6] # Keep only microseconds
|
|
201
|
+
val = f"{base}.{frac}Z"
|
|
202
|
+
dt = datetime.fromisoformat(val.replace("Z", "+00:00"))
|
|
203
|
+
now = datetime.now(UTC)
|
|
204
|
+
delta = dt - now
|
|
205
|
+
|
|
206
|
+
if delta.total_seconds() <= 0:
|
|
207
|
+
return "reset"
|
|
208
|
+
return f"resets in {format_duration(int(delta.total_seconds()))}"
|
|
209
|
+
except (ValueError, TypeError):
|
|
210
|
+
return f"resets at {val}"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _to_int(value: Any) -> int | None:
|
|
214
|
+
try:
|
|
215
|
+
return int(value)
|
|
216
|
+
except (TypeError, ValueError):
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _build_usage_panel(summary: UsageRow | None, limits: list[UsageRow]) -> Panel:
|
|
221
|
+
rows = ([summary] if summary else []) + limits
|
|
222
|
+
if not rows:
|
|
223
|
+
return Panel(
|
|
224
|
+
Text("No usage data", style="grey50"), title="API Usage", border_style="wheat4"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Calculate label width for alignment
|
|
228
|
+
label_width = max(len(r.label) for r in rows)
|
|
229
|
+
label_width = max(label_width, 6) # minimum width
|
|
230
|
+
|
|
231
|
+
lines: list[RenderableType] = []
|
|
232
|
+
for row in rows:
|
|
233
|
+
lines.append(_format_row(row, label_width))
|
|
234
|
+
|
|
235
|
+
return Panel(
|
|
236
|
+
Group(*lines),
|
|
237
|
+
title="API Usage",
|
|
238
|
+
border_style="wheat4",
|
|
239
|
+
padding=(0, 2),
|
|
240
|
+
expand=False,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _format_row(row: UsageRow, label_width: int) -> RenderableType:
|
|
245
|
+
ratio = row.used / row.limit if row.limit > 0 else 0
|
|
246
|
+
color = _ratio_color(ratio)
|
|
247
|
+
|
|
248
|
+
label = Text(f"{row.label:<{label_width}} ", style="cyan")
|
|
249
|
+
bar = ProgressBar(total=row.limit or 1, completed=row.used, width=20, complete_style=color)
|
|
250
|
+
|
|
251
|
+
detail = Text()
|
|
252
|
+
detail.append(
|
|
253
|
+
f" {row.used:,} / {row.limit:,}" if row.limit else f" {row.used:,}", style="bold"
|
|
254
|
+
)
|
|
255
|
+
if row.reset_hint:
|
|
256
|
+
detail.append(f" ({row.reset_hint})", style="grey50")
|
|
257
|
+
|
|
258
|
+
t = Table.grid(padding=0)
|
|
259
|
+
t.add_column(width=label_width + 2)
|
|
260
|
+
t.add_column(width=20)
|
|
261
|
+
t.add_column()
|
|
262
|
+
t.add_row(label, bar, detail)
|
|
263
|
+
return t
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _ratio_color(ratio: float) -> str:
|
|
267
|
+
if ratio >= 0.9:
|
|
268
|
+
return "red"
|
|
269
|
+
if ratio >= 0.7:
|
|
270
|
+
return "yellow"
|
|
271
|
+
return "green"
|