u-foo 1.0.3 → 1.1.9

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 (179) hide show
  1. package/README.md +110 -11
  2. package/README.zh-CN.md +9 -7
  3. package/SKILLS/ufoo/SKILL.md +132 -0
  4. package/SKILLS/uinit/SKILL.md +78 -0
  5. package/SKILLS/ustatus/SKILL.md +36 -0
  6. package/bin/uclaude.js +13 -0
  7. package/bin/ucode-core.js +15 -0
  8. package/bin/ucode.js +125 -0
  9. package/bin/ucodex.js +13 -0
  10. package/bin/ufoo +9 -31
  11. package/bin/ufoo-assistant-agent.js +5 -0
  12. package/bin/ufoo-engine.js +25 -0
  13. package/bin/ufoo.js +17 -0
  14. package/modules/AGENTS.template.md +29 -11
  15. package/modules/bus/README.md +33 -25
  16. package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
  17. package/modules/context/README.md +18 -40
  18. package/modules/context/SKILLS/uctx/SKILL.md +63 -1
  19. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  20. package/package.json +25 -4
  21. package/scripts/import-pi-mono.js +124 -0
  22. package/scripts/postinstall.js +30 -0
  23. package/scripts/sync-claude-skills.sh +21 -0
  24. package/src/agent/cliRunner.js +554 -33
  25. package/src/agent/internalRunner.js +150 -56
  26. package/src/agent/launcher.js +754 -0
  27. package/src/agent/normalizeOutput.js +1 -1
  28. package/src/agent/notifier.js +340 -0
  29. package/src/agent/ptyRunner.js +847 -0
  30. package/src/agent/ptyWrapper.js +379 -0
  31. package/src/agent/readyDetector.js +175 -0
  32. package/src/agent/ucode.js +443 -0
  33. package/src/agent/ucodeBootstrap.js +113 -0
  34. package/src/agent/ucodeBuild.js +67 -0
  35. package/src/agent/ucodeDoctor.js +184 -0
  36. package/src/agent/ucodeRuntimeConfig.js +129 -0
  37. package/src/agent/ufooAgent.js +46 -42
  38. package/src/assistant/agent.js +260 -0
  39. package/src/assistant/bridge.js +172 -0
  40. package/src/assistant/engine.js +252 -0
  41. package/src/assistant/stdio.js +58 -0
  42. package/src/assistant/ufooEngineCli.js +306 -0
  43. package/src/bus/activate.js +172 -0
  44. package/src/bus/daemon.js +436 -0
  45. package/src/bus/index.js +842 -0
  46. package/src/bus/inject.js +315 -0
  47. package/src/bus/message.js +430 -0
  48. package/src/bus/nickname.js +88 -0
  49. package/src/bus/queue.js +136 -0
  50. package/src/bus/shake.js +26 -0
  51. package/src/bus/store.js +189 -0
  52. package/src/bus/subscriber.js +312 -0
  53. package/src/bus/utils.js +363 -0
  54. package/src/chat/agentBar.js +117 -0
  55. package/src/chat/agentDirectory.js +88 -0
  56. package/src/chat/agentSockets.js +225 -0
  57. package/src/chat/agentViewController.js +298 -0
  58. package/src/chat/chatLogController.js +115 -0
  59. package/src/chat/commandExecutor.js +700 -0
  60. package/src/chat/commands.js +132 -0
  61. package/src/chat/completionController.js +414 -0
  62. package/src/chat/cronScheduler.js +160 -0
  63. package/src/chat/daemonConnection.js +166 -0
  64. package/src/chat/daemonCoordinator.js +64 -0
  65. package/src/chat/daemonMessageRouter.js +257 -0
  66. package/src/chat/daemonReconnect.js +41 -0
  67. package/src/chat/daemonTransport.js +36 -0
  68. package/src/chat/daemonTransportDefaults.js +10 -0
  69. package/src/chat/dashboardKeyController.js +480 -0
  70. package/src/chat/dashboardView.js +154 -0
  71. package/src/chat/index.js +1011 -1392
  72. package/src/chat/inputHistoryController.js +105 -0
  73. package/src/chat/inputListenerController.js +304 -0
  74. package/src/chat/inputMath.js +104 -0
  75. package/src/chat/inputSubmitHandler.js +171 -0
  76. package/src/chat/layout.js +165 -0
  77. package/src/chat/pasteController.js +81 -0
  78. package/src/chat/rawKeyMap.js +42 -0
  79. package/src/chat/settingsController.js +132 -0
  80. package/src/chat/statusLineController.js +177 -0
  81. package/src/chat/streamTracker.js +138 -0
  82. package/src/chat/text.js +70 -0
  83. package/src/chat/transport.js +61 -0
  84. package/src/cli/busCoreCommands.js +59 -0
  85. package/src/cli/ctxCoreCommands.js +199 -0
  86. package/src/cli/onlineCoreCommands.js +379 -0
  87. package/src/cli.js +1162 -96
  88. package/src/code/README.md +29 -0
  89. package/src/code/UCODE_PROMPT.md +32 -0
  90. package/src/code/agent.js +1651 -0
  91. package/src/code/cli.js +158 -0
  92. package/src/code/config +0 -0
  93. package/src/code/dispatch.js +42 -0
  94. package/src/code/index.js +70 -0
  95. package/src/code/nativeRunner.js +1213 -0
  96. package/src/code/runtime.js +154 -0
  97. package/src/code/sessionStore.js +162 -0
  98. package/src/code/taskDecomposer.js +269 -0
  99. package/src/code/tools/bash.js +53 -0
  100. package/src/code/tools/common.js +42 -0
  101. package/src/code/tools/edit.js +70 -0
  102. package/src/code/tools/read.js +44 -0
  103. package/src/code/tools/write.js +35 -0
  104. package/src/code/tui.js +1580 -0
  105. package/src/config.js +56 -3
  106. package/src/context/decisions.js +324 -0
  107. package/src/context/doctor.js +183 -0
  108. package/src/context/index.js +55 -0
  109. package/src/context/sync.js +127 -0
  110. package/src/daemon/agentProcessManager.js +74 -0
  111. package/src/daemon/cronOps.js +241 -0
  112. package/src/daemon/index.js +998 -170
  113. package/src/daemon/ipcServer.js +99 -0
  114. package/src/daemon/ops.js +630 -48
  115. package/src/daemon/promptLoop.js +319 -0
  116. package/src/daemon/promptRequest.js +101 -0
  117. package/src/daemon/providerSessions.js +306 -0
  118. package/src/daemon/reporting.js +90 -0
  119. package/src/daemon/run.js +31 -1
  120. package/src/daemon/status.js +48 -8
  121. package/src/doctor/index.js +50 -0
  122. package/src/init/index.js +318 -0
  123. package/src/online/bridge.js +663 -0
  124. package/src/online/client.js +245 -0
  125. package/src/online/runner.js +253 -0
  126. package/src/online/server.js +992 -0
  127. package/src/online/tokens.js +103 -0
  128. package/src/report/store.js +331 -0
  129. package/src/shared/eventContract.js +35 -0
  130. package/src/shared/ptySocketContract.js +21 -0
  131. package/src/skills/index.js +159 -0
  132. package/src/status/index.js +285 -0
  133. package/src/terminal/adapterContract.js +87 -0
  134. package/src/terminal/adapterRouter.js +84 -0
  135. package/src/terminal/adapters/externalAdapter.js +14 -0
  136. package/src/terminal/adapters/internalAdapter.js +13 -0
  137. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  138. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  139. package/src/terminal/adapters/terminalAdapter.js +31 -0
  140. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  141. package/src/terminal/detect.js +64 -0
  142. package/src/terminal/index.js +8 -0
  143. package/src/terminal/iterm2.js +126 -0
  144. package/src/ufoo/agentsStore.js +107 -0
  145. package/src/ufoo/paths.js +46 -0
  146. package/src/utils/banner.js +76 -0
  147. package/bin/uclaude +0 -65
  148. package/bin/ucodex +0 -65
  149. package/modules/bus/scripts/bus-alert.sh +0 -185
  150. package/modules/bus/scripts/bus-listen.sh +0 -117
  151. package/modules/context/ASSUMPTIONS.md +0 -7
  152. package/modules/context/CONSTRAINTS.md +0 -7
  153. package/modules/context/CONTEXT-STRUCTURE.md +0 -49
  154. package/modules/context/DECISION-PROTOCOL.md +0 -62
  155. package/modules/context/HANDOFF.md +0 -33
  156. package/modules/context/RULES.md +0 -15
  157. package/modules/context/SKILLS/README.md +0 -14
  158. package/modules/context/SYSTEM.md +0 -18
  159. package/modules/context/TEMPLATES/assumptions.md +0 -4
  160. package/modules/context/TEMPLATES/constraints.md +0 -4
  161. package/modules/context/TEMPLATES/decision.md +0 -16
  162. package/modules/context/TEMPLATES/project-context-readme.md +0 -6
  163. package/modules/context/TEMPLATES/system.md +0 -3
  164. package/modules/context/TEMPLATES/terminology.md +0 -4
  165. package/modules/context/TERMINOLOGY.md +0 -10
  166. package/scripts/banner.sh +0 -89
  167. package/scripts/bus-alert.sh +0 -6
  168. package/scripts/bus-autotrigger.sh +0 -6
  169. package/scripts/bus-daemon.sh +0 -231
  170. package/scripts/bus-inject.sh +0 -144
  171. package/scripts/bus-listen.sh +0 -6
  172. package/scripts/bus.sh +0 -984
  173. package/scripts/context-decisions.sh +0 -167
  174. package/scripts/context-doctor.sh +0 -72
  175. package/scripts/context-lint.sh +0 -110
  176. package/scripts/doctor.sh +0 -22
  177. package/scripts/init.sh +0 -247
  178. package/scripts/skills.sh +0 -113
  179. package/scripts/status.sh +0 -125
@@ -0,0 +1,430 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const {
4
+ getTimestamp,
5
+ getDate,
6
+ readJSONL,
7
+ appendJSONL,
8
+ readLastLine,
9
+ isPidAlive,
10
+ } = require("./utils");
11
+ const NicknameManager = require("./nickname");
12
+
13
+ const SEQ_LOCK_TIMEOUT_MS = 5000;
14
+ const SEQ_LOCK_POLL_MS = 25;
15
+ const SEQ_LOCK_STALE_MS = 30000;
16
+
17
+ function normalizeAgentTypeAlias(value = "") {
18
+ const text = String(value || "").trim().toLowerCase();
19
+ if (!text) return "";
20
+ if (text === "codex") return "codex";
21
+ if (text === "claude" || text === "claude-code") return "claude-code";
22
+ if (text === "ufoo" || text === "ucode" || text === "ufoo-code") return "ufoo-code";
23
+ return text;
24
+ }
25
+
26
+ /**
27
+ * 消息管理器
28
+ */
29
+ class MessageManager {
30
+ constructor(busDir, busData, queueManager) {
31
+ this.busDir = busDir;
32
+ this.busData = busData;
33
+ this.queueManager = queueManager;
34
+ this.eventsDir = path.join(busDir, "events");
35
+ this.seqFile = path.join(busDir, "seq.counter");
36
+ this.seqLockFile = path.join(busDir, "seq.counter.lock");
37
+ }
38
+
39
+ /**
40
+ * 从 events 日志中恢复最大序号(仅用于 counter 缺失时)
41
+ */
42
+ readMaxSeqFromEvents() {
43
+ let maxSeq = 0;
44
+ if (!fs.existsSync(this.eventsDir)) {
45
+ return maxSeq;
46
+ }
47
+
48
+ const files = fs.readdirSync(this.eventsDir)
49
+ .filter((f) => f.endsWith(".jsonl"))
50
+ .sort()
51
+ .reverse(); // 从最新的文件开始读
52
+
53
+ for (const file of files) {
54
+ const filePath = path.join(this.eventsDir, file);
55
+ const lastLine = readLastLine(filePath);
56
+
57
+ if (lastLine) {
58
+ try {
59
+ const event = JSON.parse(lastLine);
60
+ if (event.seq && event.seq > maxSeq) {
61
+ maxSeq = event.seq;
62
+ break; // 找到最大 seq 后立即退出
63
+ }
64
+ } catch {
65
+ // 忽略解析错误
66
+ }
67
+ }
68
+ }
69
+
70
+ return maxSeq;
71
+ }
72
+
73
+ readSeqCounter() {
74
+ try {
75
+ const raw = fs.readFileSync(this.seqFile, "utf8").trim();
76
+ const parsed = parseInt(raw, 10);
77
+ if (Number.isFinite(parsed) && parsed > 0) {
78
+ return parsed;
79
+ }
80
+ } catch {
81
+ // ignore
82
+ }
83
+ return 0;
84
+ }
85
+
86
+ writeSeqCounter(seq) {
87
+ fs.mkdirSync(path.dirname(this.seqFile), { recursive: true });
88
+ fs.writeFileSync(this.seqFile, `${seq}\n`, "utf8");
89
+ }
90
+
91
+ cleanupStaleSeqLock() {
92
+ if (!fs.existsSync(this.seqLockFile)) return;
93
+ let shouldRemove = false;
94
+
95
+ try {
96
+ const raw = fs.readFileSync(this.seqLockFile, "utf8").trim();
97
+ const pid = parseInt(raw, 10);
98
+ if (!Number.isFinite(pid) || pid <= 0) {
99
+ shouldRemove = true;
100
+ } else if (!isPidAlive(pid)) {
101
+ shouldRemove = true;
102
+ }
103
+ } catch {
104
+ shouldRemove = true;
105
+ }
106
+
107
+ if (!shouldRemove) {
108
+ try {
109
+ const stat = fs.statSync(this.seqLockFile);
110
+ if (Date.now() - stat.mtimeMs > SEQ_LOCK_STALE_MS) {
111
+ shouldRemove = true;
112
+ }
113
+ } catch {
114
+ shouldRemove = true;
115
+ }
116
+ }
117
+
118
+ if (shouldRemove) {
119
+ try {
120
+ fs.unlinkSync(this.seqLockFile);
121
+ } catch {
122
+ // ignore stale lock cleanup errors
123
+ }
124
+ }
125
+ }
126
+
127
+ async acquireSeqLock() {
128
+ const deadline = Date.now() + SEQ_LOCK_TIMEOUT_MS;
129
+ while (Date.now() < deadline) {
130
+ try {
131
+ const fd = fs.openSync(this.seqLockFile, "wx");
132
+ fs.writeSync(fd, `${process.pid}\n`);
133
+ return fd;
134
+ } catch (err) {
135
+ if (err && err.code === "EEXIST") {
136
+ this.cleanupStaleSeqLock();
137
+ // eslint-disable-next-line no-await-in-loop
138
+ await new Promise((resolve) => setTimeout(resolve, SEQ_LOCK_POLL_MS));
139
+ continue;
140
+ }
141
+ throw err;
142
+ }
143
+ }
144
+ throw new Error("Failed to acquire sequence lock");
145
+ }
146
+
147
+ releaseSeqLock(lockFd) {
148
+ try {
149
+ if (typeof lockFd === "number") {
150
+ fs.closeSync(lockFd);
151
+ }
152
+ } catch {
153
+ // ignore
154
+ }
155
+ try {
156
+ if (fs.existsSync(this.seqLockFile)) {
157
+ fs.unlinkSync(this.seqLockFile);
158
+ }
159
+ } catch {
160
+ // ignore
161
+ }
162
+ }
163
+
164
+ /**
165
+ * 获取下一个全局序号(文件锁保证跨进程原子递增)
166
+ */
167
+ async getNextSeq() {
168
+ const lockFd = await this.acquireSeqLock();
169
+ try {
170
+ let current = this.readSeqCounter();
171
+ if (current === 0) {
172
+ current = this.readMaxSeqFromEvents();
173
+ }
174
+ const next = current + 1;
175
+ this.writeSeqCounter(next);
176
+ return next;
177
+ } finally {
178
+ this.releaseSeqLock(lockFd);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * 解析目标(支持昵称、代理类型、订阅者 ID)
184
+ */
185
+ resolveTarget(target) {
186
+ const nicknameManager = new NicknameManager(this.busData);
187
+ const normalizedTarget = normalizeAgentTypeAlias(target);
188
+
189
+ // 0. Exact subscriber ID match (allows ids without ":" e.g. "ufoo-agent")
190
+ const subscribers = this.busData.agents || {};
191
+ if (target && typeof target === "string" && subscribers[target]) {
192
+ return [target];
193
+ }
194
+
195
+ // 1. 尝试作为订阅者 ID
196
+ if (target.includes(":")) {
197
+ return [target];
198
+ }
199
+
200
+ // 2. 尝试作为昵称
201
+ const byNickname = nicknameManager.resolveNickname(target);
202
+ if (byNickname) {
203
+ return [byNickname];
204
+ }
205
+
206
+ // 3. 尝试作为代理类型(匹配所有该类型的订阅者)
207
+ const isActive = (meta) => !meta || meta.status === "active";
208
+
209
+ const byType = Object.entries(subscribers)
210
+ .filter(([, meta]) => normalizeAgentTypeAlias(meta.agent_type) === normalizedTarget && isActive(meta))
211
+ .map(([id]) => id);
212
+
213
+ if (byType.length > 0) {
214
+ return byType;
215
+ }
216
+
217
+ // 4. 通配符(所有活跃订阅者)
218
+ if (target === "*") {
219
+ return Object.entries(subscribers)
220
+ .filter(([, meta]) => isActive(meta))
221
+ .map(([id]) => id);
222
+ }
223
+
224
+ // 未找到目标
225
+ return [];
226
+ }
227
+
228
+ /**
229
+ * 检查目标是否匹配订阅者
230
+ */
231
+ targetMatches(target, subscriber) {
232
+ const normalizedTarget = normalizeAgentTypeAlias(target);
233
+ // 精确匹配
234
+ if (target === subscriber) return true;
235
+
236
+ // 代理类型匹配
237
+ const meta = this.busData.agents?.[subscriber];
238
+ if (meta && normalizedTarget === normalizeAgentTypeAlias(meta.agent_type)) return true;
239
+
240
+ // 昵称匹配
241
+ if (meta && target === meta.nickname) return true;
242
+
243
+ // 通配符
244
+ if (target === "*") return true;
245
+
246
+ return false;
247
+ }
248
+
249
+ /**
250
+ * 发送消息
251
+ */
252
+ async send(target, message, publisher = "unknown") {
253
+ const seq = await this.getNextSeq();
254
+ const timestamp = getTimestamp();
255
+ const date = getDate();
256
+
257
+ // 解析目标
258
+ const targets = this.resolveTarget(target);
259
+ if (targets.length === 0) {
260
+ throw new Error(`Target "${target}" not found`);
261
+ }
262
+
263
+ // 构建事件
264
+ const event = {
265
+ seq,
266
+ timestamp,
267
+ type: "message/targeted",
268
+ event: "message",
269
+ publisher,
270
+ target,
271
+ data: { message },
272
+ };
273
+
274
+ // 写入事件日志
275
+ const eventFile = path.join(this.eventsDir, `${date}.jsonl`);
276
+ appendJSONL(eventFile, event);
277
+
278
+ // 为每个目标订阅者添加到待处理队列
279
+ for (const targetSubscriber of targets) {
280
+ // 检查订阅者的 offset,如果已经消费过这个 seq,不再添加
281
+ const offset = await this.queueManager.getOffset(targetSubscriber);
282
+ if (seq > offset) {
283
+ await this.queueManager.appendPending(targetSubscriber, event);
284
+ }
285
+ }
286
+
287
+ return { seq, targets };
288
+ }
289
+
290
+ /**
291
+ * 广播消息
292
+ */
293
+ async broadcast(message, publisher = "unknown") {
294
+ return this.send("*", message, publisher);
295
+ }
296
+
297
+ /**
298
+ * 发送系统事件(非消息)
299
+ */
300
+ async emit(target, eventName, data = {}, publisher = "unknown", type = "status/agent") {
301
+ const seq = await this.getNextSeq();
302
+ const timestamp = getTimestamp();
303
+ const date = getDate();
304
+
305
+ // 解析目标
306
+ const targets = this.resolveTarget(target);
307
+ if (targets.length === 0) {
308
+ throw new Error(`Target "${target}" not found`);
309
+ }
310
+
311
+ const event = {
312
+ seq,
313
+ timestamp,
314
+ type,
315
+ event: eventName,
316
+ publisher,
317
+ target,
318
+ data,
319
+ };
320
+
321
+ const eventFile = path.join(this.eventsDir, `${date}.jsonl`);
322
+ appendJSONL(eventFile, event);
323
+
324
+ for (const targetSubscriber of targets) {
325
+ const offset = await this.queueManager.getOffset(targetSubscriber);
326
+ if (seq > offset) {
327
+ await this.queueManager.appendPending(targetSubscriber, event);
328
+ }
329
+ }
330
+
331
+ return { seq, targets };
332
+ }
333
+
334
+ /**
335
+ * 检查待处理消息
336
+ */
337
+ async check(subscriber) {
338
+ const pending = await this.queueManager.readPending(subscriber);
339
+ return pending;
340
+ }
341
+
342
+ /**
343
+ * 确认消息(清空待处理队列)
344
+ */
345
+ async ack(subscriber) {
346
+ const pending = await this.queueManager.readPending(subscriber);
347
+ const count = pending.length;
348
+
349
+ if (count > 0) {
350
+ await this.queueManager.clearPending(subscriber);
351
+ }
352
+
353
+ return count;
354
+ }
355
+
356
+ /**
357
+ * 消费事件(从 offset 开始)
358
+ */
359
+ async consume(subscriber, fromBeginning = false) {
360
+ let offset = fromBeginning ? 0 : await this.queueManager.getOffset(subscriber);
361
+ const consumed = [];
362
+
363
+ // 读取所有事件文件
364
+ if (!fs.existsSync(this.eventsDir)) {
365
+ return { consumed, newOffset: offset };
366
+ }
367
+
368
+ const files = fs.readdirSync(this.eventsDir)
369
+ .filter((f) => f.endsWith(".jsonl"))
370
+ .sort(); // 按日期排序
371
+
372
+ for (const file of files) {
373
+ const filePath = path.join(this.eventsDir, file);
374
+ const events = readJSONL(filePath);
375
+
376
+ for (const event of events) {
377
+ if (event.seq <= offset) continue;
378
+
379
+ // 检查是否针对此订阅者
380
+ if (
381
+ this.targetMatches(event.target, subscriber) ||
382
+ event.target === "*"
383
+ ) {
384
+ consumed.push(event);
385
+ offset = Math.max(offset, event.seq);
386
+ }
387
+ }
388
+ }
389
+
390
+ // 更新 offset
391
+ if (consumed.length > 0) {
392
+ await this.queueManager.setOffset(subscriber, offset);
393
+ }
394
+
395
+ return { consumed, newOffset: offset };
396
+ }
397
+
398
+ /**
399
+ * 智能路由解析(找出所有匹配的候选者)
400
+ */
401
+ async resolve(myId, targetType) {
402
+ const normalizedTargetType = normalizeAgentTypeAlias(targetType);
403
+ const subscribers = this.busData.agents || {};
404
+ const candidates = Object.entries(subscribers)
405
+ .filter(([id, meta]) => {
406
+ if (id === myId) return false; // 排除自己
407
+ if (meta.status !== "active") return false;
408
+
409
+ if (normalizeAgentTypeAlias(meta.agent_type) === normalizedTargetType) return true;
410
+
411
+ return false;
412
+ })
413
+ .map(([id, meta]) => ({
414
+ id,
415
+ nickname: meta.nickname,
416
+ agent_type: meta.agent_type,
417
+ last_seen: meta.last_seen,
418
+ }));
419
+
420
+ // 如果只有一个候选者,直接返回
421
+ if (candidates.length === 1) {
422
+ return { single: candidates[0].id, candidates };
423
+ }
424
+
425
+ // 多个候选者,返回列表供调用者选择
426
+ return { single: null, candidates };
427
+ }
428
+ }
429
+
430
+ module.exports = MessageManager;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * 昵称管理和解析
3
+ */
4
+ class NicknameManager {
5
+ constructor(busData) {
6
+ this.busData = busData;
7
+ }
8
+
9
+ /**
10
+ * 解析昵称到订阅者 ID
11
+ * @param {string} nickname - 昵称
12
+ * @returns {string|null} - 订阅者 ID 或 null
13
+ */
14
+ resolveNickname(nickname) {
15
+ const subscribers = this.busData.agents || {};
16
+ for (const [id, meta] of Object.entries(subscribers)) {
17
+ if (meta.nickname === nickname) {
18
+ return id;
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+
24
+ /**
25
+ * 检查昵称是否已存在
26
+ * @param {string} nickname - 昵称
27
+ * @param {string} excludeSubscriber - 排除的订阅者 ID(用于重命名时)
28
+ * @returns {boolean} - 是否已存在
29
+ */
30
+ nicknameExists(nickname, excludeSubscriber = null) {
31
+ const subscribers = this.busData.agents || {};
32
+ for (const [id, meta] of Object.entries(subscribers)) {
33
+ if (id !== excludeSubscriber && meta.nickname === nickname) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ /**
41
+ * 生成自动昵称
42
+ * @param {string} agentType - 代理类型(codex, claude-code)
43
+ * @returns {string} - 自动生成的昵称(如 codex-1, claude-1)
44
+ */
45
+ generateAutoNickname(agentType) {
46
+ const subscribers = this.busData.agents || {};
47
+ const prefix = agentType === "claude-code" ? "claude"
48
+ : agentType === "ufoo-code" ? "ucode"
49
+ : agentType;
50
+
51
+ // 找出所有相同前缀的昵称
52
+ const existing = Object.values(subscribers)
53
+ .map((meta) => meta.nickname)
54
+ .filter((nick) => nick && nick.startsWith(`${prefix}-`))
55
+ .map((nick) => {
56
+ const match = nick.match(/^[^-]+-(\d+)$/);
57
+ return match ? parseInt(match[1], 10) : 0;
58
+ })
59
+ .filter((n) => !isNaN(n));
60
+
61
+ // 找到下一个可用的编号
62
+ const maxNumber = existing.length > 0 ? Math.max(...existing) : 0;
63
+ return `${prefix}-${maxNumber + 1}`;
64
+ }
65
+
66
+ /**
67
+ * 获取订阅者的昵称
68
+ */
69
+ getNickname(subscriber) {
70
+ const meta = this.busData.agents?.[subscriber];
71
+ return meta?.nickname || null;
72
+ }
73
+
74
+ /**
75
+ * 设置订阅者的昵称
76
+ */
77
+ setNickname(subscriber, nickname) {
78
+ if (!this.busData.agents) {
79
+ this.busData.agents = {};
80
+ }
81
+ if (!this.busData.agents[subscriber]) {
82
+ this.busData.agents[subscriber] = {};
83
+ }
84
+ this.busData.agents[subscriber].nickname = nickname;
85
+ }
86
+ }
87
+
88
+ module.exports = NicknameManager;
@@ -0,0 +1,136 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const {
4
+ subscriberToSafeName,
5
+ ensureDir,
6
+ readJSONL,
7
+ appendJSONL,
8
+ truncateFile,
9
+ } = require("./utils");
10
+
11
+ /**
12
+ * 队列管理器
13
+ */
14
+ class QueueManager {
15
+ constructor(busDir) {
16
+ this.busDir = busDir;
17
+ this.queuesDir = path.join(busDir, "queues");
18
+ this.offsetsDir = path.join(busDir, "offsets");
19
+ }
20
+
21
+ /**
22
+ * 获取订阅者的队列目录
23
+ */
24
+ getQueueDir(subscriber) {
25
+ const safeName = subscriberToSafeName(subscriber);
26
+ return path.join(this.queuesDir, safeName);
27
+ }
28
+
29
+ /**
30
+ * 确保队列目录存在
31
+ */
32
+ ensureQueueDir(subscriber) {
33
+ const queueDir = this.getQueueDir(subscriber);
34
+ ensureDir(queueDir);
35
+ return queueDir;
36
+ }
37
+
38
+ /**
39
+ * 获取 offset 文件路径
40
+ */
41
+ getOffsetPath(subscriber) {
42
+ return path.join(this.offsetsDir, `${subscriberToSafeName(subscriber)}.offset`);
43
+ }
44
+
45
+ /**
46
+ * 获取 pending 文件路径
47
+ */
48
+ getPendingPath(subscriber) {
49
+ return path.join(this.getQueueDir(subscriber), "pending.jsonl");
50
+ }
51
+
52
+ /**
53
+ * 获取 tty 文件路径
54
+ */
55
+ getTtyPath(subscriber) {
56
+ return path.join(this.getQueueDir(subscriber), "tty");
57
+ }
58
+
59
+ /**
60
+ * 读取 offset
61
+ */
62
+ async getOffset(subscriber) {
63
+ const offsetPath = this.getOffsetPath(subscriber);
64
+ if (!fs.existsSync(offsetPath)) {
65
+ return 0;
66
+ }
67
+ const content = fs.readFileSync(offsetPath, "utf8").trim();
68
+ return parseInt(content, 10) || 0;
69
+ }
70
+
71
+ /**
72
+ * 设置 offset
73
+ */
74
+ async setOffset(subscriber, seq) {
75
+ const offsetPath = this.getOffsetPath(subscriber);
76
+ ensureDir(path.dirname(offsetPath));
77
+ fs.writeFileSync(offsetPath, `${seq}\n`, "utf8");
78
+ }
79
+
80
+ /**
81
+ * 读取待处理消息
82
+ */
83
+ async readPending(subscriber) {
84
+ const pendingPath = this.getPendingPath(subscriber);
85
+ return readJSONL(pendingPath);
86
+ }
87
+
88
+ /**
89
+ * 追加待处理消息
90
+ */
91
+ async appendPending(subscriber, event) {
92
+ this.ensureQueueDir(subscriber);
93
+ const pendingPath = this.getPendingPath(subscriber);
94
+ appendJSONL(pendingPath, event);
95
+ if (event && event.event === "wake") {
96
+ const wakePath = path.join(this.getQueueDir(subscriber), "wake");
97
+ fs.writeFileSync(wakePath, String(event.seq || Date.now()), "utf8");
98
+ }
99
+ }
100
+
101
+ /**
102
+ * 清空待处理消息
103
+ */
104
+ async clearPending(subscriber) {
105
+ const pendingPath = this.getPendingPath(subscriber);
106
+ truncateFile(pendingPath);
107
+ }
108
+
109
+ /**
110
+ * 检查是否有待处理消息
111
+ */
112
+ async hasPending(subscriber) {
113
+ const pending = await this.readPending(subscriber);
114
+ return pending.length > 0;
115
+ }
116
+
117
+ /**
118
+ * 保存 tty 设备路径
119
+ */
120
+ async saveTty(subscriber, tty) {
121
+ this.ensureQueueDir(subscriber);
122
+ const ttyPath = this.getTtyPath(subscriber);
123
+ fs.writeFileSync(ttyPath, tty, "utf8");
124
+ }
125
+
126
+ /**
127
+ * 读取 tty 设备路径
128
+ */
129
+ async readTty(subscriber) {
130
+ const ttyPath = this.getTtyPath(subscriber);
131
+ if (!fs.existsSync(ttyPath)) return null;
132
+ return fs.readFileSync(ttyPath, "utf8").trim();
133
+ }
134
+ }
135
+
136
+ module.exports = QueueManager;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Terminal notification - sends visual alert to a terminal by TTY path.
3
+ *
4
+ * Supports:
5
+ * - iTerm2: OSC 9 notification (native macOS notification)
6
+ * - All terminals: terminal bell (\x07)
7
+ */
8
+
9
+ const fs = require("fs");
10
+ const { isITerm2 } = require("../terminal/detect");
11
+
12
+ function shakeTerminalByTty(ttyPath, options = {}) {
13
+ if (!ttyPath) return false;
14
+
15
+ try {
16
+ const fd = fs.openSync(ttyPath, "w");
17
+ // Terminal bell works universally
18
+ fs.writeSync(fd, "\x07");
19
+ fs.closeSync(fd);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ module.exports = { shakeTerminalByTty };