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,785 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { spawn } = require("child_process");
4
+ const {
5
+ getTimestamp,
6
+ ensureDir,
7
+ logInfo,
8
+ logOk,
9
+ logWarn,
10
+ logError,
11
+ colors,
12
+ generateInstanceId,
13
+ subscriberToSafeName,
14
+ isPidAlive,
15
+ truncateFile,
16
+ getCurrentTty,
17
+ } = require("./utils");
18
+ const { shakeTerminalByTty } = require("./shake");
19
+ const QueueManager = require("./queue");
20
+ const SubscriberManager = require("./subscriber");
21
+ const MessageManager = require("./message");
22
+ const NicknameManager = require("./nickname");
23
+ const BusDaemon = require("./daemon");
24
+ const Injector = require("./inject");
25
+ const { getUfooPaths } = require("../ufoo/paths");
26
+ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
27
+
28
+ /**
29
+ * Event Bus - 项目级 Agent 事件总线
30
+ */
31
+ class EventBus {
32
+ constructor(projectRoot) {
33
+ this.projectRoot = projectRoot;
34
+ this.paths = getUfooPaths(projectRoot);
35
+ this.busDir = this.paths.busDir;
36
+ this.agentsFile = this.paths.agentsFile;
37
+ this.eventsDir = this.paths.busEventsDir;
38
+ this.logsDir = this.paths.busLogsDir;
39
+
40
+ this.busData = null;
41
+ this.queueManager = null;
42
+ this.subscriberManager = null;
43
+ this.messageManager = null;
44
+ }
45
+
46
+ /**
47
+ * 确保 bus 已初始化
48
+ */
49
+ ensureBus() {
50
+ if (!fs.existsSync(this.busDir) || !fs.existsSync(this.paths.agentDir)) {
51
+ throw new Error(
52
+ "Event bus not initialized. Please run: ufoo bus init or /uinit"
53
+ );
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 加载 bus 数据
59
+ */
60
+ loadBusData() {
61
+ this.busData = loadAgentsData(this.agentsFile);
62
+
63
+ this.queueManager = new QueueManager(this.busDir);
64
+ this.subscriberManager = new SubscriberManager(
65
+ this.busData,
66
+ this.queueManager
67
+ );
68
+ this.messageManager = new MessageManager(
69
+ this.busDir,
70
+ this.busData,
71
+ this.queueManager
72
+ );
73
+
74
+ // 自动清理不活跃的 agents
75
+ this.subscriberManager.cleanupInactive();
76
+ }
77
+
78
+ /**
79
+ * 保存 bus 数据
80
+ */
81
+ saveBusData() {
82
+ if (this.busData) {
83
+ saveAgentsData(this.agentsFile, this.busData);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 获取当前订阅者 ID
89
+ */
90
+ getCurrentSubscriber() {
91
+ // 优先使用 UFOO_SUBSCRIBER_ID(daemon 启动的情况)
92
+ if (process.env.UFOO_SUBSCRIBER_ID) {
93
+ return process.env.UFOO_SUBSCRIBER_ID;
94
+ }
95
+
96
+ if (!fs.existsSync(this.agentsFile)) {
97
+ return null;
98
+ }
99
+
100
+ // 尝试从 session.txt 读取
101
+ const sessionFile = path.join(this.paths.agentDir, "session.txt");
102
+ if (fs.existsSync(sessionFile)) {
103
+ const sessionId = fs.readFileSync(sessionFile, "utf8").trim();
104
+ if (sessionId) {
105
+ return sessionId;
106
+ }
107
+ }
108
+
109
+ // 尝试通过 tty 查找订阅者
110
+ let currentTty = null;
111
+ try {
112
+ const ttyPath = fs.realpathSync("/dev/tty");
113
+ if (ttyPath && ttyPath.startsWith("/dev/")) {
114
+ currentTty = ttyPath;
115
+ }
116
+ } catch {
117
+ // tty 不可用
118
+ }
119
+
120
+ if (currentTty && this.busData && this.busData.agents) {
121
+ for (const [id, meta] of Object.entries(this.busData.agents)) {
122
+ if (meta.tty === currentTty) {
123
+ return id;
124
+ }
125
+ }
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * 初始化事件总线
133
+ */
134
+ async init() {
135
+ // 创建目录结构
136
+ ensureDir(this.busDir);
137
+ ensureDir(this.paths.agentDir);
138
+ ensureDir(this.eventsDir);
139
+ ensureDir(path.join(this.busDir, "queues"));
140
+ ensureDir(this.logsDir);
141
+ ensureDir(path.join(this.busDir, "offsets"));
142
+ ensureDir(this.paths.busDaemonDir);
143
+ ensureDir(this.paths.busDaemonCountsDir);
144
+
145
+ // 创建初始 agents 文件(如不存在)
146
+ if (!fs.existsSync(this.agentsFile)) {
147
+ const busData = {
148
+ created_at: getTimestamp(),
149
+ agents: {},
150
+ };
151
+ saveAgentsData(this.agentsFile, busData);
152
+ }
153
+ logOk("Event bus initialized");
154
+ }
155
+
156
+ /**
157
+ * 加入总线
158
+ */
159
+ async join(sessionId, agentType, nickname = null) {
160
+ this.ensureBus();
161
+ this.loadBusData();
162
+
163
+ // 自动检测 session ID 和 agent type
164
+ if (!sessionId) {
165
+ sessionId = generateInstanceId();
166
+ }
167
+
168
+ if (!agentType) {
169
+ // 默认为 claude-code(手动启动情况)
170
+ agentType = "claude-code";
171
+ }
172
+
173
+ const result = await this.subscriberManager.join(
174
+ sessionId,
175
+ agentType,
176
+ nickname
177
+ );
178
+
179
+ this.saveBusData();
180
+
181
+ logOk(
182
+ `Joined event bus: ${result.subscriber}${result.nickname ? ` (${result.nickname})` : ""}`
183
+ );
184
+ return result.subscriber;
185
+ }
186
+
187
+ /**
188
+ * 离开总线
189
+ */
190
+ async leave(subscriber) {
191
+ this.ensureBus();
192
+ this.loadBusData();
193
+
194
+ const success = await this.subscriberManager.leave(subscriber);
195
+
196
+ if (success) {
197
+ this.saveBusData();
198
+ logOk(`Left event bus: ${subscriber}`);
199
+ } else {
200
+ logError(`Subscriber not found: ${subscriber}`);
201
+ }
202
+
203
+ return success;
204
+ }
205
+
206
+ /**
207
+ * 重命名订阅者
208
+ */
209
+ async rename(subscriber, newNickname, publisher = null) {
210
+ this.ensureBus();
211
+ this.loadBusData();
212
+
213
+ try {
214
+ const result = await this.subscriberManager.rename(
215
+ subscriber,
216
+ newNickname
217
+ );
218
+ this.saveBusData();
219
+ const pub = publisher || this.getDefaultPublisher() || "unknown";
220
+ try {
221
+ await this.messageManager.emit(
222
+ "*",
223
+ "agent_renamed",
224
+ {
225
+ agent_id: result.subscriber,
226
+ old_nickname: result.oldNickname,
227
+ new_nickname: result.newNickname,
228
+ },
229
+ pub
230
+ );
231
+ } catch {
232
+ // ignore event emit failures
233
+ }
234
+ logOk(
235
+ `Renamed ${result.subscriber}: "${result.oldNickname}" -> "${result.newNickname}"`
236
+ );
237
+ return result;
238
+ } catch (err) {
239
+ logError(err.message);
240
+ throw err;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * 获取当前订阅者 ID
246
+ */
247
+ async whoami() {
248
+ this.ensureBus();
249
+ this.loadBusData();
250
+
251
+ // 优先使用 UFOO_SUBSCRIBER_ID(daemon 启动的情况)
252
+ if (process.env.UFOO_SUBSCRIBER_ID) {
253
+ const subscriber = process.env.UFOO_SUBSCRIBER_ID;
254
+ const meta = this.subscriberManager.getSubscriber(subscriber);
255
+
256
+ if (meta) {
257
+ console.log(subscriber);
258
+ return subscriber;
259
+ }
260
+ }
261
+
262
+ logError("Not joined to bus. Please run: ufoo bus join");
263
+ return null;
264
+ }
265
+
266
+ /**
267
+ * 发送消息
268
+ */
269
+ async send(target, message, publisher = null) {
270
+ this.ensureBus();
271
+ this.loadBusData();
272
+
273
+ // 自动检测 publisher
274
+ if (!publisher) {
275
+ publisher =
276
+ process.env.AI_BUS_PUBLISHER ||
277
+ this.getDefaultPublisher() ||
278
+ this.getCurrentSubscriber() ||
279
+ "unknown";
280
+ }
281
+
282
+ // 如果 publisher 还是 unknown,尝试从命令行参数或环境推断
283
+ if (publisher === "unknown") {
284
+ // 尝试从 tty 查找可能的 subscriber
285
+ const possibleSubscriber = this.getCurrentSubscriber();
286
+ if (possibleSubscriber) {
287
+ publisher = possibleSubscriber;
288
+ }
289
+ }
290
+
291
+ // 如果 publisher 不在 agents 列表中,自动注册它(懒加载模式)
292
+ if (publisher !== "unknown" && this.busData.agents && !this.busData.agents[publisher]) {
293
+ // 解析 agent 信息
294
+ const parts = publisher.split(":");
295
+ const agentType = parts[0] || "unknown-agent";
296
+ const sessionId = parts[1] || require("./utils").generateInstanceId();
297
+
298
+ // 自动加入总线(静默模式,不输出日志)
299
+ const subscriber = await this.subscriberManager.join(sessionId, agentType, null);
300
+ this.saveBusData();
301
+ publisher = subscriber; // 使用规范化的 subscriber ID
302
+ }
303
+
304
+ // 更新 publisher 的心跳
305
+ if (publisher !== "unknown" && this.busData.agents && this.busData.agents[publisher]) {
306
+ this.subscriberManager.updateLastSeen(publisher);
307
+ this.saveBusData();
308
+ }
309
+
310
+ try {
311
+ const result = await this.messageManager.send(target, message, publisher);
312
+ logOk(
313
+ `Message sent: seq=${result.seq} -> ${result.targets.join(", ")}`
314
+ );
315
+ return result;
316
+ } catch (err) {
317
+ logError(err.message);
318
+ throw err;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * 广播消息
324
+ */
325
+ async broadcast(message, publisher = null) {
326
+ return this.send("*", message, publisher);
327
+ }
328
+
329
+ /**
330
+ * 检查待处理消息
331
+ */
332
+ async check(subscriber, autoAck = false) {
333
+ this.ensureBus();
334
+ this.loadBusData();
335
+
336
+ // 更新心跳
337
+ this.subscriberManager.updateLastSeen(subscriber);
338
+ this.saveBusData();
339
+
340
+ const pending = await this.messageManager.check(subscriber);
341
+
342
+ if (pending.length === 0) {
343
+ logOk("No pending messages");
344
+ return pending;
345
+ }
346
+
347
+ logWarn(`You have ${pending.length} pending event(s):`);
348
+ console.log();
349
+
350
+ for (const event of pending) {
351
+ console.log(` ${colors.yellow}@you${colors.reset} from ${colors.cyan}${event.publisher}${colors.reset}`);
352
+ console.log(` Type: ${event.type}/${event.event}`);
353
+ console.log(` Content: ${JSON.stringify(event.data)}`);
354
+ console.log();
355
+ }
356
+
357
+ console.log(`${colors.cyan}After handling, run: ufoo bus ack ${subscriber}${colors.reset}`);
358
+
359
+ if (autoAck) {
360
+ await this.ack(subscriber);
361
+ }
362
+
363
+ return pending;
364
+ }
365
+
366
+ /**
367
+ * 确认消息
368
+ */
369
+ async ack(subscriber) {
370
+ this.ensureBus();
371
+ this.loadBusData();
372
+
373
+ const count = await this.messageManager.ack(subscriber);
374
+
375
+ if (count > 0) {
376
+ logOk(`Acknowledged and cleared ${count} message(s)`);
377
+ } else {
378
+ logOk("No pending messages to acknowledge");
379
+ }
380
+
381
+ return count;
382
+ }
383
+
384
+ /**
385
+ * 消费事件
386
+ */
387
+ async consume(subscriber, fromBeginning = false) {
388
+ this.ensureBus();
389
+ this.loadBusData();
390
+
391
+ const result = await this.messageManager.consume(subscriber, fromBeginning);
392
+
393
+ for (const event of result.consumed) {
394
+ console.log(JSON.stringify(event));
395
+ }
396
+
397
+ logInfo(`Consumed ${result.consumed.length} events, new offset: ${result.newOffset}`);
398
+
399
+ return result;
400
+ }
401
+
402
+ /**
403
+ * 查看总线状态
404
+ */
405
+ async status() {
406
+ this.ensureBus();
407
+ this.loadBusData();
408
+
409
+ // 清理不活跃的订阅者
410
+ this.subscriberManager.cleanupInactive();
411
+
412
+ // 尝试获取当前 subscriber 并更新 last_seen + 重新激活(保持心跳)
413
+ const currentSubscriber = this.getCurrentSubscriber();
414
+ if (currentSubscriber && this.busData.agents && this.busData.agents[currentSubscriber]) {
415
+ this.subscriberManager.updateLastSeen(currentSubscriber);
416
+ this.busData.agents[currentSubscriber].status = "active";
417
+ this.saveBusData();
418
+ }
419
+
420
+ console.log(`${colors.cyan}=== Event Bus Status ===${colors.reset}`);
421
+ console.log();
422
+
423
+ // 显示 bus ID
424
+ const busId = path.basename(this.projectRoot) || "ai-workspace";
425
+ console.log(`Bus ID: ${busId}`);
426
+ console.log();
427
+
428
+ // 显示在线订阅者
429
+ const active = this.subscriberManager.getActiveSubscribers();
430
+ console.log(`${colors.cyan}Online agents:${colors.reset}`);
431
+ if (active.length === 0) {
432
+ console.log(" (none)");
433
+ } else {
434
+ for (const sub of active) {
435
+ const nickname = sub.nickname ? ` (${sub.nickname})` : "";
436
+ console.log(` ${sub.id}${nickname}`);
437
+ }
438
+ }
439
+ console.log();
440
+
441
+ // 显示事件统计
442
+ console.log(`${colors.cyan}Event statistics:${colors.reset}`);
443
+ if (fs.existsSync(this.eventsDir)) {
444
+ const files = fs.readdirSync(this.eventsDir)
445
+ .filter((f) => f.endsWith(".jsonl"))
446
+ .sort();
447
+
448
+ let totalEvents = 0;
449
+ for (const file of files) {
450
+ const filePath = path.join(this.eventsDir, file);
451
+ const lines = fs.readFileSync(filePath, "utf8").trim().split("\n").filter(Boolean);
452
+ const count = lines.length;
453
+ totalEvents += count;
454
+ console.log(` ${file}: ${count} events`);
455
+ }
456
+ console.log(` Total: ${totalEvents} events`);
457
+ } else {
458
+ console.log(" (no events yet)");
459
+ }
460
+
461
+ return { active, busId };
462
+ }
463
+
464
+ /**
465
+ * 智能路由
466
+ */
467
+ async resolve(myId, targetType) {
468
+ this.ensureBus();
469
+ this.loadBusData();
470
+
471
+ const result = await this.messageManager.resolve(myId, targetType);
472
+
473
+ if (result.single) {
474
+ console.log(result.single);
475
+ return result.single;
476
+ }
477
+
478
+ if (result.candidates.length === 0) {
479
+ logError(`No ${targetType} agents found`);
480
+ return null;
481
+ }
482
+
483
+ console.log(`Multiple ${targetType} agents found:`);
484
+ for (const candidate of result.candidates) {
485
+ const nickname = candidate.nickname ? ` (${candidate.nickname})` : "";
486
+ console.log(` ${candidate.id}${nickname}`);
487
+ }
488
+
489
+ return null;
490
+ }
491
+
492
+ /**
493
+ * 获取默认发布者
494
+ */
495
+ getDefaultPublisher() {
496
+ // 使用 UFOO_SUBSCRIBER_ID(daemon 启动的情况)
497
+ return process.env.UFOO_SUBSCRIBER_ID || null;
498
+ }
499
+
500
+ /**
501
+ * 确保当前 agent 已经 join 总线(如果没有则自动 join)
502
+ */
503
+ async ensureJoined() {
504
+ this.ensureBus();
505
+ this.loadBusData();
506
+
507
+ // 检查是否已经 join
508
+ const currentSubscriber = this.getCurrentSubscriber();
509
+ if (currentSubscriber && this.busData.agents && this.busData.agents[currentSubscriber]) {
510
+ // 已经 join,只需更新心跳
511
+ this.subscriberManager.updateLastSeen(currentSubscriber);
512
+ this.saveBusData();
513
+ return currentSubscriber;
514
+ }
515
+
516
+ // 还没有 join,自动 join
517
+ const sessionId = null; // 自动生成
518
+ const agentType = null; // 自动检测
519
+ const nickname = null; // 自动生成
520
+ const subscriber = await this.join(sessionId, agentType, nickname);
521
+
522
+ // 静默加入(不输出 "Joined event bus" 信息)
523
+ return subscriber;
524
+ }
525
+
526
+ /**
527
+ * 后台消息提醒
528
+ */
529
+ async alert(subscriber, intervalSeconds = 2, options = {}) {
530
+ this.ensureBus();
531
+ this.loadBusData();
532
+
533
+ if (!subscriber) {
534
+ throw new Error("alert requires <subscriber-id>");
535
+ }
536
+
537
+ const interval = Math.max(1, parseInt(intervalSeconds, 10) || 2);
538
+ const intervalMs = interval * 1000;
539
+ const useNotify = Boolean(options.notify);
540
+ const useTitle = options.title !== false;
541
+ const useBell = options.bell !== false;
542
+ const daemon = Boolean(options.daemon);
543
+ const stop = Boolean(options.stop);
544
+
545
+ const safeName = subscriberToSafeName(subscriber);
546
+ const pidDir = path.join(this.busDir, "pids");
547
+ const pidFile = path.join(pidDir, `alert-${safeName}.pid`);
548
+ const logDir = path.join(this.busDir, "logs");
549
+ const logFile = path.join(logDir, `alert-${safeName}.log`);
550
+
551
+ ensureDir(pidDir);
552
+
553
+ if (stop) {
554
+ if (fs.existsSync(pidFile)) {
555
+ const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
556
+ if (pid && isPidAlive(pid)) {
557
+ try {
558
+ process.kill(pid);
559
+ console.log(`[alert] Stopped ${subscriber} (pid=${pid})`);
560
+ } catch {
561
+ console.log("[alert] Not running");
562
+ }
563
+ } else {
564
+ console.log(`[alert] Not running for ${subscriber}`);
565
+ }
566
+ fs.rmSync(pidFile, { force: true });
567
+ } else {
568
+ console.log(`[alert] Not running for ${subscriber}`);
569
+ }
570
+ return;
571
+ }
572
+
573
+ if (daemon) {
574
+ if (fs.existsSync(pidFile)) {
575
+ const existing = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
576
+ if (existing && isPidAlive(existing)) {
577
+ console.log(`[alert] Already running for ${subscriber} (pid=${existing})`);
578
+ return;
579
+ }
580
+ }
581
+
582
+ ensureDir(logDir);
583
+
584
+ const args = [
585
+ path.join(__dirname, "..", "..", "bin", "ufoo.js"),
586
+ "bus",
587
+ "alert",
588
+ subscriber,
589
+ String(interval),
590
+ ];
591
+ if (useNotify) args.push("--notify");
592
+ if (!useTitle) args.push("--no-title");
593
+ if (!useBell) args.push("--no-bell");
594
+
595
+ const logStream = fs.openSync(logFile, "a");
596
+ const child = spawn(process.execPath, args, {
597
+ detached: true,
598
+ stdio: ["ignore", logStream, logStream],
599
+ cwd: process.cwd(),
600
+ });
601
+
602
+ child.unref();
603
+ fs.writeFileSync(pidFile, `${child.pid}\n`, "utf8");
604
+ console.log(`[alert] Started for ${subscriber} (pid=${child.pid}, log=${logFile})`);
605
+ return;
606
+ }
607
+
608
+ fs.writeFileSync(pidFile, `${process.pid}\n`, "utf8");
609
+ const cleanup = () => {
610
+ if (fs.existsSync(pidFile)) fs.rmSync(pidFile, { force: true });
611
+ };
612
+ process.on("exit", cleanup);
613
+ process.on("SIGINT", () => {
614
+ cleanup();
615
+ process.exit(0);
616
+ });
617
+ process.on("SIGTERM", () => {
618
+ cleanup();
619
+ process.exit(0);
620
+ });
621
+
622
+ const queuePath = this.queueManager.getPendingPath(subscriber);
623
+ this.queueManager.ensureQueueDir(subscriber);
624
+
625
+ const countLines = () => {
626
+ if (!fs.existsSync(queuePath)) return 0;
627
+ const content = fs.readFileSync(queuePath, "utf8").trim();
628
+ if (!content) return 0;
629
+ return content.split("\n").filter(Boolean).length;
630
+ };
631
+
632
+ let lastCount = countLines();
633
+ console.log(`[alert] Watching ${subscriber} (interval=${interval}s)`);
634
+
635
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
636
+
637
+ while (true) {
638
+ const count = countLines();
639
+ if (count > lastCount) {
640
+ const newCount = count - lastCount;
641
+ const now = new Date().toISOString().split("T")[1].slice(0, 8);
642
+ console.log(`[alert] ${now} +${newCount} new message(s)`);
643
+
644
+ if (useBell) {
645
+ const tty = getCurrentTty();
646
+ if (tty) shakeTerminalByTty(tty);
647
+ }
648
+ if (useTitle) {
649
+ process.stdout.write(`\x1b]0;[${count}] ${subscriber}\x07`);
650
+ }
651
+ if (useNotify && process.platform === "darwin") {
652
+ const message = `${newCount} new message(s)`;
653
+ spawn(
654
+ "osascript",
655
+ [
656
+ "-e",
657
+ `display notification "${message}" with title "ufoo bus" subtitle "${subscriber}"`,
658
+ ],
659
+ { detached: true, stdio: "ignore" }
660
+ ).unref();
661
+ }
662
+ }
663
+
664
+ if (useTitle && count > 0) {
665
+ process.stdout.write(`\x1b]0;[${count}] ${subscriber}\x07`);
666
+ }
667
+
668
+ lastCount = count;
669
+ await sleep(intervalMs);
670
+ }
671
+ }
672
+
673
+ /**
674
+ * 前台消息监听
675
+ */
676
+ async listen(subscriber, options = {}) {
677
+ this.ensureBus();
678
+ this.loadBusData();
679
+
680
+ let target = subscriber;
681
+ if (!target && options.autoJoin) {
682
+ target = await this.join();
683
+ console.log(`[listen] Auto-joined as: ${target}`);
684
+ }
685
+
686
+ if (!target) {
687
+ throw new Error("listen requires <subscriber-id> (or --auto-join)");
688
+ }
689
+
690
+ const queuePath = this.queueManager.getPendingPath(target);
691
+ this.queueManager.ensureQueueDir(target);
692
+ if (!fs.existsSync(queuePath)) {
693
+ fs.writeFileSync(queuePath, "", "utf8");
694
+ }
695
+
696
+ if (options.reset) {
697
+ console.log("[listen] Resetting queue...");
698
+ truncateFile(queuePath);
699
+ }
700
+
701
+ const readLines = () => {
702
+ if (!fs.existsSync(queuePath)) return [];
703
+ const content = fs.readFileSync(queuePath, "utf8").trim();
704
+ if (!content) return [];
705
+ return content.split("\n").filter(Boolean);
706
+ };
707
+
708
+ const formatLine = (line) => {
709
+ let data = null;
710
+ try {
711
+ data = JSON.parse(line);
712
+ } catch {
713
+ data = null;
714
+ }
715
+ const msg = data?.data?.message ?? data?.data ?? line;
716
+ const from = data?.publisher ?? "unknown";
717
+ const ts = data?.ts || data?.timestamp || "";
718
+ const shortTs = ts ? ts.slice(11, 19) : "";
719
+ const prefix = shortTs ? `[${shortTs}] ` : "";
720
+ console.log(`${prefix}<${from}> ${msg}`);
721
+ };
722
+
723
+ if (options.fromBeginning) {
724
+ const lines = readLines();
725
+ if (lines.length > 0) {
726
+ console.log("[listen] Existing messages:");
727
+ console.log("---");
728
+ lines.forEach((line) => formatLine(line));
729
+ console.log("---");
730
+ }
731
+ }
732
+
733
+ console.log("[listen] Listening for new messages... (Ctrl+C to stop)");
734
+
735
+ let lastLines = readLines().length;
736
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
737
+
738
+ while (true) {
739
+ const lines = readLines();
740
+ if (lines.length > lastLines) {
741
+ const newLines = lines.slice(lastLines);
742
+ const tty = getCurrentTty();
743
+ if (tty) shakeTerminalByTty(tty);
744
+ newLines.forEach((line) => {
745
+ formatLine(line);
746
+ });
747
+ lastLines = lines.length;
748
+ }
749
+ await sleep(1000);
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Daemon 管理
755
+ */
756
+ async daemon(action, options = {}) {
757
+ const interval = options.interval || 2000;
758
+ const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, interval);
759
+
760
+ switch (action) {
761
+ case "start":
762
+ await daemon.start(options.background || false);
763
+ break;
764
+ case "stop":
765
+ daemon.stop();
766
+ break;
767
+ case "status":
768
+ daemon.status();
769
+ break;
770
+ default:
771
+ throw new Error(`Unknown daemon action: ${action}`);
772
+ }
773
+ }
774
+
775
+ /**
776
+ * 注入命令到订阅者终端
777
+ */
778
+ async inject(subscriber, commandOverride = "") {
779
+ this.ensureBus();
780
+ const injector = new Injector(this.busDir, this.agentsFile);
781
+ await injector.inject(subscriber, commandOverride);
782
+ }
783
+ }
784
+
785
+ module.exports = EventBus;