kimi-cli 0.52__tar.gz → 0.56__tar.gz

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 (102) hide show
  1. {kimi_cli-0.52 → kimi_cli-0.56}/PKG-INFO +2 -2
  2. {kimi_cli-0.52 → kimi_cli-0.56}/pyproject.toml +5 -5
  3. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/CHANGELOG.md +33 -0
  4. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/agents/default/agent.yaml +1 -1
  5. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/agentspec.py +17 -8
  6. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/app.py +29 -5
  7. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/cli.py +49 -23
  8. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/config.py +2 -0
  9. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/constant.py +2 -0
  10. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/exception.py +3 -0
  11. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/llm.py +22 -10
  12. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/metadata.py +7 -3
  13. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/prompts/__init__.py +2 -0
  14. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/session.py +7 -27
  15. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/share.py +2 -0
  16. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/__init__.py +10 -7
  17. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/agent.py +10 -5
  18. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/approval.py +8 -1
  19. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/compaction.py +2 -0
  20. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/context.py +3 -1
  21. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/denwarenji.py +2 -0
  22. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/kimisoul.py +25 -14
  23. kimi_cli-0.56/src/kimi_cli/soul/message.py +74 -0
  24. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/runtime.py +10 -27
  25. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/soul/toolset.py +2 -0
  26. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/glob.py +3 -4
  27. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/mcp.py +29 -4
  28. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/task/__init__.py +14 -4
  29. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/todo/__init__.py +7 -1
  30. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/web/fetch.py +8 -3
  31. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/acp/__init__.py +18 -11
  32. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/print/__init__.py +11 -38
  33. kimi_cli-0.56/src/kimi_cli/ui/print/visualize.py +129 -0
  34. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/__init__.py +14 -1
  35. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/console.py +2 -0
  36. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/debug.py +3 -1
  37. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/keyboard.py +2 -0
  38. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/metacmd.py +23 -10
  39. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/prompt.py +5 -3
  40. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/replay.py +2 -0
  41. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/setup.py +4 -2
  42. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/update.py +2 -0
  43. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/shell/visualize.py +15 -26
  44. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/wire/__init__.py +2 -0
  45. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/wire/jsonrpc.py +2 -0
  46. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/aiohttp.py +2 -0
  47. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/changelog.py +2 -0
  48. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/clipboard.py +2 -0
  49. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/logging.py +2 -0
  50. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/message.py +2 -0
  51. kimi_cli-0.56/src/kimi_cli/utils/path.py +86 -0
  52. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/pyinstaller.py +2 -0
  53. kimi_cli-0.56/src/kimi_cli/utils/rich/columns.py +99 -0
  54. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/signals.py +2 -0
  55. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/string.py +2 -0
  56. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/term.py +2 -0
  57. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/wire/__init__.py +12 -3
  58. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/wire/message.py +38 -29
  59. kimi_cli-0.52/src/kimi_cli/soul/message.py +0 -76
  60. kimi_cli-0.52/src/kimi_cli/utils/path.py +0 -23
  61. {kimi_cli-0.52 → kimi_cli-0.56}/README.md +0 -0
  62. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/__init__.py +0 -0
  63. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/agents/default/sub.yaml +0 -0
  64. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/agents/default/system.md +0 -0
  65. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/prompts/compact.md +0 -0
  66. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/prompts/init.md +0 -0
  67. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/py.typed +0 -0
  68. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/__init__.py +0 -0
  69. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/bash/__init__.py +0 -0
  70. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/bash/bash.md +0 -0
  71. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/bash/cmd.md +0 -0
  72. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/dmail/__init__.py +0 -0
  73. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/dmail/dmail.md +0 -0
  74. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/__init__.py +0 -0
  75. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/glob.md +0 -0
  76. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/grep.md +0 -0
  77. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/grep.py +0 -0
  78. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/patch.md +0 -0
  79. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/patch.py +0 -0
  80. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/read.md +0 -0
  81. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/read.py +0 -0
  82. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/replace.md +0 -0
  83. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/replace.py +0 -0
  84. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/write.md +0 -0
  85. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/file/write.py +0 -0
  86. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/task/task.md +0 -0
  87. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/test.py +0 -0
  88. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/think/__init__.py +0 -0
  89. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/think/think.md +0 -0
  90. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/todo/set_todo_list.md +0 -0
  91. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/utils.py +0 -0
  92. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/web/__init__.py +0 -0
  93. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/web/fetch.md +0 -0
  94. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/web/search.md +0 -0
  95. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/tools/web/search.py +0 -0
  96. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/__init__.py +0 -0
  97. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/ui/wire/README.md +0 -0
  98. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/__init__.py +0 -0
  99. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/rich/__init__.py +0 -0
  100. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/rich/markdown.py +0 -0
  101. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/rich/markdown_sample.md +0 -0
  102. {kimi_cli-0.52 → kimi_cli-0.56}/src/kimi_cli/utils/rich/markdown_sample_short.md +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kimi-cli
3
- Version: 0.52
3
+ Version: 0.56
4
4
  Summary: Kimi CLI is your next CLI agent.
5
5
  Requires-Dist: agent-client-protocol==0.6.3
6
6
  Requires-Dist: aiofiles==25.1.0
7
7
  Requires-Dist: aiohttp==3.13.2
8
8
  Requires-Dist: typer==0.20.0
9
- Requires-Dist: kosong==0.22.0
9
+ Requires-Dist: kosong==0.26.1
10
10
  Requires-Dist: loguru==0.7.3
11
11
  Requires-Dist: patch-ng==1.19.0
12
12
  Requires-Dist: prompt-toolkit==3.0.52
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kimi-cli"
3
- version = "0.52"
3
+ version = "0.56"
4
4
  description = "Kimi CLI is your next CLI agent."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -9,7 +9,7 @@ dependencies = [
9
9
  "aiofiles==25.1.0",
10
10
  "aiohttp==3.13.2",
11
11
  "typer==0.20.0",
12
- "kosong==0.22.0",
12
+ "kosong==0.26.1",
13
13
  "loguru==0.7.3",
14
14
  "patch-ng==1.19.0",
15
15
  "prompt-toolkit==3.0.52",
@@ -30,8 +30,8 @@ dev = [
30
30
  "inline-snapshot[black]>=0.31.1",
31
31
  "pyinstaller>=6.16.0",
32
32
  "pyright>=1.1.407",
33
- "pytest>=8.4.2",
34
- "pytest-asyncio>=1.2.0",
33
+ "pytest>=9.0.1",
34
+ "pytest-asyncio>=1.3.0",
35
35
  "ruff>=0.14.4",
36
36
  ]
37
37
 
@@ -40,7 +40,7 @@ requires = ["uv_build>=0.8.5,<0.9.0"]
40
40
  build-backend = "uv_build"
41
41
 
42
42
  [tool.uv.build-backend]
43
- module-name = ["kimi_cli"]
43
+ module-name = "kimi_cli"
44
44
  source-exclude = ["tests/**/*", "src/kimi_cli/deps/**/*"]
45
45
 
46
46
  [project.scripts]
@@ -9,6 +9,39 @@ Internal builds may append content to the Unreleased section.
9
9
  Only write entries that are worth mentioning to users.
10
10
  -->
11
11
 
12
+ ## [0.56] - 2025-11-19
13
+
14
+ - LLM: Add support for Google GenAI provider
15
+
16
+ ## [0.55] - 2025-11-18
17
+
18
+ - Lib: Add `kimi_cli.app.enable_logging` function to enable logging when directly using `KimiCLI` class
19
+ - Core: Fix relative path resolution in agent spec files
20
+ - Core: Prevent from panic when LLM API connection failed
21
+ - Tool: Optimize `FetchURL` tool for better content extraction
22
+ - Tool: Increase MCP tool call timeout to 60 seconds
23
+ - Tool: Provide better error message in `Glob` tool when pattern is `**`
24
+ - ACP: Fix thinking content not displayed properly
25
+ - UI: Minor UI improvements in shell mode
26
+
27
+ ## [0.54] - 2025-11-13
28
+
29
+ - Lib: Move `WireMessage` from `kimi_cli.wire.message` to `kimi_cli.wire`
30
+ - Print: Fix `stream-json` output format missing the last assistant message
31
+ - UI: Add warning when API key is overridden by `KIMI_API_KEY` environment variable
32
+ - UI: Make a bell sound when there's an approval request
33
+ - Core: Fix context compaction and clearing on Windows
34
+
35
+ ## [0.53] - 2025-11-12
36
+
37
+ - UI: Remove unnecessary trailing spaces in console output
38
+ - Core: Throw error when there are unsupported message parts
39
+ - MetaCmd: Add `/yolo` meta command to enable YOLO mode after startup
40
+ - Tool: Add approval request for MCP tools
41
+ - Tool: Disable `Think` tool in default agent
42
+ - CLI: Restore thinking mode from last time when `--thinking` is not specified
43
+ - CLI: Fix `/reload` not working in binary packed by PyInstaller
44
+
12
45
  ## [0.52] - 2025-11-10
13
46
 
14
47
  - CLI: Remove `--ui` option in favor of `--print`, `--acp`, and `--wire` flags (shell is still the default)
@@ -7,7 +7,7 @@ agent:
7
7
  tools:
8
8
  - "kimi_cli.tools.task:Task"
9
9
  # - "kimi_cli.tools.dmail:SendDMail"
10
- - "kimi_cli.tools.think:Think"
10
+ # - "kimi_cli.tools.think:Think"
11
11
  - "kimi_cli.tools.todo:SetTodoList"
12
12
  - "kimi_cli.tools.bash:Bash"
13
13
  - "kimi_cli.tools.file:ReadFile"
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
1
4
  from pathlib import Path
2
- from typing import Any, NamedTuple
5
+ from typing import Any
3
6
 
4
7
  import yaml
5
8
  from pydantic import BaseModel, Field
@@ -27,7 +30,7 @@ class AgentSpec(BaseModel):
27
30
  )
28
31
  tools: list[str] | None = Field(default=None, description="Tools") # required
29
32
  exclude_tools: list[str] | None = Field(default=None, description="Tools to exclude")
30
- subagents: dict[str, "SubagentSpec"] | None = Field(default=None, description="Subagents")
33
+ subagents: dict[str, SubagentSpec] | None = Field(default=None, description="Subagents")
31
34
 
32
35
 
33
36
  class SubagentSpec(BaseModel):
@@ -37,7 +40,8 @@ class SubagentSpec(BaseModel):
37
40
  description: str = Field(description="Subagent description")
38
41
 
39
42
 
40
- class ResolvedAgentSpec(NamedTuple):
43
+ @dataclass(frozen=True, slots=True, kw_only=True)
44
+ class ResolvedAgentSpec:
41
45
  """Resolved agent specification."""
42
46
 
43
47
  name: str
@@ -45,7 +49,7 @@ class ResolvedAgentSpec(NamedTuple):
45
49
  system_prompt_args: dict[str, str]
46
50
  tools: list[str]
47
51
  exclude_tools: list[str]
48
- subagents: dict[str, "SubagentSpec"]
52
+ subagents: dict[str, SubagentSpec]
49
53
 
50
54
 
51
55
  def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
@@ -75,7 +79,10 @@ def load_agent_spec(agent_file: Path) -> ResolvedAgentSpec:
75
79
 
76
80
 
77
81
  def _load_agent_spec(agent_file: Path) -> AgentSpec:
78
- assert agent_file.is_file(), "expect agent file to exist"
82
+ if not agent_file.exists():
83
+ raise AgentSpecError(f"Agent spec file not found: {agent_file}")
84
+ if not agent_file.is_file():
85
+ raise AgentSpecError(f"Agent spec path is not a file: {agent_file}")
79
86
  try:
80
87
  with open(agent_file, encoding="utf-8") as f:
81
88
  data: dict[str, Any] = yaml.safe_load(f)
@@ -88,15 +95,17 @@ def _load_agent_spec(agent_file: Path) -> AgentSpec:
88
95
 
89
96
  agent_spec = AgentSpec(**data.get("agent", {}))
90
97
  if agent_spec.system_prompt_path is not None:
91
- agent_spec.system_prompt_path = agent_file.parent / agent_spec.system_prompt_path
98
+ agent_spec.system_prompt_path = (
99
+ agent_file.parent / agent_spec.system_prompt_path
100
+ ).absolute()
92
101
  if agent_spec.subagents is not None:
93
102
  for v in agent_spec.subagents.values():
94
- v.path = agent_file.parent / v.path
103
+ v.path = (agent_file.parent / v.path).absolute()
95
104
  if agent_spec.extend:
96
105
  if agent_spec.extend == "default":
97
106
  base_agent_file = DEFAULT_AGENT_FILE
98
107
  else:
99
- base_agent_file = agent_file.parent / agent_spec.extend
108
+ base_agent_file = (agent_file.parent / agent_spec.extend).absolute()
100
109
  base_agent_spec = _load_agent_spec(base_agent_file)
101
110
  if agent_spec.name is not None:
102
111
  base_agent_spec.name = agent_spec.name
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import contextlib
2
4
  import os
3
5
  import warnings
@@ -12,12 +14,26 @@ from kimi_cli.cli import InputFormat, OutputFormat
12
14
  from kimi_cli.config import LLMModel, LLMProvider, load_config
13
15
  from kimi_cli.llm import augment_provider_with_env_vars, create_llm
14
16
  from kimi_cli.session import Session
17
+ from kimi_cli.share import get_share_dir
15
18
  from kimi_cli.soul import LLMNotSet, LLMNotSupported
16
19
  from kimi_cli.soul.agent import load_agent
17
20
  from kimi_cli.soul.context import Context
18
21
  from kimi_cli.soul.kimisoul import KimiSoul
19
22
  from kimi_cli.soul.runtime import Runtime
20
23
  from kimi_cli.utils.logging import StreamToLogger, logger
24
+ from kimi_cli.utils.path import shorten_home
25
+
26
+
27
+ def enable_logging(debug: bool = False) -> None:
28
+ if debug:
29
+ logger.enable("kosong")
30
+ logger.add(
31
+ get_share_dir() / "logs" / "kimi.log",
32
+ # FIXME: configure level for different modules
33
+ level="TRACE" if debug else "INFO",
34
+ rotation="06:00",
35
+ retention="10 days",
36
+ )
21
37
 
22
38
 
23
39
  class KimiCLI:
@@ -26,20 +42,18 @@ class KimiCLI:
26
42
  session: Session,
27
43
  *,
28
44
  yolo: bool = False,
29
- stream: bool = True, # TODO: remove this when we have a correct print mode impl
30
45
  mcp_configs: list[dict[str, Any]] | None = None,
31
46
  config_file: Path | None = None,
32
47
  model_name: str | None = None,
33
48
  thinking: bool = False,
34
49
  agent_file: Path | None = None,
35
- ) -> "KimiCLI":
50
+ ) -> KimiCLI:
36
51
  """
37
52
  Create a KimiCLI instance.
38
53
 
39
54
  Args:
40
55
  session (Session): A session created by `Session.create` or `Session.continue_`.
41
56
  yolo (bool, optional): Approve all actions without confirmation. Defaults to False.
42
- stream (bool, optional): Use stream mode when calling LLM API. Defaults to True.
43
57
  config_file (Path | None, optional): Path to the configuration file. Defaults to None.
44
58
  model_name (str | None, optional): Name of the model to use. Defaults to None.
45
59
  agent_file (Path | None, optional): Path to the agent file. Defaults to None.
@@ -79,7 +93,7 @@ class KimiCLI:
79
93
  else:
80
94
  logger.info("Using LLM provider: {provider}", provider=provider)
81
95
  logger.info("Using LLM model: {model}", model=model)
82
- llm = create_llm(provider, model, stream=stream, session_id=session.id)
96
+ llm = create_llm(provider, model, session_id=session.id)
83
97
 
84
98
  runtime = await Runtime.create(config, llm, session, yolo)
85
99
 
@@ -137,7 +151,9 @@ class KimiCLI:
137
151
  from kimi_cli.ui.shell import ShellApp, WelcomeInfoItem
138
152
 
139
153
  welcome_info = [
140
- WelcomeInfoItem(name="Directory", value=str(self._runtime.session.work_dir)),
154
+ WelcomeInfoItem(
155
+ name="Directory", value=str(shorten_home(self._runtime.session.work_dir))
156
+ ),
141
157
  WelcomeInfoItem(name="Session", value=self._runtime.session.id),
142
158
  ]
143
159
  if base_url := self._env_overrides.get("KIMI_BASE_URL"):
@@ -148,6 +164,14 @@ class KimiCLI:
148
164
  level=WelcomeInfoItem.Level.WARN,
149
165
  )
150
166
  )
167
+ if self._env_overrides.get("KIMI_API_KEY"):
168
+ welcome_info.append(
169
+ WelcomeInfoItem(
170
+ name="API Key",
171
+ value="****** (from KIMI_API_KEY)",
172
+ level=WelcomeInfoItem.Level.WARN,
173
+ )
174
+ )
151
175
  if not self._runtime.llm:
152
176
  welcome_info.append(
153
177
  WelcomeInfoItem(
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import json
3
5
  import sys
@@ -185,19 +187,18 @@ def kimi(
185
187
  ),
186
188
  ] = False,
187
189
  thinking: Annotated[
188
- bool,
190
+ bool | None,
189
191
  typer.Option(
190
192
  "--thinking",
191
- help="Enable thinking mode if supported. Default: no.",
193
+ help="Enable thinking mode if supported. Default: same as last time.",
192
194
  ),
193
- ] = False,
195
+ ] = None,
194
196
  ):
195
197
  """Kimi, your next CLI agent."""
196
198
  del version # handled in the callback
197
199
 
198
- from kimi_cli.app import KimiCLI
200
+ from kimi_cli.app import KimiCLI, enable_logging
199
201
  from kimi_cli.session import Session
200
- from kimi_cli.share import get_share_dir
201
202
  from kimi_cli.utils.logging import logger
202
203
 
203
204
  def _noop_echo(*args: Any, **kwargs: Any):
@@ -224,16 +225,7 @@ def kimi(
224
225
  ui = "wire"
225
226
 
226
227
  echo: Callable[..., None] = typer.echo if verbose else _noop_echo
227
-
228
- if debug:
229
- logger.enable("kosong")
230
- logger.add(
231
- get_share_dir() / "logs" / "kimi.log",
232
- # FIXME: configure level for different modules
233
- level="TRACE" if debug else "INFO",
234
- rotation="06:00",
235
- retention="10 days",
236
- )
228
+ enable_logging(debug)
237
229
 
238
230
  work_dir = (work_dir or Path.cwd()).absolute()
239
231
  if continue_:
@@ -279,20 +271,27 @@ def kimi(
279
271
  raise typer.BadParameter(f"Invalid JSON: {e}", param_hint="--mcp-config") from e
280
272
 
281
273
  async def _run() -> bool:
274
+ from kimi_cli.metadata import WorkDirMeta, load_metadata, save_metadata
275
+
276
+ if thinking is None:
277
+ metadata = load_metadata()
278
+ thinking_mode = metadata.thinking
279
+ else:
280
+ thinking_mode = thinking
281
+
282
282
  instance = await KimiCLI.create(
283
283
  session,
284
284
  yolo=yolo or (ui == "print"), # print mode implies yolo
285
- stream=ui != "print", # use non-streaming mode only for print UI
286
285
  mcp_configs=mcp_configs,
287
286
  model_name=model_name,
288
- thinking=thinking,
287
+ thinking=thinking_mode,
289
288
  agent_file=agent_file,
290
289
  )
291
290
  match ui:
292
291
  case "shell":
293
- return await instance.run_shell_mode(command)
292
+ succeeded = await instance.run_shell_mode(command)
294
293
  case "print":
295
- return await instance.run_print_mode(
294
+ succeeded = await instance.run_print_mode(
296
295
  input_format or "text",
297
296
  output_format or "text",
298
297
  command,
@@ -300,17 +299,41 @@ def kimi(
300
299
  case "acp":
301
300
  if command is not None:
302
301
  logger.warning("ACP server ignores command argument")
303
- return await instance.run_acp_server()
302
+ succeeded = await instance.run_acp_server()
304
303
  case "wire":
305
304
  if command is not None:
306
305
  logger.warning("Wire server ignores command argument")
307
- return await instance.run_wire_server()
306
+ succeeded = await instance.run_wire_server()
307
+
308
+ if succeeded:
309
+ metadata = load_metadata()
310
+
311
+ # Update work_dir metadata with last session
312
+ work_dir_meta = next(
313
+ (wd for wd in metadata.work_dirs if wd.path == str(session.work_dir)), None
314
+ )
315
+
316
+ if work_dir_meta is None:
317
+ logger.warning(
318
+ "Work dir metadata missing when marking last session, recreating: {work_dir}",
319
+ work_dir=session.work_dir,
320
+ )
321
+ work_dir_meta = WorkDirMeta(path=str(session.work_dir))
322
+ metadata.work_dirs.append(work_dir_meta)
323
+
324
+ work_dir_meta.last_session_id = session.id
325
+
326
+ # Update thinking mode
327
+ metadata.thinking = instance.soul.thinking
328
+
329
+ save_metadata(metadata)
330
+
331
+ return succeeded
308
332
 
309
333
  while True:
310
334
  try:
311
335
  succeeded = asyncio.run(_run())
312
336
  if succeeded:
313
- session.mark_as_last()
314
337
  break
315
338
  sys.exit(1)
316
339
  except Reload:
@@ -318,4 +341,7 @@ def kimi(
318
341
 
319
342
 
320
343
  if __name__ == "__main__":
321
- cli()
344
+ if "kimi_cli.cli" not in sys.modules:
345
+ sys.modules["kimi_cli.cli"] = sys.modules[__name__]
346
+
347
+ sys.exit(cli())
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  from pathlib import Path
3
5
  from typing import Self
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import importlib.metadata
2
4
 
3
5
  VERSION = importlib.metadata.version("kimi-cli")
@@ -1,3 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+
1
4
  class KimiCLIException(Exception):
2
5
  """Base exception class for Kimi CLI."""
3
6
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  from dataclasses import dataclass
3
5
  from typing import TYPE_CHECKING, Literal, cast, get_args
@@ -10,7 +12,14 @@ from kimi_cli.constant import USER_AGENT
10
12
  if TYPE_CHECKING:
11
13
  from kimi_cli.config import LLMModel, LLMProvider
12
14
 
13
- type ProviderType = Literal["kimi", "openai_legacy", "openai_responses", "anthropic", "_chaos"]
15
+ type ProviderType = Literal[
16
+ "kimi",
17
+ "openai_legacy",
18
+ "openai_responses",
19
+ "anthropic",
20
+ "google_genai",
21
+ "_chaos",
22
+ ]
14
23
 
15
24
  type ModelCapability = Literal["image_in", "thinking"]
16
25
  ALL_MODEL_CAPABILITIES: set[ModelCapability] = set(get_args(ModelCapability))
@@ -27,7 +36,7 @@ class LLM:
27
36
  return self.chat_provider.model_name
28
37
 
29
38
 
30
- def augment_provider_with_env_vars(provider: "LLMProvider", model: "LLMModel") -> dict[str, str]:
39
+ def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel) -> dict[str, str]:
31
40
  """Override provider/model settings from environment variables.
32
41
 
33
42
  Returns:
@@ -69,10 +78,9 @@ def augment_provider_with_env_vars(provider: "LLMProvider", model: "LLMModel") -
69
78
 
70
79
 
71
80
  def create_llm(
72
- provider: "LLMProvider",
73
- model: "LLMModel",
81
+ provider: LLMProvider,
82
+ model: LLMModel,
74
83
  *,
75
- stream: bool = True,
76
84
  session_id: str | None = None,
77
85
  ) -> LLM:
78
86
  match provider.type:
@@ -83,7 +91,6 @@ def create_llm(
83
91
  model=model.model,
84
92
  base_url=provider.base_url,
85
93
  api_key=provider.api_key.get_secret_value(),
86
- stream=stream,
87
94
  default_headers={
88
95
  "User-Agent": USER_AGENT,
89
96
  **(provider.custom_headers or {}),
@@ -98,7 +105,6 @@ def create_llm(
98
105
  model=model.model,
99
106
  base_url=provider.base_url,
100
107
  api_key=provider.api_key.get_secret_value(),
101
- stream=stream,
102
108
  )
103
109
  case "openai_responses":
104
110
  from kosong.contrib.chat_provider.openai_responses import OpenAIResponses
@@ -107,7 +113,6 @@ def create_llm(
107
113
  model=model.model,
108
114
  base_url=provider.base_url,
109
115
  api_key=provider.api_key.get_secret_value(),
110
- stream=stream,
111
116
  )
112
117
  case "anthropic":
113
118
  from kosong.contrib.chat_provider.anthropic import Anthropic
@@ -116,9 +121,16 @@ def create_llm(
116
121
  model=model.model,
117
122
  base_url=provider.base_url,
118
123
  api_key=provider.api_key.get_secret_value(),
119
- stream=stream,
120
124
  default_max_tokens=50000,
121
125
  )
126
+ case "google_genai":
127
+ from kosong.contrib.chat_provider.google_genai import GoogleGenAI
128
+
129
+ chat_provider = GoogleGenAI(
130
+ model=model.model,
131
+ base_url=provider.base_url,
132
+ api_key=provider.api_key.get_secret_value(),
133
+ )
122
134
  case "_chaos":
123
135
  from kosong.chat_provider.chaos import ChaosChatProvider, ChaosConfig
124
136
 
@@ -139,7 +151,7 @@ def create_llm(
139
151
  )
140
152
 
141
153
 
142
- def _derive_capabilities(provider: "LLMProvider", model: "LLMModel") -> set[ModelCapability]:
154
+ def _derive_capabilities(provider: LLMProvider, model: LLMModel) -> set[ModelCapability]:
143
155
  capabilities = model.capabilities or set()
144
156
  if provider.type != "kimi":
145
157
  return capabilities
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
2
4
  from hashlib import md5
3
5
  from pathlib import Path
@@ -31,9 +33,11 @@ class WorkDirMeta(BaseModel):
31
33
  class Metadata(BaseModel):
32
34
  """Kimi metadata structure."""
33
35
 
34
- work_dirs: list[WorkDirMeta] = Field(
35
- default_factory=list[WorkDirMeta], description="Work directory list"
36
- )
36
+ work_dirs: list[WorkDirMeta] = Field(default_factory=list[WorkDirMeta])
37
+ """Work directory list."""
38
+
39
+ thinking: bool = False
40
+ """Whether the last session was in thinking mode."""
37
41
 
38
42
 
39
43
  def load_metadata() -> Metadata:
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
 
3
5
  INIT = (Path(__file__).parent / "init.md").read_text(encoding="utf-8")
@@ -1,12 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  import uuid
4
+ from dataclasses import dataclass
2
5
  from pathlib import Path
3
- from typing import NamedTuple
4
6
 
5
7
  from kimi_cli.metadata import WorkDirMeta, load_metadata, save_metadata
6
8
  from kimi_cli.utils.logging import logger
7
9
 
8
10
 
9
- class Session(NamedTuple):
11
+ @dataclass(frozen=True, slots=True, kw_only=True)
12
+ class Session:
10
13
  """A session of a work directory."""
11
14
 
12
15
  id: str
@@ -14,7 +17,7 @@ class Session(NamedTuple):
14
17
  history_file: Path
15
18
 
16
19
  @staticmethod
17
- def create(work_dir: Path, _history_file: Path | None = None) -> "Session":
20
+ def create(work_dir: Path, _history_file: Path | None = None) -> Session:
18
21
  """Create a new session for a work directory."""
19
22
  logger.debug("Creating new session for work directory: {work_dir}", work_dir=work_dir)
20
23
 
@@ -53,7 +56,7 @@ class Session(NamedTuple):
53
56
  )
54
57
 
55
58
  @staticmethod
56
- def continue_(work_dir: Path) -> "Session | None":
59
+ def continue_(work_dir: Path) -> Session | None:
57
60
  """Get the last session for a work directory."""
58
61
  logger.debug("Continuing session for work directory: {work_dir}", work_dir=work_dir)
59
62
 
@@ -78,26 +81,3 @@ class Session(NamedTuple):
78
81
  work_dir=work_dir,
79
82
  history_file=history_file,
80
83
  )
81
-
82
- def mark_as_last(self) -> None:
83
- """Mark this session as the last completed session for its work directory."""
84
- metadata = load_metadata()
85
- work_dir_meta = next(
86
- (wd for wd in metadata.work_dirs if wd.path == str(self.work_dir)), None
87
- )
88
-
89
- if work_dir_meta is None:
90
- logger.warning(
91
- "Work directory metadata missing when marking last session, recreating: {work_dir}",
92
- work_dir=self.work_dir,
93
- )
94
- work_dir_meta = WorkDirMeta(path=str(self.work_dir))
95
- metadata.work_dirs.append(work_dir_meta)
96
-
97
- work_dir_meta.last_session_id = self.id
98
- logger.debug(
99
- "Updated last session for work directory: {work_dir} -> {session_id}",
100
- work_dir=self.work_dir,
101
- session_id=self.id,
102
- )
103
- save_metadata(metadata)
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from pathlib import Path
2
4
 
3
5
 
@@ -1,14 +1,16 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import contextlib
3
5
  from collections.abc import Callable, Coroutine
4
6
  from contextvars import ContextVar
5
- from typing import TYPE_CHECKING, Any, NamedTuple, Protocol, runtime_checkable
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
6
9
 
7
10
  from kosong.message import ContentPart
8
11
 
9
12
  from kimi_cli.utils.logging import logger
10
- from kimi_cli.wire import Wire, WireUISide
11
- from kimi_cli.wire.message import WireMessage
13
+ from kimi_cli.wire import Wire, WireMessage, WireUISide
12
14
 
13
15
  if TYPE_CHECKING:
14
16
  from kimi_cli.llm import LLM, ModelCapability
@@ -23,7 +25,7 @@ class LLMNotSet(Exception):
23
25
  class LLMNotSupported(Exception):
24
26
  """Raised when the LLM does not have required capabilities."""
25
27
 
26
- def __init__(self, llm: "LLM", capabilities: "list[ModelCapability]"):
28
+ def __init__(self, llm: LLM, capabilities: list[ModelCapability]):
27
29
  self.llm = llm
28
30
  self.capabilities = capabilities
29
31
  capabilities_str = "capability" if len(capabilities) == 1 else "capabilities"
@@ -43,7 +45,8 @@ class MaxStepsReached(Exception):
43
45
  self.n_steps = n_steps
44
46
 
45
47
 
46
- class StatusSnapshot(NamedTuple):
48
+ @dataclass(frozen=True, slots=True)
49
+ class StatusSnapshot:
47
50
  context_usage: float
48
51
  """The usage of the context, in percentage."""
49
52
 
@@ -61,7 +64,7 @@ class Soul(Protocol):
61
64
  ...
62
65
 
63
66
  @property
64
- def model_capabilities(self) -> "set[ModelCapability] | None":
67
+ def model_capabilities(self) -> set[ModelCapability] | None:
65
68
  """The capabilities of the LLM model used by the soul. None indicates no LLM configured."""
66
69
  ...
67
70
 
@@ -96,7 +99,7 @@ class RunCancelled(Exception):
96
99
 
97
100
 
98
101
  async def run_soul(
99
- soul: "Soul",
102
+ soul: Soul,
100
103
  user_input: str | list[ContentPart],
101
104
  ui_loop_fn: UILoopFn,
102
105
  cancel_event: asyncio.Event,