CLI
状态:✅ packages/cli(v0.29)· 默认偏向本机 coding,写文件 / 跑命令前先问 · 终端 TUI(v0.24)· 网页工作台 tah serve(v0.25)· 约定式项目热更新(v0.26)· 附件(v0.27)· 项目配置能力包(v0.28)· Skills(v0.29)
模型回复边生成边显示(agent.assistant-stream),不必等整段 generate 结束。
命令
npx @typescript-agent-harness/cli run --mock "列出当前目录并说明这个项目"
npx @typescript-agent-harness/cli --mock chat全局安装:npm install -g @typescript-agent-harness/cli,之后直接 tah …。仓库内:pnpm build 后用 pnpm tah -- …。
| 命令 | 作用 |
|---|---|
tah run <prompt> | 一次性 Session |
tah chat | 多轮;默认续上一次持久化 Session;终端里是 TUI(v0.24),管道或 --plain 仍是逐行模式;/exit / /reset / /compact / /sessions / /resume / /fork |
tah serve | 在本机起网页工作台,浏览器里操作当前目录的 Agent(v0.25,见下文「网页工作台」) |
tah sessions | 列出已保存的 Session(最新在前;不需要 API key,v0.22) |
tah init | 在当前目录生成约定式项目骨架并装好依赖(v0.23,见下文「约定式项目」) |
tah help | 用法 |
tah version | 打印 CLI 版本(也可 --version / -v) |
标志
| 标志 | 作用 |
|---|---|
--cwd <path> | 工作区(工具的根目录) |
--mock | 本地假模型;没有 API key 时必须加,否则退出码 1 |
--persist | SQLite:<cwd>/.tah/cli.db(默认开) |
--no-persist | 关掉 SQLite;chat 不会跨 /exit 续聊 |
--quiet | 只打印模型回复;tah run 只在结束时输出最后一条回复,调工具前说的话不输出,适合脚本(v0.26.2) |
--exec | 挂上 execute_command(默认开) |
--no-exec | 关掉 execute_command |
--yes / -y | 跳过 write_file / edit_file / execute_command 的审批 |
--max-steps <n> | 每轮 LLM 上限(默认不限,Ctrl+C 停) |
--system-file <path> | 用文件内容替换默认角色「workspace coding agent」(v0.22) |
--session <id|n> | tah chat 续指定 Session,而不是最近一个(v0.22) |
--mcp <cmd> | 挂上 stdio MCP 工具(默认关) |
--mcp-arg <a> | --mcp 的额外参数(可重复) |
--allow <tool> | 白名单(可重复;不写则不限制) |
--deny <tool> | 黑名单(可重复;优先于 --allow) |
--once <ms> | 延迟后跑一轮 tah run(默认关;仅 run) |
--builtin-tools | 约定式项目有 tools/ 时,仍挂上内置的文件 / 命令工具(v0.23) |
--no-install | tah init 只生成文件,不装依赖(v0.23.1) |
--plain | tah chat 强制逐行模式,即使在终端里也不进 TUI(v0.24) |
--port <n> | tah serve 的端口(默认 7420;被占用时报错,不自动换,v0.25) |
--no-open | tah serve 不自动打开浏览器(v0.25) |
--no-watch | 约定式项目里不热更新(终端 tah chat / tah serve,v0.26) |
--vision | tah serve:图片附件以多模态 parts 发给模型(也可用 TAH_VISION=1,v0.27) |
不加 --mock 时必须有 DEEPSEEK_API_KEY 或 OPENAI_API_KEY(<cwd>/.env,以及仓库内 examples/basic-agent/.env)。没有 key 不会再静默 mock。只有 OPENAI_API_KEY 时连 OpenAI 官方(https://api.openai.com/v1,模型 gpt-4o-mini,v0.26.3);其他厂商走 OpenAI 兼容接口,还要设 OPENAI_BASE_URL 和 OPENAI_MODEL(DEEPSEEK_MODEL 可换 DeepSeek 的模型)。.env 里值为空的行(如 DEEPSEEK_API_KEY=)等于没写(v0.26.3)。也可以在 tah serve 网页左下角的 Settings 里添加模型(存在 ~/.tah/settings.json,选中的模型优先于 .env,tah chat / tah run 也会用它);tah serve 没有 key 也能启动,再去 Settings 里加(v0.30)。各家地址见 教程 · 接上真模型。
默认一轮跑到模型自己停下为止,中途 Ctrl+C 打断。传了 --max-steps 后,走到上限会打印 finishReason=max_steps(状态行的 steps 是本轮 LLM 次数);tah chat 在终端里会接着问「继续?」,回车就用新的一份额度继续同一轮,n 停下。tah run 或管道输入时不问,可之后在会话里说「继续」。
管道多轮:
printf '第一句\n第二句\n' | npx @typescript-agent-harness/cli --mock chat续聊(默认 persist):
tah --mock chat # 聊几句后 /exit
tah --mock chat # 应看到 resumed session …默认挂上的能力
LLM + list_files / read_file / grep / write_file / edit_file + execute_command + Agent Loop + SQLite。
MCP / scheduler 仍默认关。
execute_command 默认 30 秒超时;装依赖、构建、跑测试这类慢命令,模型会按次传 timeoutMs(最长 10 分钟)。--max-steps 对续上的旧 Session 同样生效。
pnpm tah -- --mock run "用 node 打印 1+1"审批(v0.22)
write_file、edit_file 和 execute_command 每次执行前会弹一张卡片(写文件时预览前 8 行内容,改文件时用 - / + 预览被替换的原文和新文本)。终端 TUI 里直接按 y / a / n(回车、Esc 也是拒绝),网页里点卡片上的按钮;--plain 逐行模式下输入字母后回车:
╭─ Run command
│ $ npm test
╰─
y allow once · a always allow execute_command · n / Enter reject ›| 回答 | 效果 |
|---|---|
y | 只放行这一次 |
a | 本次进程内该工具不再问(热更新后仍有效) |
| 其他 / 回车 | 拒绝;模型收到 rejected by user,应改问你怎么办 |
--yes/-y:全部放行,不问- stdin 不是终端(管道、CI)且没加
--yes:直接拒绝,避免把管道里的下一行当成回答 - 被
--deny拦下的工具不会弹审批
printf '把 README 第一行改成 Hello\n' | tah --yes chat # 脚本里要写文件必须 --yes多个 Session:列出、切换、分叉(v0.22)
tah sessions
# 1 session_31_muyxw3su completed 2 turns 2026-10-08 10:50 帮我看下 auth 模块…
# 2 session_1_muyxvyp2 completed 3 turns 2026-10-08 10:42 跑一下单测…
tah chat --session 2 # 序号或完整 idchat 里:
| 命令 | 作用 |
|---|---|
/sessions | 列出 Session,* 是当前这个 |
/resume <id|n> | 切到另一个 Session |
/fork | 复制当前 Session 到新 Session 并切过去;原 Session 不变(想试另一条路时用) |
/fork <n> | 只保留前 n 轮再分叉(「回到第 n 轮重来」) |
/compact | 把到目前为止的对话压缩成摘要,之后只发摘要和新对话(0.31,见「长会话上下文」) |
序号随最近更新时间变化,以最近一次 tah sessions / /sessions 为准。分叉不需要 persist;--session / /resume 需要(默认开)。
自定义角色(v0.22)
echo "You are a strict code reviewer. Report bugs first, cite file:line. Answer in Chinese." > reviewer.md
tah --system-file reviewer.md chat- 只替换角色那一句;工具使用规则、exec 提示仍会追加在后面
- 续上一次 Session 时同样生效(存下来的旧 system 会被换掉)
- 每次启动都要带上;不带就回到默认 coding 角色
约定式项目(v0.23)
不写启动代码,按约定放文件就能做出自己的 Agent:
mkdir my-agent && cd my-agent
npx @typescript-agent-harness/cli init # 生成文件并装依赖
npx tah --mock chattah init 生成文件后自动装依赖(v0.23.1):目录里有 pnpm-lock.yaml / yarn.lock / package-lock.json 就用对应的包管理器,否则用启动它的那个(pnpm dlx → pnpm),默认 npm。项目放在别的 pnpm 工作区(上层有 pnpm-workspace.yaml)里时,用 pnpm install --ignore-workspace,否则 pnpm 会去装外层工作区、跳过这个项目。装失败或加了 --no-install,会打印要自己跑的命令。
my-agent/
├── AGENTS.md # 人设和规则,替换默认的 coding agent 角色
├── tah.config.json # 可选:官方能力包 / 扩展(v0.28)
├── tools/ # 一个文件 = 一个工具
│ └── query-order.ts # → 工具 query_order
├── plugins/ # 一个文件 = 一个插件(可选)
│ └── audit-log.ts # → 插件 project:audit-log
├── skills/ # Agent Skills(Markdown 指令,v0.29)
│ └── hello/SKILL.md # init 示例;模型用 skill 工具或 /hello 加载正文
├── lib/ # 共用代码,不自动加载(改了会热更新)
└── package.json # 依赖 @typescript-agent-harness/cli项目配置 tah.config.json(v0.28)
约定目录管角色与自定义工具;根目录可选 tah.config.json 管官方能力(coding / vision)和 extensions.mcp。
{
"capabilities": { "coding": true, "vision": true },
"extensions": {
"mcp": { "command": "npx", "args": ["-y", "some-mcp-server"] }
}
}合并顺序:内置默认 → config → CLI / 环境变量。键位、与 tools/ 的关系、上网(capabilities.web:web_fetch 默认开,web_search 填 Tavily key 后开;capabilities.browser:内置浏览器,browser_open 批准后弹出 Chrome)见配置参考:tah.config.json。
工具文件:
// tools/query-order.ts
import { defineTool } from "@typescript-agent-harness/cli";
export default defineTool({
description: "按订单号查询订单状态",
inputSchema: {
type: "object",
properties: { orderId: { type: "string" } },
required: ["orderId"],
},
async execute(input: { orderId: string }, ctx) {
return { id: input.orderId, status: "shipped" };
},
});插件文件:export default definePlugin({ setup(ctx) { … } }),可以 ctx.on(...) 监听事件、ctx.get(TOOLS).register(...) 按条件加工具。
规则:
- 何时生效:只有
--cwd(默认当前目录)的package.json在dependencies或devDependencies里有@typescript-agent-harness/cli时才算项目。普通代码仓库里的tools/、plugins/、AGENTS.md不会被加载。 - 人设:
AGENTS.md替换默认角色;--system-file优先于AGENTS.md。 - 内置工具:有
tools/目录时,list_files/read_file/grep/write_file/edit_file/execute_command和对应的提示词规则都不挂(web_fetch/web_search也跟着不挂,除非tah.config.json写"web": true);加--builtin-tools恢复。只有AGENTS.md没有tools/时,内置工具照常挂上。 - 工具名:文件名去掉扩展名、
-换成_;必须是小写字母开头的a-z0-9_。defineTool({ name })可以覆盖。 - 扫描:只扫
tools/、plugins/第一层的.ts/.mts/.js/.mjs;跳过_开头、*.test.*、*.spec.*、.d.ts和子目录。按文件名排序加载,插件间有先后依赖时用01-前缀。 - 注册顺序:项目插件在所有内置插件之后注册,setup 时 LLM、Tools、Session 都已可用。
- TypeScript:用 jiti 加载,不用编译;可以
import "../lib/x.ts"。 - 出错:文件加载失败、导出不对、名字不合法、重名,都在启动时报错并给出文件路径,退出码 1。运行中改出错不会退出,见下文「热更新」。
- 审批:自定义工具执行前不询问。
启动行会多一行 [tah] project: AGENTS.md tools+1 plugins+1;有可用 skill 时还会带 skills+N。完整示例见 examples/convention-agent(pnpm demo:convention);从零做到部署的教程见 做一个文档问答 Agent。
Skills(v0.29)
skills/<name>/SKILL.md 放可复用流程说明(对齐 Agent Skills):启动只在 system prompt 里列出 name + description;模型用内置 skill 工具加载正文,或在 tah chat / tah serve 里输入 /hello 这类 slash(UI 仍只显示你打的那行字)。也读 .agents/skills/、.cursor/skills/ 与 ~/.tah/skills/(优先级与格式见 Skills 设计)。tah init 会生成 skills/hello/SKILL.md 作试玩示例。工具名 skill 由 CLI 保留,不要在 tools/ 里占同名文件。
热更新(v0.26)
终端里的 tah chat 和 tah serve 会监听 AGENTS.md、tools/、plugins/、lib/(含子目录),以及 skills/、.agents/skills/、.cursor/skills/ 与(若存在)~/.tah/skills/(v0.29)。保存后自动重新加载,当前会话和历史都保留,新的人设、工具与 skill catalog 从下一轮起生效:
[tah] reloaded: AGENTS.md tools+1 plugins+1- 一轮进行中(包括等待审批)改了文件:等这一轮结束再重载,不会中途换工具。重载期间发的消息会等重载完成再发。
- 新文件有错(语法错误、导出不对、重名、插件
setup抛错):继续用旧版本,提示[tah] reload failed — still using the previous version: …,改对后保存即可。 - 重载会重建整个 Runtime:插件的
dispose会被调用,MCP 子进程会重启;「总是允许」的审批选择保留。 --no-persist时会话只在内存里,重建会丢,所以只提示… changed — restart to apply (persist is off),不重载。- 其他文件(
package.json、.env、--system-file指定的文件、lib/以外的目录)改了要重启。tah run、管道 /--plain逐行模式没有热更新。--no-watch关闭。
设计说明见 约定式项目热更新。
终端 UI(v0.24)
终端里直接 tah chat 进入 Ink TUI(内联滚动区,不切备用屏):流式 Markdown、工具调用行、界面内审批(y / a / n)、斜杠命令补全、会话选择器、多行输入(Alt+Enter 或行尾 \ 换行,粘贴整段进输入框)。管道、CI、--plain 仍是原来的逐行模式。
| 操作 | 行为 |
|---|---|
| Enter | 发送 |
Alt+Enter / Shift+Enter(支持的终端)/ 行尾 \ + Enter | 换行 |
/ | 命令菜单;/resume 不带参数打开会话列表 |
| Ctrl+C | 回复中:停本轮;有输入:清空;空闲:退出 |
| Ctrl+D | 空闲且输入为空时退出 |
设计说明见 TUI 与会话接口。
网页工作台(v0.25)
tah serve # 打开 http://127.0.0.1:7420/?token=…
tah serve --yes --port 8000 --no-opentah serve 在本机起服务并打开浏览器,网页里操作的就是当前目录的 Agent:流式 Markdown、工具调用行、审批卡片(允许一次 / 总是允许 / 拒绝)、步数上限的「继续」、会话侧栏(切换 / 新建 / 分叉)、停止当前轮。和 TUI 一样同一时间只有一个当前会话;开多个标签页看到的是同一个会话,操作实时同步,刷新或断线重连后从服务端补齐。tah chat 的其他参数(--yes、--max-steps、--no-persist、--session、--mock …)照常可用。
工作文件夹(v0.30):侧栏按文件夹分组列出会话(Pinned 跨文件夹置顶);「Workspaces」标题旁的文件夹图标有两项:「Open project…」先列最近移除过的项目,「Choose folder…」弹系统的选文件夹对话框(macOS / Windows;其他系统用网页里的目录浏览器),「New folder…」默认在桌面新建(可换位置)并直接打开。左下角齿轮打开 Settings:Models 管理模型,Web search 填 Tavily key 打开 web_search(v0.31),Skills 列出已加载的 skill,并可把 ~/.cursor/skills 这类文件夹里的 skill 导入 ~/.tah/skills(v0.30.1,见 Skills)。文件夹列表存在 ~/.tah/workspaces.json。打开另一个文件夹里的会话(或点该组的 +)时,Agent 的工作目录随之切换;运行中的一轮结束前不能切换。.env 始终用启动时那个目录的。见 工作文件夹设计。
它是本机工作台,不是对外服务:
- 只监听
127.0.0.1。要远程用,走 SSH 端口转发(ssh -L 7420:127.0.0.1:7420 …)。 - 每次启动生成一次性令牌,只有终端打印的链接能登录;网页拿令牌换成 HttpOnly cookie 后从地址栏去掉令牌。
- 校验
Host/Origin头,挡住其他网页伪造请求。 - 可附加文件与图片(选择 / 拖拽 / 粘贴):存到
<cwd>/.tah/attachments/,默认把只读路径写进 user 文本供工具读取;--vision时图片进多模态请求。见 附件设计。
Ctrl+C 停止服务并打印续聊命令(tah chat --session …),再按一次强制退出。
改网页时:先 tah serve --no-open,再在 packages/web 里 pnpm dev,Vite 把 /api 代理到 127.0.0.1:7420;打开 Vite 的地址并带上终端里的 ?token=…。
设计说明见 tah serve 与网页工作台。
Ctrl+C 与失败重试(v0.22)
| 场景 | 行为 |
|---|---|
tah chat(TUI)正在跑一轮(含等审批) | 中断这一轮;历史保留 |
tah chat(TUI)空闲 | 有输入则清空,否则退出(同 /exit) |
tah chat --plain 正在跑一轮 | 中断这一轮,回到 you>;历史保留 |
tah chat --plain 空闲在 you> | 退出(同 /exit) |
tah run | 直接退出 |
| 模型接口网络错误 / 429 / 5xx | 自动重试 3 次(1s、2s、4s),打印 [llm] … retry N in Xs |
| 重试仍失败 | chat 打印失败提示并继续可聊,不退出 |
模型在调用工具前说的话(如「我先读一下文件」)会单独成行,和后面的 [tool] 日志分开;最终回答若复述了过程,那是模型行为,不是重复打印。
输出样式
在终端里,模型回复的 Markdown 用 marked-terminal 的默认样式渲染(标题、粗体、行内代码、列表、带边框的表格、代码块语法高亮);[tah] / [llm] / [tool] / [ctx] 日志变灰;审批卡片用黄色边框。
- 按 Markdown 块显示:一段文字、一个列表、一张表或一个代码块收完才出现(不再逐字出现)
- 输出不是终端(管道、重定向)、设置了
NO_COLOR、或TERM=dumb时,原样输出纯文本
长会话上下文(v0.22)
每次请求最多发约 100,000 字符(约 25k token 的英文/代码;中文更费)。超了会打印:
[ctx] trimmed: 2 old turn(s) dropped, 5 tool output(s) omitted, 98712 chars sent (full history kept; /reset for a fresh session)先省略旧的工具输出,再整轮丢最早的对话;当前这一轮始终完整。SQLite 里的完整历史不删。话题换了就 /reset,比靠裁剪更省 token。
从 0.31 起,超出预算时会先尝试压缩:让模型把之前的轮次总结成一份摘要,之后的请求只发摘要和新的对话。压缩成功会打印:
[ctx] compacted: 3 earlier turn(s) summarized in 1840 chars (full history kept)压缩失败(例如模型报错)时退回上面的裁剪。也可以随时手动压缩:终端里输入 /compact,网页点状态栏右侧的 Compact。压缩过程中 Ctrl+C(网页上点停止)可以中止。
关掉命令执行:
pnpm tah -- --no-exec --mock chat挂 stdio MCP(子进程,不是官方 SDK;约定式项目也可写在 tah.config.json 的 extensions.mcp,见 配置参考):
pnpm tah -- --mcp node --mcp-arg ./packages/mcp/test/fixtures/ping-server.mjs --mock "调用 ping"只读策略:
pnpm tah -- --deny write_file --deny execute_command --mock "只读这个仓库"延迟一轮:
pnpm tah -- --once 200 --mock run "列出当前目录"