diff --git a/cli/README.md b/cli/README.md
new file mode 100644
index 0000000..c31819e
--- /dev/null
+++ b/cli/README.md
@@ -0,0 +1,68 @@
+# TJWater CLI
+
+独立于服务端主代码的 Python CLI 文件夹,放在 `TJWaterServerBinary/cli/` 下,供 agent 服务器**直接调用并通过 stdout/stderr 参与管道**。
+
+## 直接使用
+
+```bash
+cd TJWaterServerBinary/cli
+./tjwater help --json
+```
+
+这个入口文件可以直接参与管道:
+
+```bash
+./tjwater help --json | jq
+```
+
+它会优先使用:
+1. `cli/.venv/bin/python`
+2. 环境变量 `PYTHON`
+3. 当前环境里的 `python`
+4. 最后回退到 `python3`
+
+如果需要,也可以显式走 Python:
+
+```bash
+python -m tjwater_agent_cli help --json
+```
+
+## 部署到 agent 服务器
+
+最简单的方式是把整个 `TJWaterServerBinary/cli/` 文件夹同步到 agent 服务器,然后直接执行:
+
+```bash
+cd TJWaterServerBinary/cli
+./tjwater help --json
+```
+
+如果希望放到 PATH 中:
+
+```bash
+chmod +x tjwater
+ln -s /path/to/TJWaterServerBinary/cli/tjwater /usr/local/bin/tjwater
+tjwater help --json
+```
+
+## Python 依赖
+
+```bash
+cd TJWaterServerBinary/cli
+python -m pip install -r requirements.txt
+```
+
+只保留运行 CLI 必需依赖,不再包含安装包构建相关内容。
+
+## 认证上下文
+
+CLI 通过 `--auth-context` 读取 JSON 文件。常用字段:
+
+```json
+{
+ "server": "http://backend-host:8000",
+ "access_token": "...",
+ "project_id": "...",
+ "network": "...",
+ "username": "..."
+}
+```
diff --git a/cli/pyrightconfig.json b/cli/pyrightconfig.json
new file mode 100644
index 0000000..39b6dd1
--- /dev/null
+++ b/cli/pyrightconfig.json
@@ -0,0 +1,14 @@
+{
+ "include": [
+ "tjwater_agent_cli",
+ "tests"
+ ],
+ "executionEnvironments": [
+ {
+ "root": ".",
+ "extraPaths": [
+ "."
+ ]
+ }
+ ]
+}
diff --git a/cli/requirements.txt b/cli/requirements.txt
new file mode 100644
index 0000000..8eb3f9b
--- /dev/null
+++ b/cli/requirements.txt
@@ -0,0 +1,3 @@
+click>=8.1,<9
+requests>=2.31,<3
+typer>=0.12,<1
diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py
new file mode 100644
index 0000000..17cdbe1
--- /dev/null
+++ b/cli/tests/conftest.py
@@ -0,0 +1,6 @@
+from pathlib import Path
+import sys
+
+ROOT = Path(__file__).resolve().parents[1]
+if str(ROOT) not in sys.path:
+ sys.path.insert(0, str(ROOT))
diff --git a/cli/tests/unit/test_tjwater_cli.py b/cli/tests/unit/test_tjwater_cli.py
new file mode 100644
index 0000000..c546396
--- /dev/null
+++ b/cli/tests/unit/test_tjwater_cli.py
@@ -0,0 +1,306 @@
+from pathlib import Path
+
+from typer.testing import CliRunner
+
+from tjwater_agent_cli import core
+from tjwater_agent_cli.main import app, main
+
+
+runner = CliRunner()
+
+
+class DummyResponse:
+ def __init__(self, *, status_code=200, json_data=None, text="", headers=None, content=None):
+ self.status_code = status_code
+ self._json_data = json_data
+ self.text = text
+ self.headers = headers or {"content-type": "application/json"}
+ self.content = content if content is not None else text.encode("utf-8")
+
+ @property
+ def ok(self):
+ return 200 <= self.status_code < 300
+
+ def json(self):
+ if self._json_data is None:
+ raise ValueError("no json")
+ return self._json_data
+
+
+def test_load_auth_context_supports_aliases(tmp_path: Path):
+ auth_path = tmp_path / "auth.json"
+ auth_path.write_text(
+ '{"base_url":"http://server","token":"abc","projectId":"p1","userId":"u1","username":"tester","projectCode":"net1"}',
+ encoding="utf-8",
+ )
+
+ auth = core.load_auth_context(auth_path)
+
+ assert auth.server == "http://server"
+ assert auth.access_token == "abc"
+ assert auth.project_id == "p1"
+ assert auth.user_id == "u1"
+ assert auth.username == "tester"
+ assert auth.network == "net1"
+
+
+def test_build_runtime_context_uses_default_server(monkeypatch):
+ monkeypatch.delenv("TJWATER_SERVER", raising=False)
+ monkeypatch.delenv("TJWATER_ACCESS_TOKEN", raising=False)
+ monkeypatch.delenv("TJWATER_PROJECT_ID", raising=False)
+ monkeypatch.delenv("TJWATER_USER_ID", raising=False)
+ monkeypatch.delenv("TJWATER_USERNAME", raising=False)
+ monkeypatch.delenv("TJWATER_NETWORK", raising=False)
+ monkeypatch.delenv("TJWATER_EXTRA_HEADERS", raising=False)
+
+ runtime = core.build_runtime_context(
+ server=None,
+ auth_context_path=None,
+ scheme=None,
+ timeout=core.DEFAULT_TIMEOUT,
+ request_id="req-1",
+ )
+
+ assert runtime.server == core.DEFAULT_SERVER
+
+
+def test_help_json_lists_commands():
+ result = runner.invoke(app, ["help", "--json"])
+
+ assert result.exit_code == 0
+ assert '"schema_version": "tjwater-cli/v1"' in result.stdout
+ assert '"command": "project"' in result.stdout
+ assert '"command": "analysis"' in result.stdout
+ assert '"menu_level": 1' in result.stdout
+ assert '"command": "project list"' not in result.stdout
+
+
+def test_help_defaults_to_text():
+ result = runner.invoke(app, ["help"])
+
+ assert result.exit_code == 0
+ assert "Commands:" in result.stdout
+ assert "project: 项目与项目级元数据相关命令。" in result.stdout
+ assert "analysis: 分析计算与诊断相关命令。" in result.stdout
+ assert "Use `tjwater