cofluxd 0.8.0 → 0.9.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 +20 -0
- package/cofluxd.mjs +105 -1
- package/package.json +3 -2
- package/skills/coflux/SKILL.md +90 -0
package/README.md
CHANGED
|
@@ -24,6 +24,26 @@ cofluxd uninstall [--purge] # 卸载(--purge 连二进制/配置/凭证一
|
|
|
24
24
|
|
|
25
25
|
默认连公共服务 `wss://api.coflux.dev/daemon`(自托管用 `--server` 改;已保存的地址继续生效,非默认时会有醒目提示)。
|
|
26
26
|
|
|
27
|
+
## 给 agent 用的命令
|
|
28
|
+
|
|
29
|
+
跑在 coflux 终端里的 claude/codex 可以用下面几条,把工作外化成用户在 web/手机上**看得见、能接管**的东西——而不是在自己的 Bash 里后台起一个谁也看不见的进程:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
cofluxd terminal new --title "跑单测" --cmd "pnpm test" # 开真实终端,用户可接管
|
|
33
|
+
cofluxd terminal list # 本工作区的终端 + 状态/退出码
|
|
34
|
+
cofluxd terminal read <taskId> [--lines N] # 读终端内容(纯文本,已退出也能读)
|
|
35
|
+
cofluxd notify "需要你定一下用哪个方案" # 叫人:侧栏转「等待交互」
|
|
36
|
+
cofluxd ports # 端口 + 可直接打开的预览 URL
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
不需要任何凭证:daemon 用调用方 pid 反查进程树确认它属于哪个会话,**coflux 会话之外的进程一律拒绝**,权限也天然限定在该会话所属的工作区内。
|
|
40
|
+
|
|
41
|
+
配套的 skill 在 `skills/coflux/SKILL.md`(随包分发),装给 Claude Code:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
mkdir -p ~/.claude/skills && ln -sfn "$(npm root -g)/cofluxd/skills/coflux" ~/.claude/skills/coflux
|
|
45
|
+
```
|
|
46
|
+
|
|
27
47
|
`cofluxd up` 起服务后会打印一个一次性授权链接,在浏览器用已登录的账号打开确认即可(链接可在任意设备打开,包括无头设备),无需先去 web 控制台生成密钥。已登记设备重跑 `up` 不会重新触发授权。
|
|
28
48
|
|
|
29
49
|
## 本地优先与 doctor
|
package/cofluxd.mjs
CHANGED
|
@@ -747,6 +747,97 @@ async function cmdHook() {
|
|
|
747
747
|
}
|
|
748
748
|
}
|
|
749
749
|
|
|
750
|
+
/* --------------------- agent 协同控制(plan 074) --------------------- */
|
|
751
|
+
// 跑在 coflux 终端里的 claude/codex 用这组命令,把自己的工作外化成用户在 web/手机上
|
|
752
|
+
// **看得见、能接管**的 coflux 实体——而不是在自己的 Bash 里后台起一个谁也看不见的进程。
|
|
753
|
+
//
|
|
754
|
+
// 不需要任何凭证:daemon 用调用方 pid 反查进程树确认它属于哪个会话,树外一律拒。
|
|
755
|
+
// 与 `hook` 子命令的约定**相反**:这些命令必须写 stdout——输出就是给 agent 读的返回值。
|
|
756
|
+
// 也刻意不做自动重试:terminal new 有副作用,重试会开出两个终端,失败就把错误交给 agent。
|
|
757
|
+
|
|
758
|
+
const AGENT_TIMEOUT_MS = 30_000;
|
|
759
|
+
const DEFAULT_READ_LINES = 200;
|
|
760
|
+
|
|
761
|
+
async function agentPost(body) {
|
|
762
|
+
const portResult = localGatewayPort();
|
|
763
|
+
if (!portResult.ok) die(portResult.error);
|
|
764
|
+
let res;
|
|
765
|
+
try {
|
|
766
|
+
res = await fetch(`http://127.0.0.1:${portResult.port}/agent`, {
|
|
767
|
+
method: "POST",
|
|
768
|
+
headers: { "content-type": "application/json" },
|
|
769
|
+
body: JSON.stringify({ ...body, pid: process.pid, ppid: process.ppid }),
|
|
770
|
+
signal: AbortSignal.timeout(AGENT_TIMEOUT_MS),
|
|
771
|
+
});
|
|
772
|
+
} catch (error) {
|
|
773
|
+
die(`连不上本机 daemon:${error?.message || error}(daemon 没在跑?先看 cofluxd status)`);
|
|
774
|
+
}
|
|
775
|
+
let parsed = null;
|
|
776
|
+
try { parsed = await res.json(); } catch { /* 非 JSON 响应按下面的兜底报错处理 */ }
|
|
777
|
+
if (!res.ok || !parsed?.ok) die(parsed?.error || `daemon 返回 ${res.status}`);
|
|
778
|
+
return parsed;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// 剥掉 ANSI/OSC 转义与 C0 控制字符,保留 \t 与 \n——snapshot 是给终端渲染的字节流,
|
|
782
|
+
// agent 要的是能读的纯文本。去转义放在 CLI 侧:daemon 的 snapshot 同时是 checkpoint 的
|
|
783
|
+
// 数据来源,不为 agent 的可读性改它的语义。
|
|
784
|
+
const ANSI_RE =
|
|
785
|
+
/\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;
|
|
786
|
+
|
|
787
|
+
function stripAnsi(raw) {
|
|
788
|
+
return String(raw ?? "").replace(ANSI_RE, "");
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** 取最后 n 行并去掉尾部空行——VT snapshot 的下半屏通常是成片空行,对 agent 是纯噪音。 */
|
|
792
|
+
function tailLines(text, n) {
|
|
793
|
+
const lines = text.split("\n");
|
|
794
|
+
while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
|
|
795
|
+
return lines.slice(-n).join("\n");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function cmdTerminal(values) {
|
|
799
|
+
const sub = positionals[1];
|
|
800
|
+
if (sub === "new") {
|
|
801
|
+
const command = values.cmd;
|
|
802
|
+
if (!command) die(`terminal new 需要 --cmd "<命令>"`);
|
|
803
|
+
const result = await agentPost({ action: "terminal.new", title: values.title || "", command });
|
|
804
|
+
console.log(`已开终端 ${result.taskId}(用户可在 coflux 侧栏看到并随时接管)`);
|
|
805
|
+
console.log(`看输出:cofluxd terminal read ${result.taskId}`);
|
|
806
|
+
} else if (sub === "list") {
|
|
807
|
+
const { terminals } = await agentPost({ action: "terminal.list" });
|
|
808
|
+
if (!terminals.length) return void console.log("本工作区暂无终端");
|
|
809
|
+
for (const t of terminals) {
|
|
810
|
+
const exit = t.exitCode === undefined || t.exitCode === null ? "" : ` exit=${t.exitCode}`;
|
|
811
|
+
console.log(`${t.taskId} ${t.status}${exit} ${t.title}`);
|
|
812
|
+
}
|
|
813
|
+
} else if (sub === "read") {
|
|
814
|
+
const taskId = positionals[2];
|
|
815
|
+
if (!taskId) die("terminal read 需要 <taskId>(用 cofluxd terminal list 查)");
|
|
816
|
+
const requested = Number(values.lines);
|
|
817
|
+
const lines = Number.isInteger(requested) && requested > 0 ? requested : DEFAULT_READ_LINES;
|
|
818
|
+
const result = await agentPost({ action: "terminal.read", taskId });
|
|
819
|
+
const exit = result.exitCode === undefined || result.exitCode === null ? "" : ` exit=${result.exitCode}`;
|
|
820
|
+
console.log(`# ${result.status}${exit}`);
|
|
821
|
+
const text = tailLines(stripAnsi(result.ansi), lines);
|
|
822
|
+
console.log(text || "(暂无输出)");
|
|
823
|
+
} else {
|
|
824
|
+
die(`terminal 需要子命令:new | list | read`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function cmdNotify() {
|
|
829
|
+
const message = positionals.slice(1).join(" ").trim();
|
|
830
|
+
if (!message) die(`notify 需要一句话,例如:cofluxd notify "两个方案拿不准,需要你定"`);
|
|
831
|
+
await agentPost({ action: "notify", message });
|
|
832
|
+
console.log("已通知用户(工作区在侧栏转为「等待交互」)");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function cmdPorts() {
|
|
836
|
+
const { ports } = await agentPost({ action: "ports" });
|
|
837
|
+
if (!ports.length) return void console.log("本工作区暂无监听端口");
|
|
838
|
+
for (const p of ports) console.log(`${p.port} ${p.url}`);
|
|
839
|
+
}
|
|
840
|
+
|
|
750
841
|
const HELP = `cofluxd —— coflux daemon 管理
|
|
751
842
|
|
|
752
843
|
cofluxd 首次=up(打印浏览器授权链接),已配置=status
|
|
@@ -762,6 +853,16 @@ const HELP = `cofluxd —— coflux daemon 管理
|
|
|
762
853
|
cofluxd hook <claude|codex> [agent hook 信使] 读 stdin/argv 的事件 JSON,转发给本机 daemon
|
|
763
854
|
(在 claude/codex 的 hook 配置里指向本命令;失败静默,不干扰 agent)
|
|
764
855
|
|
|
856
|
+
以下几条供**跑在 coflux 终端里的 agent** 调用,把工作变成用户看得见、能接管的东西:
|
|
857
|
+
|
|
858
|
+
cofluxd terminal new --cmd "<命令>" [--title "<标题>"]
|
|
859
|
+
开一个真实终端跑命令,用户在 coflux 侧栏能看到并随时接管
|
|
860
|
+
cofluxd terminal list 列出本工作区的终端(含 status / 退出码)
|
|
861
|
+
cofluxd terminal read <taskId> [--lines N]
|
|
862
|
+
读某个终端的内容(纯文本,默认最后 200 行;终端已退出也能读)
|
|
863
|
+
cofluxd notify "<一句话>" 叫人:工作区在侧栏转为「等待交互」并显示这句话
|
|
864
|
+
cofluxd ports 列出本工作区的监听端口及可直接打开的预览 URL
|
|
865
|
+
|
|
765
866
|
up flags: --server <ws://.../daemon> --name <名> --shell <路径>
|
|
766
867
|
通用: --version <vX|latest>(不传时 up 沿用已有二进制,update 默认 latest) --bin-dir <dir>(用本地 cargo 产物) --no-start
|
|
767
868
|
配置都在 ~/.coflux/settings.json(serverUrl/deviceName/shell),daemon 直接读;改后重跑 cofluxd up 生效。`;
|
|
@@ -779,6 +880,9 @@ const { values, positionals } = parseArgs({
|
|
|
779
880
|
server: { type: "string" },
|
|
780
881
|
name: { type: "string" },
|
|
781
882
|
shell: { type: "string" },
|
|
883
|
+
title: { type: "string" },
|
|
884
|
+
cmd: { type: "string" },
|
|
885
|
+
lines: { type: "string" },
|
|
782
886
|
version: { type: "string" },
|
|
783
887
|
"bin-dir": { type: "string" },
|
|
784
888
|
"no-start": { type: "boolean", default: false },
|
|
@@ -792,7 +896,7 @@ let cmd = positionals[0];
|
|
|
792
896
|
if (values.help || cmd === "help") { console.log(HELP); process.exit(0); }
|
|
793
897
|
if (!cmd) cmd = fs.existsSync(SETTINGS) ? "status" : "up"; // 首次裸跑 → 引导
|
|
794
898
|
|
|
795
|
-
const handlers = { up: cmdUp, update: cmdUpdate, restart: cmdRestart, down: cmdDown, status: cmdStatus, doctor: cmdDoctor, fda: cmdFda, logs: cmdLogs, uninstall: cmdUninstall, hook: cmdHook };
|
|
899
|
+
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, ports: cmdPorts };
|
|
796
900
|
const h = handlers[cmd];
|
|
797
901
|
if (!h) die(`未知命令: ${cmd}${MIGRATED[cmd] ? `\n${MIGRATED[cmd]}` : ""}\n\n${HELP}`);
|
|
798
902
|
await h(values);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cofluxd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "coflux daemon 管理 CLI:装/起/停/升级 Rust daemon(supervisor + worker)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cofluxd.mjs",
|
|
11
|
-
"README.md"
|
|
11
|
+
"README.md",
|
|
12
|
+
"skills"
|
|
12
13
|
],
|
|
13
14
|
"engines": {
|
|
14
15
|
"node": ">=20"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: coflux
|
|
3
|
+
description: 当你运行在 coflux 终端里时,把长任务、并行工作和求助外化成用户在 coflux web/手机上看得见、能随时接管的真实终端。适用于跑测试/构建/dev server 等耗时命令、需要用户接管或决策、想给用户一个可点开的预览 URL 的场景。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 在 coflux 里工作
|
|
7
|
+
|
|
8
|
+
你可能正跑在 coflux 的一个终端里。coflux 让用户在浏览器和手机上盯着各台机器上的
|
|
9
|
+
agent 干活,随时接管。这套命令让你把自己的工作**变成用户看得见的东西**。
|
|
10
|
+
|
|
11
|
+
先判断自己在不在 coflux 里:任意一条命令返回「不在 coflux 终端里」就说明不在,
|
|
12
|
+
这时忘掉这个 skill,照常用你自己的工具。
|
|
13
|
+
|
|
14
|
+
## 什么时候用
|
|
15
|
+
|
|
16
|
+
**用 `cofluxd terminal new` 而不是自己后台起进程**——只要这条命令满足任一条:
|
|
17
|
+
|
|
18
|
+
- 要跑超过十几秒(测试、构建、安装依赖、迁移)
|
|
19
|
+
- 会一直跑下去(dev server、watch、日志跟随)
|
|
20
|
+
- 用户可能想接管(需要交互、可能要中途叫停、失败了要人去调)
|
|
21
|
+
|
|
22
|
+
这类工作在你自己的 Bash 里后台跑,用户**什么也看不见**:看不到它在跑、接管不了、
|
|
23
|
+
出问题只能等你转述。开成 coflux 终端,它就是侧栏里一个有标题的条目,用户能点进去、
|
|
24
|
+
能接管、能自己敲命令。
|
|
25
|
+
|
|
26
|
+
**不要用**在一次性的快命令上(`ls`、`grep`、`git status`、读文件)——你自己的工具更快,
|
|
27
|
+
给用户开一堆一秒就结束的终端只是噪音。
|
|
28
|
+
|
|
29
|
+
## 命令
|
|
30
|
+
|
|
31
|
+
### 开终端跑命令
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
cofluxd terminal new --title "跑单测" --cmd "pnpm -C tests test"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--title` 是用户在侧栏看到的名字,**认真起**:写「跑单测」「起 dev server」,
|
|
38
|
+
别写「terminal 1」。命令在当前工作区目录下、用登录 shell 执行。
|
|
39
|
+
|
|
40
|
+
命令跑完终端就退出,任务转 `exited` 并带上退出码——这是你判断成没成的依据。
|
|
41
|
+
所以别指望在同一个终端里接着跑第二条命令,要么写成 `a && b`,要么再开一个。
|
|
42
|
+
|
|
43
|
+
命令的输出会同时落一份日志供你回读,代价是它的 stdout 是管道而不是 tty——多数程序会因此
|
|
44
|
+
关掉颜色和进度条。极少数程序在非 tty 下行为不同(比如不输出进度、切成 CI 模式),如果你
|
|
45
|
+
依赖那种行为,自己在 Bash 里跑。
|
|
46
|
+
|
|
47
|
+
### 看跑到哪了
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
cofluxd terminal list # 本工作区所有终端:id、状态、退出码、标题
|
|
51
|
+
cofluxd terminal read <taskId> # 某个终端的内容(纯文本,默认最后 200 行)
|
|
52
|
+
cofluxd terminal read <taskId> --lines 50
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`list` 的状态是 `running` / `exited` / `idle`;`exited` 会带 `exit=<码>`。
|
|
56
|
+
**终端已经退出也能 read**——「命令跑完了看输出」正是最常用的场景。
|
|
57
|
+
|
|
58
|
+
内容有最多约 2 秒的延迟(来自中心的定期快照),所以刚 `new` 完立刻 `read` 可能是空的。
|
|
59
|
+
要等一条命令跑完,隔几秒 `list` 一次看它转没转 `exited`,别忙等。
|
|
60
|
+
|
|
61
|
+
### 叫人
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
cofluxd notify "两个方案都能走通,需要你定一下用哪个"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
用户的侧栏里这个工作区会转成「等待交互」并显示这句话——他在手机上也看得到。
|
|
68
|
+
用在你**真的卡住**的时候:需要用户决策、要密码/权限、发现了必须人来判断的问题。
|
|
69
|
+
一句话说清要什么,别写成日志。
|
|
70
|
+
|
|
71
|
+
(你正常的提问和权限请求已经会自动反映到侧栏状态上,不需要额外 notify。
|
|
72
|
+
这条是给「你要说的事,用户光看状态图标猜不出来」准备的。)
|
|
73
|
+
|
|
74
|
+
### 给用户可点开的预览
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
cofluxd ports
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
列出本工作区所有监听端口和对应的公网预览 URL。起了 dev server 之后用它拿 URL 直接
|
|
81
|
+
告诉用户,他点开就能看,不用自己去翻。
|
|
82
|
+
|
|
83
|
+
## 边界
|
|
84
|
+
|
|
85
|
+
- 你**只能开和读**,不能往别人的终端里打字。要交互就 `notify` 让用户接管。
|
|
86
|
+
- 能看到的只有**你自己所在的工作区**,别的工作区和别的机器都看不见也碰不到。
|
|
87
|
+
- 一个工作区同时活着的终端有上限(默认 8,含用户自己开的)。撞上限先 `list` 看看,
|
|
88
|
+
多半是有跑完没收的;真是用户占满了,就 `notify` 告诉他,别硬试。
|
|
89
|
+
- 这些命令都要 daemon 连着中心才能用——毕竟「让用户看见」就是它们的全部意义。
|
|
90
|
+
连不上时会明确报错,不会默默降级。
|