cofluxd 0.14.0 → 1.0.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/cofluxd.mjs CHANGED
@@ -851,232 +851,6 @@ function cmdUninstall(v) {
851
851
  else console.log(`✓ 已卸载服务(保留二进制/配置/凭证于 ${HOME};--purge 可全清)`);
852
852
  }
853
853
 
854
- /* ------------------------------ hook:agent 事件信使 ------------------------------ */
855
- // agent hook 的上报信使:用户在 claude/codex 的 hook 配置里指向本命令,事件发生时它把
856
- // 事件名转发给本机 worker 的固定 gateway(POST /hook),供活动状态判定。
857
- //
858
- // 输入两种形态都收:claude 与 codex hooks 引擎走 stdin JSON;codex 旧式 notify 把 payload
859
- // 作为最后一个 argv 传入。只转发事件名 + notification 类型 + agent 会话 id + 本进程
860
- // pid/ppid——payload 里的 prompt / 回答原文 / 通知正文一律不出机(隐私边界)。
861
- //
862
- // 契约(worker 侧将来实现 /hook 时依赖):请求保持到收到响应才退出——worker 在处理期间
863
- // 用上报的 pid 反查进程树归属哪个 session,本进程活着扫描才有效。
864
- //
865
- // 纪律:本命令绝不能干扰 agent 本体——任何失败(daemon 不在/端口不通/payload 畸形)都
866
- // 静默退出 0(claude 把 Stop hook 的非零退出码解释为"阻止收尾");绝不写 stdout(claude
867
- // 会把 hook 的 stdout 当决策 JSON 解析),调试信息走 stderr(COFLUX_HOOK_DEBUG=1 开启)。
868
- const HOOK_STDIN_TIMEOUT_MS = 300; // stdin 没有数据时不能干等(notify 形态下 stdin 是继承的 TTY/空管道)
869
- const HOOK_POST_TIMEOUT_MS = 2000;
870
-
871
- const hookDebug = (...args) => { if (process.env.COFLUX_HOOK_DEBUG) console.error("[cofluxd hook]", ...args); };
872
-
873
- async function readStdinJson() {
874
- if (process.stdin.isTTY) return null;
875
- const chunks = [];
876
- const drained = (async () => { for await (const chunk of process.stdin) chunks.push(chunk); })().catch(() => {});
877
- await Promise.race([drained, sleep(HOOK_STDIN_TIMEOUT_MS)]);
878
- process.stdin.destroy(); // 超时后放掉 stdin,否则 for await 会吊着进程不退出
879
- const raw = Buffer.concat(chunks).toString("utf8").trim();
880
- if (!raw) return null;
881
- try { return JSON.parse(raw); } catch { return null; }
882
- }
883
-
884
- async function cmdHook() {
885
- try {
886
- const agent = positionals[1];
887
- if (agent !== "claude" && agent !== "codex") {
888
- hookDebug(`未知 agent: ${agent ?? "(缺参)"}(需 claude|codex)`);
889
- return;
890
- }
891
- let payload = null;
892
- if (positionals[2]) {
893
- try { payload = JSON.parse(positionals[2]); } catch { /* 非 JSON 的多余参数,忽略 */ }
894
- }
895
- if (!payload) payload = await readStdinJson();
896
- if (!payload || typeof payload !== "object") {
897
- hookDebug("无有效 payload,忽略");
898
- return;
899
- }
900
- // claude/codex hooks 引擎用 hook_event_name;codex notify 用 type
901
- const event = payload.hook_event_name || payload.type;
902
- if (typeof event !== "string" || !event) {
903
- hookDebug("payload 缺事件名,忽略");
904
- return;
905
- }
906
- const portResult = localGatewayPort();
907
- if (!portResult.ok) {
908
- hookDebug(portResult.error);
909
- return;
910
- }
911
- const notification = payload.notification_type ?? payload.notificationType;
912
- const body = {
913
- agent,
914
- event,
915
- pid: process.pid,
916
- ppid: process.ppid,
917
- // agent 自身的会话标识(claude: session_id / codex notify: thread-id),供 worker 去重与调试
918
- agentSessionId: payload.session_id ?? payload["thread-id"] ?? undefined,
919
- // Claude Notification 的类型枚举(permission_prompt / agent_needs_input …),不含正文
920
- notification: typeof notification === "string" && notification ? notification : undefined,
921
- // Stop/SubagentStop 独有:本会话在飞的后台工作(shell/subagent/monitor/workflow)。claude
922
- // 官方设它就是为了让 hook 区分「真做完了」与「挂起等后台把自己叫醒」。只传条数——条目里的
923
- // description 是自由文本(含路径与代码片段),按本命令的隐私边界不出机。
924
- backgroundTasks: Array.isArray(payload.background_tasks) ? payload.background_tasks.length : undefined,
925
- };
926
- hookDebug("POST /hook", JSON.stringify(body));
927
- const res = await fetch(`http://127.0.0.1:${portResult.port}/hook`, {
928
- method: "POST",
929
- headers: { "content-type": "application/json" },
930
- body: JSON.stringify(body),
931
- signal: AbortSignal.timeout(HOOK_POST_TIMEOUT_MS),
932
- });
933
- hookDebug(`响应 ${res.status}`);
934
- } catch (error) {
935
- hookDebug(error?.message || String(error));
936
- } finally {
937
- process.exit(0); // 无论成败都干净退出:不给 agent 留非零退出码,也不让残留句柄吊住进程
938
- }
939
- }
940
-
941
- /* --------------------- agent 协同控制(plan 074) --------------------- */
942
- // 跑在 coflux 终端里的 claude/codex 用这组命令,把自己的工作外化成用户在 web/手机上
943
- // **看得见、能接管**的 coflux 实体——而不是在自己的 Bash 里后台起一个谁也看不见的进程。
944
- //
945
- // 不需要任何凭证:daemon 用调用方 pid 反查进程树确认它属于哪个会话,树外一律拒。
946
- // local-first(plan 094):send/read/wait/notify/progress 在 daemon 本地闭环,不经中心;只有
947
- // new/list/ports 由 daemon 代问中心(Task 要落库广播、预览 URL 由中心生成)。
948
- // 与 `hook` 子命令的约定**相反**:这些命令必须写 stdout——输出就是给 agent 读的返回值。
949
- // 也刻意不做自动重试:terminal new 有副作用,重试会开出两个终端,失败就把错误交给 agent。
950
-
951
- const AGENT_TIMEOUT_MS = 30_000;
952
- const DEFAULT_READ_LINES = 200;
953
- // wait 的循环必须在 CLI 侧:单次 agentPost 有 25 秒的 loopback 应答上限。默认 30 分钟——编码任务
954
- // 常跑很久;轮询走 terminal.status(daemon 本地账本直接答,不经中心),3 秒一次对本机 loopback
955
- // 是零负担。
956
- const DEFAULT_WAIT_TIMEOUT_S = 1800;
957
- const WAIT_POLL_MS = 3000;
958
-
959
- async function agentPost(body) {
960
- const portResult = localGatewayPort();
961
- if (!portResult.ok) die(portResult.error);
962
- let res;
963
- try {
964
- res = await fetch(`http://127.0.0.1:${portResult.port}/agent`, {
965
- method: "POST",
966
- headers: { "content-type": "application/json" },
967
- body: JSON.stringify({ ...body, pid: process.pid, ppid: process.ppid }),
968
- signal: AbortSignal.timeout(AGENT_TIMEOUT_MS),
969
- });
970
- } catch (error) {
971
- die(`连不上本机 daemon:${error?.message || error}(daemon 没在跑?先看 cofluxd status)`);
972
- }
973
- let parsed = null;
974
- try { parsed = await res.json(); } catch { /* 非 JSON 响应按下面的兜底报错处理 */ }
975
- if (!res.ok || !parsed?.ok) die(parsed?.error || `daemon 返回 ${res.status}`);
976
- return parsed;
977
- }
978
-
979
- // 剥掉 ANSI/OSC 转义与 C0 控制字符,保留 \t 与 \n——snapshot 是给终端渲染的字节流,
980
- // agent 要的是能读的纯文本。去转义放在 CLI 侧:daemon 的 snapshot 同时是 checkpoint 的
981
- // 数据来源,不为 agent 的可读性改它的语义。
982
- const ANSI_RE =
983
- /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b[[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PR-TZcf-ntqry=><~]|[\u0000-\u0008\u000b-\u001f\u007f]/g;
984
-
985
- function stripAnsi(raw) {
986
- return String(raw ?? "").replace(ANSI_RE, "");
987
- }
988
-
989
- /** 取最后 n 行并去掉尾部空行——VT snapshot 的下半屏通常是成片空行,对 agent 是纯噪音。 */
990
- function tailLines(text, n) {
991
- const lines = text.split("\n");
992
- while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
993
- return lines.slice(-n).join("\n");
994
- }
995
-
996
- async function cmdTerminal(values) {
997
- const sub = positionals[1];
998
- if (sub === "new") {
999
- // 两种终端只看「命令是否为空」(plan 101):--cmd 缺省与 --cmd= 空白等价,都开会话终端
1000
- // ——工作区目录下的默认登录 shell,stdin/stdout 都是真 tty,不自动退出。带命令的仍是作业
1001
- // 终端:命令跑完终端退出并带退出码,语义一字不变。空白在这里统一收敛成空串,好让中心的
1002
- // 「默认标题取命令首行」落到它自己的兜底。
1003
- const command = (values.cmd ?? "").trim() ? values.cmd : "";
1004
- const result = await agentPost({ action: "terminal.new", title: values.title || "", command });
1005
- console.log(`已开终端 ${result.taskId}(用户可在 coflux 侧栏看到并随时接管)`);
1006
- if (command) {
1007
- console.log(`看输出:cofluxd terminal read ${result.taskId}`);
1008
- } else {
1009
- console.log(`会话终端:常驻的登录 shell(全 tty),不会自己退出`);
1010
- console.log(`先等提示符:cofluxd terminal read ${result.taskId}`);
1011
- console.log(`再输命令:cofluxd terminal send ${result.taskId} --text "<命令>" --enter(送 exit 才结束)`);
1012
- }
1013
- } else if (sub === "list") {
1014
- const { terminals } = await agentPost({ action: "terminal.list" });
1015
- if (!terminals.length) return void console.log("本工作区暂无终端");
1016
- for (const t of terminals) {
1017
- const exit = t.exitCode === undefined || t.exitCode === null ? "" : ` exit=${t.exitCode}`;
1018
- console.log(`${t.taskId} ${t.status}${exit} ${t.title}`);
1019
- }
1020
- } else if (sub === "read") {
1021
- const taskId = positionals[2];
1022
- if (!taskId) die("terminal read 需要 <taskId>(用 cofluxd terminal list 查)");
1023
- const requested = Number(values.lines);
1024
- const lines = Number.isInteger(requested) && requested > 0 ? requested : DEFAULT_READ_LINES;
1025
- const result = await agentPost({ action: "terminal.read", taskId });
1026
- const exit = result.exitCode === undefined || result.exitCode === null ? "" : ` exit=${result.exitCode}`;
1027
- console.log(`# ${result.status}${exit}`);
1028
- const text = tailLines(stripAnsi(result.ansi), lines);
1029
- console.log(text || "(暂无输出)");
1030
- } else if (sub === "send") {
1031
- const taskId = positionals[2];
1032
- if (!taskId) die("terminal send 需要 <taskId>(用 cofluxd terminal list 查)");
1033
- const text = values.text ?? "";
1034
- if (!text && !values.enter) die(`terminal send 需要 --text "<文本>"(或至少 --enter 发一个回车)`);
1035
- await agentPost({ action: "terminal.send", taskId, text, enter: Boolean(values.enter) });
1036
- console.log(`已写入终端 ${taskId}(用 cofluxd terminal read ${taskId} 核对效果)`);
1037
- } else if (sub === "wait") {
1038
- const taskId = positionals[2];
1039
- if (!taskId) die("terminal wait 需要 <taskId>(用 cofluxd terminal list 查)");
1040
- const requested = Number(values.timeout);
1041
- const timeoutSec = Number.isFinite(requested) && requested > 0 ? requested : DEFAULT_WAIT_TIMEOUT_S;
1042
- const deadline = Date.now() + timeoutSec * 1000;
1043
- for (;;) {
1044
- // 按 taskId 直接问本地账本;目标不存在/不在本工作区时 daemon 回可读错误,agentPost 直接 die。
1045
- const t = await agentPost({ action: "terminal.status", taskId });
1046
- if (t.status === "exited") {
1047
- const exit = t.exitCode === undefined || t.exitCode === null ? "" : ` exit=${t.exitCode}`;
1048
- return void console.log(`# exited${exit}`);
1049
- }
1050
- if (Date.now() >= deadline) {
1051
- die(`等待超时(${timeoutSec}s):终端 ${taskId} 仍是 ${t.status}。可加大 --timeout,或 cofluxd terminal read ${taskId} 看现场`);
1052
- }
1053
- await sleep(WAIT_POLL_MS);
1054
- }
1055
- } else {
1056
- die(`terminal 需要子命令:new | list | read | wait | send`);
1057
- }
1058
- }
1059
-
1060
- async function cmdNotify() {
1061
- const message = positionals.slice(1).join(" ").trim();
1062
- if (!message) die(`notify 需要一句话,例如:cofluxd notify "两个方案拿不准,需要你定"`);
1063
- await agentPost({ action: "notify", message });
1064
- console.log("已通知用户(工作区在侧栏转为「等待交互」)");
1065
- }
1066
-
1067
- async function cmdProgress() {
1068
- const message = positionals.slice(1).join(" ").trim();
1069
- if (!message) die(`progress 需要一句话,例如:cofluxd progress "复现了,正在定位 relay 重连"`);
1070
- await agentPost({ action: "progress", message });
1071
- console.log("已更新进度(显示在工作区卡片上,被下一条覆盖)");
1072
- }
1073
-
1074
- async function cmdPorts() {
1075
- const { ports } = await agentPost({ action: "ports" });
1076
- if (!ports.length) return void console.log("本工作区暂无监听端口");
1077
- for (const p of ports) console.log(`${p.port} ${p.url}`);
1078
- }
1079
-
1080
854
  const HELP = `cofluxd —— coflux daemon 管理
1081
855
 
1082
856
  cofluxd 首次=up(打印浏览器授权链接),已配置=status
@@ -1089,65 +863,23 @@ const HELP = `cofluxd —— coflux daemon 管理
1089
863
  cofluxd logs [-f] 看 daemon 日志
1090
864
  cofluxd down 停止
1091
865
  cofluxd uninstall [--purge] 卸载(--purge 连二进制/配置/凭证一并删)
1092
- cofluxd hook <claude|codex> [agent hook 信使] 读 stdin/argv 的事件 JSON,转发给本机 daemon
1093
- (在 claude/codex 的 hook 配置里指向本命令;失败静默,不干扰 agent)
1094
-
1095
- 以下几条供**跑在 coflux 终端里的 agent** 调用,把工作变成用户看得见、能接管的东西:
1096
-
1097
- cofluxd terminal new [--cmd "<命令>"] [--title "<标题>"]
1098
- 开一个真实终端,用户在 coflux 侧栏能看到并随时接管
1099
- 带 --cmd = 作业终端:命令在登录 shell 里跑完即退出并带退出码,输出另
1100
- 落一份日志供 read 回读(代价:stdout 是管道,不是 tty)
1101
- 不带 --cmd = 会话终端:工作区目录下的常驻登录 shell,stdin/stdout 都是
1102
- 真 tty(能跑 vim/htop、有颜色),先 read 等提示符再 send,送 exit 才结束
1103
- cofluxd terminal list 列出本工作区的终端(含 status / 退出码)
1104
- cofluxd terminal read <taskId> [--lines N]
1105
- 读某个终端的内容(纯文本,默认最后 200 行;终端已退出也能读)
1106
- cofluxd terminal wait <taskId> [--timeout <秒>]
1107
- 阻塞等到该终端退出,打印退出码(默认超时 30 分钟)
1108
- cofluxd terminal send <taskId> --text "<文本>" [--enter]
1109
- 往终端里输入文本(--enter 追加回车)。用户正在接管时会被拒
1110
- cofluxd notify "<一句话>" 叫人:工作区在侧栏转为「等待交互」并显示这句话
1111
- cofluxd progress "<一句话>" 播报进度:显示在工作区卡片上,被下一条覆盖(不打扰用户)
1112
- cofluxd ports 列出本工作区的监听端口及可直接打开的预览 URL
1113
866
 
1114
867
  up flags: --server <ws://.../daemon> --name <名> --shell <路径>
1115
868
  通用: --version <vX|latest>(不传时 up 沿用已有二进制,update 默认 latest) --bin-dir <dir>(用本地 cargo 产物) --no-start
1116
869
  配置都在 ~/.coflux/settings.json(serverUrl/deviceName/shell),daemon 直接读;改后重跑 cofluxd up 生效。`;
1117
870
 
1118
- // onboard/reload 已在命令面重梳中移除(用户拍板 2026-07-23,见 plans/035):onboard 的交互
1119
- // 问答只剩"问设备名",价值不足以撑一个命令;reload 与幂等化后的 up 语义重复。
1120
- const MIGRATED = {
1121
- onboard: "onboard 已并入 up,直接运行 `cofluxd up`",
1122
- reload: "reload 已并入 up(up 现幂等,会按 settings.json 重装服务并重启),直接运行 `cofluxd up`",
1123
- };
1124
-
1125
871
  const { values, positionals } = parseArgs({
1126
872
  allowPositionals: true,
1127
873
  options: {
1128
- server: { type: "string" },
1129
- name: { type: "string" },
1130
- shell: { type: "string" },
1131
- title: { type: "string" },
1132
- cmd: { type: "string" },
1133
- lines: { type: "string" },
1134
- timeout: { type: "string" },
1135
- text: { type: "string" },
1136
- enter: { type: "boolean", default: false },
1137
- version: { type: "string" },
1138
- "bin-dir": { type: "string" },
1139
- "no-start": { type: "boolean", default: false },
1140
- purge: { type: "boolean", default: false },
1141
- follow: { type: "boolean", short: "f", default: false },
1142
- help: { type: "boolean", short: "h", default: false },
874
+ server: { type: "string" }, name: { type: "string" }, shell: { type: "string" },
875
+ version: { type: "string" }, "bin-dir": { type: "string" },
876
+ "no-start": { type: "boolean", default: false }, purge: { type: "boolean", default: false },
877
+ follow: { type: "boolean", short: "f", default: false }, help: { type: "boolean", short: "h", default: false },
1143
878
  },
1144
879
  });
1145
-
1146
- let cmd = positionals[0];
880
+ const cmd = positionals[0] || (fs.existsSync(SETTINGS) ? "status" : "up");
1147
881
  if (values.help || cmd === "help") { console.log(HELP); process.exit(0); }
1148
- if (!cmd) cmd = fs.existsSync(SETTINGS) ? "status" : "up"; // 首次裸跑 → 引导
1149
-
1150
- 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 };
1151
- const h = handlers[cmd];
1152
- if (!h) die(`未知命令: ${cmd}${MIGRATED[cmd] ? `\n${MIGRATED[cmd]}` : ""}\n\n${HELP}`);
1153
- await h(values);
882
+ const handlers = { up: cmdUp, update: cmdUpdate, restart: cmdRestart, down: cmdDown, status: cmdStatus, doctor: cmdDoctor, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall };
883
+ const handler = handlers[cmd];
884
+ if (!handler) die(`未知命令: ${cmd}\n登录与终端操作请使用 coflux。\n\n${HELP}`);
885
+ await handler(values);
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "cofluxd",
3
- "version": "0.14.0",
4
- "description": "coflux daemon 管理 CLI:装/起/停/升级 Rust daemon(supervisor + worker)",
3
+ "version": "1.0.0",
4
+ "description": "Coflux 无界面宿主(cofluxd)与统一操作工具(coflux)",
5
5
  "type": "module",
6
6
  "bin": {
7
- "cofluxd": "cofluxd.mjs"
7
+ "cofluxd": "cofluxd.mjs",
8
+ "coflux": "coflux.mjs"
8
9
  },
9
10
  "files": [
10
11
  "cofluxd.mjs",
12
+ "coflux.mjs",
13
+ "account-client.mjs",
11
14
  "release-pubkey.hex",
12
15
  "release-trust.mjs",
13
16
  "README.md",