kimi-cli 0.40__py3-none-any.whl → 0.42__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kimi-cli might be problematic. Click here for more details.
- kimi_cli/CHANGELOG.md +27 -0
- kimi_cli/__init__.py +127 -359
- kimi_cli/agents/{koder → default}/agent.yaml +1 -1
- kimi_cli/agents/{koder → default}/system.md +1 -1
- kimi_cli/agentspec.py +115 -0
- kimi_cli/cli.py +249 -0
- kimi_cli/config.py +28 -14
- kimi_cli/constant.py +4 -0
- kimi_cli/exception.py +16 -0
- kimi_cli/llm.py +70 -0
- kimi_cli/metadata.py +5 -68
- kimi_cli/prompts/__init__.py +2 -2
- kimi_cli/session.py +81 -0
- kimi_cli/soul/__init__.py +102 -6
- kimi_cli/soul/agent.py +152 -0
- kimi_cli/soul/approval.py +1 -1
- kimi_cli/soul/compaction.py +4 -4
- kimi_cli/soul/kimisoul.py +39 -46
- kimi_cli/soul/runtime.py +94 -0
- kimi_cli/tools/dmail/__init__.py +1 -1
- kimi_cli/tools/file/glob.md +1 -1
- kimi_cli/tools/file/glob.py +2 -2
- kimi_cli/tools/file/grep.py +1 -1
- kimi_cli/tools/file/patch.py +2 -2
- kimi_cli/tools/file/read.py +1 -1
- kimi_cli/tools/file/replace.py +2 -2
- kimi_cli/tools/file/write.py +2 -2
- kimi_cli/tools/task/__init__.py +48 -40
- kimi_cli/tools/task/task.md +1 -1
- kimi_cli/tools/todo/__init__.py +1 -1
- kimi_cli/tools/utils.py +1 -1
- kimi_cli/tools/web/search.py +5 -2
- kimi_cli/ui/__init__.py +0 -69
- kimi_cli/ui/acp/__init__.py +8 -9
- kimi_cli/ui/print/__init__.py +21 -37
- kimi_cli/ui/shell/__init__.py +8 -19
- kimi_cli/ui/shell/liveview.py +1 -1
- kimi_cli/ui/shell/metacmd.py +5 -10
- kimi_cli/ui/shell/prompt.py +10 -3
- kimi_cli/ui/shell/setup.py +5 -5
- kimi_cli/ui/shell/update.py +2 -2
- kimi_cli/ui/shell/visualize.py +10 -7
- kimi_cli/utils/changelog.py +3 -1
- kimi_cli/wire/__init__.py +69 -0
- kimi_cli/{soul/wire.py → wire/message.py} +4 -39
- {kimi_cli-0.40.dist-info → kimi_cli-0.42.dist-info}/METADATA +51 -18
- kimi_cli-0.42.dist-info/RECORD +86 -0
- kimi_cli-0.42.dist-info/entry_points.txt +3 -0
- kimi_cli/agent.py +0 -261
- kimi_cli/agents/koder/README.md +0 -3
- kimi_cli/utils/provider.py +0 -70
- kimi_cli-0.40.dist-info/RECORD +0 -81
- kimi_cli-0.40.dist-info/entry_points.txt +0 -3
- /kimi_cli/agents/{koder → default}/sub.yaml +0 -0
- {kimi_cli-0.40.dist-info → kimi_cli-0.42.dist-info}/WHEEL +0 -0
kimi_cli/cli.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal, get_args
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from kimi_cli import KimiCLI
|
|
11
|
+
from kimi_cli.constant import VERSION
|
|
12
|
+
from kimi_cli.session import Session
|
|
13
|
+
from kimi_cli.share import get_share_dir
|
|
14
|
+
from kimi_cli.ui.print import InputFormat, OutputFormat
|
|
15
|
+
from kimi_cli.utils.logging import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Reload(Exception):
|
|
19
|
+
"""Reload configuration."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
UIMode = Literal["shell", "print", "acp"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.command(context_settings=dict(help_option_names=["-h", "--help"]))
|
|
28
|
+
@click.version_option(VERSION)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--verbose",
|
|
31
|
+
is_flag=True,
|
|
32
|
+
default=False,
|
|
33
|
+
help="Print verbose information. Default: no.",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--debug",
|
|
37
|
+
is_flag=True,
|
|
38
|
+
default=False,
|
|
39
|
+
help="Log debug information. Default: no.",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--agent-file",
|
|
43
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
|
|
44
|
+
default=None,
|
|
45
|
+
help="Custom agent specification file. Default: builtin default agent.",
|
|
46
|
+
)
|
|
47
|
+
@click.option(
|
|
48
|
+
"--model",
|
|
49
|
+
"-m",
|
|
50
|
+
"model_name",
|
|
51
|
+
type=str,
|
|
52
|
+
default=None,
|
|
53
|
+
help="LLM model to use. Default: default model set in config file.",
|
|
54
|
+
)
|
|
55
|
+
@click.option(
|
|
56
|
+
"--work-dir",
|
|
57
|
+
"-w",
|
|
58
|
+
type=click.Path(exists=True, file_okay=False, dir_okay=True, path_type=Path),
|
|
59
|
+
default=Path.cwd(),
|
|
60
|
+
help="Working directory for the agent. Default: current directory.",
|
|
61
|
+
)
|
|
62
|
+
@click.option(
|
|
63
|
+
"--continue",
|
|
64
|
+
"-C",
|
|
65
|
+
"continue_",
|
|
66
|
+
is_flag=True,
|
|
67
|
+
default=False,
|
|
68
|
+
help="Continue the previous session for the working directory. Default: no.",
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--command",
|
|
72
|
+
"-c",
|
|
73
|
+
"--query",
|
|
74
|
+
"-q",
|
|
75
|
+
"command",
|
|
76
|
+
type=str,
|
|
77
|
+
default=None,
|
|
78
|
+
help="User query to the agent. Default: prompt interactively.",
|
|
79
|
+
)
|
|
80
|
+
@click.option(
|
|
81
|
+
"--ui",
|
|
82
|
+
"ui",
|
|
83
|
+
type=click.Choice(get_args(UIMode)),
|
|
84
|
+
default="shell",
|
|
85
|
+
help="UI mode to use. Default: shell.",
|
|
86
|
+
)
|
|
87
|
+
@click.option(
|
|
88
|
+
"--print",
|
|
89
|
+
"ui",
|
|
90
|
+
flag_value="print",
|
|
91
|
+
help="Run in print mode. Shortcut for `--ui print`. Note: print mode implicitly adds `--yolo`.",
|
|
92
|
+
)
|
|
93
|
+
@click.option(
|
|
94
|
+
"--acp",
|
|
95
|
+
"ui",
|
|
96
|
+
flag_value="acp",
|
|
97
|
+
help="Start ACP server. Shortcut for `--ui acp`.",
|
|
98
|
+
)
|
|
99
|
+
@click.option(
|
|
100
|
+
"--input-format",
|
|
101
|
+
type=click.Choice(get_args(InputFormat)),
|
|
102
|
+
default=None,
|
|
103
|
+
help=(
|
|
104
|
+
"Input format to use. Must be used with `--print` "
|
|
105
|
+
"and the input must be piped in via stdin. "
|
|
106
|
+
"Default: text."
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--output-format",
|
|
111
|
+
type=click.Choice(get_args(OutputFormat)),
|
|
112
|
+
default=None,
|
|
113
|
+
help="Output format to use. Must be used with `--print`. Default: text.",
|
|
114
|
+
)
|
|
115
|
+
@click.option(
|
|
116
|
+
"--mcp-config-file",
|
|
117
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path),
|
|
118
|
+
multiple=True,
|
|
119
|
+
help=(
|
|
120
|
+
"MCP config file to load. Add this option multiple times to specify multiple MCP configs. "
|
|
121
|
+
"Default: none."
|
|
122
|
+
),
|
|
123
|
+
)
|
|
124
|
+
@click.option(
|
|
125
|
+
"--mcp-config",
|
|
126
|
+
type=str,
|
|
127
|
+
multiple=True,
|
|
128
|
+
help=(
|
|
129
|
+
"MCP config JSON to load. Add this option multiple times to specify multiple MCP configs. "
|
|
130
|
+
"Default: none."
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
@click.option(
|
|
134
|
+
"--yolo",
|
|
135
|
+
"--yes",
|
|
136
|
+
"-y",
|
|
137
|
+
"--auto-approve",
|
|
138
|
+
"yolo",
|
|
139
|
+
is_flag=True,
|
|
140
|
+
default=False,
|
|
141
|
+
help="Automatically approve all actions. Default: no.",
|
|
142
|
+
)
|
|
143
|
+
def kimi(
|
|
144
|
+
verbose: bool,
|
|
145
|
+
debug: bool,
|
|
146
|
+
agent_file: Path | None,
|
|
147
|
+
model_name: str | None,
|
|
148
|
+
work_dir: Path,
|
|
149
|
+
continue_: bool,
|
|
150
|
+
command: str | None,
|
|
151
|
+
ui: UIMode,
|
|
152
|
+
input_format: InputFormat | None,
|
|
153
|
+
output_format: OutputFormat | None,
|
|
154
|
+
mcp_config_file: list[Path],
|
|
155
|
+
mcp_config: list[str],
|
|
156
|
+
yolo: bool,
|
|
157
|
+
):
|
|
158
|
+
"""Kimi, your next CLI agent."""
|
|
159
|
+
|
|
160
|
+
def _noop_echo(*args: Any, **kwargs: Any):
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
echo: Callable[..., None] = click.echo if verbose else _noop_echo
|
|
164
|
+
|
|
165
|
+
logger.add(
|
|
166
|
+
get_share_dir() / "logs" / "kimi.log",
|
|
167
|
+
level="DEBUG" if debug else "INFO",
|
|
168
|
+
rotation="06:00",
|
|
169
|
+
retention="10 days",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
work_dir = work_dir.absolute()
|
|
173
|
+
if continue_:
|
|
174
|
+
session = Session.continue_(work_dir)
|
|
175
|
+
if session is None:
|
|
176
|
+
raise click.BadOptionUsage(
|
|
177
|
+
"--continue", "No previous session found for the working directory"
|
|
178
|
+
)
|
|
179
|
+
echo(f"✓ Continuing previous session: {session.id}")
|
|
180
|
+
else:
|
|
181
|
+
session = Session.create(work_dir)
|
|
182
|
+
echo(f"✓ Created new session: {session.id}")
|
|
183
|
+
echo(f"✓ Session history file: {session.history_file}")
|
|
184
|
+
|
|
185
|
+
if command is not None:
|
|
186
|
+
command = command.strip()
|
|
187
|
+
if not command:
|
|
188
|
+
raise click.BadOptionUsage("--command", "Command cannot be empty")
|
|
189
|
+
|
|
190
|
+
if input_format is not None and ui != "print":
|
|
191
|
+
raise click.BadOptionUsage(
|
|
192
|
+
"--input-format",
|
|
193
|
+
"Input format is only supported for print UI",
|
|
194
|
+
)
|
|
195
|
+
if output_format is not None and ui != "print":
|
|
196
|
+
raise click.BadOptionUsage(
|
|
197
|
+
"--output-format",
|
|
198
|
+
"Output format is only supported for print UI",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
mcp_configs = [json.loads(conf.read_text(encoding="utf-8")) for conf in mcp_config_file]
|
|
203
|
+
except json.JSONDecodeError as e:
|
|
204
|
+
raise click.BadOptionUsage("--mcp-config-file", f"Invalid JSON: {e}") from e
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
mcp_configs += [json.loads(conf) for conf in mcp_config]
|
|
208
|
+
except json.JSONDecodeError as e:
|
|
209
|
+
raise click.BadOptionUsage("--mcp-config", f"Invalid JSON: {e}") from e
|
|
210
|
+
|
|
211
|
+
async def _run() -> bool:
|
|
212
|
+
instance = await KimiCLI.create(
|
|
213
|
+
session,
|
|
214
|
+
yolo=yolo or (ui == "print"), # print mode implies yolo
|
|
215
|
+
stream=ui != "print", # use non-streaming mode only for print UI
|
|
216
|
+
mcp_configs=mcp_configs,
|
|
217
|
+
model_name=model_name,
|
|
218
|
+
agent_file=agent_file,
|
|
219
|
+
)
|
|
220
|
+
match ui:
|
|
221
|
+
case "shell":
|
|
222
|
+
return await instance.run_shell_mode(command)
|
|
223
|
+
case "print":
|
|
224
|
+
return await instance.run_print_mode(
|
|
225
|
+
input_format or "text",
|
|
226
|
+
output_format or "text",
|
|
227
|
+
command,
|
|
228
|
+
)
|
|
229
|
+
case "acp":
|
|
230
|
+
if command is not None:
|
|
231
|
+
logger.warning("ACP server ignores command argument")
|
|
232
|
+
return await instance.run_acp_server()
|
|
233
|
+
|
|
234
|
+
while True:
|
|
235
|
+
try:
|
|
236
|
+
succeeded = asyncio.run(_run())
|
|
237
|
+
if not succeeded:
|
|
238
|
+
sys.exit(1)
|
|
239
|
+
break
|
|
240
|
+
except Reload:
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main():
|
|
245
|
+
kimi()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
main()
|
kimi_cli/config.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import Literal, Self
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field, SecretStr, ValidationError, field_serializer, model_validator
|
|
6
6
|
|
|
7
|
+
from kimi_cli.exception import ConfigError
|
|
7
8
|
from kimi_cli.share import get_share_dir
|
|
8
9
|
from kimi_cli.utils.logging import logger
|
|
9
10
|
|
|
@@ -17,6 +18,8 @@ class LLMProvider(BaseModel):
|
|
|
17
18
|
"""API base URL"""
|
|
18
19
|
api_key: SecretStr
|
|
19
20
|
"""API key"""
|
|
21
|
+
custom_headers: dict[str, str] = Field(default_factory=dict)
|
|
22
|
+
"""Custom headers to include in API requests"""
|
|
20
23
|
|
|
21
24
|
@field_serializer("api_key", when_used="json")
|
|
22
25
|
def dump_secret(self, v: SecretStr):
|
|
@@ -50,6 +53,8 @@ class MoonshotSearchConfig(BaseModel):
|
|
|
50
53
|
"""Base URL for Moonshot Search service."""
|
|
51
54
|
api_key: SecretStr
|
|
52
55
|
"""API key for Moonshot Search service."""
|
|
56
|
+
custom_headers: dict[str, str] = Field(default_factory=dict)
|
|
57
|
+
"""Custom headers to include in API requests."""
|
|
53
58
|
|
|
54
59
|
@field_serializer("api_key", when_used="json")
|
|
55
60
|
def dump_secret(self, v: SecretStr):
|
|
@@ -99,13 +104,21 @@ def get_default_config() -> Config:
|
|
|
99
104
|
)
|
|
100
105
|
|
|
101
106
|
|
|
102
|
-
def load_config() -> Config:
|
|
103
|
-
"""
|
|
107
|
+
def load_config(config_file: Path | None = None) -> Config:
|
|
108
|
+
"""
|
|
109
|
+
Load configuration from config file.
|
|
110
|
+
If the config file does not exist, create it with default configuration.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
config_file (Path | None): Path to the configuration file. If None, use default path.
|
|
104
114
|
|
|
105
115
|
Returns:
|
|
106
116
|
Validated Config object.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ConfigError: If the configuration file is invalid.
|
|
107
120
|
"""
|
|
108
|
-
config_file = get_config_file()
|
|
121
|
+
config_file = config_file or get_config_file()
|
|
109
122
|
logger.debug("Loading config from file: {file}", file=config_file)
|
|
110
123
|
|
|
111
124
|
if not config_file.exists():
|
|
@@ -119,20 +132,21 @@ def load_config() -> Config:
|
|
|
119
132
|
with open(config_file, encoding="utf-8") as f:
|
|
120
133
|
data = json.load(f)
|
|
121
134
|
return Config(**data)
|
|
122
|
-
except
|
|
123
|
-
raise ConfigError(f"Invalid configuration file: {
|
|
124
|
-
|
|
135
|
+
except json.JSONDecodeError as e:
|
|
136
|
+
raise ConfigError(f"Invalid JSON in configuration file: {e}") from e
|
|
137
|
+
except ValidationError as e:
|
|
138
|
+
raise ConfigError(f"Invalid configuration file: {e}") from e
|
|
125
139
|
|
|
126
|
-
class ConfigError(Exception):
|
|
127
|
-
"""Configuration error."""
|
|
128
|
-
|
|
129
|
-
def __init__(self, message: str):
|
|
130
|
-
super().__init__(message)
|
|
131
140
|
|
|
141
|
+
def save_config(config: Config, config_file: Path | None = None):
|
|
142
|
+
"""
|
|
143
|
+
Save configuration to config file.
|
|
132
144
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
Args:
|
|
146
|
+
config (Config): Config object to save.
|
|
147
|
+
config_file (Path | None): Path to the configuration file. If None, use default path.
|
|
148
|
+
"""
|
|
149
|
+
config_file = config_file or get_config_file()
|
|
136
150
|
logger.debug("Saving config to file: {file}", file=config_file)
|
|
137
151
|
with open(config_file, "w", encoding="utf-8") as f:
|
|
138
152
|
f.write(config.model_dump_json(indent=2, exclude_none=True))
|
kimi_cli/constant.py
ADDED
kimi_cli/exception.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class KimiCLIException(Exception):
|
|
2
|
+
"""Base exception class for Kimi CLI."""
|
|
3
|
+
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConfigError(KimiCLIException):
|
|
8
|
+
"""Configuration error."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentSpecError(KimiCLIException):
|
|
14
|
+
"""Agent specification error."""
|
|
15
|
+
|
|
16
|
+
pass
|
kimi_cli/llm.py
CHANGED
|
@@ -1,8 +1,78 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from typing import NamedTuple
|
|
2
3
|
|
|
3
4
|
from kosong.base.chat_provider import ChatProvider
|
|
5
|
+
from kosong.chat_provider.chaos import ChaosChatProvider, ChaosConfig
|
|
6
|
+
from kosong.chat_provider.kimi import Kimi
|
|
7
|
+
from kosong.chat_provider.openai_legacy import OpenAILegacy
|
|
8
|
+
from pydantic import SecretStr
|
|
9
|
+
|
|
10
|
+
from kimi_cli.config import LLMModel, LLMProvider
|
|
11
|
+
from kimi_cli.constant import USER_AGENT
|
|
4
12
|
|
|
5
13
|
|
|
6
14
|
class LLM(NamedTuple):
|
|
7
15
|
chat_provider: ChatProvider
|
|
8
16
|
max_context_size: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def augment_provider_with_env_vars(provider: LLMProvider, model: LLMModel):
|
|
20
|
+
match provider.type:
|
|
21
|
+
case "kimi":
|
|
22
|
+
if base_url := os.getenv("KIMI_BASE_URL"):
|
|
23
|
+
provider.base_url = base_url
|
|
24
|
+
if api_key := os.getenv("KIMI_API_KEY"):
|
|
25
|
+
provider.api_key = SecretStr(api_key)
|
|
26
|
+
if model_name := os.getenv("KIMI_MODEL_NAME"):
|
|
27
|
+
model.model = model_name
|
|
28
|
+
if max_context_size := os.getenv("KIMI_MODEL_MAX_CONTEXT_SIZE"):
|
|
29
|
+
model.max_context_size = int(max_context_size)
|
|
30
|
+
case "openai_legacy":
|
|
31
|
+
if base_url := os.getenv("OPENAI_BASE_URL"):
|
|
32
|
+
provider.base_url = base_url
|
|
33
|
+
if api_key := os.getenv("OPENAI_API_KEY"):
|
|
34
|
+
provider.api_key = SecretStr(api_key)
|
|
35
|
+
case _:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_llm(
|
|
40
|
+
provider: LLMProvider,
|
|
41
|
+
model: LLMModel,
|
|
42
|
+
*,
|
|
43
|
+
stream: bool = True,
|
|
44
|
+
session_id: str | None = None,
|
|
45
|
+
) -> LLM:
|
|
46
|
+
match provider.type:
|
|
47
|
+
case "kimi":
|
|
48
|
+
chat_provider = Kimi(
|
|
49
|
+
model=model.model,
|
|
50
|
+
base_url=provider.base_url,
|
|
51
|
+
api_key=provider.api_key.get_secret_value(),
|
|
52
|
+
stream=stream,
|
|
53
|
+
default_headers={
|
|
54
|
+
"User-Agent": USER_AGENT,
|
|
55
|
+
**provider.custom_headers,
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
if session_id:
|
|
59
|
+
chat_provider = chat_provider.with_generation_kwargs(prompt_cache_key=session_id)
|
|
60
|
+
case "openai_legacy":
|
|
61
|
+
chat_provider = OpenAILegacy(
|
|
62
|
+
model=model.model,
|
|
63
|
+
base_url=provider.base_url,
|
|
64
|
+
api_key=provider.api_key.get_secret_value(),
|
|
65
|
+
stream=stream,
|
|
66
|
+
)
|
|
67
|
+
case "_chaos":
|
|
68
|
+
chat_provider = ChaosChatProvider(
|
|
69
|
+
model=model.model,
|
|
70
|
+
base_url=provider.base_url,
|
|
71
|
+
api_key=provider.api_key.get_secret_value(),
|
|
72
|
+
chaos_config=ChaosConfig(
|
|
73
|
+
error_probability=0.8,
|
|
74
|
+
error_types=[429, 500, 503],
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return LLM(chat_provider=chat_provider, max_context_size=model.max_context_size)
|
kimi_cli/metadata.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import uuid
|
|
3
2
|
from hashlib import md5
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import NamedTuple
|
|
6
4
|
|
|
7
5
|
from pydantic import BaseModel, Field
|
|
8
6
|
|
|
@@ -33,10 +31,12 @@ class WorkDirMeta(BaseModel):
|
|
|
33
31
|
class Metadata(BaseModel):
|
|
34
32
|
"""Kimi metadata structure."""
|
|
35
33
|
|
|
36
|
-
work_dirs: list[WorkDirMeta] = Field(
|
|
34
|
+
work_dirs: list[WorkDirMeta] = Field(
|
|
35
|
+
default_factory=list[WorkDirMeta], description="Work directory list"
|
|
36
|
+
)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def
|
|
39
|
+
def load_metadata() -> Metadata:
|
|
40
40
|
metadata_file = get_metadata_file()
|
|
41
41
|
logger.debug("Loading metadata from file: {file}", file=metadata_file)
|
|
42
42
|
if not metadata_file.exists():
|
|
@@ -47,71 +47,8 @@ def _load_metadata() -> Metadata:
|
|
|
47
47
|
return Metadata(**data)
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def
|
|
50
|
+
def save_metadata(metadata: Metadata):
|
|
51
51
|
metadata_file = get_metadata_file()
|
|
52
52
|
logger.debug("Saving metadata to file: {file}", file=metadata_file)
|
|
53
53
|
with open(metadata_file, "w", encoding="utf-8") as f:
|
|
54
54
|
json.dump(metadata.model_dump(), f, indent=2, ensure_ascii=False)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class Session(NamedTuple):
|
|
58
|
-
"""A session of a work directory."""
|
|
59
|
-
|
|
60
|
-
id: str
|
|
61
|
-
work_dir: WorkDirMeta
|
|
62
|
-
history_file: Path
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def new_session(work_dir: Path, _history_file: Path | None = None) -> Session:
|
|
66
|
-
"""Create a new session for a work directory."""
|
|
67
|
-
logger.debug("Creating new session for work directory: {work_dir}", work_dir=work_dir)
|
|
68
|
-
|
|
69
|
-
metadata = _load_metadata()
|
|
70
|
-
work_dir_meta = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
|
|
71
|
-
if work_dir_meta is None:
|
|
72
|
-
work_dir_meta = WorkDirMeta(path=str(work_dir))
|
|
73
|
-
metadata.work_dirs.append(work_dir_meta)
|
|
74
|
-
|
|
75
|
-
session_id = str(uuid.uuid4())
|
|
76
|
-
if _history_file is None:
|
|
77
|
-
history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
78
|
-
work_dir_meta.last_session_id = session_id
|
|
79
|
-
else:
|
|
80
|
-
logger.warning("Using provided history file: {history_file}", history_file=_history_file)
|
|
81
|
-
_history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
-
if _history_file.exists():
|
|
83
|
-
assert _history_file.is_file()
|
|
84
|
-
history_file = _history_file
|
|
85
|
-
|
|
86
|
-
if history_file.exists():
|
|
87
|
-
# truncate if exists
|
|
88
|
-
logger.warning(
|
|
89
|
-
"History file already exists, truncating: {history_file}", history_file=history_file
|
|
90
|
-
)
|
|
91
|
-
history_file.unlink()
|
|
92
|
-
history_file.touch()
|
|
93
|
-
|
|
94
|
-
_save_metadata(metadata)
|
|
95
|
-
return Session(id=session_id, work_dir=work_dir_meta, history_file=history_file)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def continue_session(work_dir: Path) -> Session | None:
|
|
99
|
-
"""Get the last session for a work directory."""
|
|
100
|
-
logger.debug("Continuing session for work directory: {work_dir}", work_dir=work_dir)
|
|
101
|
-
|
|
102
|
-
metadata = _load_metadata()
|
|
103
|
-
work_dir_meta = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
|
|
104
|
-
if work_dir_meta is None:
|
|
105
|
-
logger.debug("Work directory never been used")
|
|
106
|
-
return None
|
|
107
|
-
if work_dir_meta.last_session_id is None:
|
|
108
|
-
logger.debug("Work directory never had a session")
|
|
109
|
-
return None
|
|
110
|
-
|
|
111
|
-
logger.debug(
|
|
112
|
-
"Found last session for work directory: {session_id}",
|
|
113
|
-
session_id=work_dir_meta.last_session_id,
|
|
114
|
-
)
|
|
115
|
-
session_id = work_dir_meta.last_session_id
|
|
116
|
-
history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
117
|
-
return Session(id=session_id, work_dir=work_dir_meta, history_file=history_file)
|
kimi_cli/prompts/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
|
|
3
|
-
INIT = (Path(__file__).parent / "init.md").read_text()
|
|
4
|
-
COMPACT = (Path(__file__).parent / "compact.md").read_text()
|
|
3
|
+
INIT = (Path(__file__).parent / "init.md").read_text(encoding="utf-8")
|
|
4
|
+
COMPACT = (Path(__file__).parent / "compact.md").read_text(encoding="utf-8")
|
kimi_cli/session.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import NamedTuple
|
|
4
|
+
|
|
5
|
+
from kimi_cli.metadata import WorkDirMeta, load_metadata, save_metadata
|
|
6
|
+
from kimi_cli.utils.logging import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Session(NamedTuple):
|
|
10
|
+
"""A session of a work directory."""
|
|
11
|
+
|
|
12
|
+
id: str
|
|
13
|
+
work_dir: Path
|
|
14
|
+
history_file: Path
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def create(work_dir: Path, _history_file: Path | None = None) -> "Session":
|
|
18
|
+
"""Create a new session for a work directory."""
|
|
19
|
+
logger.debug("Creating new session for work directory: {work_dir}", work_dir=work_dir)
|
|
20
|
+
|
|
21
|
+
metadata = load_metadata()
|
|
22
|
+
work_dir_meta = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
|
|
23
|
+
if work_dir_meta is None:
|
|
24
|
+
work_dir_meta = WorkDirMeta(path=str(work_dir))
|
|
25
|
+
metadata.work_dirs.append(work_dir_meta)
|
|
26
|
+
|
|
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
|
|
31
|
+
else:
|
|
32
|
+
logger.warning(
|
|
33
|
+
"Using provided history file: {history_file}", history_file=_history_file
|
|
34
|
+
)
|
|
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
|
|
39
|
+
|
|
40
|
+
if history_file.exists():
|
|
41
|
+
# truncate if exists
|
|
42
|
+
logger.warning(
|
|
43
|
+
"History file already exists, truncating: {history_file}", history_file=history_file
|
|
44
|
+
)
|
|
45
|
+
history_file.unlink()
|
|
46
|
+
history_file.touch()
|
|
47
|
+
|
|
48
|
+
save_metadata(metadata)
|
|
49
|
+
|
|
50
|
+
return Session(
|
|
51
|
+
id=session_id,
|
|
52
|
+
work_dir=work_dir,
|
|
53
|
+
history_file=history_file,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def continue_(work_dir: Path) -> "Session | None":
|
|
58
|
+
"""Get the last session for a work directory."""
|
|
59
|
+
logger.debug("Continuing session for work directory: {work_dir}", work_dir=work_dir)
|
|
60
|
+
|
|
61
|
+
metadata = load_metadata()
|
|
62
|
+
work_dir_meta = next((wd for wd in metadata.work_dirs if wd.path == str(work_dir)), None)
|
|
63
|
+
if work_dir_meta is None:
|
|
64
|
+
logger.debug("Work directory never been used")
|
|
65
|
+
return None
|
|
66
|
+
if work_dir_meta.last_session_id is None:
|
|
67
|
+
logger.debug("Work directory never had a session")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
logger.debug(
|
|
71
|
+
"Found last session for work directory: {session_id}",
|
|
72
|
+
session_id=work_dir_meta.last_session_id,
|
|
73
|
+
)
|
|
74
|
+
session_id = work_dir_meta.last_session_id
|
|
75
|
+
history_file = work_dir_meta.sessions_dir / f"{session_id}.jsonl"
|
|
76
|
+
|
|
77
|
+
return Session(
|
|
78
|
+
id=session_id,
|
|
79
|
+
work_dir=work_dir,
|
|
80
|
+
history_file=history_file,
|
|
81
|
+
)
|