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.

Files changed (137) hide show
  1. kimi_cli/CHANGELOG.md +349 -40
  2. kimi_cli/__init__.py +6 -0
  3. kimi_cli/acp/AGENTS.md +91 -0
  4. kimi_cli/acp/__init__.py +13 -0
  5. kimi_cli/acp/convert.py +111 -0
  6. kimi_cli/acp/kaos.py +270 -0
  7. kimi_cli/acp/mcp.py +46 -0
  8. kimi_cli/acp/server.py +335 -0
  9. kimi_cli/acp/session.py +445 -0
  10. kimi_cli/acp/tools.py +158 -0
  11. kimi_cli/acp/types.py +13 -0
  12. kimi_cli/agents/default/agent.yaml +4 -4
  13. kimi_cli/agents/default/sub.yaml +2 -1
  14. kimi_cli/agents/default/system.md +79 -21
  15. kimi_cli/agents/okabe/agent.yaml +17 -0
  16. kimi_cli/agentspec.py +53 -25
  17. kimi_cli/app.py +180 -52
  18. kimi_cli/cli/__init__.py +595 -0
  19. kimi_cli/cli/__main__.py +8 -0
  20. kimi_cli/cli/info.py +63 -0
  21. kimi_cli/cli/mcp.py +349 -0
  22. kimi_cli/config.py +153 -17
  23. kimi_cli/constant.py +3 -0
  24. kimi_cli/exception.py +23 -2
  25. kimi_cli/flow/__init__.py +117 -0
  26. kimi_cli/flow/d2.py +376 -0
  27. kimi_cli/flow/mermaid.py +218 -0
  28. kimi_cli/llm.py +129 -23
  29. kimi_cli/metadata.py +32 -7
  30. kimi_cli/platforms.py +262 -0
  31. kimi_cli/prompts/__init__.py +2 -0
  32. kimi_cli/prompts/compact.md +4 -5
  33. kimi_cli/session.py +223 -31
  34. kimi_cli/share.py +2 -0
  35. kimi_cli/skill.py +145 -0
  36. kimi_cli/skills/kimi-cli-help/SKILL.md +55 -0
  37. kimi_cli/skills/skill-creator/SKILL.md +351 -0
  38. kimi_cli/soul/__init__.py +51 -20
  39. kimi_cli/soul/agent.py +213 -85
  40. kimi_cli/soul/approval.py +86 -17
  41. kimi_cli/soul/compaction.py +64 -53
  42. kimi_cli/soul/context.py +38 -5
  43. kimi_cli/soul/denwarenji.py +2 -0
  44. kimi_cli/soul/kimisoul.py +442 -60
  45. kimi_cli/soul/message.py +54 -54
  46. kimi_cli/soul/slash.py +72 -0
  47. kimi_cli/soul/toolset.py +387 -6
  48. kimi_cli/toad.py +74 -0
  49. kimi_cli/tools/AGENTS.md +5 -0
  50. kimi_cli/tools/__init__.py +42 -34
  51. kimi_cli/tools/display.py +25 -0
  52. kimi_cli/tools/dmail/__init__.py +10 -10
  53. kimi_cli/tools/dmail/dmail.md +11 -9
  54. kimi_cli/tools/file/__init__.py +1 -3
  55. kimi_cli/tools/file/glob.py +20 -23
  56. kimi_cli/tools/file/grep.md +1 -1
  57. kimi_cli/tools/file/{grep.py → grep_local.py} +51 -23
  58. kimi_cli/tools/file/read.md +24 -6
  59. kimi_cli/tools/file/read.py +134 -50
  60. kimi_cli/tools/file/replace.md +1 -1
  61. kimi_cli/tools/file/replace.py +36 -29
  62. kimi_cli/tools/file/utils.py +282 -0
  63. kimi_cli/tools/file/write.py +43 -22
  64. kimi_cli/tools/multiagent/__init__.py +7 -0
  65. kimi_cli/tools/multiagent/create.md +11 -0
  66. kimi_cli/tools/multiagent/create.py +50 -0
  67. kimi_cli/tools/{task/__init__.py → multiagent/task.py} +48 -53
  68. kimi_cli/tools/shell/__init__.py +120 -0
  69. kimi_cli/tools/{bash → shell}/bash.md +1 -2
  70. kimi_cli/tools/shell/powershell.md +25 -0
  71. kimi_cli/tools/test.py +4 -4
  72. kimi_cli/tools/think/__init__.py +2 -2
  73. kimi_cli/tools/todo/__init__.py +14 -8
  74. kimi_cli/tools/utils.py +64 -24
  75. kimi_cli/tools/web/fetch.py +68 -13
  76. kimi_cli/tools/web/search.py +10 -12
  77. kimi_cli/ui/acp/__init__.py +65 -412
  78. kimi_cli/ui/print/__init__.py +37 -49
  79. kimi_cli/ui/print/visualize.py +179 -0
  80. kimi_cli/ui/shell/__init__.py +141 -84
  81. kimi_cli/ui/shell/console.py +2 -0
  82. kimi_cli/ui/shell/debug.py +28 -23
  83. kimi_cli/ui/shell/keyboard.py +5 -1
  84. kimi_cli/ui/shell/prompt.py +220 -194
  85. kimi_cli/ui/shell/replay.py +111 -46
  86. kimi_cli/ui/shell/setup.py +89 -82
  87. kimi_cli/ui/shell/slash.py +422 -0
  88. kimi_cli/ui/shell/update.py +4 -2
  89. kimi_cli/ui/shell/usage.py +271 -0
  90. kimi_cli/ui/shell/visualize.py +574 -72
  91. kimi_cli/ui/wire/__init__.py +267 -0
  92. kimi_cli/ui/wire/jsonrpc.py +142 -0
  93. kimi_cli/ui/wire/protocol.py +1 -0
  94. kimi_cli/utils/__init__.py +0 -0
  95. kimi_cli/utils/aiohttp.py +2 -0
  96. kimi_cli/utils/aioqueue.py +72 -0
  97. kimi_cli/utils/broadcast.py +37 -0
  98. kimi_cli/utils/changelog.py +12 -7
  99. kimi_cli/utils/clipboard.py +12 -0
  100. kimi_cli/utils/datetime.py +37 -0
  101. kimi_cli/utils/environment.py +58 -0
  102. kimi_cli/utils/envvar.py +12 -0
  103. kimi_cli/utils/frontmatter.py +44 -0
  104. kimi_cli/utils/logging.py +7 -6
  105. kimi_cli/utils/message.py +9 -14
  106. kimi_cli/utils/path.py +99 -9
  107. kimi_cli/utils/pyinstaller.py +6 -0
  108. kimi_cli/utils/rich/__init__.py +33 -0
  109. kimi_cli/utils/rich/columns.py +99 -0
  110. kimi_cli/utils/rich/markdown.py +961 -0
  111. kimi_cli/utils/rich/markdown_sample.md +108 -0
  112. kimi_cli/utils/rich/markdown_sample_short.md +2 -0
  113. kimi_cli/utils/signals.py +2 -0
  114. kimi_cli/utils/slashcmd.py +124 -0
  115. kimi_cli/utils/string.py +2 -0
  116. kimi_cli/utils/term.py +168 -0
  117. kimi_cli/utils/typing.py +20 -0
  118. kimi_cli/wire/__init__.py +98 -29
  119. kimi_cli/wire/serde.py +45 -0
  120. kimi_cli/wire/types.py +299 -0
  121. kimi_cli-0.78.dist-info/METADATA +200 -0
  122. kimi_cli-0.78.dist-info/RECORD +135 -0
  123. kimi_cli-0.78.dist-info/entry_points.txt +4 -0
  124. kimi_cli/cli.py +0 -250
  125. kimi_cli/soul/runtime.py +0 -96
  126. kimi_cli/tools/bash/__init__.py +0 -99
  127. kimi_cli/tools/file/patch.md +0 -8
  128. kimi_cli/tools/file/patch.py +0 -143
  129. kimi_cli/tools/mcp.py +0 -85
  130. kimi_cli/ui/shell/liveview.py +0 -386
  131. kimi_cli/ui/shell/metacmd.py +0 -262
  132. kimi_cli/wire/message.py +0 -91
  133. kimi_cli-0.44.dist-info/METADATA +0 -188
  134. kimi_cli-0.44.dist-info/RECORD +0 -89
  135. kimi_cli-0.44.dist-info/entry_points.txt +0 -3
  136. /kimi_cli/tools/{task → multiagent}/task.md +0 -0
  137. {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"