lithe-cli 0.1.0__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.
lithe_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """lithe-cli: the command-line interface for the lithe agent kernel."""
2
+
3
+ __version__ = "0.1.0"
lithe_cli/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Support ``python -m lithe_cli`` alongside the console script."""
2
+ import sys
3
+
4
+ from .main import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
lithe_cli/agent.py ADDED
@@ -0,0 +1,123 @@
1
+ """Host wiring: tools, store, LLM config, and the event printer.
2
+
3
+ Everything here maps lithe's host contract onto CLI defaults:
4
+
5
+ - workspace — the sandboxed file root the agent reads/writes (``--workspace``,
6
+ default the current directory);
7
+ - store — ``JsonlRunStore`` under ``~/.lithe/runs`` (``--store``), the
8
+ kernel's zero-database default;
9
+ - tools — the workspace bundle (read/write/edit/list/search/glob +
10
+ apply_patch) and todos; ``download_file`` is opt-in (``--download``)
11
+ because it grants the agent network fetch access;
12
+ - undo — the bundled tools register reverters, so ``lithe undo`` works with
13
+ zero configuration.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from typing import Any
19
+
20
+ from lithe import AgentContext, LLMConfig, ToolRegistry
21
+ from lithe.bundles import (
22
+ AgentHost, JsonlRunStore, JsonTodoStore, Workspace, undo_run,
23
+ )
24
+ from lithe.bundles.download import register_download_tools
25
+ from lithe.bundles.patch import register_apply_patch_tool
26
+ from lithe.bundles.workspace import register_file_tools
27
+ from lithe.bundles.todos import register_todo_tools
28
+
29
+ from .config import Config
30
+
31
+ SYSTEM_PROMPT = (
32
+ "你是运行在命令行里的助理,工作区是用户的当前目录。"
33
+ "用提供的文件工具完成任务(读写前先读、谨慎修改),"
34
+ "需要多步任务时先用 update_todos 列出计划,最后用简洁中文汇报结果。"
35
+ )
36
+
37
+
38
+ def build_registry(cfg: Config) -> ToolRegistry:
39
+ """Register the CLI's tool set against the configured workspace."""
40
+ reg = ToolRegistry()
41
+ ws_root = cfg.workspace_dir.resolve()
42
+ workspace_for = lambda ctx: Workspace(ws_root) # noqa: E731
43
+ register_file_tools(reg, workspace_for)
44
+ register_apply_patch_tool(reg, workspace_for)
45
+ todo_path = cfg.store_dir / f"todos-{cfg.user_id}.json"
46
+ register_todo_tools(reg, lambda ctx: JsonTodoStore(todo_path))
47
+ if cfg.download:
48
+ register_download_tools(reg, workspace_for)
49
+ return reg
50
+
51
+
52
+ def build_host(cfg: Config, reg: ToolRegistry) -> AgentHost:
53
+ """Build the AgentHost; the store lands under cfg.store_dir."""
54
+ store = JsonlRunStore(cfg.store_dir)
55
+ llm = LLMConfig(
56
+ model=cfg.model or "unused",
57
+ base_url=cfg.base_url or "unused",
58
+ api_key=cfg.api_key or "unused",
59
+ timeout=cfg.timeout,
60
+ attempts=cfg.attempts,
61
+ stream=cfg.stream,
62
+ context_window=cfg.context_window,
63
+ transport=cfg.transport,
64
+ )
65
+ return AgentHost(
66
+ reg, llm, store, max_steps=cfg.max_steps,
67
+ build_system_prompt=lambda ctx, mode, anchor: SYSTEM_PROMPT,
68
+ )
69
+
70
+
71
+ def print_event(ev: dict, verbose: bool = False) -> None:
72
+ """Render one runtime event onto the terminal."""
73
+ t = ev.get("type")
74
+ if t == "assistant_delta":
75
+ print(ev.get("text", ""), end="", flush=True)
76
+ elif t == "assistant":
77
+ if ev.get("text"):
78
+ print(ev.get("text", ""))
79
+ elif t == "tool_call":
80
+ args = json.dumps(ev.get("args", {}), ensure_ascii=False)
81
+ print(f" ⚒ {ev.get('name')} {args[:120]}")
82
+ elif t == "tool_result":
83
+ summary = (ev.get("summary") or "").replace("\n", " ")
84
+ mark = "✓" if ev.get("ok") else "✗"
85
+ print(f" {mark} {summary[:160]}")
86
+ elif t == "error":
87
+ print(f" ! {ev.get('message')}")
88
+ elif verbose and t == "usage":
89
+ print(f" · tokens p{ev.get('prompt_tokens')}/c{ev.get('completion_tokens')}"
90
+ f" ctx={ev.get('context_tokens') or '?'}tk"
91
+ f" ({ev.get('context_percent')}%)")
92
+ elif verbose and t == "reasoning":
93
+ digest = (ev.get("summary") or "").replace("\n", " ")
94
+ if digest:
95
+ print(f" ~ {digest[:120]}")
96
+
97
+
98
+ async def execute(
99
+ cfg: Config, task: str, history: list[dict] | None = None,
100
+ run_id: str | None = None,
101
+ ) -> tuple[str, dict, Any]:
102
+ """Run one task end-to-end; returns (run_id, done_event, host)."""
103
+ import uuid
104
+
105
+ reg = build_registry(cfg)
106
+ host = build_host(cfg, reg)
107
+ rid = run_id or uuid.uuid4().hex[:12]
108
+ ctx = AgentContext(run_id=rid, user_id=cfg.user_id)
109
+ ev: dict = {}
110
+ async for ev in host.run(ctx, task, history=history):
111
+ print_event(ev, cfg.verbose)
112
+ if cfg.stream:
113
+ print() # close the streaming line
114
+ return rid, ev, host
115
+
116
+
117
+ async def undo(cfg: Config, run_id: str) -> int:
118
+ """Revert a run's mutations with the bundled tools' reverters."""
119
+ reg = build_registry(cfg)
120
+ host = build_host(cfg, reg)
121
+ report = await undo_run(host, run_id, cfg.user_id)
122
+ print(f"已撤销 {report.reverted} 个操作(run {run_id})")
123
+ return 0
lithe_cli/config.py ADDED
@@ -0,0 +1,91 @@
1
+ """Configuration resolved from CLI flags and the environment.
2
+
3
+ Endpoint credentials follow the same convention as lithe's ``live_host``
4
+ example: ``LITHE_API_KEY`` / ``LITHE_BASE_URL`` / ``LITHE_MODEL`` — flags
5
+ override env vars. There is no default endpoint on purpose: the CLI refuses
6
+ to run rather than silently hitting some third-party URL.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ ENV_API_KEY = "LITHE_API_KEY"
16
+ ENV_BASE_URL = "LITHE_BASE_URL"
17
+ ENV_MODEL = "LITHE_MODEL"
18
+ ENV_HOME = "LITHE_HOME"
19
+
20
+ DEFAULT_USER = "cli"
21
+
22
+
23
+ @dataclass
24
+ class Config:
25
+ """Everything one CLI invocation needs (endpoint, store, workspace)."""
26
+
27
+ api_key: str | None = None
28
+ base_url: str | None = None
29
+ model: str | None = None
30
+ store_dir: Path = field(default_factory=lambda: default_store_dir())
31
+ workspace_dir: Path = field(default_factory=lambda: Path.cwd())
32
+ user_id: str = DEFAULT_USER
33
+ stream: bool = False
34
+ max_steps: int = 35
35
+ context_window: int | None = None
36
+ timeout: float = 180.0
37
+ attempts: int = 2
38
+ download: bool = False
39
+ verbose: bool = False
40
+ # Wire protocol ("chat" | "responses") or a custom LLMTransport instance.
41
+ # Not exposed as a flag: the injection point for offline tests.
42
+ transport: Any = "chat"
43
+
44
+ @property
45
+ def has_endpoint(self) -> bool:
46
+ if not isinstance(self.transport, str):
47
+ return True # scripted/custom transport needs no endpoint
48
+ return bool(self.api_key and self.base_url and self.model)
49
+
50
+
51
+ def default_store_dir() -> Path:
52
+ home = Path(os.environ.get(ENV_HOME, "~/.lithe")).expanduser()
53
+ return home / "runs"
54
+
55
+
56
+ def load_config(args: Any) -> Config:
57
+ """Merge parsed argparse flags over environment defaults."""
58
+ store_dir = Path(args.store).expanduser() if args.store else default_store_dir()
59
+ workspace_dir = (Path(args.workspace).expanduser()
60
+ if args.workspace else Path.cwd())
61
+ # Capability flags only run/chat define; getattr keeps tools/runs/log/undo
62
+ # parsers minimal while sharing one Config shape.
63
+ g = lambda name, default: getattr(args, name, default) # noqa: E731
64
+ return Config(
65
+ api_key=args.api_key or os.environ.get(ENV_API_KEY),
66
+ base_url=args.base_url or os.environ.get(ENV_BASE_URL),
67
+ model=args.model or os.environ.get(ENV_MODEL),
68
+ store_dir=store_dir,
69
+ workspace_dir=workspace_dir,
70
+ user_id=args.user or DEFAULT_USER,
71
+ stream=g("stream", False),
72
+ max_steps=g("max_steps", 35),
73
+ context_window=g("context_window", None),
74
+ timeout=g("timeout", 180.0),
75
+ attempts=g("attempts", 2),
76
+ download=g("download", False),
77
+ verbose=args.verbose,
78
+ )
79
+
80
+
81
+ def require_endpoint(cfg: Config) -> None:
82
+ """Exit with a how-to message when no endpoint is configured."""
83
+ if cfg.has_endpoint:
84
+ return
85
+ raise SystemExit(
86
+ "缺少模型端点配置。请设置环境变量(或使用对应flag):\n"
87
+ f" export {ENV_API_KEY}=sk-...\n"
88
+ f" export {ENV_BASE_URL}=https://your-endpoint/api/v1\n"
89
+ f" export {ENV_MODEL}=your-model\n"
90
+ "本 CLI 不内置任何默认端点。"
91
+ )
lithe_cli/main.py ADDED
@@ -0,0 +1,201 @@
1
+ """The `lithe` command-line entry point.
2
+
3
+ Subcommands:
4
+ run TASK one-shot task against the configured endpoint
5
+ chat interactive session (history carried across turns)
6
+ tools list the tools this CLI registers
7
+ runs list stored runs
8
+ log RUN_ID show a stored run's messages and actions
9
+ undo RUN_ID revert a run's file/todo mutations
10
+
11
+ Endpoint config: LITHE_API_KEY / LITHE_BASE_URL / LITHE_MODEL (flags
12
+ override). Storage lands in ~/.lithe/runs (LITHE_HOME to move it); the
13
+ agent's workspace is the current directory unless --workspace says else.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import asyncio
19
+ import sys
20
+ from typing import Any
21
+
22
+ from lithe import __version__ as lithe_version
23
+ from lithe.bundles import JsonlRunStore
24
+
25
+ from . import __version__
26
+ from .agent import build_registry, execute, undo
27
+ from .config import load_config, require_endpoint
28
+
29
+
30
+ def build_parser() -> argparse.ArgumentParser:
31
+ p = argparse.ArgumentParser(
32
+ prog="lithe",
33
+ description="Command-line interface for the lithe agent kernel.",
34
+ )
35
+ p.add_argument("--version", action="version",
36
+ version=f"lithe-cli {__version__} (lithe {lithe_version})")
37
+ sub = p.add_subparsers(dest="command")
38
+
39
+ def common(sp: argparse.ArgumentParser) -> None:
40
+ sp.add_argument("--api-key", help="endpoint API key (env LITHE_API_KEY)")
41
+ sp.add_argument("--base-url", help="endpoint base URL (env LITHE_BASE_URL)")
42
+ sp.add_argument("--model", help="model name (env LITHE_MODEL)")
43
+ sp.add_argument("--workspace", help="agent workspace dir (default: cwd)")
44
+ sp.add_argument("--store", help="run store dir (default: ~/.lithe/runs)")
45
+ sp.add_argument("--user", help="user id for the store (default: cli)")
46
+ sp.add_argument("--download", action="store_true",
47
+ help="grant the agent download_file (network fetch)")
48
+ sp.add_argument("--verbose", "-v", action="store_true",
49
+ help="show usage/reasoning events")
50
+
51
+ def runtime(sp: argparse.ArgumentParser) -> None:
52
+ common(sp)
53
+ sp.add_argument("--stream", action="store_true",
54
+ help="stream tokens as they generate")
55
+ sp.add_argument("--max-steps", type=int, default=35,
56
+ help="tool-loop step budget (default: 35)")
57
+ sp.add_argument("--context-window", type=int, default=None,
58
+ help="model context window, for fullness gauges")
59
+ sp.add_argument("--timeout", type=float, default=180.0,
60
+ help="per-call timeout in seconds")
61
+ sp.add_argument("--attempts", type=int, default=2,
62
+ help="per-call retry attempts")
63
+
64
+ run_p = sub.add_parser("run", help="run one task")
65
+ runtime(run_p)
66
+ run_p.add_argument("task", help="the task for the agent")
67
+
68
+ chat_p = sub.add_parser("chat", help="interactive session")
69
+ runtime(chat_p)
70
+
71
+ tools_p = sub.add_parser("tools", help="list registered tools")
72
+ common(tools_p)
73
+
74
+ runs_p = sub.add_parser("runs", help="list stored runs")
75
+ common(runs_p)
76
+ runs_p.add_argument("--limit", type=int, default=30)
77
+
78
+ log_p = sub.add_parser("log", help="show a run's messages and actions")
79
+ common(log_p)
80
+ log_p.add_argument("run_id")
81
+
82
+ undo_p = sub.add_parser("undo", help="revert a run's mutations")
83
+ common(undo_p)
84
+ undo_p.add_argument("run_id")
85
+
86
+ return p
87
+
88
+
89
+ def _cmd_tools(cfg: Any) -> int:
90
+ reg = build_registry(cfg)
91
+ for name in sorted(reg.names()):
92
+ spec = reg.spec(name)
93
+ cat = spec.category.name if spec.category is not None else "?"
94
+ desc = (spec.description or "").splitlines()[0]
95
+ print(f"{name:<16} {cat:<6} {desc}")
96
+ return 0
97
+
98
+
99
+ def _cmd_runs(cfg: Any, limit: int) -> int:
100
+ store = JsonlRunStore(cfg.store_dir)
101
+ runs = store.list_runs(cfg.user_id, limit=limit)
102
+ if not runs:
103
+ print(f"({cfg.store_dir} 下没有 run 记录)")
104
+ return 0
105
+ for r in reversed(runs): # newest last, like a log
106
+ task = (r.task or "").replace("\n", " ")
107
+ print(f"{r.run_id:<14} {r.status:<12} steps={r.steps:<3} "
108
+ f"cost={r.cost:<8.4f} {task[:60]}")
109
+ return 0
110
+
111
+
112
+ def _cmd_log(cfg: Any, run_id: str) -> int:
113
+ store = JsonlRunStore(cfg.store_dir)
114
+ run = store.get_run(run_id, cfg.user_id)
115
+ if run is None:
116
+ print(f"找不到 run {run_id}(user={cfg.user_id})")
117
+ return 1
118
+ print(f"run {run.run_id} status={run.status} steps={run.steps} "
119
+ f"model={run.model}")
120
+ print(f"task: {run.task}")
121
+ if run.final:
122
+ print(f"final: {run.final[:500]}")
123
+ print("-- messages --")
124
+ for row in store.messages_for_run(run_id, cfg.user_id):
125
+ role = row.get("role", "?")
126
+ content = row.get("content") or ""
127
+ calls = row.get("tool_calls")
128
+ extra = f" tool_calls={len(calls)}" if calls else ""
129
+ print(f"[{role}] {str(content)[:300]}{extra}")
130
+ print("-- actions --")
131
+ actions = store.list_actions(run_id, cfg.user_id)
132
+ if not actions:
133
+ print("(无)")
134
+ for a in actions:
135
+ print(f"#{a.id} {a.kind:<14} {a.target:<40} {a.status}")
136
+ return 0
137
+
138
+
139
+ def _cmd_run(cfg: Any, task: str) -> int:
140
+ require_endpoint(cfg)
141
+ _, done, _ = asyncio.run(execute(cfg, task))
142
+ return 0 if done.get("status") == "done" else 1
143
+
144
+
145
+ def _cmd_chat(cfg: Any) -> int:
146
+ require_endpoint(cfg)
147
+ return _chat_loop(cfg)
148
+
149
+
150
+ def _chat_loop(cfg: Any) -> int:
151
+ store = JsonlRunStore(cfg.store_dir)
152
+ run_ids: list[str] = []
153
+ print(f"lithe {__version__} — /exit 退出,输入任务开始。")
154
+ while True:
155
+ try:
156
+ line = input("lithe> ").strip()
157
+ except (EOFError, KeyboardInterrupt):
158
+ print()
159
+ break
160
+ if not line:
161
+ continue
162
+ if line in ("/exit", "/quit", "exit", "quit"):
163
+ break
164
+ history = (store.messages_for_runs(run_ids, cfg.user_id)
165
+ if run_ids else None)
166
+ try:
167
+ # One asyncio.run per turn: each run is self-contained, and the
168
+ # blocking input() stays out of async context.
169
+ rid, done, _ = asyncio.run(execute(cfg, line, history=history))
170
+ except KeyboardInterrupt:
171
+ print("\n(已中断本轮;输入继续)")
172
+ continue
173
+ run_ids.append(rid)
174
+ if done.get("status") != "done":
175
+ print(f"(本轮状态:{done.get('status')})")
176
+ return 0
177
+
178
+
179
+ def main(argv: list[str] | None = None) -> int:
180
+ args = build_parser().parse_args(argv)
181
+ if args.command is None:
182
+ build_parser().print_help()
183
+ return 2
184
+ cfg = load_config(args)
185
+ if args.command == "run":
186
+ return _cmd_run(cfg, args.task)
187
+ if args.command == "chat":
188
+ return _cmd_chat(cfg)
189
+ if args.command == "tools":
190
+ return _cmd_tools(cfg)
191
+ if args.command == "runs":
192
+ return _cmd_runs(cfg, args.limit)
193
+ if args.command == "log":
194
+ return _cmd_log(cfg, args.run_id)
195
+ if args.command == "undo":
196
+ return asyncio.run(undo(cfg, args.run_id))
197
+ return 2
198
+
199
+
200
+ if __name__ == "__main__":
201
+ sys.exit(main())
lithe_cli/py.typed ADDED
File without changes
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: lithe-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line interface for the lithe agent kernel: chat, one-shot runs, undo, and run inspection.
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 betaloop contributors
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Keywords: agent,llm,react,cli,tool-calling
28
+ Classifier: Development Status :: 4 - Beta
29
+ Classifier: Environment :: Console
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.10
34
+ Classifier: Programming Language :: Python :: 3.11
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Programming Language :: Python :: 3.13
37
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
38
+ Classifier: Typing :: Typed
39
+ Requires-Python: >=3.10
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Requires-Dist: lithe>=0.9.5
43
+ Provides-Extra: dev
44
+ Requires-Dist: pytest>=8; extra == "dev"
45
+ Requires-Dist: ruff>=0.4; extra == "dev"
46
+ Dynamic: license-file
47
+
48
+ # lithe-cli
49
+
50
+ [![PyPI](https://img.shields.io/pypi/v/lithe-cli.svg)](https://pypi.org/project/lithe-cli/)
51
+ [![Python](https://img.shields.io/pypi/pyversions/lithe-cli.svg)](https://pypi.org/project/lithe-cli/)
52
+ [![License: MIT](https://img.shields.io/pypi/l/lithe-cli.svg)](https://pypi.org/project/lithe-cli/)
53
+
54
+ **lithe-cli** is the command-line interface for
55
+ [lithe](https://pypi.org/project/lithe/) — the storage-free ReAct agent
56
+ kernel. Installing it pulls in `lithe` automatically and gives you a `lithe`
57
+ command: chat with an agent that reads and writes files in your workspace,
58
+ run one-shot tasks, inspect stored runs, and undo a run's mutations.
59
+
60
+ ```bash
61
+ pip install lithe-cli
62
+ ```
63
+
64
+ ## Configure
65
+
66
+ There is no default endpoint — the CLI refuses to run rather than silently
67
+ hitting some third-party URL. Point it at any OpenAI-compatible endpoint:
68
+
69
+ ```bash
70
+ export LITHE_API_KEY=sk-...
71
+ export LITHE_BASE_URL=https://your-endpoint/api/v1
72
+ export LITHE_MODEL=your-model
73
+ ```
74
+
75
+ Flags (`--api-key`, `--base-url`, `--model`) override the environment.
76
+ `LITHE_HOME` (default `~/.lithe`) locates the run store; the agent's
77
+ workspace defaults to the current directory (`--workspace` to change).
78
+
79
+ ## Use
80
+
81
+ ### One-shot task
82
+
83
+ ```bash
84
+ $ cd my-project
85
+ $ lithe run "总结 README.md 的要点,存到 SUMMARY.md"
86
+ ⚒ read_file {"path": "README.md"}
87
+ ✓ 已读取 README.md(120 行)
88
+ ⚒ write_file {"path": "SUMMARY.md", "content": "..."}
89
+ ✓ 写入 SUMMARY.md
90
+ 已把要点写入 SUMMARY.md。
91
+ ```
92
+
93
+ `--stream` streams tokens as they generate; `-v` adds per-call
94
+ usage/context gauges; `--max-steps` caps the tool loop;
95
+ `--context-window` enables fullness gauges.
96
+
97
+ ### Interactive chat
98
+
99
+ ```bash
100
+ $ lithe chat
101
+ lithe 0.1.0 — /exit 退出,输入任务开始。
102
+ lithe> 把 a.txt 改成三行待办清单
103
+ ⚒ write_file {"path": "a.txt", ...}
104
+ ✓ 写入 a.txt
105
+ 已完成。
106
+ lithe> /exit
107
+ ```
108
+
109
+ History carries across turns within a session (each turn is its own run in
110
+ the store, replayed as context for the next).
111
+
112
+ ### Undo a run
113
+
114
+ The bundled file and todo tools register reverters, so undo works with zero
115
+ configuration:
116
+
117
+ ```bash
118
+ $ lithe runs # find the run
119
+ abc123def456 done steps=3 ...
120
+ $ lithe undo abc123def456
121
+ 已撤销 2 个操作(run abc123def456)
122
+ ```
123
+
124
+ A file the run created is deleted; a file it overwrote is restored.
125
+
126
+ ### Inspect
127
+
128
+ ```bash
129
+ $ lithe runs # stored runs (newest last)
130
+ $ lithe log abc123def456 # messages + actions of one run
131
+ ```
132
+
133
+ ### Tools
134
+
135
+ ```bash
136
+ $ lithe tools # what the agent can do
137
+ apply_patch WRITE 以行级 patch 一次修改多个文件...
138
+ edit_file WRITE 局部替换文件中的文本...
139
+ read_file READ 读取文件内容...
140
+ ...
141
+ ```
142
+
143
+ The default tool set is the workspace bundle (`read_file` / `write_file` /
144
+ `edit_file` / `list_files` / `search_files` / `glob_files` /
145
+ `apply_patch`) plus todos (`update_todos` / `list_todos`).
146
+ `--download` additionally grants `download_file` — the SSRF-guarded,
147
+ size-capped network fetch — for runs and chats.
148
+
149
+ ## Where things land
150
+
151
+ | Path | Contents |
152
+ | --- | --- |
153
+ | current dir (or `--workspace`) | the agent's sandboxed workspace — every tool path resolves strictly inside it |
154
+ | `~/.lithe/runs` (or `--store`) | JSONL run store: messages, actions, undo records |
155
+ | `~/.lithe/runs/todos-<user>.json` | the agent's task list |
156
+
157
+ ## Design notes
158
+
159
+ - The CLI is a thin host: it supplies tools, a system prompt, and the
160
+ kernel's `JsonlRunStore`; everything else (ReAct loop, streaming, budgets,
161
+ replay, undo engine) is reused from lithe.
162
+ - End-to-end behavior is tested offline against a scripted transport — no
163
+ test spends tokens.
164
+ - `python -m lithe_cli` works alongside the `lithe` console script.
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,12 @@
1
+ lithe_cli/__init__.py,sha256=q1xPbNFxhW5ddYQe-uaaDxqRlBmmyAr0I3hoWZ2T9Nc,95
2
+ lithe_cli/__main__.py,sha256=i_VzFEuvHPLVcXcqC1UBy_7nfk0H86nyJLvTB3yF_0w,152
3
+ lithe_cli/agent.py,sha256=AXWPDbRzNqm-AkgaAXLrkxgqCkd6BUlMmLIyR1u0AJM,4615
4
+ lithe_cli/config.py,sha256=KlhI2HRtqGcCiNGPbOFVUeaxrginz9IQPEz7tduhMMs,3275
5
+ lithe_cli/main.py,sha256=s7TtMHyQ40U7HyJzePwREPGYJTEVr5w3imwSQ6NS_No,7226
6
+ lithe_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ lithe_cli-0.1.0.dist-info/licenses/LICENSE,sha256=MLG-37iuP9mqLJOtdaLDjt-_hTW-rKE4CLQaCz_215c,1078
8
+ lithe_cli-0.1.0.dist-info/METADATA,sha256=cbw3SaEaX3wc07viP8aXog6ig2KE5h563tTb-41GlKU,6031
9
+ lithe_cli-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
10
+ lithe_cli-0.1.0.dist-info/entry_points.txt,sha256=HzDcc8FMDzt1FbPppIi21MgGAy2f8tJqnpmFiiQzDGw,78
11
+ lithe_cli-0.1.0.dist-info/top_level.txt,sha256=Zep9WGTghjOTFQJvKKLf3w7zy1E_ZUgmUbXb8gPqOD8,10
12
+ lithe_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ lithe = lithe_cli.main:main
3
+ lithe-cli = lithe_cli.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 betaloop contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ lithe_cli