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
kimi_cli/session.py
CHANGED
|
@@ -1,65 +1,251 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import builtins
|
|
5
|
+
import shutil
|
|
1
6
|
import uuid
|
|
7
|
+
from dataclasses import dataclass
|
|
2
8
|
from pathlib import Path
|
|
3
|
-
from
|
|
9
|
+
from textwrap import shorten
|
|
10
|
+
|
|
11
|
+
import aiofiles
|
|
12
|
+
from kaos.path import KaosPath
|
|
13
|
+
from kosong.message import Message
|
|
4
14
|
|
|
5
15
|
from kimi_cli.metadata import WorkDirMeta, load_metadata, save_metadata
|
|
6
16
|
from kimi_cli.utils.logging import logger
|
|
17
|
+
from kimi_cli.wire.serde import WireMessageRecord
|
|
18
|
+
from kimi_cli.wire.types import TurnBegin
|
|
7
19
|
|
|
8
20
|
|
|
9
|
-
|
|
21
|
+
@dataclass(slots=True, kw_only=True)
|
|
22
|
+
class Session:
|
|
10
23
|
"""A session of a work directory."""
|
|
11
24
|
|
|
25
|
+
# static metadata
|
|
12
26
|
id: str
|
|
13
|
-
|
|
14
|
-
|
|
27
|
+
"""The session ID."""
|
|
28
|
+
work_dir: KaosPath
|
|
29
|
+
"""The absolute path of the work directory."""
|
|
30
|
+
work_dir_meta: WorkDirMeta
|
|
31
|
+
"""The metadata of the work directory."""
|
|
32
|
+
context_file: Path
|
|
33
|
+
"""The absolute path to the file storing the message history."""
|
|
34
|
+
|
|
35
|
+
# refreshable metadata
|
|
36
|
+
title: str
|
|
37
|
+
"""The title of the session."""
|
|
38
|
+
updated_at: float
|
|
39
|
+
"""The timestamp of the last update to the session."""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def dir(self) -> Path:
|
|
43
|
+
"""The absolute path of the session directory."""
|
|
44
|
+
path = self.work_dir_meta.sessions_dir / self.id
|
|
45
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
return path
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def wire_file(self) -> Path:
|
|
50
|
+
"""The file backend for persisting Wire messages."""
|
|
51
|
+
return self.dir / "wire.jsonl"
|
|
52
|
+
|
|
53
|
+
def is_empty(self) -> bool:
|
|
54
|
+
"""Whether the session has any context history."""
|
|
55
|
+
try:
|
|
56
|
+
return self.context_file.stat().st_size == 0
|
|
57
|
+
except FileNotFoundError:
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
async def delete(self) -> None:
|
|
61
|
+
"""Delete the session directory."""
|
|
62
|
+
session_dir = self.work_dir_meta.sessions_dir / self.id
|
|
63
|
+
if not session_dir.exists():
|
|
64
|
+
return
|
|
65
|
+
await asyncio.to_thread(shutil.rmtree, session_dir, True)
|
|
66
|
+
|
|
67
|
+
async def refresh(self) -> None:
|
|
68
|
+
self.title = f"Untitled ({self.id})"
|
|
69
|
+
self.updated_at = self.context_file.stat().st_mtime if self.context_file.exists() else 0.0
|
|
70
|
+
|
|
71
|
+
if not self.wire_file.exists():
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
async with aiofiles.open(self.wire_file, encoding="utf-8") as f:
|
|
76
|
+
async for line in f:
|
|
77
|
+
if not line.strip():
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
record = WireMessageRecord.model_validate_json(line)
|
|
81
|
+
wire_msg = record.to_wire_message()
|
|
82
|
+
except Exception:
|
|
83
|
+
logger.exception(
|
|
84
|
+
"Failed to parse line in wire file {file}:", file=self.wire_file
|
|
85
|
+
)
|
|
86
|
+
continue
|
|
87
|
+
if isinstance(wire_msg, TurnBegin):
|
|
88
|
+
title = shorten(
|
|
89
|
+
Message(role="user", content=wire_msg.user_input).extract_text(" "),
|
|
90
|
+
width=50,
|
|
91
|
+
)
|
|
92
|
+
self.title = f"{title} ({self.id})"
|
|
93
|
+
return
|
|
94
|
+
except Exception:
|
|
95
|
+
logger.exception(
|
|
96
|
+
"Failed to derive session title from wire file {file}:", file=self.wire_file
|
|
97
|
+
)
|
|
15
98
|
|
|
16
99
|
@staticmethod
|
|
17
|
-
def create(
|
|
100
|
+
async def create(
|
|
101
|
+
work_dir: KaosPath,
|
|
102
|
+
session_id: str | None = None,
|
|
103
|
+
_context_file: Path | None = None,
|
|
104
|
+
) -> Session:
|
|
18
105
|
"""Create a new session for a work directory."""
|
|
106
|
+
work_dir = work_dir.canonical()
|
|
19
107
|
logger.debug("Creating new session for work directory: {work_dir}", work_dir=work_dir)
|
|
20
108
|
|
|
21
109
|
metadata = load_metadata()
|
|
22
|
-
work_dir_meta =
|
|
110
|
+
work_dir_meta = metadata.get_work_dir_meta(work_dir)
|
|
23
111
|
if work_dir_meta is None:
|
|
24
|
-
work_dir_meta =
|
|
25
|
-
metadata.work_dirs.append(work_dir_meta)
|
|
112
|
+
work_dir_meta = metadata.new_work_dir_meta(work_dir)
|
|
26
113
|
|
|
27
|
-
session_id
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
114
|
+
if session_id is None:
|
|
115
|
+
session_id = str(uuid.uuid4())
|
|
116
|
+
session_dir = work_dir_meta.sessions_dir / session_id
|
|
117
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
118
|
+
|
|
119
|
+
if _context_file is None:
|
|
120
|
+
context_file = session_dir / "context.jsonl"
|
|
31
121
|
else:
|
|
32
122
|
logger.warning(
|
|
33
|
-
"Using provided
|
|
123
|
+
"Using provided context file: {context_file}", context_file=_context_file
|
|
34
124
|
)
|
|
35
|
-
|
|
36
|
-
if
|
|
37
|
-
assert
|
|
38
|
-
|
|
125
|
+
_context_file.parent.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
if _context_file.exists():
|
|
127
|
+
assert _context_file.is_file()
|
|
128
|
+
context_file = _context_file
|
|
39
129
|
|
|
40
|
-
if
|
|
130
|
+
if context_file.exists():
|
|
41
131
|
# truncate if exists
|
|
42
132
|
logger.warning(
|
|
43
|
-
"
|
|
133
|
+
"Context file already exists, truncating: {context_file}", context_file=context_file
|
|
44
134
|
)
|
|
45
|
-
|
|
46
|
-
|
|
135
|
+
context_file.unlink()
|
|
136
|
+
context_file.touch()
|
|
47
137
|
|
|
48
138
|
save_metadata(metadata)
|
|
49
139
|
|
|
50
|
-
|
|
140
|
+
session = Session(
|
|
141
|
+
id=session_id,
|
|
142
|
+
work_dir=work_dir,
|
|
143
|
+
work_dir_meta=work_dir_meta,
|
|
144
|
+
context_file=context_file,
|
|
145
|
+
title="",
|
|
146
|
+
updated_at=0.0,
|
|
147
|
+
)
|
|
148
|
+
await session.refresh()
|
|
149
|
+
return session
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
async def find(work_dir: KaosPath, session_id: str) -> Session | None:
|
|
153
|
+
"""Find a session by work directory and session ID."""
|
|
154
|
+
work_dir = work_dir.canonical()
|
|
155
|
+
logger.debug(
|
|
156
|
+
"Finding session for work directory: {work_dir}, session ID: {session_id}",
|
|
157
|
+
work_dir=work_dir,
|
|
158
|
+
session_id=session_id,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
metadata = load_metadata()
|
|
162
|
+
work_dir_meta = metadata.get_work_dir_meta(work_dir)
|
|
163
|
+
if work_dir_meta is None:
|
|
164
|
+
logger.debug("Work directory never been used")
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
_migrate_session_context_file(work_dir_meta, session_id)
|
|
168
|
+
|
|
169
|
+
session_dir = work_dir_meta.sessions_dir / session_id
|
|
170
|
+
if not session_dir.is_dir():
|
|
171
|
+
logger.debug("Session directory not found: {session_dir}", session_dir=session_dir)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
context_file = session_dir / "context.jsonl"
|
|
175
|
+
if not context_file.exists():
|
|
176
|
+
logger.debug(
|
|
177
|
+
"Session context file not found: {context_file}", context_file=context_file
|
|
178
|
+
)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
session = Session(
|
|
51
182
|
id=session_id,
|
|
52
183
|
work_dir=work_dir,
|
|
53
|
-
|
|
184
|
+
work_dir_meta=work_dir_meta,
|
|
185
|
+
context_file=context_file,
|
|
186
|
+
title="",
|
|
187
|
+
updated_at=0.0,
|
|
54
188
|
)
|
|
189
|
+
await session.refresh()
|
|
190
|
+
return session
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
async def list(work_dir: KaosPath) -> builtins.list[Session]:
|
|
194
|
+
"""List all sessions for a work directory."""
|
|
195
|
+
work_dir = work_dir.canonical()
|
|
196
|
+
logger.debug("Listing sessions for work directory: {work_dir}", work_dir=work_dir)
|
|
197
|
+
|
|
198
|
+
metadata = load_metadata()
|
|
199
|
+
work_dir_meta = metadata.get_work_dir_meta(work_dir)
|
|
200
|
+
if work_dir_meta is None:
|
|
201
|
+
logger.debug("Work directory never been used")
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
session_ids = {
|
|
205
|
+
path.name if path.is_dir() else path.stem
|
|
206
|
+
for path in work_dir_meta.sessions_dir.iterdir()
|
|
207
|
+
if path.is_dir() or path.suffix == ".jsonl"
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
sessions: list[Session] = []
|
|
211
|
+
for session_id in session_ids:
|
|
212
|
+
_migrate_session_context_file(work_dir_meta, session_id)
|
|
213
|
+
session_dir = work_dir_meta.sessions_dir / session_id
|
|
214
|
+
if not session_dir.is_dir():
|
|
215
|
+
logger.debug("Session directory not found: {session_dir}", session_dir=session_dir)
|
|
216
|
+
continue
|
|
217
|
+
context_file = session_dir / "context.jsonl"
|
|
218
|
+
if not context_file.exists():
|
|
219
|
+
logger.debug(
|
|
220
|
+
"Session context file not found: {context_file}", context_file=context_file
|
|
221
|
+
)
|
|
222
|
+
continue
|
|
223
|
+
session = Session(
|
|
224
|
+
id=session_id,
|
|
225
|
+
work_dir=work_dir,
|
|
226
|
+
work_dir_meta=work_dir_meta,
|
|
227
|
+
context_file=context_file,
|
|
228
|
+
title="",
|
|
229
|
+
updated_at=0.0,
|
|
230
|
+
)
|
|
231
|
+
if session.is_empty():
|
|
232
|
+
logger.debug(
|
|
233
|
+
"Session context file is empty: {context_file}", context_file=context_file
|
|
234
|
+
)
|
|
235
|
+
continue
|
|
236
|
+
await session.refresh()
|
|
237
|
+
sessions.append(session)
|
|
238
|
+
sessions.sort(key=lambda session: session.updated_at, reverse=True)
|
|
239
|
+
return sessions
|
|
55
240
|
|
|
56
241
|
@staticmethod
|
|
57
|
-
def continue_(work_dir:
|
|
242
|
+
async def continue_(work_dir: KaosPath) -> Session | None:
|
|
58
243
|
"""Get the last session for a work directory."""
|
|
244
|
+
work_dir = work_dir.canonical()
|
|
59
245
|
logger.debug("Continuing session for work directory: {work_dir}", work_dir=work_dir)
|
|
60
246
|
|
|
61
247
|
metadata = load_metadata()
|
|
62
|
-
work_dir_meta =
|
|
248
|
+
work_dir_meta = metadata.get_work_dir_meta(work_dir)
|
|
63
249
|
if work_dir_meta is None:
|
|
64
250
|
logger.debug("Work directory never been used")
|
|
65
251
|
return None
|
|
@@ -71,11 +257,17 @@ class Session(NamedTuple):
|
|
|
71
257
|
"Found last session for work directory: {session_id}",
|
|
72
258
|
session_id=work_dir_meta.last_session_id,
|
|
73
259
|
)
|
|
74
|
-
|
|
75
|
-
history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
260
|
+
return await Session.find(work_dir, work_dir_meta.last_session_id)
|
|
76
261
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
262
|
+
|
|
263
|
+
def _migrate_session_context_file(work_dir_meta: WorkDirMeta, session_id: str) -> None:
|
|
264
|
+
old_context_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
265
|
+
new_context_file = work_dir_meta.sessions_dir / session_id / "context.jsonl"
|
|
266
|
+
if old_context_file.exists() and not new_context_file.exists():
|
|
267
|
+
new_context_file.parent.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
old_context_file.rename(new_context_file)
|
|
269
|
+
logger.info(
|
|
270
|
+
"Migrated session context file from {old} to {new}",
|
|
271
|
+
old=old_context_file,
|
|
272
|
+
new=new_context_file,
|
|
81
273
|
)
|
kimi_cli/share.py
CHANGED
kimi_cli/skill.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Skill specification discovery and loading utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
from kimi_cli.share import get_share_dir
|
|
12
|
+
from kimi_cli.utils.frontmatter import read_frontmatter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_skills_dir() -> Path:
|
|
16
|
+
"""
|
|
17
|
+
Get the default skills directory path.
|
|
18
|
+
"""
|
|
19
|
+
return get_share_dir() / "skills"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_builtin_skills_dir() -> Path:
|
|
23
|
+
"""
|
|
24
|
+
Get the built-in skills directory path.
|
|
25
|
+
"""
|
|
26
|
+
return Path(__file__).parent / "skills"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_claude_skills_dir() -> Path:
|
|
30
|
+
"""
|
|
31
|
+
Get the default skills directory path of Claude.
|
|
32
|
+
"""
|
|
33
|
+
return Path.home() / ".claude" / "skills"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_skill_name(name: str) -> str:
|
|
37
|
+
"""Normalize a skill name for lookup."""
|
|
38
|
+
return name.casefold()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def index_skills(skills: Iterable[Skill]) -> dict[str, Skill]:
|
|
42
|
+
"""Build a lookup table for skills by normalized name."""
|
|
43
|
+
return {normalize_skill_name(skill.name): skill for skill in skills}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def discover_skills_from_roots(skills_dirs: Iterable[Path]) -> list[Skill]:
|
|
47
|
+
"""
|
|
48
|
+
Discover skills from multiple directory roots.
|
|
49
|
+
"""
|
|
50
|
+
skills_by_name: dict[str, Skill] = {}
|
|
51
|
+
for skills_dir in skills_dirs:
|
|
52
|
+
for skill in discover_skills(skills_dir):
|
|
53
|
+
skills_by_name[normalize_skill_name(skill.name)] = skill
|
|
54
|
+
return sorted(skills_by_name.values(), key=lambda s: s.name)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_skill_text(skill: Skill) -> str | None:
|
|
58
|
+
"""Read the SKILL.md contents for a skill."""
|
|
59
|
+
try:
|
|
60
|
+
return skill.skill_md_file.read_text(encoding="utf-8").strip()
|
|
61
|
+
except OSError as exc:
|
|
62
|
+
logger.warning(
|
|
63
|
+
"Failed to read skill file {path}: {error}",
|
|
64
|
+
path=skill.skill_md_file,
|
|
65
|
+
error=exc,
|
|
66
|
+
)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Skill(BaseModel):
|
|
71
|
+
"""Information about a single skill."""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(extra="ignore")
|
|
74
|
+
|
|
75
|
+
name: str
|
|
76
|
+
description: str
|
|
77
|
+
dir: Path
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def skill_md_file(self) -> Path:
|
|
81
|
+
"""Path to the SKILL.md file."""
|
|
82
|
+
return self.dir / "SKILL.md"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def discover_skills(skills_dir: Path) -> list[Skill]:
|
|
86
|
+
"""
|
|
87
|
+
Discover all skills in the given directory.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
skills_dir: Path to the directory containing skills.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
List of Skill objects, one for each valid skill found.
|
|
94
|
+
"""
|
|
95
|
+
if not skills_dir.is_dir():
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
skills: list[Skill] = []
|
|
99
|
+
|
|
100
|
+
# Iterate through all subdirectories in the skills directory
|
|
101
|
+
for skill_dir in skills_dir.iterdir():
|
|
102
|
+
if not skill_dir.is_dir():
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
skill_md = skill_dir / "SKILL.md"
|
|
106
|
+
if not skill_md.is_file():
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
# Try to parse the SKILL.md file
|
|
110
|
+
try:
|
|
111
|
+
skills.append(parse_skill_md(skill_md))
|
|
112
|
+
except Exception as e:
|
|
113
|
+
# Skip invalid skills, but log for debugging
|
|
114
|
+
logger.info("Skipping invalid skill at {}: {}", skill_md, e)
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
return sorted(skills, key=lambda s: s.name)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def parse_skill_md(skill_md_file: Path) -> Skill:
|
|
121
|
+
"""
|
|
122
|
+
Parse a SKILL.md file to extract name and description.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
skill_md_file: Path to the SKILL.md file.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Skill object.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValueError: If the SKILL.md file is not valid.
|
|
132
|
+
"""
|
|
133
|
+
frontmatter = read_frontmatter(skill_md_file) or {}
|
|
134
|
+
|
|
135
|
+
if "name" not in frontmatter:
|
|
136
|
+
frontmatter["name"] = skill_md_file.parent.name
|
|
137
|
+
if "description" not in frontmatter:
|
|
138
|
+
frontmatter["description"] = "No description provided."
|
|
139
|
+
|
|
140
|
+
return Skill.model_validate(
|
|
141
|
+
{
|
|
142
|
+
**frontmatter,
|
|
143
|
+
"dir": skill_md_file.parent.absolute(),
|
|
144
|
+
}
|
|
145
|
+
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kimi-cli-help
|
|
3
|
+
description: Answer Kimi CLI usage, configuration, and troubleshooting questions. Use when user asks about Kimi CLI installation, setup, configuration, slash commands, keyboard shortcuts, MCP integration, providers, environment variables, how something works internally, or any questions about Kimi CLI itself.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Kimi CLI Help
|
|
7
|
+
|
|
8
|
+
Help users with Kimi CLI questions by consulting documentation and source code.
|
|
9
|
+
|
|
10
|
+
## Strategy
|
|
11
|
+
|
|
12
|
+
1. **Prefer official documentation** for most questions
|
|
13
|
+
2. **Read local source** when in kimi-cli project itself, or when user is developing with kimi-cli as a library (e.g., importing from `kimi_cli` in their code)
|
|
14
|
+
3. **Clone and explore source** for complex internals not covered in docs - **ask user for confirmation first**
|
|
15
|
+
|
|
16
|
+
## Documentation
|
|
17
|
+
|
|
18
|
+
Base URL: `https://moonshotai.github.io/kimi-cli/`
|
|
19
|
+
|
|
20
|
+
Fetch documentation index to find relevant pages:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
https://moonshotai.github.io/kimi-cli/llms.txt
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Page URL Pattern
|
|
27
|
+
|
|
28
|
+
- English: `https://moonshotai.github.io/kimi-cli/en/...`
|
|
29
|
+
- Chinese: `https://moonshotai.github.io/kimi-cli/zh/...`
|
|
30
|
+
|
|
31
|
+
### Topic Mapping
|
|
32
|
+
|
|
33
|
+
| Topic | Page |
|
|
34
|
+
|-------|------|
|
|
35
|
+
| Installation, first run | `/en/guides/getting-started.md` |
|
|
36
|
+
| Config files | `/en/configuration/config-files.md` |
|
|
37
|
+
| Providers, models | `/en/configuration/providers.md` |
|
|
38
|
+
| Environment variables | `/en/configuration/env-vars.md` |
|
|
39
|
+
| Slash commands | `/en/reference/slash-commands.md` |
|
|
40
|
+
| CLI flags | `/en/reference/kimi-command.md` |
|
|
41
|
+
| Keyboard shortcuts | `/en/reference/keyboard.md` |
|
|
42
|
+
| MCP | `/en/customization/mcp.md` |
|
|
43
|
+
| Agents | `/en/customization/agents.md` |
|
|
44
|
+
| Skills | `/en/customization/skills.md` |
|
|
45
|
+
| FAQ | `/en/faq.md` |
|
|
46
|
+
|
|
47
|
+
## Source Code
|
|
48
|
+
|
|
49
|
+
Repository: `https://github.com/MoonshotAI/kimi-cli`
|
|
50
|
+
|
|
51
|
+
When to read source:
|
|
52
|
+
|
|
53
|
+
- In kimi-cli project directory (check `pyproject.toml` for `name = "kimi-cli"`)
|
|
54
|
+
- User is importing `kimi_cli` as a library in their project
|
|
55
|
+
- Question about internals not covered in docs (ask user before cloning)
|