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,12 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ _TRUE_VALUES = {"1", "true", "t", "yes", "y"}
6
+
7
+
8
+ def get_env_bool(name: str, default: bool = False) -> bool:
9
+ value = os.getenv(name)
10
+ if value is None:
11
+ return default
12
+ return value.strip().lower() in _TRUE_VALUES
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, cast
5
+
6
+ import yaml
7
+
8
+
9
+ def read_frontmatter(path: Path) -> dict[str, Any] | None:
10
+ """
11
+ Read the YAML frontmatter at the start of a file.
12
+
13
+ Args:
14
+ path: Path to an existing file that may contain frontmatter.
15
+
16
+ Raises:
17
+ ValueError: If the frontmatter YAML is invalid.
18
+ """
19
+ with path.open(encoding="utf-8", errors="replace") as handle:
20
+ first_line = handle.readline()
21
+ if not first_line or first_line.strip() != "---":
22
+ return None
23
+
24
+ frontmatter_lines: list[str] = []
25
+ for line in handle:
26
+ if line.strip() == "---":
27
+ break
28
+ frontmatter_lines.append(line)
29
+ else:
30
+ return None
31
+
32
+ frontmatter = "".join(frontmatter_lines).strip()
33
+ if not frontmatter:
34
+ return None
35
+
36
+ try:
37
+ raw_data: Any = yaml.safe_load(frontmatter)
38
+ except yaml.YAMLError as exc:
39
+ raise ValueError("Invalid frontmatter YAML.") from exc
40
+
41
+ if not isinstance(raw_data, dict):
42
+ raise ValueError("Frontmatter YAML must be a mapping.")
43
+
44
+ return cast(dict[str, Any], raw_data)
kimi_cli/utils/logging.py CHANGED
@@ -1,18 +1,19 @@
1
- from typing import IO
1
+ from __future__ import annotations
2
2
 
3
- from loguru import logger
3
+ from typing import IO, AnyStr
4
4
 
5
- logger.remove()
5
+ from loguru import logger
6
6
 
7
7
 
8
8
  class StreamToLogger(IO[str]):
9
9
  def __init__(self, level: str = "ERROR"):
10
10
  self._level = level
11
11
 
12
- def write(self, buffer: str) -> int:
13
- for line in buffer.rstrip().splitlines():
12
+ def write(self, s: AnyStr, /) -> int:
13
+ text = repr(s) if isinstance(s, bytes) else s
14
+ for line in text.rstrip().splitlines():
14
15
  logger.opt(depth=1).log(self._level, line.rstrip())
15
- return len(buffer)
16
+ return len(s)
16
17
 
17
18
  def flush(self) -> None:
18
19
  pass
kimi_cli/utils/message.py CHANGED
@@ -1,22 +1,17 @@
1
- from kosong.base.message import Message, TextPart
1
+ from __future__ import annotations
2
2
 
3
+ from kosong.message import Message
3
4
 
4
- def message_extract_text(message: Message) -> str:
5
- """Extract text from a message."""
6
- if isinstance(message.content, str):
7
- return message.content
8
- return "\n".join(part.text for part in message.content if isinstance(part, TextPart))
5
+ from kimi_cli.wire.types import TextPart
9
6
 
10
7
 
11
8
  def message_stringify(message: Message) -> str:
12
9
  """Get a string representation of a message."""
10
+ # TODO: this should be merged into `kosong.message.Message.extract_text`
13
11
  parts: list[str] = []
14
- if isinstance(message.content, str):
15
- parts.append(message.content)
16
- else:
17
- for part in message.content:
18
- if isinstance(part, TextPart):
19
- parts.append(part.text)
20
- else:
21
- parts.append(f"[{part.type}]")
12
+ for part in message.content:
13
+ if isinstance(part, TextPart):
14
+ parts.append(part.text)
15
+ else:
16
+ parts.append(f"[{part.type}]")
22
17
  return "".join(parts)
kimi_cli/utils/path.py CHANGED
@@ -1,23 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
1
5
  import re
2
- from pathlib import Path
6
+ from pathlib import Path, PurePath
7
+ from stat import S_ISDIR
3
8
 
4
9
  import aiofiles.os
10
+ from kaos.path import KaosPath
11
+
12
+ _ROTATION_OPEN_FLAGS = os.O_CREAT | os.O_EXCL | os.O_WRONLY
13
+ _ROTATION_FILE_MODE = 0o600
14
+
15
+
16
+ async def _reserve_rotation_path(path: Path) -> bool:
17
+ """Atomically create an empty file as a reservation for *path*."""
18
+
19
+ def _create() -> None:
20
+ fd = os.open(str(path), _ROTATION_OPEN_FLAGS, _ROTATION_FILE_MODE)
21
+ os.close(fd)
22
+
23
+ try:
24
+ await asyncio.to_thread(_create)
25
+ except FileExistsError:
26
+ return False
27
+ return True
5
28
 
6
29
 
7
30
  async def next_available_rotation(path: Path) -> Path | None:
31
+ """Return a reserved rotation path for *path* or ``None`` if parent is missing.
32
+
33
+ The caller must overwrite/reuse the returned path immediately because this helper
34
+ commits an empty placeholder file to guarantee uniqueness. It is therefore suited
35
+ for rotating *files* (like history logs) but **not** directory creation.
8
36
  """
9
- Find the next available rotation path for a given path.
10
- """
37
+
11
38
  if not path.parent.exists():
12
39
  return None
40
+
13
41
  base_name = path.stem
14
42
  suffix = path.suffix
15
43
  pattern = re.compile(rf"^{re.escape(base_name)}_(\d+){re.escape(suffix)}$")
16
44
  max_num = 0
17
- # FIXME: protect from race condition
18
- for p in await aiofiles.os.listdir(path.parent):
19
- if m := pattern.match(p):
20
- max_num = max(max_num, int(m.group(1)))
45
+ for entry in await aiofiles.os.listdir(path.parent):
46
+ if match := pattern.match(entry):
47
+ max_num = max(max_num, int(match.group(1)))
48
+
21
49
  next_num = max_num + 1
22
- next_path = path.parent / f"{base_name}_{next_num}{suffix}"
23
- return next_path
50
+ while True:
51
+ next_path = path.parent / f"{base_name}_{next_num}{suffix}"
52
+ if await _reserve_rotation_path(next_path):
53
+ return next_path
54
+ next_num += 1
55
+
56
+
57
+ async def list_directory(work_dir: KaosPath) -> str:
58
+ """Return an ``ls``-like listing of *work_dir*.
59
+
60
+ This helper is used mainly to provide context to the LLM (for example
61
+ ``KIMI_WORK_DIR_LS``) and to show top-level directory contents in tools.
62
+ It should therefore be robust against per-entry filesystem issues such as
63
+ broken symlinks or permission errors: a single bad entry must not crash
64
+ the whole CLI.
65
+ """
66
+
67
+ entries: list[str] = []
68
+ # Iterate entries; tolerate per-entry stat failures (broken symlinks, permissions, etc.).
69
+ async for entry in work_dir.iterdir():
70
+ try:
71
+ st = await entry.stat()
72
+ except OSError:
73
+ # Broken symlink, permission error, etc. – keep listing other entries.
74
+ entries.append(f"?--------- {'?':>10} {entry.name} [stat failed]")
75
+ continue
76
+ mode = "d" if S_ISDIR(st.st_mode) else "-"
77
+ mode += "r" if st.st_mode & 0o400 else "-"
78
+ mode += "w" if st.st_mode & 0o200 else "-"
79
+ mode += "x" if st.st_mode & 0o100 else "-"
80
+ mode += "r" if st.st_mode & 0o040 else "-"
81
+ mode += "w" if st.st_mode & 0o020 else "-"
82
+ mode += "x" if st.st_mode & 0o010 else "-"
83
+ mode += "r" if st.st_mode & 0o004 else "-"
84
+ mode += "w" if st.st_mode & 0o002 else "-"
85
+ mode += "x" if st.st_mode & 0o001 else "-"
86
+ entries.append(f"{mode} {st.st_size:>10} {entry.name}")
87
+ return "\n".join(entries)
88
+
89
+
90
+ def shorten_home(path: KaosPath) -> KaosPath:
91
+ """
92
+ Convert absolute path to use `~` for home directory.
93
+ """
94
+ try:
95
+ home = KaosPath.home()
96
+ p = path.relative_to(home)
97
+ return KaosPath("~") / p
98
+ except Exception:
99
+ return path
100
+
101
+
102
+ def is_within_directory(path: KaosPath, directory: KaosPath) -> bool:
103
+ """
104
+ Check whether *path* is contained within *directory* using pure path semantics.
105
+ Both arguments should already be canonicalized (e.g. via KaosPath.canonical()).
106
+ """
107
+ candidate = PurePath(str(path))
108
+ base = PurePath(str(directory))
109
+ try:
110
+ candidate.relative_to(base)
111
+ return True
112
+ except ValueError:
113
+ return False
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from PyInstaller.utils.hooks import collect_data_files, collect_submodules
2
4
 
3
5
  hiddenimports = collect_submodules("kimi_cli.tools")
@@ -9,9 +11,13 @@ datas = (
9
11
  "agents/**/*.md",
10
12
  "deps/bin/**",
11
13
  "prompts/**/*.md",
14
+ "skills/**",
12
15
  "tools/**/*.md",
13
16
  "CHANGELOG.md",
14
17
  ],
18
+ excludes=[
19
+ "tools/*.md",
20
+ ],
15
21
  )
16
22
  + collect_data_files(
17
23
  "dateparser",
@@ -0,0 +1,33 @@
1
+ """Project-wide Rich configuration helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Final
7
+
8
+ from rich import _wrap
9
+
10
+ # Regex used by Rich to compute break opportunities during wrapping.
11
+ _DEFAULT_WRAP_PATTERN: Final[re.Pattern[str]] = re.compile(r"\s*\S+\s*")
12
+ _CHAR_WRAP_PATTERN: Final[re.Pattern[str]] = re.compile(r".", re.DOTALL)
13
+
14
+
15
+ def enable_character_wrap() -> None:
16
+ """Switch Rich's wrapping logic to break on every character.
17
+
18
+ Rich's default behavior tries to preserve whole words; we override the
19
+ internal regex so markdown rendering can fold text at any column once it
20
+ exceeds the terminal width.
21
+ """
22
+
23
+ _wrap.re_word = _CHAR_WRAP_PATTERN
24
+
25
+
26
+ def restore_word_wrap() -> None:
27
+ """Restore Rich's default word-based wrapping."""
28
+
29
+ _wrap.re_word = _DEFAULT_WRAP_PATTERN
30
+
31
+
32
+ # Apply character-based wrapping globally for the CLI.
33
+ enable_character_wrap()
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ from rich.columns import Columns
4
+ from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
5
+ from rich.measure import Measurement
6
+ from rich.segment import Segment
7
+ from rich.text import Text
8
+
9
+
10
+ class _ShrinkToWidth:
11
+ def __init__(self, renderable: RenderableType, max_width: int) -> None:
12
+ self._renderable = renderable
13
+ self._max_width = max(max_width, 1)
14
+
15
+ def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
16
+ width = self._resolve_width(options)
17
+ return Measurement(0, width)
18
+
19
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
20
+ width = self._resolve_width(options)
21
+ child_options = options.update(width=width)
22
+ yield from console.render(self._renderable, child_options)
23
+
24
+ def _resolve_width(self, options: ConsoleOptions) -> int:
25
+ return max(1, min(self._max_width, options.max_width))
26
+
27
+
28
+ def _strip_trailing_spaces(segments: list[Segment]) -> list[Segment]:
29
+ lines = list(Segment.split_lines(segments))
30
+ trimmed: list[Segment] = []
31
+ n_lines = len(lines)
32
+ for index, line in enumerate(lines):
33
+ line_segments = list(line)
34
+ while line_segments:
35
+ segment = line_segments[-1]
36
+ if segment.control is not None:
37
+ break
38
+ trimmed_text = segment.text.rstrip(" ")
39
+ if trimmed_text != segment.text:
40
+ if trimmed_text:
41
+ line_segments[-1] = Segment(trimmed_text, segment.style, segment.control)
42
+ break
43
+ line_segments.pop()
44
+ continue
45
+ break
46
+ trimmed.extend(line_segments)
47
+ if index != n_lines - 1:
48
+ trimmed.append(Segment.line())
49
+ if trimmed:
50
+ trimmed.append(Segment.line())
51
+ return trimmed
52
+
53
+
54
+ class BulletColumns:
55
+ def __init__(
56
+ self,
57
+ renderable: RenderableType,
58
+ *,
59
+ bullet_style: str | None = None,
60
+ bullet: RenderableType | None = None,
61
+ padding: int = 1,
62
+ ) -> None:
63
+ self._renderable = renderable
64
+ self._bullet = bullet
65
+ self._bullet_style = bullet_style
66
+ self._padding = padding
67
+
68
+ def _bullet_renderable(self) -> RenderableType:
69
+ if self._bullet is not None:
70
+ return self._bullet
71
+ return Text("•", style=self._bullet_style or "")
72
+
73
+ def _available_width(self, console: Console, options: ConsoleOptions, bullet_width: int) -> int:
74
+ max_width = options.max_width or console.width or (bullet_width + self._padding + 1)
75
+ available = max_width - bullet_width - self._padding
76
+ return max(available, 1)
77
+
78
+ def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
79
+ bullet = self._bullet_renderable()
80
+ bullet_measure = Measurement.get(console, options, bullet)
81
+ bullet_width = max(bullet_measure.maximum, 1)
82
+ available = self._available_width(console, options, bullet_width)
83
+ constrained = _ShrinkToWidth(self._renderable, available)
84
+ columns = Columns([bullet, constrained], expand=False, padding=(0, self._padding))
85
+ return Measurement.get(console, options, columns)
86
+
87
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
88
+ bullet = self._bullet_renderable()
89
+ bullet_measure = Measurement.get(console, options, bullet)
90
+ bullet_width = max(bullet_measure.maximum, 1)
91
+ available = self._available_width(console, options, bullet_width)
92
+ columns = Columns(
93
+ [bullet, _ShrinkToWidth(self._renderable, available)],
94
+ expand=False,
95
+ padding=(0, self._padding),
96
+ )
97
+ segments = list(console.render(columns, options))
98
+ trimmed = _strip_trailing_spaces(segments)
99
+ yield from trimmed