u-foo 1.0.3 → 1.0.6

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.
Files changed (91) hide show
  1. package/README.md +67 -8
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +117 -0
  4. package/SKILLS/uinit/SKILL.md +73 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucodex.js +13 -0
  8. package/bin/ufoo +9 -31
  9. package/bin/ufoo.js +13 -0
  10. package/modules/AGENTS.template.md +15 -7
  11. package/modules/bus/README.md +28 -23
  12. package/modules/bus/SKILLS/ubus/SKILL.md +18 -8
  13. package/modules/context/README.md +18 -40
  14. package/modules/context/SKILLS/uctx/SKILL.md +61 -1
  15. package/package.json +16 -4
  16. package/scripts/.archived/bash-to-js-migration/README.md +46 -0
  17. package/scripts/.archived/bash-to-js-migration/banner.sh +89 -0
  18. package/scripts/{bus-inject.sh → .archived/bash-to-js-migration/bus-inject.sh} +35 -3
  19. package/scripts/{bus.sh → .archived/bash-to-js-migration/bus.sh} +3 -1
  20. package/scripts/banner.sh +2 -89
  21. package/scripts/postinstall.js +59 -0
  22. package/src/agent/cliRunner.js +33 -5
  23. package/src/agent/internalRunner.js +78 -51
  24. package/src/agent/launcher.js +702 -0
  25. package/src/agent/notifier.js +200 -0
  26. package/src/agent/ptyRunner.js +377 -0
  27. package/src/agent/ptyWrapper.js +354 -0
  28. package/src/agent/readyDetector.js +159 -0
  29. package/src/agent/ufooAgent.js +37 -42
  30. package/src/bus/API_DESIGN.md +204 -0
  31. package/src/bus/activate.js +156 -0
  32. package/src/bus/daemon.js +308 -0
  33. package/src/bus/index.js +785 -0
  34. package/src/bus/inject.js +285 -0
  35. package/src/bus/message.js +302 -0
  36. package/src/bus/nickname.js +86 -0
  37. package/src/bus/queue.js +131 -0
  38. package/src/bus/shake.js +26 -0
  39. package/src/bus/subscriber.js +296 -0
  40. package/src/bus/utils.js +357 -0
  41. package/src/chat/index.js +1842 -249
  42. package/src/cli.js +658 -95
  43. package/src/config.js +9 -2
  44. package/src/context/decisions.js +314 -0
  45. package/src/context/doctor.js +183 -0
  46. package/src/context/index.js +38 -0
  47. package/src/daemon/index.js +749 -94
  48. package/src/daemon/ops.js +395 -51
  49. package/src/daemon/providerSessions.js +291 -0
  50. package/src/daemon/run.js +34 -1
  51. package/src/daemon/status.js +24 -7
  52. package/src/doctor/index.js +50 -0
  53. package/src/init/index.js +264 -0
  54. package/src/skills/index.js +159 -0
  55. package/src/status/index.js +252 -0
  56. package/src/terminal/detect.js +64 -0
  57. package/src/terminal/index.js +8 -0
  58. package/src/terminal/iterm2.js +126 -0
  59. package/src/ufoo/agentsStore.js +41 -0
  60. package/src/ufoo/paths.js +46 -0
  61. package/src/utils/banner.js +73 -0
  62. package/bin/uclaude +0 -65
  63. package/bin/ucodex +0 -65
  64. package/modules/bus/scripts/bus-alert.sh +0 -185
  65. package/modules/bus/scripts/bus-listen.sh +0 -117
  66. package/modules/context/ASSUMPTIONS.md +0 -7
  67. package/modules/context/CONSTRAINTS.md +0 -7
  68. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  69. package/modules/context/DECISION-PROTOCOL.md +0 -62
  70. package/modules/context/HANDOFF.md +0 -33
  71. package/modules/context/RULES.md +0 -15
  72. package/modules/context/SKILLS/README.md +0 -14
  73. package/modules/context/SYSTEM.md +0 -18
  74. package/modules/context/TEMPLATES/assumptions.md +0 -4
  75. package/modules/context/TEMPLATES/constraints.md +0 -4
  76. package/modules/context/TEMPLATES/decision.md +0 -16
  77. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  78. package/modules/context/TEMPLATES/system.md +0 -3
  79. package/modules/context/TEMPLATES/terminology.md +0 -4
  80. package/modules/context/TERMINOLOGY.md +0 -10
  81. /package/scripts/{bus-alert.sh → .archived/bash-to-js-migration/bus-alert.sh} +0 -0
  82. /package/scripts/{bus-autotrigger.sh → .archived/bash-to-js-migration/bus-autotrigger.sh} +0 -0
  83. /package/scripts/{bus-daemon.sh → .archived/bash-to-js-migration/bus-daemon.sh} +0 -0
  84. /package/scripts/{bus-listen.sh → .archived/bash-to-js-migration/bus-listen.sh} +0 -0
  85. /package/scripts/{context-decisions.sh → .archived/bash-to-js-migration/context-decisions.sh} +0 -0
  86. /package/scripts/{context-doctor.sh → .archived/bash-to-js-migration/context-doctor.sh} +0 -0
  87. /package/scripts/{context-lint.sh → .archived/bash-to-js-migration/context-lint.sh} +0 -0
  88. /package/scripts/{doctor.sh → .archived/bash-to-js-migration/doctor.sh} +0 -0
  89. /package/scripts/{init.sh → .archived/bash-to-js-migration/init.sh} +0 -0
  90. /package/scripts/{skills.sh → .archived/bash-to-js-migration/skills.sh} +0 -0
  91. /package/scripts/{status.sh → .archived/bash-to-js-migration/status.sh} +0 -0
@@ -0,0 +1,204 @@
1
+ # Event Bus JavaScript API 设计
2
+
3
+ ## 概述
4
+
5
+ 已将 Event Bus 从 bash 迁移到 JavaScript,核心能力由 `src/bus` 模块提供。
6
+
7
+ ## 核心类设计
8
+
9
+ ```javascript
10
+ class EventBus {
11
+ constructor(projectRoot);
12
+
13
+ // 初始化
14
+ async init();
15
+
16
+ // 订阅者管理
17
+ async join(sessionId, agentType, nickname);
18
+ async leave(subscriber);
19
+ async rename(subscriber, nickname);
20
+ async whoami(); // 获取当前订阅者 ID
21
+
22
+ // 消息发送
23
+ async send(target, message, publisher);
24
+ async broadcast(message, publisher);
25
+
26
+ // 消息接收
27
+ async check(subscriber, autoAck);
28
+ async ack(subscriber);
29
+ async consume(subscriber, fromBeginning);
30
+
31
+ // 查询与路由
32
+ async status();
33
+ async resolve(myId, targetType);
34
+
35
+ // 后台监听
36
+ async alert(subscriber, interval, options);
37
+ async listen(subscriber, options);
38
+ }
39
+ ```
40
+
41
+ ## 文件结构
42
+
43
+ ```
44
+ src/bus/
45
+ ├── index.js # EventBus 主类
46
+ ├── subscriber.js # 订阅者管理
47
+ ├── message.js # 消息发送/接收
48
+ ├── queue.js # 队列管理(offset, pending)
49
+ ├── nickname.js # 昵称解析
50
+ ├── daemon.js # bus daemon(自动注入 /ubus)
51
+ ├── utils.js # 工具函数
52
+ └── API_DESIGN.md # 本文件
53
+ ```
54
+
55
+ ## 数据结构
56
+
57
+ ### .ufoo/agent/all-agents.json
58
+ ```json
59
+ {
60
+ "schema_version": 1,
61
+ "created_at": "2026-01-29T...",
62
+ "agents": {
63
+ "claude-code:abc123": {
64
+ "agent_type": "claude-code",
65
+ "nickname": "architect",
66
+ "status": "active",
67
+ "joined_at": "...",
68
+ "last_seen": "...",
69
+ "pid": 12345,
70
+ "tty": "/dev/ttys001"
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### events/YYYY-MM-DD.jsonl
77
+ ```json
78
+ {"seq":1,"timestamp":"...","type":"message/targeted","event":"message","publisher":"...","target":"...","data":{...}}
79
+ ```
80
+
81
+ ### queues/{subscriber}/pending.jsonl
82
+ ```json
83
+ {"seq":1,"timestamp":"...","type":"message/targeted","event":"message","publisher":"...","data":{...}}
84
+ ```
85
+
86
+ ### queues/{subscriber}/offset
87
+ ```
88
+ 5
89
+ ```
90
+
91
+ ## 关键功能实现
92
+
93
+ ### 1. 消息路由(支持昵称)
94
+
95
+ ```javascript
96
+ async resolveTarget(target) {
97
+ // 优先级:
98
+ // 1. 精确订阅者 ID (claude-code:abc123)
99
+ // 2. 昵称匹配 (architect -> claude-code:abc123)
100
+ // 3. 代理类型 (codex -> 所有 codex 代理)
101
+ // 4. 通配符 (* -> 所有代理)
102
+ }
103
+ ```
104
+
105
+ ### 2. 队列管理
106
+
107
+ ```javascript
108
+ class QueueManager {
109
+ async getOffset(subscriber);
110
+ async setOffset(subscriber, seq);
111
+ async appendPending(subscriber, event);
112
+ async readPending(subscriber);
113
+ async clearPending(subscriber);
114
+ }
115
+ ```
116
+
117
+ ### 3. 序号生成(全局唯一)
118
+
119
+ ```javascript
120
+ async getNextSeq() {
121
+ // 读取所有 events/*.jsonl 文件的最后一行
122
+ // 返回 max(seq) + 1
123
+ // 保证全局唯一、单调递增
124
+ }
125
+ ```
126
+
127
+ ### 4. 昵称冲突检测
128
+
129
+ ```javascript
130
+ async ensureUniqueNickname(nickname, excludeSubscriber) {
131
+ // 检查 all-agents.json 中是否已存在该昵称
132
+ // 返回是否唯一
133
+ }
134
+ ```
135
+
136
+ ## 向后兼容性
137
+
138
+ ### CLI 接口保持不变
139
+
140
+ ```bash
141
+ ufoo bus init
142
+ ufoo bus join [session] [type] [nick]
143
+ ufoo bus send <target> <message>
144
+ # ... 所有命令保持原样
145
+ ```
146
+
147
+ ### 环境变量支持
148
+
149
+ - `AI_BUS_PUBLISHER` - 发送者 ID
150
+ - `CLAUDE_SESSION_ID` / `CODEX_SESSION_ID` - 会话 ID
151
+ - `UFOO_NICKNAME` - 昵称
152
+
153
+ ## 错误处理
154
+
155
+ ```javascript
156
+ class BusError extends Error {
157
+ constructor(code, message) {
158
+ super(message);
159
+ this.code = code;
160
+ }
161
+ }
162
+
163
+ // 错误码:
164
+ // BUS_NOT_INITIALIZED
165
+ // SUBSCRIBER_NOT_FOUND
166
+ // NICKNAME_CONFLICT
167
+ // INVALID_TARGET
168
+ // ...
169
+ ```
170
+
171
+ ## 测试策略
172
+
173
+ ### 单元测试
174
+ - 每个模块独立测试
175
+ - Mock 文件系统操作
176
+
177
+ ### 集成测试
178
+ - 完整消息流测试(send -> check -> ack)
179
+ - 多订阅者场景
180
+ - 昵称冲突处理
181
+
182
+ ### 性能测试
183
+ - 消息发送延迟 < 50ms
184
+ - 序号生成 < 10ms
185
+ - 状态查询 < 100ms
186
+
187
+ ## 迁移检查清单
188
+
189
+ - [ ] init 命令
190
+ - [ ] join/leave 命令
191
+ - [ ] send/broadcast 命令
192
+ - [ ] check/ack 命令
193
+ - [ ] status 命令
194
+ - [ ] resolve 命令
195
+ - [ ] rename 命令(支持昵称)
196
+ - [ ] consume 命令
197
+ - [ ] alert 后台监听
198
+ - [ ] listen 前台监听
199
+ - [ ] autotrigger 自动触发
200
+ - [ ] 昵称解析(send 支持昵称)
201
+ - [ ] 全局序号唯一性
202
+ - [ ] 文件并发安全
203
+ - [ ] 错误处理和日志
204
+ - [ ] CLI 向后兼容
@@ -0,0 +1,156 @@
1
+ const fs = require("fs");
2
+ const { getUfooPaths } = require("../ufoo/paths");
3
+ const { spawn, spawnSync } = require("child_process");
4
+
5
+ /**
6
+ * 激活指定 agent 的终端
7
+ *
8
+ * 支持的方式:
9
+ * - tmux: 使用 tmux select-pane 激活
10
+ * - terminal: 使用 AppleScript 通过 tty 定位并激活 Terminal.app 的 tab/window
11
+ * - internal: 不支持自动激活(由 chat PTY view 处理)
12
+ */
13
+ class AgentActivator {
14
+ constructor(projectRoot) {
15
+ this.projectRoot = projectRoot;
16
+ const paths = getUfooPaths(projectRoot);
17
+ this.agentsFile = paths.agentsFile;
18
+ }
19
+
20
+ /**
21
+ * 获取 agent 信息
22
+ */
23
+ getAgentInfo(agentId) {
24
+ try {
25
+ if (!fs.existsSync(this.agentsFile)) {
26
+ throw new Error("Bus not initialized");
27
+ }
28
+
29
+ const busData = JSON.parse(fs.readFileSync(this.agentsFile, "utf8"));
30
+ const meta = busData.agents?.[agentId];
31
+
32
+ if (!meta) {
33
+ throw new Error(`Agent not found: ${agentId}`);
34
+ }
35
+
36
+ return {
37
+ id: agentId,
38
+ nickname: meta.nickname || "",
39
+ tty: meta.tty || "",
40
+ tmux_pane: meta.tmux_pane || "",
41
+ launch_mode: meta.launch_mode || "",
42
+ };
43
+ } catch (err) {
44
+ throw new Error(`Failed to get agent info: ${err.message}`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 激活 tmux pane
50
+ */
51
+ activateTmuxPane(paneId) {
52
+ return new Promise((resolve, reject) => {
53
+ // 首先检查 pane 是否存在
54
+ const checkProc = spawn("tmux", ["list-panes", "-a", "-F", "#{pane_id}"]);
55
+ let output = "";
56
+
57
+ checkProc.stdout.on("data", (data) => {
58
+ output += data.toString();
59
+ });
60
+
61
+ checkProc.on("close", (code) => {
62
+ if (code !== 0) {
63
+ reject(new Error("tmux is not running"));
64
+ return;
65
+ }
66
+
67
+ const panes = output.trim().split("\n");
68
+ if (!panes.includes(paneId)) {
69
+ reject(new Error(`tmux pane not found: ${paneId}`));
70
+ return;
71
+ }
72
+
73
+ // 激活 pane(选择 window 和 pane)
74
+ const selectProc = spawn("tmux", ["select-pane", "-t", paneId]);
75
+
76
+ selectProc.on("close", (selectCode) => {
77
+ if (selectCode === 0) {
78
+ resolve();
79
+ } else {
80
+ reject(new Error("Failed to select tmux pane"));
81
+ }
82
+ });
83
+
84
+ selectProc.on("error", reject);
85
+ });
86
+
87
+ checkProc.on("error", reject);
88
+ });
89
+ }
90
+
91
+ /**
92
+ * 通过 tty 激活 Terminal.app 中对应的 tab/window
93
+ */
94
+ activateTerminalByTty(ttyPath) {
95
+ if (process.platform !== "darwin") {
96
+ throw new Error("Terminal activation is only supported on macOS");
97
+ }
98
+ if (!ttyPath) {
99
+ throw new Error("Cannot activate: tty path required");
100
+ }
101
+
102
+ const script = `
103
+ tell application "Terminal"
104
+ repeat with w in windows
105
+ repeat with t in tabs of w
106
+ if tty of t is "${ttyPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}" then
107
+ set selected tab of w to t
108
+ set index of w to 1
109
+ activate
110
+ return "ok"
111
+ end if
112
+ end repeat
113
+ end repeat
114
+ return "not found"
115
+ end tell`;
116
+
117
+ const result = spawnSync("osascript", ["-e", script], {
118
+ encoding: "utf8",
119
+ timeout: 5000,
120
+ });
121
+
122
+ if (result.status !== 0) {
123
+ throw new Error(`AppleScript failed: ${(result.stderr || "").trim()}`);
124
+ }
125
+
126
+ const output = (result.stdout || "").trim();
127
+ if (output === "not found") {
128
+ throw new Error(`Terminal tab not found for tty: ${ttyPath}`);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * 激活 agent 的终端
134
+ */
135
+ async activate(agentId) {
136
+ const info = this.getAgentInfo(agentId);
137
+
138
+ if (info.launch_mode === "internal" || info.launch_mode === "internal-pty") {
139
+ throw new Error("Internal mode agents cannot be activated (no terminal window)");
140
+ }
141
+
142
+ if (info.launch_mode === "tmux" && info.tmux_pane) {
143
+ await this.activateTmuxPane(info.tmux_pane);
144
+ return;
145
+ }
146
+
147
+ if (info.launch_mode === "terminal" && info.tty) {
148
+ this.activateTerminalByTty(info.tty);
149
+ return;
150
+ }
151
+
152
+ throw new Error("Cannot activate: missing tty or tmux_pane for agent");
153
+ }
154
+ }
155
+
156
+ module.exports = AgentActivator;
@@ -0,0 +1,308 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { readJSON, writeJSON, isPidAlive, isAgentPidAlive, ensureDir, safeNameToSubscriber, subscriberToSafeName } = require("./utils");
4
+ const Injector = require("./inject");
5
+ const QueueManager = require("./queue");
6
+
7
+ /**
8
+ * Bus Daemon - 监控消息并自动注入命令
9
+ */
10
+ class BusDaemon {
11
+ constructor(busDir, agentsFile, daemonDir, interval = 2000) {
12
+ this.busDir = busDir;
13
+ this.agentsFile = agentsFile;
14
+ this.interval = interval;
15
+ this.daemonDir = daemonDir;
16
+ this.pidFile = path.join(this.daemonDir, "daemon.pid");
17
+ this.logFile = path.join(this.daemonDir, "daemon.log");
18
+ this.countsDir = path.join(this.daemonDir, "counts", `${process.pid}`);
19
+ this.running = false;
20
+ this.cleanupCounter = 0;
21
+ this.cleanupInterval = 5; // 每 5 个周期清理一次
22
+
23
+ this.queueManager = new QueueManager(busDir);
24
+ this.injector = new Injector(busDir, agentsFile);
25
+ }
26
+
27
+ /**
28
+ * 检查 daemon 是否正在运行
29
+ */
30
+ isRunning() {
31
+ if (!fs.existsSync(this.pidFile)) {
32
+ return false;
33
+ }
34
+
35
+ const pid = parseInt(fs.readFileSync(this.pidFile, "utf8").trim(), 10);
36
+ return isPidAlive(pid);
37
+ }
38
+
39
+ /**
40
+ * 获取运行中的 daemon PID
41
+ */
42
+ getRunningPid() {
43
+ if (!fs.existsSync(this.pidFile)) {
44
+ return null;
45
+ }
46
+
47
+ const pid = parseInt(fs.readFileSync(this.pidFile, "utf8").trim(), 10);
48
+ return isPidAlive(pid) ? pid : null;
49
+ }
50
+
51
+ /**
52
+ * 启动 daemon
53
+ */
54
+ async start(background = false) {
55
+ // 检查是否已经在运行
56
+ if (this.isRunning()) {
57
+ const pid = this.getRunningPid();
58
+ console.log(`[daemon] Already running (pid=${pid})`);
59
+ return;
60
+ }
61
+ ensureDir(this.daemonDir);
62
+ ensureDir(path.join(this.daemonDir, "counts"));
63
+
64
+ if (background) {
65
+ // 后台模式:spawn 独立进程
66
+ const { spawn } = require("child_process");
67
+ const logStream = fs.openSync(this.logFile, "a");
68
+
69
+ const child = spawn(
70
+ process.execPath,
71
+ [
72
+ path.join(__dirname, "..", "..", "bin", "ufoo.js"),
73
+ "bus",
74
+ "daemon",
75
+ "--interval",
76
+ String(this.interval / 1000),
77
+ ],
78
+ {
79
+ detached: true,
80
+ stdio: ["ignore", logStream, logStream],
81
+ cwd: process.cwd(),
82
+ }
83
+ );
84
+
85
+ child.unref();
86
+
87
+ // 等待 PID 文件创建
88
+ await new Promise((resolve) => setTimeout(resolve, 500));
89
+
90
+ const pid = this.getRunningPid();
91
+ console.log(`[daemon] Started in background (pid=${pid}, log: ${this.logFile})`);
92
+ } else {
93
+ // 前台模式
94
+ this.run();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 停止 daemon
100
+ */
101
+ stop() {
102
+ const pid = this.getRunningPid();
103
+ if (!pid) {
104
+ console.log("[daemon] Not running");
105
+ return;
106
+ }
107
+
108
+ try {
109
+ process.kill(pid, "SIGTERM");
110
+ console.log(`[daemon] Stopped (pid=${pid})`);
111
+ if (fs.existsSync(this.pidFile)) {
112
+ fs.unlinkSync(this.pidFile);
113
+ }
114
+ } catch (err) {
115
+ console.error(`[daemon] Failed to stop: ${err.message}`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 显示 daemon 状态
121
+ */
122
+ status() {
123
+ const pid = this.getRunningPid();
124
+ if (pid) {
125
+ console.log(`[daemon] Running (pid=${pid})`);
126
+ } else {
127
+ console.log("[daemon] Not running");
128
+ // 清理过时的 PID 文件
129
+ if (fs.existsSync(this.pidFile)) {
130
+ fs.unlinkSync(this.pidFile);
131
+ }
132
+ }
133
+ }
134
+
135
+ /**
136
+ * 运行 daemon(前台)
137
+ */
138
+ run() {
139
+ // 记录 PID
140
+ ensureDir(path.dirname(this.pidFile));
141
+ fs.writeFileSync(this.pidFile, `${process.pid}\n`, "utf8");
142
+
143
+ // 设置清理钩子
144
+ const cleanup = () => {
145
+ this.running = false;
146
+ if (fs.existsSync(this.pidFile)) {
147
+ fs.unlinkSync(this.pidFile);
148
+ }
149
+ if (fs.existsSync(this.countsDir)) {
150
+ fs.rmSync(this.countsDir, { recursive: true, force: true });
151
+ }
152
+ };
153
+
154
+ process.on("SIGTERM", cleanup);
155
+ process.on("SIGINT", cleanup);
156
+ process.on("exit", cleanup);
157
+
158
+ // 创建计数目录
159
+ ensureDir(this.countsDir);
160
+
161
+ console.log(`[daemon] Started (pid=${process.pid}, interval=${this.interval / 1000}s)`);
162
+ console.log(`[daemon] Watching: ${this.busDir}/queues/*/pending.jsonl`);
163
+
164
+ this.running = true;
165
+ this.watchLoop();
166
+ }
167
+
168
+ /**
169
+ * 主监控循环
170
+ */
171
+ async watchLoop() {
172
+ while (this.running) {
173
+ try {
174
+ // 定期清理死掉的 agent
175
+ this.cleanupCounter++;
176
+ if (this.cleanupCounter >= this.cleanupInterval) {
177
+ await this.cleanupDeadAgents();
178
+ this.cleanupCounter = 0;
179
+ }
180
+
181
+ // 检查所有订阅者的队列
182
+ await this.checkQueues();
183
+ } catch (err) {
184
+ console.error(`[daemon] Error: ${err.message}`);
185
+ }
186
+
187
+ // 等待下一个周期
188
+ await new Promise((resolve) => setTimeout(resolve, this.interval));
189
+ }
190
+ }
191
+
192
+ /**
193
+ * 检查所有队列
194
+ */
195
+ async checkQueues() {
196
+ const queuesDir = path.join(this.busDir, "queues");
197
+ if (!fs.existsSync(queuesDir)) {
198
+ return;
199
+ }
200
+
201
+ const subscribers = fs.readdirSync(queuesDir);
202
+
203
+ for (const safeName of subscribers) {
204
+ const pendingFile = path.join(queuesDir, safeName, "pending.jsonl");
205
+ if (!fs.existsSync(pendingFile)) {
206
+ continue;
207
+ }
208
+
209
+ // 获取当前消息数
210
+ let count = 0;
211
+ if (fs.statSync(pendingFile).size > 0) {
212
+ const content = fs.readFileSync(pendingFile, "utf8").trim();
213
+ count = content ? content.split("\n").length : 0;
214
+ }
215
+
216
+ // 获取上次的消息数
217
+ const lastCount = this.getLastCount(safeName);
218
+
219
+ // 如果有新消息,注入命令
220
+ if (count > lastCount) {
221
+ const subscriber = safeNameToSubscriber(safeName);
222
+ const now = new Date().toISOString().split("T")[1].slice(0, 8);
223
+ console.log(`[daemon] ${now} New message for ${subscriber} (${lastCount} -> ${count})`);
224
+
225
+ try {
226
+ await this.injector.inject(subscriber);
227
+ console.log(`[daemon] Injected /bus into ${subscriber}`);
228
+ } catch (err) {
229
+ console.error(`[daemon] Failed to inject: ${err.message}`);
230
+ }
231
+ }
232
+
233
+ // 更新计数
234
+ this.setLastCount(safeName, count);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * 获取上次的消息计数
240
+ */
241
+ getLastCount(safeName) {
242
+ const countFile = path.join(this.countsDir, safeName);
243
+ if (!fs.existsSync(countFile)) {
244
+ return 0;
245
+ }
246
+ const content = fs.readFileSync(countFile, "utf8").trim();
247
+ return parseInt(content, 10) || 0;
248
+ }
249
+
250
+ /**
251
+ * 设置消息计数
252
+ */
253
+ setLastCount(safeName, count) {
254
+ const countFile = path.join(this.countsDir, safeName);
255
+ ensureDir(path.dirname(countFile));
256
+ fs.writeFileSync(countFile, `${count}\n`, "utf8");
257
+ }
258
+
259
+ /**
260
+ * 清理死掉的 agent
261
+ */
262
+ async cleanupDeadAgents() {
263
+ const agentsFile = this.agentsFile;
264
+ if (!fs.existsSync(agentsFile)) {
265
+ return;
266
+ }
267
+
268
+ const busData = readJSON(agentsFile);
269
+ if (!busData || !busData.agents) {
270
+ return;
271
+ }
272
+
273
+ let changed = false;
274
+
275
+ for (const [subscriber, meta] of Object.entries(busData.agents)) {
276
+ if (meta.status !== "active") {
277
+ continue;
278
+ }
279
+
280
+ // 检查 PID 是否仍然存活
281
+ if (meta.pid && !isAgentPidAlive(meta.pid)) {
282
+ const now = new Date().toISOString().split("T")[1].slice(0, 8);
283
+ console.log(`[daemon] ${now} Agent ${subscriber} (pid=${meta.pid}) is dead, marking inactive`);
284
+
285
+ meta.status = "inactive";
286
+ changed = true;
287
+
288
+ // 清理队列目录和 offset
289
+ const safeName = subscriberToSafeName(subscriber);
290
+ const queueDir = path.join(this.busDir, "queues", safeName);
291
+ const offsetFile = path.join(this.busDir, "offsets", `${safeName}.offset`);
292
+
293
+ if (fs.existsSync(queueDir)) {
294
+ fs.rmSync(queueDir, { recursive: true, force: true });
295
+ }
296
+ if (fs.existsSync(offsetFile)) {
297
+ fs.unlinkSync(offsetFile);
298
+ }
299
+ }
300
+ }
301
+
302
+ if (changed) {
303
+ writeJSON(agentsFile, busData);
304
+ }
305
+ }
306
+ }
307
+
308
+ module.exports = BusDaemon;