""" Mind CLI — 命令行入口。 通过 Click 定义 `mind` 命令族,内部委托给 _vendor/ 中的 Hermes CLI。 Phase 0 只实现 chat / ask / health 三个核心命令。 """ import click import json import os import sys # 确保 _vendor/ 已注入 sys.path import mindcli # noqa: F401 — 触发 __init__.py 的 sys.path 注入 @click.group(invoke_without_command=True) @click.version_option(version=mindcli.__version__, prog_name="mind") @click.pass_context def main(ctx): """MindOS NEXT CLI — Cloud Hermes 的本地执行节点。""" if ctx.invoked_subcommand is None: click.echo(ctx.get_help()) @main.command() @click.option("--model", "-m", default="", help="模型名称(默认使用配置文件)") @click.option("--skills", "-s", multiple=True, help="加载指定 skill") @click.option("--resume", "-r", default="", help="恢复指定会话 ID") def chat(model, skills, resume): """进入交互式 Chat(复用 Hermes CLI 的完整 TUI)。""" from cli import main as hermes_main # 构建 Hermes CLI 参数 kwargs = {} if model: kwargs["model"] = model if skills: kwargs["skills"] = ",".join(skills) if resume: kwargs["resume"] = resume hermes_main(**kwargs) @main.command() @click.argument("question") @click.option("--model", "-m", default="", help="模型名称") @click.option("--format", "fmt", default="text", help="输出格式: text/json/markdown") def ask(question, model, fmt): """单次查询(= hermes -q)。""" from cli import main as hermes_main kwargs = {"query": question} if model: kwargs["model"] = model hermes_main(**kwargs) @main.command() def health(): """显示本地 CLI 健康状态。""" import json status = { "ok": True, "version": mindcli.__version__, "python": sys.version.split()[0], "vendor": _get_vendor_commit(), "tunnel": "disconnected", # Phase 2 实现 "tools": 0, # Phase 2 实现 } click.echo(json.dumps(status, indent=2, ensure_ascii=False)) @main.command() @click.option("--port", "-p", default=8660, help="Health Server 端口") def start(port): """启动 Health Server(前台运行,Ctrl+C 退出)。""" from mindcli.health import start_health_server start_health_server(port=port, foreground=True) @main.command(name="install-service") def install_service(): """注册为 macOS launchd 服务(开机自启)。""" from mindcli.service import install_service as _install _install() @main.command(name="uninstall-service") def uninstall_service(): """注销 macOS launchd 服务。""" from mindcli.service import uninstall_service as _uninstall _uninstall() @main.command() def capabilities(): """显示本地可用工具和 MCP Server。""" from mindcli.capability import scan_capabilities cap = scan_capabilities() click.echo(json.dumps(cap, indent=2, ensure_ascii=False)) @main.group() def tunnel(): """管理 Cloud ↔ Local 隧道。""" pass @tunnel.command() def status(): """查看 Tunnel 连接状态。""" import urllib.request try: req = urllib.request.Request("http://127.0.0.1:8660/tunnel/status") with urllib.request.urlopen(req, timeout=2) as resp: data = json.loads(resp.read()) click.echo(json.dumps(data, indent=2, ensure_ascii=False)) except Exception: click.echo(json.dumps({"status": "disconnected", "note": "Health Server 未运行"}, indent=2)) @tunnel.command() @click.option("--url", default="wss://agent.brainwork.club/mindos-next/ws/cli-tunnel", help="Cloud Tunnel WebSocket URL") @click.option("--token", required=True, help="MindPass JWT") def connect(url, token): """手动建立 Tunnel 连接(通常由浏览器自动触发)。""" import urllib.request data = json.dumps({"token": token, "tunnelUrl": url}).encode() try: req = urllib.request.Request( "http://127.0.0.1:8660/tunnel/activate", data=data, headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=5) as resp: result = json.loads(resp.read()) click.echo(f"✅ Tunnel activate: {result}") except Exception as e: click.echo(f"❌ 失败: {e}") click.echo(" 确认 Health Server 已运行(mind start)") @main.group() def record(): """管理本地系统录音。""" pass @record.command(name="start") @click.option("--token", default="", help="MindPass JWT(默认使用 Tunnel 已有的)") @click.option("--chat-id", default="", help="对话 ID") @click.option("--source", type=click.Choice(["system", "mic"]), default="system", help="音频源:system=系统音频(默认),mic=麦克风") def record_start(token, chat_id, source): """开始录音(独立音频源 → Cloud ASR)。 双工模式下,CLI 和浏览器各自独立推送,不做混音。 """ import urllib.request body = json.dumps({"token": token, "chatId": chat_id, "source": source}).encode() try: req = urllib.request.Request( "http://127.0.0.1:8660/record/start", data=body, headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=10) as resp: result = json.loads(resp.read()) if result.get("ok"): click.echo(f"🎙️ 录音已开始 source={result.get('source')} meetingId={result.get('meetingId')}") click.echo(" 使用 `mind record stop` 停止") else: click.echo(f"❌ {result.get('error')}") except Exception as e: click.echo(f"❌ 失败: {e}") click.echo(" 确认 Health Server 已运行(mind start)") @record.command(name="stop") def record_stop(): """停止录音。""" import urllib.request try: req = urllib.request.Request( "http://127.0.0.1:8660/record/stop", data=b"{}", headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(req, timeout=10) as resp: result = json.loads(resp.read()) if result.get("ok"): click.echo(f"⏹️ 录音已停止 duration={result.get('duration')}s") else: click.echo(f"❌ {result.get('error')}") except Exception as e: click.echo(f"❌ 失败: {e}") @record.command(name="status") def record_status(): """查看录音状态。""" import urllib.request try: req = urllib.request.Request("http://127.0.0.1:8660/record/status") with urllib.request.urlopen(req, timeout=2) as resp: data = json.loads(resp.read()) if data.get("running"): click.echo(f"🎙️ 录音中 duration={data.get('duration')}s chatId={data.get('chatId')}") else: click.echo("⏹️ 未在录音") except Exception: click.echo("⏹️ Health Server 未运行") def _get_vendor_commit() -> str: """读取 _vendor/HERMES_COMMIT 文件获取 vendor 版本。""" commit_file = os.path.join(mindcli._VENDOR_DIR, "HERMES_COMMIT") try: with open(commit_file) as f: return f.read().strip() except FileNotFoundError: return "unknown" if __name__ == "__main__": main()