lithe-cli 0.2.0__tar.gz → 0.2.3__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.
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/PKG-INFO +17 -5
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/README.md +16 -4
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli/__init__.py +1 -1
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli/__main__.py +1 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli/agent.py +25 -8
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli/config.py +101 -21
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli/main.py +157 -54
- lithe_cli-0.2.3/lithe_cli/setup.py +269 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli/ui.py +14 -10
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli.egg-info/PKG-INFO +17 -5
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli.egg-info/SOURCES.txt +1 -0
- lithe_cli-0.2.3/tests/test_cli.py +551 -0
- lithe_cli-0.2.0/tests/test_cli.py +0 -247
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/LICENSE +0 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli/py.typed +0 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli.egg-info/dependency_links.txt +0 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli.egg-info/entry_points.txt +0 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli.egg-info/requires.txt +0 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/lithe_cli.egg-info/top_level.txt +0 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/pyproject.toml +0 -0
- {lithe_cli-0.2.0 → lithe_cli-0.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lithe-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Command-line interface for the lithe agent kernel: chat, one-shot runs, undo, and run inspection.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -65,7 +65,14 @@ pip install lithe-cli
|
|
|
65
65
|
## Configure
|
|
66
66
|
|
|
67
67
|
There is no default endpoint — the CLI refuses to run rather than silently
|
|
68
|
-
hitting some third-party URL.
|
|
68
|
+
hitting some third-party URL. The easiest way in is the wizard: on the
|
|
69
|
+
first `lithe chat` / `lithe run` (or any time, via `lithe config`) an
|
|
70
|
+
interactive terminal prompts for the three essentials and saves them to
|
|
71
|
+
`$LITHE_HOME/config.json` (mode 0600 — it holds the key). An optional
|
|
72
|
+
connectivity probe catches typos before your first turn.
|
|
73
|
+
|
|
74
|
+
To configure by hand instead, point the CLI at any OpenAI-compatible
|
|
75
|
+
endpoint:
|
|
69
76
|
|
|
70
77
|
```bash
|
|
71
78
|
export LITHE_API_KEY=sk-...
|
|
@@ -73,13 +80,18 @@ export LITHE_BASE_URL=https://your-endpoint/api/v1
|
|
|
73
80
|
export LITHE_MODEL=your-model
|
|
74
81
|
```
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
Resolution order is flag (`--api-key`, `--base-url`, `--model`) >
|
|
84
|
+
environment > saved config file, per key. The wizard never runs without
|
|
85
|
+
a TTY on both ends, so pipes and CI keep the hard refusal; `--no-setup`
|
|
86
|
+
restores that fail-fast behavior on terminals too. `lithe config --show`
|
|
87
|
+
peeks at the saved values with a masked key.
|
|
88
|
+
|
|
77
89
|
`LITHE_HOME` (default `~/.lithe`) locates the run store; the agent's
|
|
78
90
|
workspace defaults to the current directory (`--workspace` to change).
|
|
79
91
|
|
|
80
92
|
`lithe doctor` prints the effective configuration — endpoint (with a
|
|
81
|
-
masked key), store, workspace, sandbox backend, skills, MCP
|
|
82
|
-
you can see what a run would use before starting one.
|
|
93
|
+
masked key), config file, store, workspace, sandbox backend, skills, MCP
|
|
94
|
+
servers — so you can see what a run would use before starting one.
|
|
83
95
|
|
|
84
96
|
## Use
|
|
85
97
|
|
|
@@ -18,7 +18,14 @@ pip install lithe-cli
|
|
|
18
18
|
## Configure
|
|
19
19
|
|
|
20
20
|
There is no default endpoint — the CLI refuses to run rather than silently
|
|
21
|
-
hitting some third-party URL.
|
|
21
|
+
hitting some third-party URL. The easiest way in is the wizard: on the
|
|
22
|
+
first `lithe chat` / `lithe run` (or any time, via `lithe config`) an
|
|
23
|
+
interactive terminal prompts for the three essentials and saves them to
|
|
24
|
+
`$LITHE_HOME/config.json` (mode 0600 — it holds the key). An optional
|
|
25
|
+
connectivity probe catches typos before your first turn.
|
|
26
|
+
|
|
27
|
+
To configure by hand instead, point the CLI at any OpenAI-compatible
|
|
28
|
+
endpoint:
|
|
22
29
|
|
|
23
30
|
```bash
|
|
24
31
|
export LITHE_API_KEY=sk-...
|
|
@@ -26,13 +33,18 @@ export LITHE_BASE_URL=https://your-endpoint/api/v1
|
|
|
26
33
|
export LITHE_MODEL=your-model
|
|
27
34
|
```
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
Resolution order is flag (`--api-key`, `--base-url`, `--model`) >
|
|
37
|
+
environment > saved config file, per key. The wizard never runs without
|
|
38
|
+
a TTY on both ends, so pipes and CI keep the hard refusal; `--no-setup`
|
|
39
|
+
restores that fail-fast behavior on terminals too. `lithe config --show`
|
|
40
|
+
peeks at the saved values with a masked key.
|
|
41
|
+
|
|
30
42
|
`LITHE_HOME` (default `~/.lithe`) locates the run store; the agent's
|
|
31
43
|
workspace defaults to the current directory (`--workspace` to change).
|
|
32
44
|
|
|
33
45
|
`lithe doctor` prints the effective configuration — endpoint (with a
|
|
34
|
-
masked key), store, workspace, sandbox backend, skills, MCP
|
|
35
|
-
you can see what a run would use before starting one.
|
|
46
|
+
masked key), config file, store, workspace, sandbox backend, skills, MCP
|
|
47
|
+
servers — so you can see what a run would use before starting one.
|
|
36
48
|
|
|
37
49
|
## Use
|
|
38
50
|
|
|
@@ -14,6 +14,7 @@ Everything here maps lithe's host contract onto CLI defaults:
|
|
|
14
14
|
- undo — the bundled tools register reverters, so ``lithe undo`` works with
|
|
15
15
|
zero configuration.
|
|
16
16
|
"""
|
|
17
|
+
|
|
17
18
|
from __future__ import annotations
|
|
18
19
|
|
|
19
20
|
import json
|
|
@@ -25,7 +26,11 @@ from typing import Any
|
|
|
25
26
|
|
|
26
27
|
from lithe import AgentContext, LLMConfig, ToolRegistry
|
|
27
28
|
from lithe.bundles import (
|
|
28
|
-
AgentHost,
|
|
29
|
+
AgentHost,
|
|
30
|
+
JsonlRunStore,
|
|
31
|
+
JsonTodoStore,
|
|
32
|
+
Workspace,
|
|
33
|
+
undo_run,
|
|
29
34
|
)
|
|
30
35
|
from lithe.bundles.download import register_download_tools
|
|
31
36
|
from lithe.bundles.patch import register_apply_patch_tool
|
|
@@ -91,8 +96,7 @@ def build_registry(cfg: Config) -> ToolRegistry:
|
|
|
91
96
|
from lithe.bundles.sandbox import CodeRunner, register_code_tools
|
|
92
97
|
|
|
93
98
|
runner = CodeRunner(sys.executable, backend=sandbox_backend())
|
|
94
|
-
register_code_tools(
|
|
95
|
-
reg, lambda ctx: str(cfg.workspace_dir.resolve()), runner)
|
|
99
|
+
register_code_tools(reg, lambda ctx: str(cfg.workspace_dir.resolve()), runner)
|
|
96
100
|
if cfg.skills_dir is not None:
|
|
97
101
|
from lithe.bundles.skills import SkillLibrary, register_skill_tool
|
|
98
102
|
|
|
@@ -109,13 +113,18 @@ def build_host(cfg: Config, reg: ToolRegistry) -> AgentHost:
|
|
|
109
113
|
store = JsonlRunStore(cfg.store_dir)
|
|
110
114
|
prompt = build_system_prompt(cfg)
|
|
111
115
|
return AgentHost(
|
|
112
|
-
reg,
|
|
116
|
+
reg,
|
|
117
|
+
build_llm(cfg),
|
|
118
|
+
store,
|
|
119
|
+
max_steps=cfg.max_steps,
|
|
113
120
|
build_system_prompt=lambda ctx, mode, anchor: prompt,
|
|
114
121
|
)
|
|
115
122
|
|
|
116
123
|
|
|
117
124
|
def render_event(
|
|
118
|
-
ev: dict,
|
|
125
|
+
ev: dict,
|
|
126
|
+
verbose: bool = False,
|
|
127
|
+
stream: bool = False,
|
|
119
128
|
pending: dict | None = None,
|
|
120
129
|
) -> None:
|
|
121
130
|
"""Render one runtime event onto the terminal.
|
|
@@ -150,8 +159,14 @@ def render_event(
|
|
|
150
159
|
elif t == "cancelled":
|
|
151
160
|
ui.warn("已取消")
|
|
152
161
|
elif verbose and t == "usage":
|
|
153
|
-
print(
|
|
154
|
-
|
|
162
|
+
print(
|
|
163
|
+
ui.usage(
|
|
164
|
+
ev.get("prompt_tokens"),
|
|
165
|
+
ev.get("completion_tokens"),
|
|
166
|
+
ev.get("context_tokens"),
|
|
167
|
+
ev.get("context_percent"),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
155
170
|
elif verbose and t == "reasoning":
|
|
156
171
|
digest = (ev.get("summary") or "").replace("\n", " ")
|
|
157
172
|
if digest:
|
|
@@ -159,7 +174,9 @@ def render_event(
|
|
|
159
174
|
|
|
160
175
|
|
|
161
176
|
async def execute(
|
|
162
|
-
cfg: Config,
|
|
177
|
+
cfg: Config,
|
|
178
|
+
task: str,
|
|
179
|
+
history: list[dict] | None = None,
|
|
163
180
|
run_id: str | None = None,
|
|
164
181
|
) -> tuple[str, dict, Any]:
|
|
165
182
|
"""Run one task end-to-end; returns (run_id, done_event, host)."""
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
"""Configuration resolved from CLI flags and
|
|
1
|
+
"""Configuration resolved from CLI flags, the environment, and a saved file.
|
|
2
2
|
|
|
3
3
|
Endpoint credentials follow the same convention as lithe's ``live_host``
|
|
4
|
-
example: ``LITHE_API_KEY`` / ``LITHE_BASE_URL`` / ``LITHE_MODEL
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
example: ``LITHE_API_KEY`` / ``LITHE_BASE_URL`` / ``LITHE_MODEL``. There
|
|
5
|
+
is no default endpoint on purpose: the CLI refuses to run rather than
|
|
6
|
+
silently hitting some third-party URL. Resolution order is flag > env >
|
|
7
|
+
``$LITHE_HOME/config.json`` (written by the first-run wizard or
|
|
8
|
+
``lithe-cli config``; mode 0600 since it holds the key). On an
|
|
9
|
+
interactive terminal a missing endpoint triggers the wizard instead of
|
|
10
|
+
the refusal — see :mod:`lithe_cli.setup`.
|
|
7
11
|
|
|
8
12
|
Capability flags (all off unless asked for):
|
|
9
13
|
|
|
@@ -17,6 +21,7 @@ Capability flags (all off unless asked for):
|
|
|
17
21
|
- ``--vision`` ``image_info`` + ``analyze_image`` (one vision call on
|
|
18
22
|
the main endpoint, memoized per file hash).
|
|
19
23
|
"""
|
|
24
|
+
|
|
20
25
|
from __future__ import annotations
|
|
21
26
|
|
|
22
27
|
import json
|
|
@@ -101,20 +106,74 @@ def load_mcp_spec(raw: str) -> list:
|
|
|
101
106
|
raise SystemExit(
|
|
102
107
|
f"MCP 配置解析失败:{exc}\n"
|
|
103
108
|
"spec 应为 JSON 数组(stdio: command/env,http: url/headers),"
|
|
104
|
-
|
|
105
|
-
"
|
|
109
|
+
'例如:--mcp \'[{"name":"fs","command":["npx","-y",'
|
|
110
|
+
'"@modelcontextprotocol/server-filesystem","."]}]\''
|
|
106
111
|
) from exc
|
|
107
112
|
|
|
108
113
|
|
|
114
|
+
_ENDPOINT_KEYS = ("api_key", "base_url", "model")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def config_file() -> Path:
|
|
118
|
+
"""Where the wizard-saved endpoint lives."""
|
|
119
|
+
return lithe_home() / "config.json"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_saved_endpoint() -> dict:
|
|
123
|
+
"""Read {api_key, base_url, model} from the config file; {} if none.
|
|
124
|
+
|
|
125
|
+
A corrupt file is treated as absent — a bad edit should cost the user
|
|
126
|
+
one re-prompt, not a broken CLI.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
data = json.loads(config_file().read_text(encoding="utf-8"))
|
|
130
|
+
except (OSError, json.JSONDecodeError):
|
|
131
|
+
return {}
|
|
132
|
+
if not isinstance(data, dict):
|
|
133
|
+
return {}
|
|
134
|
+
return {
|
|
135
|
+
k: data[k].strip()
|
|
136
|
+
for k in _ENDPOINT_KEYS
|
|
137
|
+
if isinstance(data.get(k), str) and data[k].strip()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def save_endpoint(api_key: str, base_url: str, model: str) -> Path:
|
|
142
|
+
"""Write the config file; 0600 because it holds the API key."""
|
|
143
|
+
path = config_file()
|
|
144
|
+
fresh_dir = not path.parent.exists()
|
|
145
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
if fresh_dir:
|
|
147
|
+
try:
|
|
148
|
+
path.parent.chmod(0o700)
|
|
149
|
+
except OSError:
|
|
150
|
+
pass
|
|
151
|
+
payload = {
|
|
152
|
+
k: v
|
|
153
|
+
for k, v in (("api_key", api_key), ("base_url", base_url), ("model", model))
|
|
154
|
+
}
|
|
155
|
+
path.write_text(
|
|
156
|
+
json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
|
|
157
|
+
)
|
|
158
|
+
try:
|
|
159
|
+
path.chmod(0o600)
|
|
160
|
+
except OSError:
|
|
161
|
+
pass
|
|
162
|
+
return path
|
|
163
|
+
|
|
164
|
+
|
|
109
165
|
def load_config(args: Any) -> Config:
|
|
110
166
|
"""Merge parsed argparse flags over environment defaults."""
|
|
167
|
+
|
|
111
168
|
def g(name: str, default):
|
|
112
169
|
return getattr(args, name, default)
|
|
113
170
|
|
|
114
|
-
store_dir = (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
171
|
+
store_dir = (
|
|
172
|
+
Path(g("store", None)).expanduser() if g("store", None) else default_store_dir()
|
|
173
|
+
)
|
|
174
|
+
workspace_dir = (
|
|
175
|
+
Path(g("workspace", None)).expanduser() if g("workspace", None) else Path.cwd()
|
|
176
|
+
)
|
|
118
177
|
skills = g("skills", None)
|
|
119
178
|
if skills is not None and skills.strip():
|
|
120
179
|
skills_dir: Path | None = Path(skills).expanduser()
|
|
@@ -125,10 +184,15 @@ def load_config(args: Any) -> Config:
|
|
|
125
184
|
skills_dir = cand if cand.is_dir() else None
|
|
126
185
|
|
|
127
186
|
mcp_raw = g("mcp", None) or os.environ.get(ENV_MCP)
|
|
187
|
+
saved = load_saved_endpoint() # flag > env > config file, per key
|
|
128
188
|
return Config(
|
|
129
|
-
api_key=
|
|
130
|
-
|
|
131
|
-
|
|
189
|
+
api_key=(
|
|
190
|
+
g("api_key", None) or os.environ.get(ENV_API_KEY) or saved.get("api_key")
|
|
191
|
+
),
|
|
192
|
+
base_url=(
|
|
193
|
+
g("base_url", None) or os.environ.get(ENV_BASE_URL) or saved.get("base_url")
|
|
194
|
+
),
|
|
195
|
+
model=(g("model", None) or os.environ.get(ENV_MODEL) or saved.get("model")),
|
|
132
196
|
store_dir=store_dir,
|
|
133
197
|
workspace_dir=workspace_dir,
|
|
134
198
|
user_id=g("user", None) or DEFAULT_USER,
|
|
@@ -144,19 +208,35 @@ def load_config(args: Any) -> Config:
|
|
|
144
208
|
mcp_spec=mcp_raw,
|
|
145
209
|
mcp_servers=load_mcp_spec(mcp_raw) if mcp_raw else [],
|
|
146
210
|
vision=g("vision", False),
|
|
147
|
-
color=(True if g("color", False) else
|
|
148
|
-
False if g("no_color", False) else None),
|
|
211
|
+
color=(True if g("color", False) else False if g("no_color", False) else None),
|
|
149
212
|
)
|
|
150
213
|
|
|
151
214
|
|
|
152
|
-
def require_endpoint(cfg: Config) -> None:
|
|
153
|
-
"""
|
|
215
|
+
def require_endpoint(cfg: Config, interactive: bool = False) -> None:
|
|
216
|
+
"""Ensure an endpoint, launching the first-run wizard when it helps.
|
|
217
|
+
|
|
218
|
+
``interactive`` asks for the wizard; it still only runs on a TTY (both
|
|
219
|
+
ends), so pipes, captures and CI keep the plain SystemExit.
|
|
220
|
+
"""
|
|
154
221
|
if cfg.has_endpoint:
|
|
155
222
|
return
|
|
223
|
+
if interactive:
|
|
224
|
+
from .setup import run_setup_wizard, stdin_is_interactive
|
|
225
|
+
|
|
226
|
+
if stdin_is_interactive():
|
|
227
|
+
saved = run_setup_wizard() or {}
|
|
228
|
+
cfg.api_key = cfg.api_key or saved.get("api_key")
|
|
229
|
+
cfg.base_url = cfg.base_url or saved.get("base_url")
|
|
230
|
+
cfg.model = cfg.model or saved.get("model")
|
|
231
|
+
if cfg.has_endpoint:
|
|
232
|
+
return
|
|
156
233
|
raise SystemExit(
|
|
157
|
-
"
|
|
158
|
-
|
|
159
|
-
f"
|
|
160
|
-
|
|
234
|
+
"缺少模型端点配置。两种方式任选:\n"
|
|
235
|
+
" 1) 交互式(推荐):运行 lithe-cli config,按提示输入后保存到\n"
|
|
236
|
+
f" {config_file()}\n"
|
|
237
|
+
" 2) 环境变量(或对应flag):\n"
|
|
238
|
+
f" export {ENV_API_KEY}=sk-...\n"
|
|
239
|
+
f" export {ENV_BASE_URL}=https://your-endpoint/api/v1\n"
|
|
240
|
+
f" export {ENV_MODEL}=your-model\n"
|
|
161
241
|
"本 CLI 不内置任何默认端点。"
|
|
162
242
|
)
|