Skip to content
dsh.fish
Bundle

dsh-cli-interactive

Interactive terminal (REPL) profile bundle for DeepSeek Harness — a Claude Code-style CLI surface over dsh-base

Source
studyinglx
stars
1 stars
License
MIT
Updated
Updated 3 days ago

Readme

# dsh-cli-interactive

在无头服务器上把 DeepSeek Harness 变成 **Claude Code 风格的交互式终端 CLI**。

这是一个 out-of-tree 的 dsh **profile bundle**:叠加在官方 `dsh-base` 之上,
复用 dsh 的全部核心能力(agent 循环、会话持久化、工具、凭据、settings、沙箱审批),
只把官方 `dsh-headless` 的"提交一次任务就退出"换成**持久 REPL 循环**——
同一个 agent 反复对话、回复流式输出、审批在终端应答、每轮结束后会话落盘、
随时 `--resume` 找回旧会话。不挂载任何 Host / HTTP / 浏览器插件。

## 特性

- 交互式 REPL:输入即消息,agent 回复流式打印(增量轮询 session 事件日志)
- 会话持久化:每轮结束后 flush 到 `$DSH_HOME/sessions/*.jsonl`(默认 `~/.dsh/sessions`)
- `--resume <sessionId>`:恢复旧会话继续聊(复用官方 `agents.resume()`)
- 审批应答:默认 `workspace-write + ask` 策略下的权限询问直接在终端 y/n 应答
- slash 命令:`/help /sessions /model /permission /cancel /session /workspace /exit`
- 忙时输入不丢:agent 执行期间键入的内容排队,下一轮自动发出
- Ctrl-C 中断当前轮(`agent.cancel({kind:'user'})`),再按一次退出
- 启动参数:`--model --provider --preset --once`,任务位置参数可选(先跑一轮再进 REPL)
- 输入历史持久化:`$DSH_HOME/cli.history`,上下键翻查,重进保留
- Tab 补全:`/he`+Tab → `/help`;`/model dee`+Tab → 模型名
- `--once`:跑完任务(或一轮)即退出,脚本友好(`dsh --profile cli --once "task"`)

### 界面

- 紧凑 banner 盒:session id / 会话标题 / workspace·模型(provider)·策略 / 快捷提示(长路径自动掐头去尾)
- 会话标题:首轮 prompt 截断生成(`--task` 或 resume 旧会话自动恢复),横幅 + 状态行显示
- 每轮状态行:`dsh-cli · session-… · "标题" · 模型 · 策略 · cwd`
- 提示符带模型名:`you (deepseek-v4-flash)> `,`/model` 切换后立即反映
- 流式输出:`assistant [step N]>`,step 推进处插入 `[step N]` 分隔(仅行首,不切断单词)
- 内联工具行:工具调用在文本流中安全插入 `⚙ bash …` 行(仅行边界,绝不切断单词;行中则走活动行)
- 工具活动:`⚙ step N · bash …`(内容变化时才原位重绘:新 step / 工具开始 / 工具完成,绝不逐帧刷屏),完成 `✓`;轮末摘要 `⚙ N tool call · S steps · ↑in ↓out tok`
- 错误轮次提示:turn 出错后提示重试 / 退出,不静默继续
- `/help` 分组排版:session / info / control 三组
- 审批提示友好化:`⚠ approval required` 框内分层显示工具名(亮黄粗体)/模式/原因
- 自适应终端:TTY 下彩色 + Unicode 圆角框;管道/`NO_COLOR` 下自动降级纯文本(`FORCE_COLOR=1` 可强制着色)

## 目录结构

```
dsh-cli-interactive/
├── package.json          # 声明 dsh.bundle.patch → 安装时自动加入 profile 层栈
├── cordis.patch.yml      # 组合层(不要动插件行,只可改 persona 等配置值)
├── lib/
│   ├── ui.js             # ANSI 样式/布局助手(零依赖):色板、可见宽度、box、表格、spinner、工具摘要
│   ├── startup.js        # 命令行参数(--resume/--model/--provider/--preset/[task]/--once)
│   └── runner.js         # REPL 主循环——banner、you> 提示符、流式输出、slash 命令、审批提示、会话横幅
├── bin/dsh-cli.js        # 便捷入口:dsh-cli ≡ dsh --profile cli
├── scripts/build.mjs     # 构建脚本(node --check 语法校验 + 组装 dist/,零依赖)
├── tests/unit.mjs        # 单元冒烟测试(node tests/unit.mjs)
└── README.md
```

## 构建

零工具链、零网络依赖(Node ≥ 22 自带):

```bash
node scripts/build.mjs          # 语法校验 + 生成 dist/
npm run check                   # 只做语法校验
```

产物在 `dist/`:`lib/ + bin/ + cordis.patch.yml + package.json + README + LICENSE`。

## 安装到 dsh(服务器 / 本机)

前置:Node 22 + 全局 `@deepseek-ai/dsh`(`npm i -g @deepseek-ai/dsh`)+
pnpm(`npm i -g pnpm`,`dsh plugin` 转发给 pnpm),
密钥放 `~/.dsh/.env`(`DEEPSEEK_API_KEY=sk-...`)。

`dsh plugin` 首次使用会初始化 profile(bundles 起始为 `[dsh-base]`),
然后转发 pnpm 安装你的包;因为包声明了 `dsh.bundle`,**会自动加入
`dsh.profile.bundles` 层栈**(无需手改 manifest)。

### 方案 A:从 GitHub 拉取(通用)

任意一台装有 dsh 的机器(服务器 / 本机),clone 本仓库后构建出 `dist/`
再安装:

```bash
# 1. 拉取源码
git clone https://github.com/studyinglx/dsh-cli.git
cd dsh-cli

# 2. 构建出 dist/(零工具链,Node ≥ 22 自带即可)
node scripts/build.mjs

# 3. 安装到 dsh profile(本地路径指向 dist/)
dsh plugin --profile cli add file:$PWD/dist

# 4. 验证组合层(不启动)
dsh --profile cli --dump-config

# 5. 使用
dsh --profile cli                                    # 进入 REPL
dsh --profile cli "先 review 一下代码库"              # 先跑一轮,然后进入 REPL
```

> - `dsh plugin` 首次运行会自动初始化 `cli` profile(bundles 起始为 `[dsh-base]`)。
> - 已安装过旧版本时,先 `dsh plugin --profile cli remove dsh-cli-interactive` 再 add,
>   或重新 add 覆盖(pnpm 会按路径重装)。
> - 拉取到的源码可直接改(lib/、cordis.patch.yml),改完 `node scripts/build.mjs`
>   重建再 add 即生效。

### 方案 B:本地路径 / 发布到 npm

```bash
# 从 dist 目录安装(本地路径)
dsh plugin --profile cli add file:/path/to/dsh-cli-interactive/dist

# 或发布到 npm 后按包名安装
npm publish
dsh plugin --profile cli add dsh-cli-interactive

# 验证组合层(不启动)
dsh --profile cli --dump-config
```

## 使用

```bash
cd /srv/workspace            # 启动目录 = workspace 根

dsh --profile cli                                    # 进入 REPL
dsh --profile cli "先 review 一下代码库"              # 先跑一轮,然后进入 REPL
dsh --profile cli --once "先 review 一下代码库"        # 跑一轮即退出(脚本友好)
dsh --profile cli --resume session-xxxx              # 恢复旧会话
dsh --profile cli --model deepseek-v4-pro            # 指定模型
dsh-cli --resume session-xxxx                        # 或用包装命令
```

### slash 命令

| 命令 | 作用 |
|---|---|
| `/help` | 命令列表(分组排版) |
| `/sessions` | 列出 `$DSH_HOME/sessions` 下最近的 20 个会话(递归嵌套布局,id/大小/时间) |
| `/session` | 显示当前会话 id |
| `/workspace` | 显示 workspace 根目录 |
| `/model [id]` | 查看 / 切换模型(下一轮生效;如 `deepseek-v4-flash`、`deepseek-v4-pro`) |
| `/permission [ask\|never]` | 查看 / 切换审批策略(写会话日志,模型会看到通知) |
| `/cancel` | 取消正在运行的轮次 |
| `/exit` `/quit` | 退出(会话已持久化) |

### 长任务 / 值守

agent 一轮内部会自主多轮调用工具直到停稳(`whenIdle()`),所以单条消息可以持续
几分钟到几小时;进程被杀则中断(会话日志保留,`--resume` 可续)。用 tmux 保活:

```bash
tmux new -s dsh 'cd /srv/workspace && dsh --profile cli'
# Ctrl-b d 分离,随时 tmux attach -t dsh 回来
```

> **管道模式(脚本友好)**:`echo "任务" | dsh --profile cli --once` 也可以正常
> 工作——readline 监听器在任何异步初始化之前注册,管道数据/EOF 不会丢失。
> 空输入(立即 EOF)同样干净退出。

## 架构说明(改动点对照)

| 官方 dsh-headless | 本包 |
|---|---|
| `headless-startup` 只解析任务位置参数 | `cli-startup` 增加 `--resume/--model/--provider/--preset/--once` |
| `headless-runner`:提交 1 次 → 打印 → 退出 | `cli-runner`:REPL 循环 + 流式 + 审批应答 + slash 命令 |
| 无审批应答(无人应答 → `unavailable` → 拒绝) | 在 `setup` 中注册 `approval/request` waterfall 监听器,终端 y/n 应答 |
| `agents.create()` 一次 | `agents.create()` / `agents.resume()`(`resumeSessionId` 字段) |
| 退出码 = 本轮结果 | 交互退出码 0;轮次 error 提示但不退出 |

## 硬性约束

1. **apply(ctx, config) 同步契约**:apply 内绝不能 await loader(会死锁导致启动无输出);
   `ctx.config` 在 apply 返回后不可读,config 必须从 apply 第二参数取。
2. **改完必须运行 `node scripts/build.mjs`** 做语法校验(`npm run check` 只校验不打包)。
3. **不要改 cordis.patch.yml 中的插件 id/name/inject 结构**,只可改 persona 等配置值。
4. 不挂载 Host / HTTP / 浏览器插件,保持 headless 终端形态。
5. 零工具链、零网络依赖构建(Node ≥ 22)。
6. 会话落盘到 `$DSH_HOME/sessions/<project>/<id>/session.jsonl[.zstd]`(嵌套布局,
   `/sessions` 按此递归遍历)。
7. 非 TTY / 管道下输出必须可 grep、无转义垃圾(颜色/spinner/Unicode 框全部按 TTY 判定降级)。

## 已知限制

- **单 agent 单进程**:一个 CLI 进程一个会话;多会话 = 多开 tmux/systemd 实例。
- **进程内会话**:会话仅存在于运行中的进程(与 web/headless 一致),重启后靠
  `$DSH_HOME/sessions/*.jsonl` + `--resume` 恢复。
- **无 web 功能**:没有浏览器 UI、/api、多客户端并发。
- **审批在终端**:无人值守(如 cron 里跑)时审批请求会卡住等待输入——
  请用 `/permission never` 或 `DSH_PERMISSION_MODE=danger-full-access` 预置策略。
- 非 TTY(管道)模式下不绘制 spinner/活动行,仅输出文本与轮末摘要。
- 默认模型/凭据解析与官方一致(`~/.dsh/settings.yaml` 的 `llm-deepseek` 段热加载;
  `DEEPSEEK_API_KEY` 环境层优先)。

## 开发提示

- 依赖解析:bundle 的插件名 = 包名(`dsh-cli-interactive`)+ `/startup` 子路径,
  与官方 `@deepseek-ai/dsh-headless/startup` 同款约定。
- **apply 必须同步**(官方 headless 契约):runner 的 `apply(ctx, config)` 只做
  `run(ctx, config).catch(...)` 就返回,`run` 内部才 `await loader`。
  async apply 会与 loader settle 互相等待而死锁(启动无任何输出);
  `ctx.config` 在 apply 返回后不可读,config 必须从 apply 第二参数取。
- 想加工具/技能:直接改 `cordis.patch.yml` 插入行,或在 profile 的
  `cordis.patch.yml`(`$DSH_HOME/profiles/cli/cordis.patch.yml`)覆盖。
- 类型检查(可选):`npm i -D typescript @types/node` 后 `npx tsc -p tsconfig.json`。

Install

dsh plugin --profile web add github:studyinglx/dsh-cli

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source