u-foo 1.0.6 → 1.2.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.
Files changed (149) hide show
  1. package/README.md +247 -23
  2. package/SKILLS/ufoo/SKILL.md +17 -2
  3. package/SKILLS/uinit/SKILL.md +8 -3
  4. package/bin/ucode-core.js +15 -0
  5. package/bin/ucode.js +125 -0
  6. package/bin/ufoo-assistant-agent.js +5 -0
  7. package/bin/ufoo-engine.js +25 -0
  8. package/bin/ufoo.js +4 -0
  9. package/modules/AGENTS.template.md +14 -4
  10. package/modules/bus/README.md +8 -5
  11. package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
  12. package/modules/context/SKILLS/uctx/SKILL.md +3 -1
  13. package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
  14. package/package.json +12 -3
  15. package/scripts/import-pi-mono.js +124 -0
  16. package/scripts/postinstall.js +20 -49
  17. package/scripts/sync-claude-skills.sh +21 -0
  18. package/src/agent/cliRunner.js +524 -31
  19. package/src/agent/internalRunner.js +76 -9
  20. package/src/agent/launcher.js +97 -45
  21. package/src/agent/normalizeOutput.js +1 -1
  22. package/src/agent/notifier.js +144 -4
  23. package/src/agent/ptyRunner.js +480 -10
  24. package/src/agent/ptyWrapper.js +28 -3
  25. package/src/agent/readyDetector.js +16 -0
  26. package/src/agent/ucode.js +443 -0
  27. package/src/agent/ucodeBootstrap.js +113 -0
  28. package/src/agent/ucodeBuild.js +67 -0
  29. package/src/agent/ucodeDoctor.js +184 -0
  30. package/src/agent/ucodeRuntimeConfig.js +129 -0
  31. package/src/agent/ufooAgent.js +168 -28
  32. package/src/assistant/agent.js +260 -0
  33. package/src/assistant/bridge.js +172 -0
  34. package/src/assistant/engine.js +252 -0
  35. package/src/assistant/stdio.js +58 -0
  36. package/src/assistant/ufooEngineCli.js +306 -0
  37. package/src/bus/activate.js +27 -11
  38. package/src/bus/daemon.js +133 -5
  39. package/src/bus/index.js +137 -80
  40. package/src/bus/inject.js +47 -17
  41. package/src/bus/message.js +145 -17
  42. package/src/bus/nickname.js +3 -1
  43. package/src/bus/queue.js +6 -1
  44. package/src/bus/store.js +189 -0
  45. package/src/bus/subscriber.js +20 -4
  46. package/src/bus/utils.js +9 -3
  47. package/src/chat/agentBar.js +117 -0
  48. package/src/chat/agentDirectory.js +88 -0
  49. package/src/chat/agentSockets.js +225 -0
  50. package/src/chat/agentViewController.js +298 -0
  51. package/src/chat/chatLogController.js +115 -0
  52. package/src/chat/commandExecutor.js +700 -0
  53. package/src/chat/commands.js +132 -0
  54. package/src/chat/completionController.js +414 -0
  55. package/src/chat/cronScheduler.js +160 -0
  56. package/src/chat/daemonConnection.js +166 -0
  57. package/src/chat/daemonCoordinator.js +64 -0
  58. package/src/chat/daemonMessageRouter.js +257 -0
  59. package/src/chat/daemonReconnect.js +41 -0
  60. package/src/chat/daemonTransport.js +36 -0
  61. package/src/chat/daemonTransportDefaults.js +10 -0
  62. package/src/chat/dashboardKeyController.js +480 -0
  63. package/src/chat/dashboardView.js +157 -0
  64. package/src/chat/index.js +938 -2910
  65. package/src/chat/inputHistoryController.js +105 -0
  66. package/src/chat/inputListenerController.js +304 -0
  67. package/src/chat/inputMath.js +104 -0
  68. package/src/chat/inputSubmitHandler.js +171 -0
  69. package/src/chat/layout.js +165 -0
  70. package/src/chat/pasteController.js +81 -0
  71. package/src/chat/rawKeyMap.js +42 -0
  72. package/src/chat/settingsController.js +133 -0
  73. package/src/chat/statusLineController.js +177 -0
  74. package/src/chat/streamTracker.js +138 -0
  75. package/src/chat/text.js +70 -0
  76. package/src/chat/transport.js +61 -0
  77. package/src/cli/busCoreCommands.js +59 -0
  78. package/src/cli/ctxCoreCommands.js +199 -0
  79. package/src/cli/onlineCoreCommands.js +379 -0
  80. package/src/cli.js +741 -238
  81. package/src/code/README.md +29 -0
  82. package/src/code/UCODE_PROMPT.md +32 -0
  83. package/src/code/agent.js +1651 -0
  84. package/src/code/cli.js +158 -0
  85. package/src/code/config +0 -0
  86. package/src/code/dispatch.js +42 -0
  87. package/src/code/index.js +70 -0
  88. package/src/code/nativeRunner.js +1213 -0
  89. package/src/code/runtime.js +154 -0
  90. package/src/code/sessionStore.js +162 -0
  91. package/src/code/taskDecomposer.js +269 -0
  92. package/src/code/tools/bash.js +53 -0
  93. package/src/code/tools/common.js +42 -0
  94. package/src/code/tools/edit.js +70 -0
  95. package/src/code/tools/read.js +44 -0
  96. package/src/code/tools/write.js +35 -0
  97. package/src/code/tui.js +1587 -0
  98. package/src/config.js +50 -2
  99. package/src/context/decisions.js +12 -2
  100. package/src/context/index.js +18 -1
  101. package/src/context/sync.js +127 -0
  102. package/src/daemon/agentProcessManager.js +74 -0
  103. package/src/daemon/cronOps.js +241 -0
  104. package/src/daemon/index.js +662 -489
  105. package/src/daemon/ipcServer.js +99 -0
  106. package/src/daemon/ops.js +417 -179
  107. package/src/daemon/promptLoop.js +319 -0
  108. package/src/daemon/promptRequest.js +101 -0
  109. package/src/daemon/providerSessions.js +32 -17
  110. package/src/daemon/reporting.js +90 -0
  111. package/src/daemon/run.js +2 -5
  112. package/src/daemon/status.js +24 -1
  113. package/src/init/index.js +68 -14
  114. package/src/online/bridge.js +663 -0
  115. package/src/online/client.js +245 -0
  116. package/src/online/runner.js +253 -0
  117. package/src/online/server.js +992 -0
  118. package/src/online/tokens.js +103 -0
  119. package/src/report/store.js +331 -0
  120. package/src/shared/eventContract.js +35 -0
  121. package/src/shared/ptySocketContract.js +21 -0
  122. package/src/status/index.js +50 -17
  123. package/src/terminal/adapterContract.js +87 -0
  124. package/src/terminal/adapterRouter.js +84 -0
  125. package/src/terminal/adapters/externalAdapter.js +14 -0
  126. package/src/terminal/adapters/internalAdapter.js +13 -0
  127. package/src/terminal/adapters/internalPtyAdapter.js +42 -0
  128. package/src/terminal/adapters/internalQueueAdapter.js +37 -0
  129. package/src/terminal/adapters/terminalAdapter.js +31 -0
  130. package/src/terminal/adapters/tmuxAdapter.js +30 -0
  131. package/src/ufoo/agentsStore.js +69 -3
  132. package/src/utils/banner.js +5 -2
  133. package/scripts/.archived/bash-to-js-migration/README.md +0 -46
  134. package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
  135. package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
  136. package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
  137. package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
  138. package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
  139. package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
  140. package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
  141. package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
  142. package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
  143. package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
  144. package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
  145. package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
  146. package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
  147. package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
  148. package/scripts/banner.sh +0 -2
  149. package/src/bus/API_DESIGN.md +0 -204
package/src/bus/index.js CHANGED
@@ -2,10 +2,9 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { spawn } = require("child_process");
4
4
  const {
5
- getTimestamp,
6
- ensureDir,
7
5
  logInfo,
8
6
  logOk,
7
+ ensureDir,
9
8
  logWarn,
10
9
  logError,
11
10
  colors,
@@ -14,6 +13,7 @@ const {
14
13
  isPidAlive,
15
14
  truncateFile,
16
15
  getCurrentTty,
16
+ sleep,
17
17
  } = require("./utils");
18
18
  const { shakeTerminalByTty } = require("./shake");
19
19
  const QueueManager = require("./queue");
@@ -22,8 +22,7 @@ const MessageManager = require("./message");
22
22
  const NicknameManager = require("./nickname");
23
23
  const BusDaemon = require("./daemon");
24
24
  const Injector = require("./inject");
25
- const { getUfooPaths } = require("../ufoo/paths");
26
- const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
25
+ const { BusStore } = require("./store");
27
26
 
28
27
  /**
29
28
  * Event Bus - 项目级 Agent 事件总线
@@ -31,11 +30,12 @@ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
31
30
  class EventBus {
32
31
  constructor(projectRoot) {
33
32
  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;
33
+ this.store = new BusStore(projectRoot);
34
+ this.paths = this.store.paths;
35
+ this.busDir = this.store.busDir;
36
+ this.agentsFile = this.store.agentsFile;
37
+ this.eventsDir = this.store.eventsDir;
38
+ this.logsDir = this.store.logsDir;
39
39
 
40
40
  this.busData = null;
41
41
  this.queueManager = null;
@@ -47,18 +47,14 @@ class EventBus {
47
47
  * 确保 bus 已初始化
48
48
  */
49
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
- }
50
+ this.store.ensure();
55
51
  }
56
52
 
57
53
  /**
58
54
  * 加载 bus 数据
59
55
  */
60
56
  loadBusData() {
61
- this.busData = loadAgentsData(this.agentsFile);
57
+ this.busData = this.store.load();
62
58
 
63
59
  this.queueManager = new QueueManager(this.busDir);
64
60
  this.subscriberManager = new SubscriberManager(
@@ -79,101 +75,100 @@ class EventBus {
79
75
  * 保存 bus 数据
80
76
  */
81
77
  saveBusData() {
82
- if (this.busData) {
83
- saveAgentsData(this.agentsFile, this.busData);
84
- }
78
+ this.store.save(this.busData);
85
79
  }
86
80
 
87
81
  /**
88
82
  * 获取当前订阅者 ID
89
83
  */
90
84
  getCurrentSubscriber() {
91
- // 优先使用 UFOO_SUBSCRIBER_ID(daemon 启动的情况)
92
- if (process.env.UFOO_SUBSCRIBER_ID) {
93
- return process.env.UFOO_SUBSCRIBER_ID;
94
- }
85
+ return this.store.getCurrentSubscriber(this.busData);
86
+ }
95
87
 
96
- if (!fs.existsSync(this.agentsFile)) {
97
- return null;
98
- }
88
+ /**
89
+ * 解析订阅者 ID
90
+ */
91
+ parseSubscriber(subscriber) {
92
+ if (!subscriber || typeof subscriber !== "string") return null;
93
+ if (subscriber === "ufoo-agent") {
94
+ return { agentType: "codex", sessionId: "ufoo-agent" };
95
+ }
96
+ const parts = subscriber.split(":");
97
+ if (parts.length !== 2 || !parts[0] || !parts[1]) return null;
98
+ return {
99
+ agentType: parts[0],
100
+ sessionId: parts[1],
101
+ };
102
+ }
99
103
 
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
- }
104
+ /**
105
+ * 推断 join 所需的 agentType
106
+ */
107
+ resolveJoinAgentType(explicitAgentType, currentSubscriber = "") {
108
+ if (explicitAgentType) return explicitAgentType;
108
109
 
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
- }
110
+ const parsedCurrent = this.parseSubscriber(currentSubscriber);
111
+ if (parsedCurrent && parsedCurrent.agentType) return parsedCurrent.agentType;
119
112
 
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
- }
113
+ const envAgentType = (process.env.UFOO_AGENT_TYPE || "").trim();
114
+ if (envAgentType) return envAgentType;
127
115
 
128
- return null;
116
+ const parsedEnv = this.parseSubscriber(process.env.UFOO_SUBSCRIBER_ID || "");
117
+ if (parsedEnv && parsedEnv.agentType) return parsedEnv.agentType;
118
+
119
+ // 最后回退(手动场景)
120
+ return "claude-code";
129
121
  }
130
122
 
131
123
  /**
132
124
  * 初始化事件总线
133
125
  */
134
126
  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
- }
127
+ this.store.init();
153
128
  logOk("Event bus initialized");
154
129
  }
155
130
 
156
131
  /**
157
132
  * 加入总线
158
133
  */
159
- async join(sessionId, agentType, nickname = null) {
134
+ async join(sessionId, agentType, nickname = null, options = {}) {
160
135
  this.ensureBus();
161
136
  this.loadBusData();
162
137
 
138
+ const currentSubscriber = this.getCurrentSubscriber();
139
+ const currentMeta = currentSubscriber && this.busData.agents
140
+ ? this.busData.agents[currentSubscriber]
141
+ : null;
142
+ const currentActive = currentMeta
143
+ && currentMeta.status === "active"
144
+ && (!currentMeta.pid || isPidAlive(currentMeta.pid));
145
+
146
+ // 已在总线中且无显式参数时,直接复用当前身份(避免二次 join 产生新 ID)
147
+ if (!sessionId && !agentType && currentSubscriber && currentActive) {
148
+ this.subscriberManager.updateLastSeen(currentSubscriber);
149
+ this.saveBusData();
150
+ const currentNickname = currentMeta.nickname ? ` (${currentMeta.nickname})` : "";
151
+ logInfo(`Already joined event bus: ${currentSubscriber}${currentNickname}`);
152
+ return currentSubscriber;
153
+ }
154
+
163
155
  // 自动检测 session ID 和 agent type
156
+ const parsedCurrent = this.parseSubscriber(currentSubscriber);
157
+ if (!sessionId && parsedCurrent && parsedCurrent.sessionId) {
158
+ sessionId = parsedCurrent.sessionId;
159
+ }
160
+
164
161
  if (!sessionId) {
165
162
  sessionId = generateInstanceId();
166
163
  }
167
164
 
168
- if (!agentType) {
169
- // 默认为 claude-code(手动启动情况)
170
- agentType = "claude-code";
171
- }
165
+ agentType = this.resolveJoinAgentType(agentType, currentSubscriber);
172
166
 
173
167
  const result = await this.subscriberManager.join(
174
168
  sessionId,
175
169
  agentType,
176
- nickname
170
+ nickname,
171
+ options
177
172
  );
178
173
 
179
174
  this.saveBusData();
@@ -266,7 +261,7 @@ class EventBus {
266
261
  /**
267
262
  * 发送消息
268
263
  */
269
- async send(target, message, publisher = null) {
264
+ async send(target, message, publisher = null, options = {}) {
270
265
  this.ensureBus();
271
266
  this.loadBusData();
272
267
 
@@ -308,7 +303,11 @@ class EventBus {
308
303
  }
309
304
 
310
305
  try {
311
- const result = await this.messageManager.send(target, message, publisher);
306
+ const eventName = options.event || "message";
307
+ const data = options.data || { message };
308
+ const result = eventName === "message"
309
+ ? await this.messageManager.send(target, message, publisher)
310
+ : await this.messageManager.emit(target, eventName, data, publisher);
312
311
  logOk(
313
312
  `Message sent: seq=${result.seq} -> ${result.targets.join(", ")}`
314
313
  );
@@ -322,8 +321,8 @@ class EventBus {
322
321
  /**
323
322
  * 广播消息
324
323
  */
325
- async broadcast(message, publisher = null) {
326
- return this.send("*", message, publisher);
324
+ async broadcast(message, publisher = null, options = {}) {
325
+ return this.send("*", message, publisher, options);
327
326
  }
328
327
 
329
328
  /**
@@ -506,13 +505,25 @@ class EventBus {
506
505
 
507
506
  // 检查是否已经 join
508
507
  const currentSubscriber = this.getCurrentSubscriber();
509
- if (currentSubscriber && this.busData.agents && this.busData.agents[currentSubscriber]) {
508
+ const currentMeta = currentSubscriber && this.busData.agents
509
+ ? this.busData.agents[currentSubscriber]
510
+ : null;
511
+ const currentActive = currentMeta
512
+ && currentMeta.status === "active"
513
+ && (!currentMeta.pid || isPidAlive(currentMeta.pid));
514
+ if (currentSubscriber && currentActive) {
510
515
  // 已经 join,只需更新心跳
511
516
  this.subscriberManager.updateLastSeen(currentSubscriber);
512
517
  this.saveBusData();
513
518
  return currentSubscriber;
514
519
  }
515
520
 
521
+ // 当前身份可解析但元数据缺失/失效时,复用同一身份重新注册
522
+ const parsedCurrent = this.parseSubscriber(currentSubscriber || "");
523
+ if (parsedCurrent) {
524
+ return this.join(parsedCurrent.sessionId, parsedCurrent.agentType, null);
525
+ }
526
+
516
527
  // 还没有 join,自动 join
517
528
  const sessionId = null; // 自动生成
518
529
  const agentType = null; // 自动检测
@@ -670,6 +681,52 @@ class EventBus {
670
681
  }
671
682
  }
672
683
 
684
+ /**
685
+ * 远程唤醒本地 agent(触发 /ubus 注入)
686
+ */
687
+ async wake(subscriber, options = {}) {
688
+ this.ensureBus();
689
+ this.loadBusData();
690
+
691
+ const publisher =
692
+ options.publisher ||
693
+ process.env.AI_BUS_PUBLISHER ||
694
+ this.getDefaultPublisher() ||
695
+ this.getCurrentSubscriber() ||
696
+ "unknown";
697
+
698
+ const targets = this.messageManager.resolveTarget(subscriber || "");
699
+ if (targets.length === 0) {
700
+ throw new Error(`Target "${subscriber}" not found`);
701
+ }
702
+
703
+ for (const target of targets) {
704
+ const safeName = subscriberToSafeName(target);
705
+ const queueDir = path.join(this.busDir, "queues", safeName);
706
+ const pendingFile = path.join(queueDir, "pending.jsonl");
707
+ ensureDir(queueDir);
708
+
709
+ const before = fs.existsSync(pendingFile) ? fs.readFileSync(pendingFile, "utf8") : "";
710
+ const countBefore = before.trim() ? before.trim().split(/\r?\n/).length : 0;
711
+ await this.messageManager.emit(target, "wake", { reason: options.reason || "remote" }, publisher, "status/wake");
712
+ const after = fs.existsSync(pendingFile) ? fs.readFileSync(pendingFile, "utf8") : "";
713
+ const countAfter = after.trim() ? after.trim().split(/\r?\n/).length : 0;
714
+
715
+ if (countAfter > countBefore) {
716
+ await sleep(50);
717
+ const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, 2000);
718
+ await daemon.injector.inject(target, options.command || "");
719
+ if (options.shake !== false) {
720
+ const tty = daemon.injector.readTty(target);
721
+ if (tty) shakeTerminalByTty(tty, { skipFrontmost: true });
722
+ }
723
+ }
724
+ }
725
+
726
+ logOk(`Wake sent -> ${targets.join(", ")}`);
727
+ return { ok: true, targets };
728
+ }
729
+
673
730
  /**
674
731
  * 前台消息监听
675
732
  */
package/src/bus/inject.js CHANGED
@@ -3,6 +3,7 @@ const fs = require("fs");
3
3
  const net = require("net");
4
4
  const path = require("path");
5
5
  const { subscriberToSafeName, isValidTty } = require("./utils");
6
+ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
6
7
 
7
8
  const SHOULD_LOG_INJECT = process.env.UFOO_INJECT_DEBUG === "1";
8
9
  const logInject = (message) => {
@@ -35,6 +36,18 @@ class Injector {
35
36
  /**
36
37
  * 获取订阅者的 tmux pane ID(从 all-agents.json)
37
38
  */
39
+ getAgentMeta(subscriber) {
40
+ const agentsFile = this.agentsFile;
41
+ if (!agentsFile || !fs.existsSync(agentsFile)) return null;
42
+
43
+ try {
44
+ const busData = JSON.parse(fs.readFileSync(agentsFile, "utf8"));
45
+ return busData.agents?.[subscriber] || null;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
38
51
  getTmuxPane(subscriber) {
39
52
  const agentsFile = this.agentsFile;
40
53
  if (!agentsFile || !fs.existsSync(agentsFile)) return null;
@@ -235,16 +248,31 @@ class Injector {
235
248
  * 2. tmux send-keys(无需权限)
236
249
  */
237
250
  async inject(subscriber, commandOverride = "") {
251
+ if (String(subscriber || "").startsWith("ufoo-code:")) {
252
+ throw new Error(`Inject disabled for ${subscriber}. ufoo-code consumes bus internally.`);
253
+ }
254
+
238
255
  // 确定注入命令(codex 用 "ubus",claude-code 用 "/ubus")
239
256
  const command = commandOverride
240
257
  ? String(commandOverride)
241
258
  : (subscriber.startsWith("codex:") ? "ubus" : "/ubus");
242
259
 
260
+ const meta = this.getAgentMeta(subscriber) || {};
261
+ const launchMode = meta.launch_mode || "";
262
+ const adapterRouter = createTerminalAdapterRouter();
263
+ const adapter = adapterRouter.getAdapter({ launchMode, agentId: subscriber });
264
+ const supportsSocket = adapter.capabilities.supportsSocketProtocol;
265
+ const supportsNotifier = adapter.capabilities.supportsNotifierInjector;
266
+
243
267
  // 1. 优先尝试 PTY socket(无需任何macOS权限)
244
268
  const injectSockPath = this.getInjectSockPath(subscriber);
245
269
  if (fs.existsSync(injectSockPath)) {
246
270
  try {
247
- logInject(`[inject] Using PTY socket: ${injectSockPath}`);
271
+ if (!supportsSocket) {
272
+ logInject(`[inject] PTY socket present but unsupported for launch_mode=${launchMode}`);
273
+ } else {
274
+ logInject(`[inject] Using PTY socket: ${injectSockPath}`);
275
+ }
248
276
  await this.injectPty(subscriber, command);
249
277
  logInject("[inject] PTY inject success");
250
278
  return;
@@ -256,24 +284,26 @@ class Injector {
256
284
  // 读取 tty(tmux 需要)
257
285
  const tty = this.readTty(subscriber);
258
286
 
259
- // 2. 尝试 tmux(无需权限)
260
- const tmuxPane = this.getTmuxPane(subscriber);
261
- if (tmuxPane) {
262
- const paneExists = await this.checkTmuxPane(tmuxPane);
263
- if (paneExists) {
264
- logInject(`[inject] Using tmux send-keys for pane: ${tmuxPane}`);
265
- await this.injectTmux(tmuxPane, command);
266
- return;
287
+ if (supportsNotifier) {
288
+ // 2. 尝试 tmux(无需权限)
289
+ const tmuxPane = meta.tmux_pane || this.getTmuxPane(subscriber);
290
+ if (tmuxPane) {
291
+ const paneExists = await this.checkTmuxPane(tmuxPane);
292
+ if (paneExists) {
293
+ logInject(`[inject] Using tmux send-keys for pane: ${tmuxPane}`);
294
+ await this.injectTmux(tmuxPane, command);
295
+ return;
296
+ }
267
297
  }
268
- }
269
298
 
270
- // 尝试通过 tty 查找 tmux pane
271
- if (tty && isValidTty(tty)) {
272
- const fallbackPane = await this.findTmuxPaneByTty(tty);
273
- if (fallbackPane) {
274
- logInject(`[inject] Using tmux send-keys for pane: ${fallbackPane}`);
275
- await this.injectTmux(fallbackPane, command);
276
- return;
299
+ // 尝试通过 tty 查找 tmux pane
300
+ if (tty && isValidTty(tty)) {
301
+ const fallbackPane = await this.findTmuxPaneByTty(tty);
302
+ if (fallbackPane) {
303
+ logInject(`[inject] Using tmux send-keys for pane: ${fallbackPane}`);
304
+ await this.injectTmux(fallbackPane, command);
305
+ return;
306
+ }
277
307
  }
278
308
  }
279
309
 
@@ -6,11 +6,23 @@ const {
6
6
  readJSONL,
7
7
  appendJSONL,
8
8
  readLastLine,
9
- readJSON,
10
- isMetaActive,
9
+ isPidAlive,
11
10
  } = require("./utils");
12
11
  const NicknameManager = require("./nickname");
13
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
+
14
26
  /**
15
27
  * 消息管理器
16
28
  */
@@ -20,17 +32,17 @@ class MessageManager {
20
32
  this.busData = busData;
21
33
  this.queueManager = queueManager;
22
34
  this.eventsDir = path.join(busDir, "events");
35
+ this.seqFile = path.join(busDir, "seq.counter");
36
+ this.seqLockFile = path.join(busDir, "seq.counter.lock");
23
37
  }
24
38
 
25
39
  /**
26
- * 获取下一个全局序号
40
+ * 从 events 日志中恢复最大序号(仅用于 counter 缺失时)
27
41
  */
28
- async getNextSeq() {
29
- // 读取所有 events/*.jsonl 文件,找到最大的 seq
42
+ readMaxSeqFromEvents() {
30
43
  let maxSeq = 0;
31
-
32
44
  if (!fs.existsSync(this.eventsDir)) {
33
- return 1;
45
+ return maxSeq;
34
46
  }
35
47
 
36
48
  const files = fs.readdirSync(this.eventsDir)
@@ -55,7 +67,116 @@ class MessageManager {
55
67
  }
56
68
  }
57
69
 
58
- return maxSeq + 1;
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
+ }
59
180
  }
60
181
 
61
182
  /**
@@ -63,6 +184,13 @@ class MessageManager {
63
184
  */
64
185
  resolveTarget(target) {
65
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
+ }
66
194
 
67
195
  // 1. 尝试作为订阅者 ID
68
196
  if (target.includes(":")) {
@@ -75,10 +203,11 @@ class MessageManager {
75
203
  return [byNickname];
76
204
  }
77
205
 
78
- // 3. 尝试作为代理类型(匹配所有该类型的活跃订阅者)
79
- const subscribers = this.busData.agents || {};
206
+ // 3. 尝试作为代理类型(匹配所有该类型的订阅者)
207
+ const isActive = (meta) => !meta || meta.status === "active";
208
+
80
209
  const byType = Object.entries(subscribers)
81
- .filter(([, meta]) => meta.agent_type === target && isMetaActive(meta))
210
+ .filter(([, meta]) => normalizeAgentTypeAlias(meta.agent_type) === normalizedTarget && isActive(meta))
82
211
  .map(([id]) => id);
83
212
 
84
213
  if (byType.length > 0) {
@@ -88,7 +217,7 @@ class MessageManager {
88
217
  // 4. 通配符(所有活跃订阅者)
89
218
  if (target === "*") {
90
219
  return Object.entries(subscribers)
91
- .filter(([, meta]) => isMetaActive(meta))
220
+ .filter(([, meta]) => isActive(meta))
92
221
  .map(([id]) => id);
93
222
  }
94
223
 
@@ -100,12 +229,13 @@ class MessageManager {
100
229
  * 检查目标是否匹配订阅者
101
230
  */
102
231
  targetMatches(target, subscriber) {
232
+ const normalizedTarget = normalizeAgentTypeAlias(target);
103
233
  // 精确匹配
104
234
  if (target === subscriber) return true;
105
235
 
106
236
  // 代理类型匹配
107
237
  const meta = this.busData.agents?.[subscriber];
108
- if (meta && target === meta.agent_type) return true;
238
+ if (meta && normalizedTarget === normalizeAgentTypeAlias(meta.agent_type)) return true;
109
239
 
110
240
  // 昵称匹配
111
241
  if (meta && target === meta.nickname) return true;
@@ -269,16 +399,14 @@ class MessageManager {
269
399
  * 智能路由解析(找出所有匹配的候选者)
270
400
  */
271
401
  async resolve(myId, targetType) {
402
+ const normalizedTargetType = normalizeAgentTypeAlias(targetType);
272
403
  const subscribers = this.busData.agents || {};
273
404
  const candidates = Object.entries(subscribers)
274
405
  .filter(([id, meta]) => {
275
406
  if (id === myId) return false; // 排除自己
276
407
  if (meta.status !== "active") return false;
277
408
 
278
- // 匹配代理类型
279
- if (targetType === "codex" && meta.agent_type === "codex") return true;
280
- if (targetType === "claude" && meta.agent_type === "claude-code") return true;
281
- if (targetType === meta.agent_type) return true;
409
+ if (normalizeAgentTypeAlias(meta.agent_type) === normalizedTargetType) return true;
282
410
 
283
411
  return false;
284
412
  })
@@ -44,7 +44,9 @@ class NicknameManager {
44
44
  */
45
45
  generateAutoNickname(agentType) {
46
46
  const subscribers = this.busData.agents || {};
47
- const prefix = agentType === "claude-code" ? "claude" : agentType;
47
+ const prefix = agentType === "claude-code" ? "claude"
48
+ : agentType === "ufoo-code" ? "ucode"
49
+ : agentType;
48
50
 
49
51
  // 找出所有相同前缀的昵称
50
52
  const existing = Object.values(subscribers)