cofluxd 0.13.0 → 0.15.0

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.
package/README.md CHANGED
@@ -34,16 +34,22 @@ CLI 还会取自身与 worker 的持久 release floor 较大值拒绝远端降
34
34
  跑在 coflux 终端里的 claude/codex 可以用下面几条,把工作外化成用户在 web/手机上**看得见、能接管**的东西——而不是在自己的 Bash 里后台起一个谁也看不见的进程:
35
35
 
36
36
  ```sh
37
- cofluxd terminal new --title "跑单测" --cmd "pnpm test" # 开真实终端,用户可接管
37
+ cofluxd terminal new --title "跑单测" --cmd "pnpm test" # 作业终端:跑完即退,带退出码
38
+ cofluxd terminal new --title "调试 shell" # 会话终端:不带命令 = 常驻登录 shell
38
39
  cofluxd terminal list # 本工作区的终端 + 状态/退出码
39
40
  cofluxd terminal read <taskId> [--lines N] # 读终端内容(纯文本,已退出也能读)
41
+ cofluxd terminal wait <taskId> [--timeout <秒>] # 阻塞到退出,打印退出码
42
+ cofluxd terminal send <taskId> --text "y" --enter # 往终端输入;用户正在接管时被拒
43
+ cofluxd progress "复现了,正在定位" # 播报进度:显示在工作区卡片上
40
44
  cofluxd notify "需要你定一下用哪个方案" # 叫人:侧栏转「等待交互」
41
45
  cofluxd ports # 端口 + 可直接打开的预览 URL
42
46
  ```
43
47
 
44
- 不需要任何凭证:daemon 用调用方 pid 反查进程树确认它属于哪个会话,**coflux 会话之外的进程一律拒绝**,权限也天然限定在该会话所属的工作区内。
48
+ `terminal new` 带不带 `--cmd` 是两种终端:带命令是**作业终端**,命令包成脚本交登录 shell 跑,跑完终端退出并带退出码,输出另落一份日志供 `read` 回读(代价是命令的 stdout 是管道而非 tty,颜色/进度条/全屏程序都没有);不带命令是**会话终端**,等价于用户在侧栏点「新建终端」——工作区目录下的默认登录 shell,stdin/stdout 都是真 tty,不会自己退出,直到 agent 或用户输入 `exit`。会话终端没有命令日志,`read` 读的是当前画面(一屏),首次 `send` 前要先 `read` 等提示符。
45
49
 
46
- 每个 coflux 开出来的 PTY 会话里还注入了一组 `COFLUX_*` 环境变量(由 supervisor 组装,中心只下发 id):`COFLUX_DEVICE_ID` / `COFLUX_PROJECT_ID`(目录工作区为空串)/ `COFLUX_WORKSPACE_ID` / `COFLUX_TASK_ID` / `COFLUX_SESSION_ID` / `COFLUX_MCP_URL`。agent 读它们就知道自己在哪台设备、哪个项目/工作区/终端,值与中心 MCP `list_*` 返回的 id 完全一致,可直接传给 MCP tools。本地命令与 MCP 的分工:本工作区内的开终端/读/等/输入/播报/叫人/端口用上面的零凭证命令;开子工作区、跨工作区/跨设备读写、或从 coflux 之外接入,用中心的 `coflux` MCP(`claude mcp add --transport http coflux "$COFLUX_MCP_URL"`,一次 OAuth 授权)。supervisor 不走热升级,旧机器要 `cofluxd update && cofluxd restart` 之后会话里才有这些变量;skill 里写了变量为空时的降级分支。
50
+ 不需要任何凭证:daemon 用调用方 pid 反查进程树确认它属于哪个会话,**coflux 会话之外的进程一律拒绝**,权限也天然限定在该会话所属的工作区内。**local-first**:send/read/wait/notify/progress daemon 本地闭环、不经中心(归属与退出码来自 daemon 自己的会话账本,内容来自本地命令日志或 sessiond 快照);只有 new/list/ports daemon 代问中心——Task 要落库广播、预览 URL 由中心生成。早于 daemon 升级开出来的终端缺归属信息,本地命令会明确拒绝,重开即可。
51
+
52
+ 每个 coflux 开出来的 PTY 会话里还注入了一组 `COFLUX_*` 环境变量(由 supervisor 组装,中心只下发 id):`COFLUX_DEVICE_ID` / `COFLUX_PROJECT_ID`(目录工作区为空串)/ `COFLUX_WORKSPACE_ID` / `COFLUX_TASK_ID` / `COFLUX_SESSION_ID` / `COFLUX_MCP_URL`。agent 读它们就知道自己在哪台设备、哪个项目/工作区/终端,值与中心 MCP `list_*` 返回的 id 完全一致,可直接传给 MCP tools。本地命令与 MCP 的分工只有一条规则:**本地能闭环的一律本地命令**(本工作区内的开终端/读/等/输入/播报/叫人/端口);只有跨出本工作区——开子工作区、跨工作区/跨设备读写、或从 coflux 之外接入——才用中心的 `coflux` MCP(`claude mcp add --transport http coflux "$COFLUX_MCP_URL"`,一次 OAuth 授权)。supervisor 不走热升级,旧机器要 `cofluxd update && cofluxd restart` 之后会话里才有这些变量;skill 里写了变量为空时的降级分支。
47
53
 
48
54
  配套的 skill 在 `skills/coflux/SKILL.md`(随包分发),装给 Claude Code:
49
55
 
package/cofluxd.mjs CHANGED
@@ -943,17 +943,41 @@ async function cmdHook() {
943
943
  // **看得见、能接管**的 coflux 实体——而不是在自己的 Bash 里后台起一个谁也看不见的进程。
944
944
  //
945
945
  // 不需要任何凭证:daemon 用调用方 pid 反查进程树确认它属于哪个会话,树外一律拒。
946
+ // local-first(plan 094):send/read/wait/notify/progress 在 daemon 本地闭环,不经中心;只有
947
+ // new/list/ports 由 daemon 代问中心(Task 要落库广播、预览 URL 由中心生成)。
948
+ // 跟随 cwd(plan 102):请求都带 process.cwd(),agent 挪进同设备另一个 coflux 工作区后,这些
949
+ // 命令就对那个工作区办事(notify/progress/ports 除外,它们挂在本会话上,与工作区无关)。
946
950
  // 与 `hook` 子命令的约定**相反**:这些命令必须写 stdout——输出就是给 agent 读的返回值。
947
951
  // 也刻意不做自动重试:terminal new 有副作用,重试会开出两个终端,失败就把错误交给 agent。
948
952
 
949
953
  const AGENT_TIMEOUT_MS = 30_000;
954
+ /** 调用方能收窄单次 `/agent` 等待的下限;再低就只够覆盖 node 自己的启动,等于必然超时。 */
955
+ const MIN_AGENT_TIMEOUT_MS = 200;
950
956
  const DEFAULT_READ_LINES = 200;
951
- // wait 的循环必须在 CLI 侧:单次 agentPost 撑不起长等待(daemon 控制 WS 有自己的往返超时)。
952
- // 默认 30 分钟——编码任务常跑很久;轮询走 terminal.liststatus 来自 sessionExit 事件链,
953
- // 不受快照 ~2s 延迟影响),3 秒一次对本机 loopback 是零负担。
957
+ // wait 的循环必须在 CLI 侧:单次 agentPost 25 秒的 loopback 应答上限。默认 30 分钟——编码任务
958
+ // 常跑很久;轮询走 terminal.statusdaemon 本地账本直接答,不经中心),3 秒一次对本机 loopback
959
+ // 是零负担。
954
960
  const DEFAULT_WAIT_TIMEOUT_S = 1800;
955
961
  const WAIT_POLL_MS = 3000;
956
962
 
963
+ // 每条请求都带调用方 cwd(plan 102):agent 可以经 `/cd` 或 EnterWorktree 把活着的会话挪进同
964
+ // 设备的另一个 coflux 工作区,daemon 据此把本次请求的**目标**解析到 cwd 所在的工作区(会话的
965
+ // 归属工作区不变)。目录被删掉时 process.cwd() 会抛,按"报不出来"处理,daemon 退回归属工作区。
966
+ function callerCwd() {
967
+ try { return process.cwd(); } catch { return ""; }
968
+ }
969
+
970
+ // 调用方可以用 COFLUX_AGENT_TIMEOUT_MS 收窄单次请求的等待上限(plan 104)。默认 30 秒是为
971
+ // agent 定的——它等得起;hook 脚本等不起:宿主按秒杀 hook(SessionStart 只给几秒),而经中心的
972
+ // 动作最坏要等 daemon 的 20 秒中心超时。被宿主杀在半路比拿不到答案坏得多(连坐标块都印不出来),
973
+ // 所以这类调用方自报一个更小的预算,到点干净失败、让脚本走回退。
974
+ // 只允许收窄不允许放宽:上限仍是 AGENT_TIMEOUT_MS,畸形值一律按默认处理。
975
+ function agentTimeoutMs() {
976
+ const raw = Number(process.env.COFLUX_AGENT_TIMEOUT_MS);
977
+ if (!Number.isFinite(raw) || raw <= 0) return AGENT_TIMEOUT_MS;
978
+ return Math.min(Math.max(Math.floor(raw), MIN_AGENT_TIMEOUT_MS), AGENT_TIMEOUT_MS);
979
+ }
980
+
957
981
  async function agentPost(body) {
958
982
  const portResult = localGatewayPort();
959
983
  if (!portResult.ok) die(portResult.error);
@@ -962,8 +986,8 @@ async function agentPost(body) {
962
986
  res = await fetch(`http://127.0.0.1:${portResult.port}/agent`, {
963
987
  method: "POST",
964
988
  headers: { "content-type": "application/json" },
965
- body: JSON.stringify({ ...body, pid: process.pid, ppid: process.ppid }),
966
- signal: AbortSignal.timeout(AGENT_TIMEOUT_MS),
989
+ body: JSON.stringify({ ...body, pid: process.pid, ppid: process.ppid, cwd: callerCwd() }),
990
+ signal: AbortSignal.timeout(agentTimeoutMs()),
967
991
  });
968
992
  } catch (error) {
969
993
  die(`连不上本机 daemon:${error?.message || error}(daemon 没在跑?先看 cofluxd status)`);
@@ -994,11 +1018,20 @@ function tailLines(text, n) {
994
1018
  async function cmdTerminal(values) {
995
1019
  const sub = positionals[1];
996
1020
  if (sub === "new") {
997
- const command = values.cmd;
998
- if (!command) die(`terminal new 需要 --cmd "<命令>"`);
1021
+ // 两种终端只看「命令是否为空」(plan 101):--cmd 缺省与 --cmd= 空白等价,都开会话终端
1022
+ // ——工作区目录下的默认登录 shell,stdin/stdout 都是真 tty,不自动退出。带命令的仍是作业
1023
+ // 终端:命令跑完终端退出并带退出码,语义一字不变。空白在这里统一收敛成空串,好让中心的
1024
+ // 「默认标题取命令首行」落到它自己的兜底。
1025
+ const command = (values.cmd ?? "").trim() ? values.cmd : "";
999
1026
  const result = await agentPost({ action: "terminal.new", title: values.title || "", command });
1000
1027
  console.log(`已开终端 ${result.taskId}(用户可在 coflux 侧栏看到并随时接管)`);
1001
- console.log(`看输出:cofluxd terminal read ${result.taskId}`);
1028
+ if (command) {
1029
+ console.log(`看输出:cofluxd terminal read ${result.taskId}`);
1030
+ } else {
1031
+ console.log(`会话终端:常驻的登录 shell(全 tty),不会自己退出`);
1032
+ console.log(`先等提示符:cofluxd terminal read ${result.taskId}`);
1033
+ console.log(`再输命令:cofluxd terminal send ${result.taskId} --text "<命令>" --enter(送 exit 才结束)`);
1034
+ }
1002
1035
  } else if (sub === "list") {
1003
1036
  const { terminals } = await agentPost({ action: "terminal.list" });
1004
1037
  if (!terminals.length) return void console.log("本工作区暂无终端");
@@ -1030,9 +1063,8 @@ async function cmdTerminal(values) {
1030
1063
  const timeoutSec = Number.isFinite(requested) && requested > 0 ? requested : DEFAULT_WAIT_TIMEOUT_S;
1031
1064
  const deadline = Date.now() + timeoutSec * 1000;
1032
1065
  for (;;) {
1033
- const { terminals } = await agentPost({ action: "terminal.list" });
1034
- const t = terminals.find((x) => x.taskId === taskId);
1035
- if (!t) die(`本工作区没有终端 ${taskId}(用 cofluxd terminal list 查)`);
1066
+ // taskId 直接问本地账本;目标不存在/不在本工作区时 daemon 回可读错误,agentPost 直接 die。
1067
+ const t = await agentPost({ action: "terminal.status", taskId });
1036
1068
  if (t.status === "exited") {
1037
1069
  const exit = t.exitCode === undefined || t.exitCode === null ? "" : ` exit=${t.exitCode}`;
1038
1070
  return void console.log(`# exited${exit}`);
@@ -1061,6 +1093,55 @@ async function cmdProgress() {
1061
1093
  console.log("已更新进度(显示在工作区卡片上,被下一条覆盖)");
1062
1094
  }
1063
1095
 
1096
+ // 「我在哪」与「跟着我搬」(plan 102 / 103)。三条都打一行 JSON,字段稳定——插件脚本按它比对,
1097
+ // agent 也直接读。
1098
+ //
1099
+ // cofluxd workspace 只读:cwd 所在的有效工作区 + 本终端的归属工作区
1100
+ // cofluxd workspace locate [path] 把本终端的**归属**搬到 path 所属的工作区(未登记先登记)
1101
+ // cofluxd workspace forget <path> 该 worktree 已被删掉:其下终端搬回主工作区、记录消失
1102
+ //
1103
+ // locate/forget 是插件在 SessionStart / PostToolUse(EnterWorktree|ExitWorktree) / WorktreeRemove
1104
+ // 上调的,同样零凭证(daemon 按进程树认身份)。daemon 旧到不认识这两个动作时它会回
1105
+ // 「未知 action …」,agentPost 原样报错并非零退出——脚本据此静默放弃,不干扰会话。
1106
+ async function cmdWorkspace() {
1107
+ const sub = positionals[1];
1108
+ if (!sub) {
1109
+ const result = await agentPost({ action: "workspace.current" });
1110
+ return void console.log(JSON.stringify({
1111
+ workspaceId: result.workspaceId,
1112
+ path: result.path,
1113
+ owningWorkspaceId: result.owningWorkspaceId,
1114
+ moved: Boolean(result.moved),
1115
+ }));
1116
+ }
1117
+ if (sub === "locate") {
1118
+ // 路径缺省取调用方 cwd;插件脚本一律显式传 hook 载荷里的 cwd(hook 在会话当前目录执行,
1119
+ // 与载荷里的 cwd 未必相同)。
1120
+ const path = positionals[2] || callerCwd();
1121
+ if (!path) die("workspace locate 需要 <path>(取不到当前目录)");
1122
+ const result = await agentPost({ action: "workspace.locate", path });
1123
+ return void console.log(JSON.stringify({
1124
+ workspaceId: result.workspaceId,
1125
+ path: result.path,
1126
+ branch: result.branch,
1127
+ created: Boolean(result.created),
1128
+ moved: Boolean(result.moved),
1129
+ }));
1130
+ }
1131
+ if (sub === "forget") {
1132
+ const path = positionals[2];
1133
+ if (!path) die("workspace forget 需要 <path>(被删掉的 worktree 目录)");
1134
+ const result = await agentPost({ action: "workspace.forget", path });
1135
+ return void console.log(JSON.stringify({
1136
+ workspaceId: result.workspaceId,
1137
+ fallbackWorkspaceId: result.fallbackWorkspaceId,
1138
+ movedTerminals: result.movedTerminals ?? 0,
1139
+ removed: Boolean(result.removed),
1140
+ }));
1141
+ }
1142
+ die(`workspace 的子命令只有 locate | forget(不带子命令 = 报出我在哪)`);
1143
+ }
1144
+
1064
1145
  async function cmdPorts() {
1065
1146
  const { ports } = await agentPost({ action: "ports" });
1066
1147
  if (!ports.length) return void console.log("本工作区暂无监听端口");
@@ -1084,8 +1165,12 @@ const HELP = `cofluxd —— coflux daemon 管理
1084
1165
 
1085
1166
  以下几条供**跑在 coflux 终端里的 agent** 调用,把工作变成用户看得见、能接管的东西:
1086
1167
 
1087
- cofluxd terminal new --cmd "<命令>" [--title "<标题>"]
1088
- 开一个真实终端跑命令,用户在 coflux 侧栏能看到并随时接管
1168
+ cofluxd terminal new [--cmd "<命令>"] [--title "<标题>"]
1169
+ 开一个真实终端,用户在 coflux 侧栏能看到并随时接管
1170
+ 带 --cmd = 作业终端:命令在登录 shell 里跑完即退出并带退出码,输出另
1171
+ 落一份日志供 read 回读(代价:stdout 是管道,不是 tty)
1172
+ 不带 --cmd = 会话终端:工作区目录下的常驻登录 shell,stdin/stdout 都是
1173
+ 真 tty(能跑 vim/htop、有颜色),先 read 等提示符再 send,送 exit 才结束
1089
1174
  cofluxd terminal list 列出本工作区的终端(含 status / 退出码)
1090
1175
  cofluxd terminal read <taskId> [--lines N]
1091
1176
  读某个终端的内容(纯文本,默认最后 200 行;终端已退出也能读)
@@ -1096,6 +1181,20 @@ const HELP = `cofluxd —— coflux daemon 管理
1096
1181
  cofluxd notify "<一句话>" 叫人:工作区在侧栏转为「等待交互」并显示这句话
1097
1182
  cofluxd progress "<一句话>" 播报进度:显示在工作区卡片上,被下一条覆盖(不打扰用户)
1098
1183
  cofluxd ports 列出本工作区的监听端口及可直接打开的预览 URL
1184
+ cofluxd workspace 一行 JSON 报出「我在哪」:workspaceId(cwd 所在的有效工作区,本地命令
1185
+ 都落在它上面)、path、owningWorkspaceId(本终端此刻归属哪个工作区)、
1186
+ moved。用 /cd 挪进另一个 coflux 工作区后用它确认目标,调 MCP 时也传这个
1187
+ workspaceId
1188
+ cofluxd workspace locate [path]
1189
+ 把本终端的**归属**搬到 path(缺省=当前目录)所属的工作区:进入/离开
1190
+ worktree 后 coflux 跟着走,未登记的同仓库 worktree 先登记出一个子工作区。
1191
+ 插件自动调,一般不用手敲
1192
+ cofluxd workspace forget <path>
1193
+ 该 worktree 已被删掉:其下所有终端搬回项目主工作区、工作区记录消失
1194
+ (不执行 git worktree remove)
1195
+
1196
+ agent 命令的环境变量:COFLUX_AGENT_TIMEOUT_MS 收窄单次请求的等待上限(默认 30000,只能调小),
1197
+ 供有硬超时的 hook 脚本用——到点干净失败,好过被宿主杀在半路。
1099
1198
 
1100
1199
  up flags: --server <ws://.../daemon> --name <名> --shell <路径>
1101
1200
  通用: --version <vX|latest>(不传时 up 沿用已有二进制,update 默认 latest) --bin-dir <dir>(用本地 cargo 产物) --no-start
@@ -1133,7 +1232,7 @@ let cmd = positionals[0];
1133
1232
  if (values.help || cmd === "help") { console.log(HELP); process.exit(0); }
1134
1233
  if (!cmd) cmd = fs.existsSync(SETTINGS) ? "status" : "up"; // 首次裸跑 → 引导
1135
1234
 
1136
- const handlers = { up: cmdUp, update: cmdUpdate, restart: cmdRestart, down: cmdDown, status: cmdStatus, doctor: cmdDoctor, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall, hook: cmdHook, terminal: cmdTerminal, notify: cmdNotify, progress: cmdProgress, ports: cmdPorts };
1235
+ const handlers = { up: cmdUp, update: cmdUpdate, restart: cmdRestart, down: cmdDown, status: cmdStatus, doctor: cmdDoctor, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall, hook: cmdHook, terminal: cmdTerminal, notify: cmdNotify, progress: cmdProgress, ports: cmdPorts, workspace: cmdWorkspace };
1137
1236
  const h = handlers[cmd];
1138
1237
  if (!h) die(`未知命令: ${cmd}${MIGRATED[cmd] ? `\n${MIGRATED[cmd]}` : ""}\n\n${HELP}`);
1139
1238
  await h(values);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cofluxd",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "coflux daemon 管理 CLI:装/起/停/升级 Rust daemon(supervisor + worker)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,222 +1,370 @@
1
1
  ---
2
2
  name: coflux
3
- description: 当你运行在 coflux 终端里时,把长任务、并行工作和求助外化成用户在 coflux web/手机上看得见、能随时接管的真实终端;读 COFLUX_* 环境变量知道自己在哪台设备/项目/工作区/终端,本工作区内用本地 cofluxd 命令,开子工作区或跨工作区/跨设备操作用中心的 coflux MCP。适用于跑测试/构建/dev server 等耗时命令、需要用户接管或决策、想给用户一个可点开的预览 URL、要在隔离的子工作区并行干活的场景。
3
+ description: When you run inside a coflux terminal, this skill documents the local cofluxd commands that open terminals the user can watch and take over from the coflux web/mobile app, report progress, call the user and hand out preview URLs, plus the center MCP for reaching other workspaces and devices. Your coordinates (device / project / workspace / terminal) arrive in a <coflux-session> block at session start, or via the COFLUX_* environment variables. For the workspace your cwd is in always use the zero-credential local cofluxd commands (open, read, wait, send, report progress, call the user, get preview URLs); use the center's coflux MCP only to reach beyond it (child workspaces, other workspaces or devices). Use when the user should be able to watch, step into or stop a command (interactive steps, dev servers, a job they are waiting on), when the user has to decide something, when you want to hand the user a clickable preview URL, or when you need an isolated child workspace for parallel work.
4
4
  ---
5
5
 
6
- # coflux 里工作
6
+ # Working inside coflux
7
7
 
8
- 你可能正跑在 coflux 的一个终端里。coflux 让用户在浏览器和手机上盯着各台机器上的
9
- agent 干活,随时接管。这个 skill 让你把自己的工作**变成用户看得见的东西**,并在需要时
10
- 操作账号下的其它工作区和设备。
8
+ You may be running inside a coflux terminal. coflux lets the user watch agents working on many
9
+ machines from a browser or a phone and take over at any time. This skill gives you terminals the
10
+ user can see and take over, a progress line and a call button on the workspace card, preview URLs,
11
+ and a way to operate the other workspaces and devices under the account when you need to.
11
12
 
12
- 有两条轨道,分工固定:
13
+ Two tracks, one rule: **whatever closes locally uses local commands; only reaching beyond the
14
+ workspace you are in goes through MCP.**
13
15
 
14
- | 轨道 | 凭证 | 能触达的范围 | 用在 |
16
+ | Track | Credentials | Reach | Use for |
15
17
  |---|---|---|---|
16
- | 本地命令 `cofluxd terminal/progress/notify/ports` | 零凭证(daemon 按你的进程树认身份) | 只有**你所在的工作区** | 本工作区内开终端、读、等、输入、播报、叫人、拿预览 URL——最省事的路径 |
17
- | 中心 MCP `coflux`(14 个 tools) | 用户在宿主里做一次 OAuth 授权 | **整个账号**:所有设备、项目、工作区、终端 | 开子工作区(git worktree)、跨工作区/跨设备读写、从 coflux 之外接入 |
18
+ | Local commands `cofluxd terminal/progress/notify/ports` | none (the daemon identifies you by process tree) | **the workspace your cwd is in** | open, read, wait, send, report progress, call the user, preview URLs: the default, fastest, no network dependency |
19
+ | Center MCP `coflux` | one OAuth authorization by the user in the host | **the whole account**: every device, project, workspace and terminal | child workspaces (git worktrees), cross-workspace / cross-device access, joining from outside coflux |
18
20
 
19
- ## 先判断自己在哪
21
+ Of the local commands, `send`/`read`/`wait`/`notify`/`progress` complete entirely inside the
22
+ local daemon and never touch the center; `new`/`list`/`ports` are relayed to the center by the
23
+ daemon on your behalf (terminals must appear in the user's sidebar, preview URLs are minted by
24
+ the center). You only ever talk to the local daemon.
20
25
 
21
- 先看环境变量:
26
+ ## Figure out where you are first
27
+
28
+ With the coflux plugin installed, Claude Code and Codex receive a `<coflux-session>` block at
29
+ session start (and again after context compaction). Your coordinates are in it; use them
30
+ directly. Without that block (no plugin, hand-wired hooks, hook not trusted yet), read the
31
+ environment:
22
32
 
23
33
  ```sh
24
34
  env | grep '^COFLUX_'
25
35
  ```
26
36
 
27
- - **`COFLUX_WORKSPACE_ID` 非空** 你在 coflux 终端里,且 daemon 已是新版。下面的变量就是
28
- 你的坐标,MCP tools 要的 id 直接从这里拿,不用去 `list_*` 里猜:
37
+ - **`COFLUX_WORKSPACE_ID` is non-empty** (or the `<coflux-session>` block is present) → you are in
38
+ a coflux terminal with an up-to-date daemon. The variables below are your coordinates; pass
39
+ these ids to MCP tools directly instead of guessing from `list_*`:
29
40
 
30
- | 变量 | 含义 |
41
+ | Variable | Meaning |
31
42
  |---|---|
32
- | `COFLUX_DEVICE_ID` | 你所在机器的设备 id(`list_devices` 的 id) |
33
- | `COFLUX_PROJECT_ID` | 所属项目 id;无仓库的目录工作区为空串 |
34
- | `COFLUX_WORKSPACE_ID` | 所属工作区 id(`list_workspaces` id) |
35
- | `COFLUX_TASK_ID` | 你这个终端的 id(`list_terminals` / `read_terminal` 用的 terminalId) |
36
- | `COFLUX_SESSION_ID` | 你这个 PTY 会话 id |
37
- | `COFLUX_MCP_URL` | 中心 MCP 地址,用户配 MCP 时就用它 |
43
+ | `COFLUX_DEVICE_ID` | id of the device you run on (the id in `list_devices`) |
44
+ | `COFLUX_PROJECT_ID` | owning project id; empty string for a directory workspace without a repository |
45
+ | `COFLUX_WORKSPACE_ID` | the workspace this terminal was **opened** in (the id in `list_workspaces`). The variable is frozen when the terminal starts; the workspace the terminal *belongs to* can still change — see below. `cofluxd workspace` is the authority |
46
+ | `COFLUX_TASK_ID` | id of this terminal (the taskId / terminalId used by local commands and `read_terminal`) |
47
+ | `COFLUX_SESSION_ID` | id of this PTY session |
48
+ | `COFLUX_MCP_URL` | the center's MCP URL; the user configures MCP with it |
49
+
50
+ - **Variables empty or absent** → treat yourself as outside coflux: forget this skill and use your
51
+ own tools as usual (if the user configured the coflux MCP in the host, the MCP tools still work;
52
+ you just have no "where am I" coordinates). If the user insists you are inside a coflux terminal,
53
+ this machine's daemon has not been upgraded: tell the user to run `cofluxd update && cofluxd restart`;
54
+ after reopening the terminal the variables and the local commands are there.
55
+
56
+ ### Two workspaces to keep apart: owning and effective
57
+
58
+ - **Owning workspace** = the workspace this terminal **belongs to**: what the user's sidebar shows it
59
+ under, what its turn state, branch and diff stats are attributed to. It starts out as
60
+ `COFLUX_WORKSPACE_ID` and moves with you when you enter or leave a git worktree (below).
61
+ - **Effective workspace** = the workspace **your current working directory is inside**. This is what
62
+ every local command acts on.
63
+
64
+ They are the same until your cwd wanders off. A plain `cd <path>` moves a *live* session — same
65
+ conversation, no restart — and a coflux child workspace is a normal registered git worktree, so a
66
+ session whose terminal belongs to workspace A can end up working inside workspace B. From that
67
+ moment, in B:
68
+
69
+ - `cofluxd terminal new` opens the terminal **in B**, under B in the user's sidebar, running in B's
70
+ directory, counting against B's terminal cap;
71
+ - `cofluxd terminal list` lists B's terminals, and A's terminals answer `read` / `wait` / `send`
72
+ with "not in this workspace or does not exist" (`cd` back to A to reach them again);
73
+ - MCP calls need **B's** id as `workspaceId`;
74
+ - the terminal itself stays under A, and `progress`, `notify` and `ports` still belong to it,
75
+ whatever your cwd is; `COFLUX_TASK_ID` and `COFLUX_SESSION_ID` never change.
76
+
77
+ If your cwd is outside every coflux workspace (say `/tmp`), local commands fall back to the owning
78
+ workspace.
79
+
80
+ A terminal opened before the daemon was upgraded is the one case with no owning workspace at all:
81
+ its local commands are refused with "predates the daemon upgrade" whatever your cwd is, because the
82
+ daemon never guesses ownership from a directory. Open a new terminal.
83
+
84
+ ### coflux follows you into a git worktree
85
+
86
+ `EnterWorktree` switches this live session into a git worktree (its own, or an existing one you point
87
+ it at), `ExitWorktree` switches back, and resuming a session that had entered one puts you straight
88
+ back in it. **coflux comes along**: the terminal's *owning* workspace moves to the workspace that
89
+ worktree is, and if coflux has never seen that worktree it registers it as a child workspace of this
90
+ project first — a new card appears in the user's sidebar, with its branch and diff stats. Nothing is
91
+ interrupted: same terminal, same PTY, same conversation, and the user keeps watching it where it now
92
+ lives. When Claude Code cleans up its own worktree on exit, that workspace's terminals move back to
93
+ the project's main workspace and the record disappears by itself.
94
+
95
+ So, after entering or leaving a worktree, owning **and** effective are both the new workspace: pass
96
+ its id to MCP tools and everything local already acts on it. The plugin drops the new id next to the
97
+ tool result, and `cofluxd workspace` always tells you. Two things stay behind on purpose:
98
+
99
+ - `COFLUX_WORKSPACE_ID` (and the id in the `<coflux-session>` block from earlier in this session)
100
+ still names where the terminal was *opened*; it is frozen when the PTY starts and cannot be
101
+ rewritten. Never reuse it after a move.
102
+ - The shell inside this terminal keeps its own directory. That is only about the shell; it does not
103
+ affect where your work is attributed.
104
+
105
+ Nothing happens when coflux cannot follow, and nothing is blocked either: another repository, a
106
+ directory that is not a git repository, a terminal opened in a directory workspace (no project), or
107
+ a daemon that is down or too old — the session just carries on with the ownership it had.
108
+
109
+ ### Ask where you are
38
110
 
39
- - **变量为空或不存在** → 跑一条 `cofluxd terminal list`:
40
- - 返回「不在 coflux 终端里」→ 你不在 coflux 里,忘掉这个 skill,照常用你自己的工具
41
- (除非用户已经在宿主里配了 coflux MCP,那时 MCP tools 照用,只是没有「我在哪」的坐标)。
42
- - 能列出终端 → 你在 coflux 里,但 daemon 还没升级到注入环境变量的版本:本地命令全部可用,
43
- 只是你不知道自己的 id;要用 MCP 就先 `list_devices` / `list_workspaces` 按路径对一下
44
- `pwd`,并顺手告诉用户 `cofluxd update && cofluxd restart` 之后就有环境变量了。
111
+ ```sh
112
+ cofluxd workspace
113
+ {"workspaceId":"ws-b","path":"/Users/me/.coflux/worktrees/ws-b","owningWorkspaceId":"ws-a","moved":true}
114
+ ```
45
115
 
46
- ## 什么时候用本地命令
116
+ One line of JSON: `workspaceId` (+ `path`) is the **effective** workspace, `owningWorkspaceId` is the
117
+ workspace this terminal belongs to right now, and `moved` says whether they differ. With the plugin
118
+ installed you also get a `<coflux-session-moved>` block at the start of every prompt while the two
119
+ differ — but that block only arrives with the **next** user prompt. **About to call an MCP tool right
120
+ after a `cd`? Run `cofluxd workspace` first** and use the `workspaceId` it prints; do not reuse
121
+ `COFLUX_WORKSPACE_ID`.
47
122
 
48
- **用 `cofluxd terminal new` 而不是自己后台起进程**——只要这条命令满足任一条:
123
+ ## When to open a terminal
49
124
 
50
- - 要跑超过十几秒(测试、构建、安装依赖、迁移)
51
- - 会一直跑下去(dev server、watch、日志跟随)
52
- - 用户可能想接管(需要交互、可能要中途叫停、失败了要人去调)
125
+ A coflux terminal is a process the user can see: a titled entry in their sidebar that they can
126
+ open, take over and type into, whose output you can read back at any time.
127
+ Whether a command runs in your own Bash or in a coflux terminal is your call; a coflux terminal is
128
+ worth it when the user's view of the process matters:
53
129
 
54
- 这类工作在你自己的 Bash 里后台跑,用户**什么也看不见**:看不到它在跑、接管不了、
55
- 出问题只能等你转述。开成 coflux 终端,它就是侧栏里一个有标题的条目,用户能点进去、
56
- 能接管、能自己敲命令。
130
+ - the user may want to step in: interactive steps, confirmations, something they may need to stop
131
+ midway or rescue when it fails
132
+ - it keeps running and the user will want to find it later (dev server, watch mode, log tailing)
133
+ - you want to hand the user something to look at (a test run they asked to watch, a build they are
134
+ waiting on)
57
135
 
58
- **不要用**在一次性的快命令上(`ls`、`grep`、`git status`、读文件)——你自己的工具更快,
59
- 给用户开一堆一秒就结束的终端只是噪音。
136
+ **Do not use it** for quick one-shot commands (`ls`, `grep`, `git status`, reading files): your
137
+ own tools are faster, and a pile of one-second terminals is just noise to the user.
60
138
 
61
- ## 本地命令
139
+ ## Local commands
62
140
 
63
- ### 开终端跑命令
141
+ ### Open a terminal
142
+
143
+ There are two kinds, told apart by one single thing: **whether you pass a command**.
64
144
 
65
145
  ```sh
66
- cofluxd terminal new --title "跑单测" --cmd "pnpm -C tests test"
146
+ cofluxd terminal new --title="Run unit tests" --cmd="pnpm -C tests test" # job terminal
147
+ cofluxd terminal new --title="Debug shell" # session terminal
67
148
  ```
68
149
 
69
- `--title` 是用户在侧栏看到的名字,**认真起**:写「跑单测」「起 dev server」,
70
- 别写「terminal 1」。命令在当前工作区目录下、用登录 shell 执行。
71
-
72
- 命令跑完终端就退出,任务转 `exited` 并带上退出码——这是你判断成没成的依据。
73
- 所以别指望在同一个终端里接着跑第二条命令,要么写成 `a && b`,要么再开一个。
150
+ `--title` is the name the user sees in the sidebar; **name it properly**: "Run unit tests",
151
+ "Start dev server", never "terminal 1". Either kind runs in the directory of the workspace your cwd
152
+ is in (which is not always the one this terminal was opened in — see "owning and effective").
153
+
154
+ Always write `--cmd=<value>` and `--title=<value>` with the `=`, never separated by a space: a
155
+ value that starts with `-` is otherwise taken for another option and the call fails outright.
156
+
157
+ **Job terminal — with `--cmd=...`.** It runs that one command under the login shell (command line
158
+ capped at 16 KB). The terminal exits when the command finishes and the task becomes `exited` with
159
+ the exit code: that is how you tell success from failure. So do not expect to run a second command
160
+ in the same terminal: write `a && b`, or open another one. The output is also written to a local
161
+ log for you to read back (roughly the last 1 MB is kept). The cost is that the command's stdout is
162
+ a pipe rather than a tty: most programs turn off colors and progress bars, full-screen programs
163
+ (vim, htop, less) do not work at all, and a few switch to a different "CI" behavior.
164
+
165
+ **Session terminal — no `--cmd` at all.** You get exactly what the user gets by clicking "new
166
+ terminal" in the sidebar: the default login shell in the workspace directory, with stdin **and**
167
+ stdout on a real tty. It runs nothing by itself and **never exits on its own** — it lives until
168
+ `exit` is typed into it (by you with `send`, or by the user), or the user stops it. Reach for it
169
+ when you need several commands in the same shell, a TUI or a program whose colors and progress
170
+ bars matter, or simply a terminal the user can step into and keep using. There is no command log
171
+ for it: `read` returns the current screen (one screenful, no history), so you judge how it went
172
+ from what is on screen, and `wait` is only meaningful after you have sent `exit`.
173
+
174
+ Driving a session terminal:
175
+
176
+ 1. `cofluxd terminal new --title="Debug shell"` → prints a taskId.
177
+ 2. `cofluxd terminal read <taskId>` until you see the shell prompt. The shell needs a moment to
178
+ start and the first read can come back empty — **never `send` before you have seen a prompt**.
179
+ 3. `cofluxd terminal send <taskId> --text="pnpm build" --enter`, then `read` again to see what
180
+ happened. One send per command; nothing signals you when a command finished, so read until the
181
+ prompt is back. To make that unambiguous, end the command with a marker of your own
182
+ (`pnpm build; echo DONE-$?`) and read until the marker shows up.
183
+ 4. `cofluxd terminal send <taskId> --text="exit" --enter` when you are done; the terminal then
184
+ becomes `exited` with the shell's exit code.
185
+
186
+ The new terminal has the same `COFLUX_*` variables (pointing at its own task/session ids, same
187
+ workspace as you).
188
+
189
+ ### See how far it got
74
190
 
75
- 命令的输出会同时落一份日志供你回读,代价是它的 stdout 是管道而不是 tty——多数程序会因此
76
- 关掉颜色和进度条。极少数程序在非 tty 下行为不同(比如不输出进度、切成 CI 模式),如果你
77
- 依赖那种行为,自己在 Bash 里跑。
191
+ ```sh
192
+ cofluxd terminal list # every terminal in the workspace your cwd is in: id, state, exit code, title
193
+ cofluxd terminal read <taskId> # a terminal's content (plain text, last 200 lines by default)
194
+ cofluxd terminal read <taskId> --lines 50
195
+ ```
78
196
 
79
- 新开的终端里同样有 `COFLUX_*` 变量(指向它自己的 task/session id,工作区与你相同)。
197
+ `list` states are `running` / `exited` / `idle`; `exited` carries `exit=<code>`.
198
+ **An exited terminal can still be read**: "the command finished, look at the output" is the most
199
+ common case. `read` reads the local log of a job terminal; terminals that have no log (session
200
+ terminals, and the ones the user opened) return the current screen instead — one screenful, no
201
+ history, and empty for the first moments after opening. Both are immediate.
80
202
 
81
- ### 看跑到哪了
203
+ ### Wait for a command to finish
82
204
 
83
205
  ```sh
84
- cofluxd terminal list # 本工作区所有终端:id、状态、退出码、标题
85
- cofluxd terminal read <taskId> # 某个终端的内容(纯文本,默认最后 200 行)
86
- cofluxd terminal read <taskId> --lines 50
206
+ cofluxd terminal wait <taskId> # block until that terminal exits and print the exit code (default cap 30 minutes)
207
+ cofluxd terminal wait <taskId> --timeout 300 # custom timeout in seconds; a timeout fails loudly with a non-zero exit
87
208
  ```
88
209
 
89
- `list` 的状态是 `running` / `exited` / `idle`;`exited` 会带 `exit=<码>`。
90
- **终端已经退出也能 read**——「命令跑完了看输出」正是最常用的场景。
210
+ To wait for a command use `wait`; **do not write your own polling loop**. One command blocks
211
+ until done and hands you the exit code. A timeout does not mean the command failed, only that it
212
+ is still running: `read` to see where it is, then decide whether to keep waiting or act.
91
213
 
92
- 内容有最多约 2 秒的延迟(来自中心的定期快照),所以刚 `new` 完立刻 `read` 可能是空的。
214
+ `wait` **always exits 0** once the terminal is done: it reports that the command finished, not
215
+ whether it succeeded. Read the result off its output line `# exited exit=<code>` (`list` shows the
216
+ same). A non-zero exit from `wait` itself means the wait timed out or the id was wrong.
93
217
 
94
- ### 等命令跑完
218
+ **Do not `wait` on a session terminal** unless you have already sent it `exit`: it never finishes
219
+ by itself, so the wait can only end in the 30-minute timeout — a timeout there means the shell is
220
+ still sitting at its prompt, nothing more. Its exit code, when it finally exits, is the shell's and
221
+ not any command's: check what a command did by reading the screen.
95
222
 
96
- ```sh
97
- cofluxd terminal wait <taskId> # 阻塞到该终端退出,打印退出码(默认最长等 30 分钟)
98
- cofluxd terminal wait <taskId> --timeout 300 # 自定超时(秒);超时会明确报错并非零退出
99
- ```
223
+ **Keep working, and be woken up when it finishes.** `wait` blocks, so run it as a backgrounded Bash
224
+ call of your own:
100
225
 
101
- 要等一条命令跑完就用 `wait`,**别自己写轮询循环**——它一条命令阻塞到位,退出码直接给你。
102
- 超时不代表命令失败,只是还没跑完:`read` 看看现场再决定继续等还是处理。
226
+ 1. `cofluxd terminal new --title="Run the test suite" --cmd="pnpm -C tests test"` → prints a taskId.
227
+ 2. Run `cofluxd terminal wait <taskId>` as a backgrounded Bash call, then go do something else.
228
+ 3. The host wakes you when that call exits. Check its output for `# exited exit=<code>`, then
229
+ `cofluxd terminal read <taskId>` to see what actually happened.
103
230
 
104
- ### 往终端里输入
231
+ That gets you both halves at once: the user watches (and can take over) a real terminal, and you are
232
+ still told the moment it is over, instead of blocking or polling for it.
233
+
234
+ ### Type into a terminal
105
235
 
106
236
  ```sh
107
- cofluxd terminal send <taskId> --text "y" --enter # 输入一行并回车
108
- cofluxd terminal send <taskId> --enter # 只按一个回车
237
+ cofluxd terminal send <taskId> --text "y" --enter # type a line and press Enter
238
+ cofluxd terminal send <taskId> --enter # just press Enter
109
239
  ```
110
240
 
111
- 用在命令要交互确认(y/N、选项)、或想在跑完的同一 shell 里补一条命令的时候。纪律:
241
+ For interactive confirmations (y/N, menus), or to add a command in the same shell after the
242
+ previous one finished. Discipline:
112
243
 
113
- - **先 `read` `send`**:看清终端现在在等什么再输入,别盲打。
114
- - **用户正在接管时会被拒**——这不是错误,是设计:人永远优先。被拒就停手,
115
- 要沟通用 `notify`,别重试。
116
- - send 超时后**不要直接重发**:先 `read` 确认刚才那次到底进没进去,重复输入比丢输入更糟。
244
+ - **`read` before `send`**: see what the terminal is waiting for before typing; never type blind.
245
+ On a freshly opened session terminal this also means waiting for the shell prompt to appear.
246
+ - **Refused while the user is taking over**: that is not an error, it is by design; humans always
247
+ win. Stop when refused; use `notify` to communicate, do not retry.
248
+ - **After a send timeout do not resend right away**: `read` first to check whether the input
249
+ actually landed; duplicated input is worse than lost input.
250
+ - A single text is capped at 64 KB; this is an interactive input channel, not a file transfer.
117
251
 
118
- ### 播报进度
252
+ ### Report progress
119
253
 
120
254
  ```sh
121
- cofluxd progress "复现了,正在定位 relay 重连的时序"
255
+ cofluxd progress "Reproduced; narrowing down the relay reconnect timing"
122
256
  ```
123
257
 
124
- 一句话告诉用户你干到哪了,显示在工作区卡片上,被下一条覆盖。在关键节点更新:复现了、
125
- 定位到了、修完在验、卡在哪。它**不打扰用户**,和 `notify` 是两条信道:
258
+ One sentence telling the user how far you are, shown on the workspace card and replaced by the
259
+ next one. Update it at milestones: reproduced, located, fixed and verifying, stuck on X. It
260
+ **does not interrupt the user**; it is a different channel from `notify`:
126
261
 
127
- - `progress` = 播报(用户扫一眼就知道进展,不需要回应)
128
- - `notify` = 叫人(工作区转「等待交互」,用户该来看看了)
262
+ - `progress` = broadcast (the user glances and knows the state, no response needed)
263
+ - `notify` = call the user (the workspace turns "waiting for interaction", the user should come and look)
129
264
 
130
- 拿不准用哪个:不需要用户做任何事就用 `progress`。
265
+ If unsure: when the user does not have to do anything, use `progress`.
131
266
 
132
- ### 叫人
267
+ ### Call the user
133
268
 
134
269
  ```sh
135
- cofluxd notify "两个方案都能走通,需要你定一下用哪个"
270
+ cofluxd notify "Both approaches work; I need you to pick one"
136
271
  ```
137
272
 
138
- 用户的侧栏里这个工作区会转成「等待交互」并显示这句话——他在手机上也看得到。
139
- 用在你**真的卡住**的时候:需要用户决策、要密码/权限、发现了必须人来判断的问题。
140
- 一句话说清要什么,别写成日志。
273
+ The user's sidebar switches this workspace to "waiting for interaction" and shows this sentence;
274
+ they see it on the phone too. Use it when you are **really stuck**: a decision is needed, a
275
+ password or a permission, a problem only a human can judge. One sentence saying what you need;
276
+ do not write a log.
141
277
 
142
- (你正常的提问和权限请求已经会自动反映到侧栏状态上,不需要额外 notify。
143
- 这条是给「你要说的事,用户光看状态图标猜不出来」准备的。)
278
+ (Your normal questions and permission prompts already show up in the sidebar state; they need no
279
+ extra notify. This is for "what you have to say cannot be guessed from the status icon".)
144
280
 
145
- ### 给用户可点开的预览
281
+ ### Hand the user a clickable preview
146
282
 
147
283
  ```sh
148
284
  cofluxd ports
149
285
  ```
150
286
 
151
- 列出本工作区所有监听端口和对应的公网预览 URL。起了 dev server 之后用它拿 URL 直接
152
- 告诉用户,他点开就能看,不用自己去翻。
153
-
154
- ## 中心 MCP:跨出本工作区
155
-
156
- 本地命令只看得见你所在的工作区。要做下面这些事,用宿主里名为 `coflux` MCP server:
157
-
158
- - **开一个隔离的子工作区并行干活**:`create_workspace`(项目 id `$COFLUX_PROJECT_ID`)
159
- 在设备上真的 `git worktree add`,然后 `create_terminal` 在那里跑命令。
160
- - **看/操作别的工作区、别的设备上的终端**:`list_*` `read_terminal` / `send_terminal_input`。
161
- - **不在 coflux 终端里**(比如用户在自己电脑上开的 Claude Code)时接入账号下的一切。
162
-
163
- ### 没配 MCP
164
-
165
- `claude mcp list`(Codex:`codex mcp list`)看有没有 `coflux`。没有就告诉用户一行接入,
166
- 地址用 `$COFLUX_MCP_URL`(它就是中心公网地址 + `/mcp`):
287
+ Lists every listening port in this workspace with its public preview URL. After starting a dev
288
+ server, use it to get the URL and tell the user directly; they click it and nobody has to dig.
289
+
290
+ ### Errors from local commands
291
+
292
+ Errors are one readable sentence; do what they say: "not inside a coflux terminal" = you are not
293
+ in a coflux session; "terminal is not in this workspace or does not exist" = check the id with
294
+ `list`, and if you moved into another workspace that is exactly what a terminal of the other one
295
+ looks like (`cofluxd workspace` to confirm, `cd` back to reach it); "predates the daemon upgrade" =
296
+ that terminal was opened before the daemon upgrade, open a new one; a `new` without `--cmd` refused for a missing command = this machine's daemon is older
297
+ than session terminals, tell the user to run `cofluxd update && cofluxd restart` (or pass a command
298
+ and use a job terminal); "daemon is not connected to the center" only appears on
299
+ `new`/`list`/`ports`, retry once it reconnects.
300
+
301
+ ## Center MCP: leaving this workspace
302
+
303
+ Local commands only see the workspace your cwd is in. Use the MCP server named `coflux` in the
304
+ host **only** for these:
305
+
306
+ - **Open an isolated child workspace to work in parallel**: `create_workspace` (project id from
307
+ `$COFLUX_PROJECT_ID`) really runs `git worktree add` on the device; then `create_terminal` runs
308
+ commands there. The same two kinds apply: `create_terminal` with a `command` opens a
309
+ job terminal, without one it opens a session terminal.
310
+ - **Look at or operate terminals in other workspaces or on other devices**: `list_*` →
311
+ `read_terminal` / `send_terminal_input`.
312
+ - **Join everything under the account when you are not inside a coflux terminal** (for example
313
+ Claude Code the user started on their own machine).
314
+ - **Deleting a workspace**: `remove_workspace` (it closes that workspace's terminals first, then
315
+ removes the worktree and the record). Inside a coflux project the plugin blocks
316
+ `git worktree remove|move` run by hand, because that leaves an orphan workspace record in the user's
317
+ sidebar. Creating a worktree is *not* blocked — coflux follows you into it (see above) — and Claude
318
+ Code's own worktrees need no cleanup from you at all.
319
+
320
+ Do not detour through MCP for work inside the workspace you are in — including one you moved into
321
+ with `cd` or EnterWorktree, where the local commands follow you: that is an extra round trip to the
322
+ center, while a local command does it in one step.
323
+
324
+ ### When MCP is not configured
325
+
326
+ Run `claude mcp list` (Codex: `codex mcp list`) to see whether `coflux` is there. If not, give
327
+ the user the one-line setup, with the URL from `$COFLUX_MCP_URL` (it is the center's public URL
328
+ + `/mcp`):
167
329
 
168
330
  ```sh
169
331
  claude mcp add --transport http coflux "$COFLUX_MCP_URL" # Claude Code
170
332
  codex mcp add coflux --url "$COFLUX_MCP_URL" # Codex
171
333
  ```
172
334
 
173
- 之后宿主会引导用户在浏览器完成一次 OAuth 授权(Claude Code 里是 `/mcp`)。授权是用户的事,
174
- 你只需要把地址和命令给他;没配好之前本工作区内的活照样用本地命令干。
175
-
176
- ### 14 个 tools
177
-
178
- id 优先从 `COFLUX_*` 环境变量拿;跨出本工作区的 id 用 `list_*` 查。
179
-
180
- | tool | 用途 |
181
- |---|---|
182
- | `list_devices` | 账号下的设备(跑着 daemon 的机器):id、名称、在线、版本 |
183
- | `list_projects` | 导入的项目(git 仓库):id、所在设备、仓库路径、默认分支 |
184
- | `list_workspaces` | 工作区(主工作区 = 仓库本身,其余是 worktree;目录工作区 projectId 为 null),可按 projectId 筛 |
185
- | `list_terminals` | 终端:id、工作区、标题、状态 idle/running/exited、退出码,可按 workspaceId |
186
- | `read_terminal` | 读终端纯文本(去 ANSI,默认尾 200 行):source=log 是命令终端的完整日志(退出后仍可读),snapshot 是当前画面,checkpoint 是设备离线时中心的最近快照 |
187
- | `list_ports` | 终端里检测到的监听端口 + 可直接打开的预览 URL |
188
- | `create_workspace` | 在项目下新建 git worktree 工作区(可新建分支),设备真在磁盘上建目录 |
189
- | `rename_workspace` | 改工作区名(纯展示) |
190
- | `remove_workspace` | 删 worktree 工作区:先关其下所有终端再 `git worktree remove --force`(未提交改动会丢);主工作区不可删 |
191
- | `create_terminal` | 在某工作区开真实终端跑一条命令(用户可接管、侧栏可见),跑完带退出码;输出落日志供 `read_terminal` |
192
- | `send_terminal_input` | 往运行中的终端写文本(默认追加回车) |
193
- | `wait_terminal` | 有界等待终端退出并拿退出码(默认 30 秒、上限 50 秒) |
194
- | `stop_terminal` | 结束终端会话(等价 web 上的停止) |
195
- | `remove_terminal` | 删终端记录;运行中的必须先 `stop_terminal` |
196
-
197
- ### MCP 的三条纪律
198
-
199
- 1. **人类优先**:`send_terminal_input` 在用户正在接管那个终端时会被拒,错误里写明
200
- 「用户正在接管」——把交互留给用户,不要重试;要沟通用本工作区的 `cofluxd notify`。
201
- `send` 之前先 `read_terminal` 看清它在等什么;回执超时先 `read` 再决定要不要重发。
202
- 2. **有界等待**:`wait_terminal` 最多等 50 秒,到期返回 `timedOut=true` 不是错误——需要更久
203
- 就再调一次,**别自己写轮询循环去 `read_terminal`**。`create_workspace` / `create_terminal`
204
- 最多等 30 秒启动回执,到期返回「已提交」并附 id,稍后用 `list_*` 查。
205
- 3. **「需要升级」就停**:写 tools 在目标设备的 daemon 太旧时立即返回「该设备的 daemon 需要
206
- 升级」——这是能力门禁,不是暂时故障。转告用户在那台机器上 `cofluxd update && cofluxd restart`,
207
- 别重试也别换 tool 绕。
208
-
209
- 本地命令的纪律(先 read 再 send、被拒即停、`progress` 与 `notify` 分界、用 `wait` 别轮询)
210
- 在 MCP 里同样成立。
211
-
212
- ## 边界
213
-
214
- - 你能开、读、等、输入,但**输入是人类优先的受限写权**:用户正在接管的终端你写不进去
215
- (会被明确拒绝),用户随时接管也会把你顶掉。别和人抢终端。
216
- - 本地命令只看得见**你自己所在的工作区**;别的工作区和别的机器要经 MCP,且限于同一账号。
217
- - 一个工作区同时活着的终端有上限(默认 8,含用户自己开的)。撞上限先 `list` 看看,
218
- 多半是有跑完没收的;真是用户占满了,就 `notify` 告诉他,别硬试。
219
- - 这些命令与 tools 都要 daemon 连着中心才能用——毕竟「让用户看见」就是它们的全部意义。
220
- 连不上时会明确报错,不会默默降级。
221
- - `COFLUX_*` 变量只在 coflux 开出来的 PTY 里有;你自己 `export` 或改它们没有任何效果,
222
- 中心只认它自己下发的 id。
335
+ The host then guides the user through a one-time OAuth authorization in the browser (`/mcp` in
336
+ Claude Code). Authorization is the user's job; you only hand over the URL and the command. Until
337
+ it is set up, keep doing the work inside this workspace with local commands.
338
+
339
+ ### Using the tools
340
+
341
+ The tool list and each tool's contract (parameters, limits, what an error means) come from the
342
+ MCP server itself: read the tool descriptions in the host, they are the source of truth and this
343
+ file does not repeat them. Take ids from the `COFLUX_*` variables first — except the workspace id
344
+ after you moved, which comes from `cofluxd workspace` (or the `<coflux-session-moved>` block); for
345
+ anything outside the workspace you are in, find ids with the `list_*` tools.
346
+
347
+ The local-command disciplines apply to MCP just the same: `read_terminal` before
348
+ `send_terminal_input`, stop when refused because the user is taking over (communicate with
349
+ `cofluxd notify` instead of retrying), `wait_terminal` instead of a polling loop around
350
+ `read_terminal`, and stop on "needs upgrade" (tell the user to run
351
+ `cofluxd update && cofluxd restart` on that device; do not retry or work around it).
352
+
353
+ ## Boundaries
354
+
355
+ - You can open, read, wait and type, but **typing is a restricted write with humans first**: you
356
+ cannot write into a terminal the user is taking over (you are refused explicitly), and the user
357
+ taking over at any time displaces you. Do not fight a human for a terminal.
358
+ - Local commands only see **the workspace your cwd is in** (`cofluxd workspace` says which one);
359
+ other workspaces and other machines go through MCP and are limited to the same account.
360
+ - A workspace has a cap on concurrently live terminals (default 8, including the user's own).
361
+ On hitting the cap, `list` first: usually some finished terminals were never collected. If the
362
+ user really filled it up, `notify` them instead of forcing it.
363
+ - `new`/`list`/`ports` and every MCP tool need the daemon connected to the center; "letting the
364
+ user see" is their whole point. `send`/`read`/`wait`/`notify`/`progress` do not depend on the
365
+ center. When disconnected they fail loudly rather than degrade silently.
366
+ - `COFLUX_*` variables exist only in PTYs opened by coflux; exporting or changing them yourself
367
+ has no effect, the center only trusts the ids it issued. `COFLUX_WORKSPACE_ID` always means the
368
+ workspace this terminal was **opened** in and goes stale the moment coflux follows you into a
369
+ worktree; both "where does this terminal belong now" and "where am I acting" come from
370
+ `cofluxd workspace`.