kimi-cli 0.35__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 (76) hide show
  1. kimi_cli/CHANGELOG.md +304 -0
  2. kimi_cli/__init__.py +374 -0
  3. kimi_cli/agent.py +261 -0
  4. kimi_cli/agents/koder/README.md +3 -0
  5. kimi_cli/agents/koder/agent.yaml +24 -0
  6. kimi_cli/agents/koder/sub.yaml +11 -0
  7. kimi_cli/agents/koder/system.md +72 -0
  8. kimi_cli/config.py +138 -0
  9. kimi_cli/llm.py +8 -0
  10. kimi_cli/metadata.py +117 -0
  11. kimi_cli/prompts/metacmds/__init__.py +4 -0
  12. kimi_cli/prompts/metacmds/compact.md +74 -0
  13. kimi_cli/prompts/metacmds/init.md +21 -0
  14. kimi_cli/py.typed +0 -0
  15. kimi_cli/share.py +8 -0
  16. kimi_cli/soul/__init__.py +59 -0
  17. kimi_cli/soul/approval.py +69 -0
  18. kimi_cli/soul/context.py +142 -0
  19. kimi_cli/soul/denwarenji.py +37 -0
  20. kimi_cli/soul/kimisoul.py +248 -0
  21. kimi_cli/soul/message.py +76 -0
  22. kimi_cli/soul/toolset.py +25 -0
  23. kimi_cli/soul/wire.py +101 -0
  24. kimi_cli/tools/__init__.py +85 -0
  25. kimi_cli/tools/bash/__init__.py +97 -0
  26. kimi_cli/tools/bash/bash.md +31 -0
  27. kimi_cli/tools/dmail/__init__.py +38 -0
  28. kimi_cli/tools/dmail/dmail.md +15 -0
  29. kimi_cli/tools/file/__init__.py +21 -0
  30. kimi_cli/tools/file/glob.md +17 -0
  31. kimi_cli/tools/file/glob.py +149 -0
  32. kimi_cli/tools/file/grep.md +5 -0
  33. kimi_cli/tools/file/grep.py +285 -0
  34. kimi_cli/tools/file/patch.md +8 -0
  35. kimi_cli/tools/file/patch.py +131 -0
  36. kimi_cli/tools/file/read.md +14 -0
  37. kimi_cli/tools/file/read.py +139 -0
  38. kimi_cli/tools/file/replace.md +7 -0
  39. kimi_cli/tools/file/replace.py +132 -0
  40. kimi_cli/tools/file/write.md +5 -0
  41. kimi_cli/tools/file/write.py +107 -0
  42. kimi_cli/tools/mcp.py +85 -0
  43. kimi_cli/tools/task/__init__.py +156 -0
  44. kimi_cli/tools/task/task.md +26 -0
  45. kimi_cli/tools/test.py +55 -0
  46. kimi_cli/tools/think/__init__.py +21 -0
  47. kimi_cli/tools/think/think.md +1 -0
  48. kimi_cli/tools/todo/__init__.py +27 -0
  49. kimi_cli/tools/todo/set_todo_list.md +15 -0
  50. kimi_cli/tools/utils.py +150 -0
  51. kimi_cli/tools/web/__init__.py +4 -0
  52. kimi_cli/tools/web/fetch.md +1 -0
  53. kimi_cli/tools/web/fetch.py +94 -0
  54. kimi_cli/tools/web/search.md +1 -0
  55. kimi_cli/tools/web/search.py +126 -0
  56. kimi_cli/ui/__init__.py +68 -0
  57. kimi_cli/ui/acp/__init__.py +441 -0
  58. kimi_cli/ui/print/__init__.py +176 -0
  59. kimi_cli/ui/shell/__init__.py +326 -0
  60. kimi_cli/ui/shell/console.py +3 -0
  61. kimi_cli/ui/shell/liveview.py +158 -0
  62. kimi_cli/ui/shell/metacmd.py +309 -0
  63. kimi_cli/ui/shell/prompt.py +574 -0
  64. kimi_cli/ui/shell/setup.py +192 -0
  65. kimi_cli/ui/shell/update.py +204 -0
  66. kimi_cli/utils/changelog.py +101 -0
  67. kimi_cli/utils/logging.py +18 -0
  68. kimi_cli/utils/message.py +8 -0
  69. kimi_cli/utils/path.py +23 -0
  70. kimi_cli/utils/provider.py +64 -0
  71. kimi_cli/utils/pyinstaller.py +24 -0
  72. kimi_cli/utils/string.py +12 -0
  73. kimi_cli-0.35.dist-info/METADATA +24 -0
  74. kimi_cli-0.35.dist-info/RECORD +76 -0
  75. kimi_cli-0.35.dist-info/WHEEL +4 -0
  76. kimi_cli-0.35.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,192 @@
1
+ import asyncio
2
+ from typing import TYPE_CHECKING, NamedTuple
3
+
4
+ import aiohttp
5
+ from prompt_toolkit import PromptSession
6
+ from prompt_toolkit.shortcuts.choice_input import ChoiceInput
7
+ from pydantic import SecretStr
8
+
9
+ from kimi_cli.config import LLMModel, LLMProvider, MoonshotSearchConfig, load_config, save_config
10
+ from kimi_cli.soul.kimisoul import KimiSoul
11
+ from kimi_cli.ui.shell.console import console
12
+ from kimi_cli.ui.shell.metacmd import meta_command
13
+
14
+ if TYPE_CHECKING:
15
+ from kimi_cli.ui.shell import ShellApp
16
+
17
+
18
+ class _Platform(NamedTuple):
19
+ id: str
20
+ name: str
21
+ base_url: str
22
+ search_url: str | None = None
23
+ allowed_models: list[str] | None = None
24
+
25
+
26
+ _PLATFORMS = [
27
+ _Platform(
28
+ id="kimi-coding",
29
+ name="Kimi Coding Plan",
30
+ base_url="https://api.kimi.com/coding/v1",
31
+ search_url="https://api.kimi.com/coding/v1/search",
32
+ ),
33
+ _Platform(
34
+ id="moonshot-cn",
35
+ name="Moonshot AI 开放平台",
36
+ base_url="https://api.moonshot.cn/v1",
37
+ allowed_models=["kimi-k2-turbo-preview", "kimi-k2-0905-preview", "kimi-k2-0711-preview"],
38
+ ),
39
+ _Platform(
40
+ id="moonshot-ai",
41
+ name="Moonshot AI Open Platform",
42
+ base_url="https://api.moonshot.ai/v1",
43
+ allowed_models=["kimi-k2-turbo-preview", "kimi-k2-0905-preview", "kimi-k2-0711-preview"],
44
+ ),
45
+ ]
46
+
47
+
48
+ @meta_command(kimi_soul_only=True)
49
+ async def setup(app: "ShellApp", args: list[str]):
50
+ """Setup Kimi CLI"""
51
+ assert isinstance(app.soul, KimiSoul)
52
+
53
+ result = await _setup()
54
+ if not result:
55
+ # error message already printed
56
+ return
57
+
58
+ config = load_config()
59
+ config.providers[result.platform.id] = LLMProvider(
60
+ type="kimi",
61
+ base_url=result.platform.base_url,
62
+ api_key=result.api_key,
63
+ )
64
+ config.models[result.model_id] = LLMModel(
65
+ provider=result.platform.id,
66
+ model=result.model_id,
67
+ max_context_size=result.max_context_size,
68
+ )
69
+ config.default_model = result.model_id
70
+
71
+ if result.platform.search_url:
72
+ config.services.moonshot_search = MoonshotSearchConfig(
73
+ base_url=result.platform.search_url,
74
+ api_key=result.api_key,
75
+ )
76
+
77
+ save_config(config)
78
+ console.print("[green]✓[/green] Kimi CLI has been setup! Reloading...")
79
+ await asyncio.sleep(1)
80
+ console.clear()
81
+
82
+ from kimi_cli import Reload
83
+
84
+ raise Reload
85
+
86
+
87
+ class _SetupResult(NamedTuple):
88
+ platform: _Platform
89
+ api_key: SecretStr
90
+ model_id: str
91
+ max_context_size: int
92
+
93
+
94
+ async def _setup() -> _SetupResult | None:
95
+ # select the API platform
96
+ platform_name = await _prompt_choice(
97
+ header="Select the API platform",
98
+ choices=[platform.name for platform in _PLATFORMS],
99
+ )
100
+ if not platform_name:
101
+ console.print("[red]No platform selected[/red]")
102
+ return None
103
+
104
+ platform = next(platform for platform in _PLATFORMS if platform.name == platform_name)
105
+
106
+ # enter the API key
107
+ api_key = await _prompt_text("Enter your API key", is_password=True)
108
+ if not api_key:
109
+ return None
110
+
111
+ # list models
112
+ models_url = f"{platform.base_url}/models"
113
+ try:
114
+ async with (
115
+ aiohttp.ClientSession() as session,
116
+ session.get(
117
+ models_url,
118
+ headers={
119
+ "Authorization": f"Bearer {api_key}",
120
+ },
121
+ raise_for_status=True,
122
+ ) as response,
123
+ ):
124
+ resp_json = await response.json()
125
+ except aiohttp.ClientError as e:
126
+ console.print(f"[red]Failed to get models: {e}[/red]")
127
+ return None
128
+
129
+ model_dict = {model["id"]: model for model in resp_json["data"]}
130
+
131
+ # select the model
132
+ if platform.allowed_models is None:
133
+ model_ids = [model["id"] for model in resp_json["data"]]
134
+ else:
135
+ id_set = set(model["id"] for model in resp_json["data"])
136
+ model_ids = [model_id for model_id in platform.allowed_models if model_id in id_set]
137
+
138
+ if not model_ids:
139
+ console.print("[red]No models available for the selected platform[/red]")
140
+ return None
141
+
142
+ model_id = await _prompt_choice(
143
+ header="Select the model",
144
+ choices=model_ids,
145
+ )
146
+ if not model_id:
147
+ console.print("[red]No model selected[/red]")
148
+ return None
149
+
150
+ model = model_dict[model_id]
151
+
152
+ return _SetupResult(
153
+ platform=platform,
154
+ api_key=SecretStr(api_key),
155
+ model_id=model_id,
156
+ max_context_size=model["context_length"],
157
+ )
158
+
159
+
160
+ async def _prompt_choice(*, header: str, choices: list[str]) -> str | None:
161
+ if not choices:
162
+ return None
163
+
164
+ try:
165
+ return await ChoiceInput(
166
+ message=header,
167
+ options=[(choice, choice) for choice in choices],
168
+ default=choices[0],
169
+ ).prompt_async()
170
+ except (EOFError, KeyboardInterrupt):
171
+ return None
172
+
173
+
174
+ async def _prompt_text(prompt: str, *, is_password: bool = False) -> str | None:
175
+ session = PromptSession()
176
+ try:
177
+ return str(
178
+ await session.prompt_async(
179
+ f" {prompt}: ",
180
+ is_password=is_password,
181
+ )
182
+ ).strip()
183
+ except (EOFError, KeyboardInterrupt):
184
+ return None
185
+
186
+
187
+ @meta_command
188
+ def reload(app: "ShellApp", args: list[str]):
189
+ """Reload configuration"""
190
+ from kimi_cli import Reload
191
+
192
+ raise Reload
@@ -0,0 +1,204 @@
1
+ import asyncio
2
+ import os
3
+ import platform
4
+ import re
5
+ import shutil
6
+ import stat
7
+ import tarfile
8
+ import tempfile
9
+ from enum import Enum, auto
10
+ from pathlib import Path
11
+
12
+ import aiohttp
13
+
14
+ from kimi_cli.ui.shell.console import console
15
+ from kimi_cli.utils.logging import logger
16
+
17
+ BASE_URL = "https://cdn.kimi.com/binaries/kimi-cli"
18
+ LATEST_VERSION_URL = f"{BASE_URL}/latest"
19
+ INSTALL_DIR = Path.home() / ".local" / "bin"
20
+
21
+
22
+ class UpdateResult(Enum):
23
+ UPDATE_AVAILABLE = auto()
24
+ UPDATED = auto()
25
+ UP_TO_DATE = auto()
26
+ FAILED = auto()
27
+ UNSUPPORTED = auto()
28
+
29
+
30
+ _UPDATE_LOCK = asyncio.Lock()
31
+
32
+
33
+ def _semver_tuple(version: str) -> tuple[int, int, int]:
34
+ v = version.strip()
35
+ if v.startswith("v"):
36
+ v = v[1:]
37
+ match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", v)
38
+ if not match:
39
+ return (0, 0, 0)
40
+ major = int(match.group(1))
41
+ minor = int(match.group(2))
42
+ patch = int(match.group(3) or 0)
43
+ return (major, minor, patch)
44
+
45
+
46
+ def _detect_target() -> str | None:
47
+ sys_name = platform.system()
48
+ mach = platform.machine()
49
+ if mach in ("x86_64", "amd64", "AMD64"):
50
+ arch = "x86_64"
51
+ elif mach in ("arm64", "aarch64"):
52
+ arch = "aarch64"
53
+ else:
54
+ logger.error("Unsupported architecture: {mach}", mach=mach)
55
+ return None
56
+ if sys_name == "Darwin":
57
+ os_name = "apple-darwin"
58
+ elif sys_name == "Linux":
59
+ os_name = "unknown-linux-gnu"
60
+ else:
61
+ logger.error("Unsupported OS: {sys_name}", sys_name=sys_name)
62
+ return None
63
+ return f"{arch}-{os_name}"
64
+
65
+
66
+ async def _get_latest_version(session: aiohttp.ClientSession) -> str | None:
67
+ try:
68
+ async with session.get(LATEST_VERSION_URL) as resp:
69
+ resp.raise_for_status()
70
+ data = await resp.text()
71
+ return data.strip()
72
+ except aiohttp.ClientError:
73
+ logger.exception("Failed to get latest version:")
74
+ return None
75
+
76
+
77
+ async def do_update(*, print: bool = True, check_only: bool = False) -> UpdateResult:
78
+ async with _UPDATE_LOCK:
79
+ return await _do_update(print=print, check_only=check_only)
80
+
81
+
82
+ async def _do_update(*, print: bool, check_only: bool) -> UpdateResult:
83
+ from kimi_cli import __version__ as current_version
84
+
85
+ def _print(message: str) -> None:
86
+ if print:
87
+ console.print(message)
88
+
89
+ target = _detect_target()
90
+ if not target:
91
+ _print("[red]Failed to detect target platform.[/red]")
92
+ return UpdateResult.UNSUPPORTED
93
+
94
+ async with aiohttp.ClientSession() as session:
95
+ logger.info("Checking for updates...")
96
+ _print("Checking for updates...")
97
+ latest_version = await _get_latest_version(session)
98
+ if not latest_version:
99
+ _print("[red]Failed to check for updates.[/red]")
100
+ return UpdateResult.FAILED
101
+
102
+ logger.debug("Latest version: {latest_version}", latest_version=latest_version)
103
+
104
+ cur_t = _semver_tuple(current_version)
105
+ lat_t = _semver_tuple(latest_version)
106
+
107
+ if cur_t >= lat_t:
108
+ logger.debug("Already up to date: {current_version}", current_version=current_version)
109
+ _print("[green]Already up to date.[/green]")
110
+ return UpdateResult.UP_TO_DATE
111
+
112
+ if check_only:
113
+ logger.info(
114
+ "Update available: current={current_version}, latest={latest_version}",
115
+ current_version=current_version,
116
+ latest_version=latest_version,
117
+ )
118
+ _print(f"[yellow]Update available: {latest_version}[/yellow]")
119
+ return UpdateResult.UPDATE_AVAILABLE
120
+
121
+ logger.info(
122
+ "Updating from {current_version} to {latest_version}...",
123
+ current_version=current_version,
124
+ latest_version=latest_version,
125
+ )
126
+ _print(f"Updating from {current_version} to {latest_version}...")
127
+
128
+ filename = f"kimi-{latest_version}-{target}.tar.gz"
129
+ download_url = f"{BASE_URL}/{latest_version}/{filename}"
130
+
131
+ with tempfile.TemporaryDirectory(prefix="kimi-cli-") as tmpdir:
132
+ tar_path = os.path.join(tmpdir, filename)
133
+
134
+ logger.info("Downloading from {download_url}...", download_url=download_url)
135
+ _print("[grey50]Downloading...[/grey50]")
136
+ try:
137
+ async with session.get(download_url) as resp:
138
+ resp.raise_for_status()
139
+ with open(tar_path, "wb") as f:
140
+ async for chunk in resp.content.iter_chunked(1024 * 64):
141
+ if chunk:
142
+ f.write(chunk)
143
+ except aiohttp.ClientError:
144
+ logger.exception(
145
+ "Failed to download update from {download_url}",
146
+ download_url=download_url,
147
+ )
148
+ _print("[red]Failed to download.[/red]")
149
+ return UpdateResult.FAILED
150
+ except Exception:
151
+ logger.exception("Failed to download:")
152
+ _print("[red]Failed to download.[/red]")
153
+ return UpdateResult.FAILED
154
+
155
+ logger.info("Extracting archive {tar_path}...", tar_path=tar_path)
156
+ _print("[grey50]Extracting...[/grey50]")
157
+ try:
158
+ with tarfile.open(tar_path, "r:gz") as tar:
159
+ tar.extractall(tmpdir)
160
+ binary_path = None
161
+ for root, _, files in os.walk(tmpdir):
162
+ if "kimi" in files:
163
+ binary_path = os.path.join(root, "kimi")
164
+ break
165
+ if not binary_path:
166
+ logger.error("Binary 'kimi' not found in archive.")
167
+ _print("[red]Binary 'kimi' not found in archive.[/red]")
168
+ return UpdateResult.FAILED
169
+ except Exception:
170
+ logger.exception("Failed to extract archive:")
171
+ _print("[red]Failed to extract archive.[/red]")
172
+ return UpdateResult.FAILED
173
+
174
+ INSTALL_DIR.mkdir(parents=True, exist_ok=True)
175
+ dest_path = INSTALL_DIR / "kimi"
176
+ logger.info("Installing to {dest_path}...", dest_path=dest_path)
177
+ _print("[grey50]Installing...[/grey50]")
178
+
179
+ try:
180
+ shutil.copy2(binary_path, dest_path)
181
+ os.chmod(
182
+ dest_path,
183
+ os.stat(dest_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH,
184
+ )
185
+ except Exception:
186
+ logger.exception("Failed to install:")
187
+ _print("[red]Failed to install.[/red]")
188
+ return UpdateResult.FAILED
189
+
190
+ _print("[green]Updated successfully![/green]")
191
+ _print("[yellow]Restart Kimi CLI to use the new version.[/yellow]")
192
+ return UpdateResult.UPDATED
193
+
194
+
195
+ # @meta_command
196
+ # async def update(app: "ShellApp", args: list[str]):
197
+ # """Check for updates"""
198
+ # await do_update(print=True)
199
+
200
+
201
+ # @meta_command(name="check-update")
202
+ # async def check_update(app: "ShellApp", args: list[str]):
203
+ # """Check for updates"""
204
+ # await do_update(print=True, check_only=True)
@@ -0,0 +1,101 @@
1
+ from pathlib import Path
2
+ from typing import NamedTuple
3
+
4
+
5
+ class ReleaseEntry(NamedTuple):
6
+ description: str
7
+ entries: list[str]
8
+
9
+
10
+ def parse_changelog(md_text: str) -> dict[str, ReleaseEntry]:
11
+ """Parse a subset of Keep a Changelog-style markdown into a map:
12
+ version -> (description, entries)
13
+
14
+ Parsing rules:
15
+ - Versions are denoted by level-2 headings starting with '## ['
16
+ Example: `## [v0.10.1] - 2025-09-18` or `## [Unreleased]`
17
+ - For each version section, description is the first contiguous block of
18
+ non-empty lines that do not start with '-' or '#'.
19
+ - Entries are all markdown list items starting with '- ' under that version
20
+ (across any subheadings like '### Added').
21
+ """
22
+ lines = md_text.splitlines()
23
+ result: dict[str, ReleaseEntry] = {}
24
+
25
+ current_ver: str | None = None
26
+ collecting_desc = False
27
+ desc_lines: list[str] = []
28
+ bullet_lines: list[str] = []
29
+ seen_content_after_header = False
30
+
31
+ def commit():
32
+ nonlocal current_ver, desc_lines, bullet_lines, result
33
+ if current_ver is None:
34
+ return
35
+ description = "\n".join([line.strip() for line in desc_lines]).strip()
36
+ # Deduplicate and normalize entries
37
+ norm_entries = [
38
+ line.strip()[2:].strip() for line in bullet_lines if line.strip().startswith("- ")
39
+ ]
40
+ result[current_ver] = ReleaseEntry(description=description, entries=norm_entries)
41
+
42
+ for raw in lines:
43
+ line = raw.rstrip()
44
+ if line.startswith("## ["):
45
+ # New version section, flush previous
46
+ commit()
47
+ # Extract version token inside brackets
48
+ end = line.find("]")
49
+ ver = line[4:end] if end != -1 else line[3:].strip()
50
+ current_ver = ver.strip()
51
+ desc_lines = []
52
+ bullet_lines = []
53
+ collecting_desc = True
54
+ seen_content_after_header = False
55
+ continue
56
+
57
+ if current_ver is None:
58
+ # Skip until first version section
59
+ continue
60
+
61
+ if not line.strip():
62
+ # blank line ends initial description block only after we've seen content
63
+ if collecting_desc and seen_content_after_header:
64
+ collecting_desc = False
65
+ continue
66
+
67
+ seen_content_after_header = True
68
+
69
+ if line.lstrip().startswith("### "):
70
+ collecting_desc = False
71
+ continue
72
+
73
+ if line.lstrip().startswith("- "):
74
+ collecting_desc = False
75
+ bullet_lines.append(line.strip())
76
+ continue
77
+
78
+ if collecting_desc:
79
+ # Accumulate description until a blank line or bullets/subheadings
80
+ desc_lines.append(line.strip())
81
+ # else: ignore any other free-form text after description block
82
+
83
+ # Final flush
84
+ commit()
85
+ return result
86
+
87
+
88
+ def format_release_notes(changelog: dict[str, ReleaseEntry]) -> str:
89
+ parts: list[str] = []
90
+ for ver, entry in changelog.items():
91
+ s = f"[bold]{ver}[/bold]"
92
+ if entry.description:
93
+ s += f": {entry.description}"
94
+ if entry.entries:
95
+ for it in entry.entries:
96
+ s += "\n[markdown.item.bullet]• [/]" + it
97
+ parts.append(s + "\n")
98
+ return "\n".join(parts).strip()
99
+
100
+
101
+ CHANGELOG = parse_changelog((Path(__file__).parent.parent / "CHANGELOG.md").read_text())
@@ -0,0 +1,18 @@
1
+ from typing import IO
2
+
3
+ from loguru import logger
4
+
5
+ logger.remove()
6
+
7
+
8
+ class StreamToLogger(IO[str]):
9
+ def __init__(self, level: str = "ERROR"):
10
+ self._level = level
11
+
12
+ def write(self, buffer: str) -> int:
13
+ for line in buffer.rstrip().splitlines():
14
+ logger.opt(depth=1).log(self._level, line.rstrip())
15
+ return len(buffer)
16
+
17
+ def flush(self) -> None:
18
+ pass
@@ -0,0 +1,8 @@
1
+ from kosong.base.message import Message, TextPart
2
+
3
+
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))
kimi_cli/utils/path.py ADDED
@@ -0,0 +1,23 @@
1
+ import re
2
+ from pathlib import Path
3
+
4
+ import aiofiles.os
5
+
6
+
7
+ async def next_available_rotation(path: Path) -> Path | None:
8
+ """
9
+ Find the next available rotation path for a given path.
10
+ """
11
+ if not path.parent.exists():
12
+ return None
13
+ base_name = path.stem
14
+ suffix = path.suffix
15
+ pattern = re.compile(rf"^{re.escape(base_name)}_(\d+){re.escape(suffix)}$")
16
+ 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)))
21
+ next_num = max_num + 1
22
+ next_path = path.parent / f"{base_name}_{next_num}{suffix}"
23
+ return next_path
@@ -0,0 +1,64 @@
1
+ import os
2
+
3
+ from kosong.chat_provider import ChaosChatProvider, Kimi, OpenAILegacy
4
+ from kosong.chat_provider.chaos import ChaosConfig
5
+ from pydantic import SecretStr
6
+
7
+ import kimi_cli
8
+ from kimi_cli.config import LLMModel, LLMProvider
9
+ from kimi_cli.llm import LLM
10
+
11
+
12
+ def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel):
13
+ match provider.type:
14
+ case "kimi":
15
+ if base_url := os.getenv("KIMI_BASE_URL"):
16
+ provider.base_url = base_url
17
+ if api_key := os.getenv("KIMI_API_KEY"):
18
+ provider.api_key = SecretStr(api_key)
19
+ if model_name := os.getenv("KIMI_MODEL_NAME"):
20
+ model.model = model_name
21
+ if max_context_size := os.getenv("KIMI_MODEL_MAX_CONTEXT_SIZE"):
22
+ model.max_context_size = int(max_context_size)
23
+ case "openai_legacy":
24
+ if base_url := os.getenv("OPENAI_BASE_URL"):
25
+ provider.base_url = base_url
26
+ if api_key := os.getenv("OPENAI_API_KEY"):
27
+ provider.api_key = SecretStr(api_key)
28
+ case _:
29
+ pass
30
+
31
+
32
+ def create_llm(provider: LLMProvider, model: LLMModel, stream: bool = True) -> LLM:
33
+ match provider.type:
34
+ case "kimi":
35
+ chat_provider = Kimi(
36
+ model=model.model,
37
+ base_url=provider.base_url,
38
+ api_key=provider.api_key.get_secret_value(),
39
+ stream=stream,
40
+ default_headers={
41
+ "User-Agent": kimi_cli.USER_AGENT,
42
+ },
43
+ )
44
+ case "openai_legacy":
45
+ chat_provider = OpenAILegacy(
46
+ model=model.model,
47
+ base_url=provider.base_url,
48
+ api_key=provider.api_key.get_secret_value(),
49
+ stream=stream,
50
+ )
51
+ case "_chaos":
52
+ chat_provider = ChaosChatProvider(
53
+ model=model.model,
54
+ base_url=provider.base_url,
55
+ api_key=provider.api_key.get_secret_value(),
56
+ chaos_config=ChaosConfig(
57
+ error_probability=0.8,
58
+ error_types=[429, 500, 503],
59
+ ),
60
+ )
61
+ case _:
62
+ raise ValueError(f"Unsupported provider: {provider.type}")
63
+
64
+ return LLM(chat_provider=chat_provider, max_context_size=model.max_context_size)
@@ -0,0 +1,24 @@
1
+ from PyInstaller.utils.hooks import collect_data_files, collect_submodules
2
+
3
+ hiddenimports = collect_submodules("kimi_cli.tools")
4
+ datas = (
5
+ collect_data_files(
6
+ "kimi_cli",
7
+ includes=[
8
+ "agents/**/*.yaml",
9
+ "agents/**/*.md",
10
+ "deps/bin/**",
11
+ "prompts/**/*.md",
12
+ "tools/**/*.md",
13
+ "CHANGELOG.md",
14
+ ],
15
+ )
16
+ + collect_data_files(
17
+ "dateparser",
18
+ includes=["**/*.pkl"],
19
+ )
20
+ + collect_data_files(
21
+ "fastmcp",
22
+ includes=["../fastmcp-*.dist-info/*"],
23
+ )
24
+ )
@@ -0,0 +1,12 @@
1
+ import re
2
+
3
+ _NEWLINE_RE = re.compile(r"[\r\n]+")
4
+
5
+
6
+ def shorten_middle(text: str, width: int, remove_newline: bool = True) -> str:
7
+ """Shorten the text by inserting ellipsis in the middle."""
8
+ if len(text) <= width:
9
+ return text
10
+ if remove_newline:
11
+ text = _NEWLINE_RE.sub(" ", text)
12
+ return text[: width // 2] + "..." + text[-width // 2 :]
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.3
2
+ Name: kimi-cli
3
+ Version: 0.35
4
+ Summary: Kimi CLI is your next CLI agent.
5
+ Requires-Dist: agent-client-protocol>=0.4.9
6
+ Requires-Dist: aiofiles>=25.1.0
7
+ Requires-Dist: aiohttp>=3.13.1
8
+ Requires-Dist: click>=8.3.0
9
+ Requires-Dist: kosong>=0.14.1
10
+ Requires-Dist: loguru>=0.7.3
11
+ Requires-Dist: patch-ng>=1.19.0
12
+ Requires-Dist: prompt-toolkit>=3.0.52
13
+ Requires-Dist: pyyaml>=6.0.3
14
+ Requires-Dist: rich>=14.2.0
15
+ Requires-Dist: ripgrepy>=2.2.0
16
+ Requires-Dist: streamingjson>=0.0.5
17
+ Requires-Dist: trafilatura>=2.0.0
18
+ Requires-Dist: tenacity>=9.1.2
19
+ Requires-Dist: fastmcp>=2.12.5
20
+ Requires-Dist: pydantic>=2.12.3
21
+ Requires-Python: >=3.13
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Kimi CLI