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
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 typing import NamedTuple
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
- class Session(NamedTuple):
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
- work_dir: Path
14
- history_file: Path
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(work_dir: Path, _history_file: Path | None = None) -> "Session":
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 = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
110
+ work_dir_meta = metadata.get_work_dir_meta(work_dir)
23
111
  if work_dir_meta is None:
24
- work_dir_meta = WorkDirMeta(path=str(work_dir))
25
- metadata.work_dirs.append(work_dir_meta)
112
+ work_dir_meta = metadata.new_work_dir_meta(work_dir)
26
113
 
27
- session_id = str(uuid.uuid4())
28
- if _history_file is None:
29
- history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
30
- work_dir_meta.last_session_id = session_id
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 history file: {history_file}", history_file=_history_file
123
+ "Using provided context file: {context_file}", context_file=_context_file
34
124
  )
35
- _history_file.parent.mkdir(parents=True, exist_ok=True)
36
- if _history_file.exists():
37
- assert _history_file.is_file()
38
- history_file = _history_file
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 history_file.exists():
130
+ if context_file.exists():
41
131
  # truncate if exists
42
132
  logger.warning(
43
- "History file already exists, truncating: {history_file}", history_file=history_file
133
+ "Context file already exists, truncating: {context_file}", context_file=context_file
44
134
  )
45
- history_file.unlink()
46
- history_file.touch()
135
+ context_file.unlink()
136
+ context_file.touch()
47
137
 
48
138
  save_metadata(metadata)
49
139
 
50
- return Session(
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
- history_file=history_file,
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: Path) -> "Session | None":
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 = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
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
- session_id = work_dir_meta.last_session_id
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
- return Session(
78
- id=session_id,
79
- work_dir=work_dir,
80
- history_file=history_file,
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
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
 
3
5
 
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)