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,189 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { getTimestamp, ensureDir, safeNameToSubscriber, getTtyProcessInfo } = require("./utils");
6
+ const { getUfooPaths } = require("../ufoo/paths");
7
+ const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
8
+
9
+ function readQueueTty(queueDir) {
10
+ try {
11
+ const value = fs.readFileSync(path.join(queueDir, "tty"), "utf8").trim();
12
+ return value || "";
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+
18
+ function nicknamePrefixForType(agentType = "") {
19
+ return agentType === "claude-code" ? "claude"
20
+ : agentType === "ufoo-code" ? "ucode"
21
+ : String(agentType || "agent");
22
+ }
23
+
24
+ function isRecoverableSessionId(sessionId = "") {
25
+ const text = String(sessionId || "").trim();
26
+ if (!text) return false;
27
+ if (text.includes(":") || text.includes("_")) return false;
28
+ return true;
29
+ }
30
+
31
+ function buildUsedNicknameSet(agents = {}) {
32
+ const set = new Set();
33
+ for (const meta of Object.values(agents || {})) {
34
+ if (!meta || meta.status !== "active") continue;
35
+ const nick = meta && typeof meta.nickname === "string" ? meta.nickname : "";
36
+ if (nick) set.add(nick);
37
+ }
38
+ return set;
39
+ }
40
+
41
+ function allocateRecoveredNickname(agentType, used) {
42
+ const prefix = nicknamePrefixForType(agentType);
43
+ let idx = 1;
44
+ while (used.has(`${prefix}-${idx}`)) idx += 1;
45
+ const nick = `${prefix}-${idx}`;
46
+ used.add(nick);
47
+ return nick;
48
+ }
49
+
50
+ class BusStore {
51
+ constructor(projectRoot) {
52
+ this.projectRoot = projectRoot;
53
+ this.paths = getUfooPaths(projectRoot);
54
+ this.busDir = this.paths.busDir;
55
+ this.agentsFile = this.paths.agentsFile;
56
+ this.eventsDir = this.paths.busEventsDir;
57
+ this.logsDir = this.paths.busLogsDir;
58
+ }
59
+
60
+ ensure() {
61
+ if (!fs.existsSync(this.busDir) || !fs.existsSync(this.paths.agentDir)) {
62
+ throw new Error(
63
+ "Event bus not initialized. Please run: ufoo bus init or /uinit"
64
+ );
65
+ }
66
+ }
67
+
68
+ load() {
69
+ const data = loadAgentsData(this.agentsFile);
70
+ if (!data.agents || typeof data.agents !== "object") {
71
+ data.agents = {};
72
+ }
73
+
74
+ const queueRoot = path.join(this.busDir, "queues");
75
+ if (!fs.existsSync(queueRoot)) return data;
76
+
77
+ const usedNicknames = buildUsedNicknameSet(data.agents);
78
+ const now = getTimestamp();
79
+ let recovered = false;
80
+
81
+ for (const entry of fs.readdirSync(queueRoot)) {
82
+ const queueDir = path.join(queueRoot, entry);
83
+ let stat;
84
+ try {
85
+ stat = fs.statSync(queueDir);
86
+ } catch {
87
+ continue;
88
+ }
89
+ if (!stat.isDirectory()) continue;
90
+
91
+ const subscriber = safeNameToSubscriber(entry);
92
+ const parts = subscriber.split(":");
93
+ if (parts.length !== 2) continue;
94
+ const [agentType, sessionId] = parts;
95
+ if (!agentType || !sessionId) continue;
96
+ if (!isRecoverableSessionId(sessionId)) continue;
97
+ if (data.agents[subscriber]) continue;
98
+
99
+ const tty = readQueueTty(queueDir);
100
+ const ttyInfo = tty ? getTtyProcessInfo(tty) : null;
101
+ const activeByTty = Boolean(ttyInfo && ttyInfo.alive && ttyInfo.hasAgent);
102
+ const nickname = activeByTty ? allocateRecoveredNickname(agentType, usedNicknames) : "";
103
+
104
+ data.agents[subscriber] = {
105
+ agent_type: agentType,
106
+ nickname,
107
+ status: activeByTty ? "active" : "inactive",
108
+ joined_at: now,
109
+ last_seen: now,
110
+ pid: 0,
111
+ tty,
112
+ tty_shell_pid: ttyInfo && ttyInfo.shellPid ? ttyInfo.shellPid : 0,
113
+ tmux_pane: "",
114
+ launch_mode: "",
115
+ };
116
+ recovered = true;
117
+ }
118
+
119
+ if (recovered) {
120
+ saveAgentsData(this.agentsFile, data);
121
+ }
122
+ return data;
123
+ }
124
+
125
+ save(busData) {
126
+ if (busData) {
127
+ saveAgentsData(this.agentsFile, busData);
128
+ }
129
+ }
130
+
131
+ init() {
132
+ ensureDir(this.busDir);
133
+ ensureDir(this.paths.agentDir);
134
+ ensureDir(this.eventsDir);
135
+ ensureDir(path.join(this.busDir, "queues"));
136
+ ensureDir(this.logsDir);
137
+ ensureDir(path.join(this.busDir, "offsets"));
138
+ ensureDir(this.paths.busDaemonDir);
139
+ ensureDir(this.paths.busDaemonCountsDir);
140
+
141
+ if (!fs.existsSync(this.agentsFile)) {
142
+ const busData = {
143
+ created_at: getTimestamp(),
144
+ agents: {},
145
+ };
146
+ saveAgentsData(this.agentsFile, busData);
147
+ }
148
+ }
149
+
150
+ getCurrentSubscriber(busData) {
151
+ if (process.env.UFOO_SUBSCRIBER_ID) {
152
+ return process.env.UFOO_SUBSCRIBER_ID;
153
+ }
154
+
155
+ if (!fs.existsSync(this.agentsFile)) {
156
+ return null;
157
+ }
158
+
159
+ const sessionFile = path.join(this.paths.agentDir, "session.txt");
160
+ if (fs.existsSync(sessionFile)) {
161
+ const sessionId = fs.readFileSync(sessionFile, "utf8").trim();
162
+ if (sessionId) {
163
+ return sessionId;
164
+ }
165
+ }
166
+
167
+ let currentTty = null;
168
+ try {
169
+ const ttyPath = fs.realpathSync("/dev/tty");
170
+ if (ttyPath && ttyPath.startsWith("/dev/")) {
171
+ currentTty = ttyPath;
172
+ }
173
+ } catch {
174
+ // tty not available
175
+ }
176
+
177
+ if (currentTty && busData && busData.agents) {
178
+ for (const [id, meta] of Object.entries(busData.agents)) {
179
+ if (meta.tty === currentTty) {
180
+ return id;
181
+ }
182
+ }
183
+ }
184
+
185
+ return null;
186
+ }
187
+ }
188
+
189
+ module.exports = { BusStore };
@@ -0,0 +1,312 @@
1
+ const fs = require("fs");
2
+ const { getTimestamp, isAgentPidAlive, isMetaActive, isValidTty, getTtyProcessInfo } = require("./utils");
3
+ const NicknameManager = require("./nickname");
4
+ const { spawnSync } = require("child_process");
5
+
6
+ function detectTerminalAppFromEnv() {
7
+ const termProgram = process.env.TERM_PROGRAM || "";
8
+ if (process.env.ITERM_SESSION_ID || termProgram === "iTerm.app") return "iterm2";
9
+ if (termProgram === "Apple_Terminal") return "terminal";
10
+ return termProgram || "";
11
+ }
12
+
13
+ /**
14
+ * 获取当前终端的 tty 路径
15
+ */
16
+ function resolveTtyFromPath(fdPath) {
17
+ try {
18
+ const real = fs.realpathSync(fdPath);
19
+ if (real && real.startsWith("/dev/")) {
20
+ return real;
21
+ }
22
+ } catch {
23
+ // ignore
24
+ }
25
+ return "";
26
+ }
27
+
28
+ function normalizeTty(ttyPath) {
29
+ if (!ttyPath) return "";
30
+ const trimmed = String(ttyPath).trim();
31
+ if (!trimmed || trimmed === "not a tty") return "";
32
+ if (trimmed === "/dev/tty") return "";
33
+ return trimmed;
34
+ }
35
+
36
+ function tryTtyWithStdin(fd) {
37
+ try {
38
+ const res = spawnSync("tty", {
39
+ stdio: [fd, "pipe", "ignore"],
40
+ encoding: "utf8",
41
+ });
42
+ if (res && res.status === 0) {
43
+ const out = normalizeTty(res.stdout || "");
44
+ if (out) return out;
45
+ }
46
+ } catch {
47
+ // ignore
48
+ }
49
+ return "";
50
+ }
51
+
52
+ function getTtyPath() {
53
+ // 0) Honor explicit ttyPath from node stdio if present (useful for tests)
54
+ const stdinTtyPath = normalizeTty(process.stdin?.ttyPath || "");
55
+ if (stdinTtyPath) return stdinTtyPath;
56
+
57
+ // 1) Try stdin directly (inherits real tty if present)
58
+ let ttyPath = tryTtyWithStdin(0);
59
+ if (ttyPath) return ttyPath;
60
+
61
+ // 2) Try controlling tty explicitly (works even if stdin is detached)
62
+ try {
63
+ const fd = fs.openSync("/dev/tty", "r");
64
+ ttyPath = tryTtyWithStdin(fd);
65
+ fs.closeSync(fd);
66
+ if (ttyPath) return ttyPath;
67
+ } catch {
68
+ // ignore
69
+ }
70
+
71
+ // 3) Fallback to stdout/stderr device paths
72
+ if (process.stdout.isTTY) {
73
+ ttyPath = normalizeTty(resolveTtyFromPath("/dev/stdout"));
74
+ if (ttyPath) return ttyPath;
75
+ }
76
+ if (process.stderr.isTTY) {
77
+ ttyPath = normalizeTty(resolveTtyFromPath("/dev/stderr"));
78
+ if (ttyPath) return ttyPath;
79
+ }
80
+
81
+ // Final fallback to controlling tty path (may be /dev/tty)
82
+ return normalizeTty(resolveTtyFromPath("/dev/tty"));
83
+ }
84
+
85
+ function getJoinedPid() {
86
+ const raw = process.env.UFOO_PARENT_PID || "";
87
+ const parsed = parseInt(raw, 10);
88
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
89
+ return process.pid;
90
+ }
91
+
92
+ /**
93
+ * 订阅者管理
94
+ */
95
+ class SubscriberManager {
96
+ constructor(busData, queueManager) {
97
+ this.busData = busData;
98
+ this.queueManager = queueManager;
99
+ }
100
+
101
+ async cleanupDuplicateTty(currentSubscriber, ttyPath) {
102
+ if (!ttyPath) return;
103
+ if (!this.busData.agents) return;
104
+
105
+ const entries = Object.entries(this.busData.agents);
106
+ for (const [id, meta] of entries) {
107
+ if (id === currentSubscriber) continue;
108
+ const metaTtyRaw = meta?.tty || "";
109
+ const metaTty = isValidTty(metaTtyRaw)
110
+ ? metaTtyRaw
111
+ : (await this.queueManager.readTty(id));
112
+ if (!metaTty) continue;
113
+ if (metaTty === ttyPath) {
114
+ // Remove stale subscriber using same tty
115
+ delete this.busData.agents[id];
116
+ try {
117
+ const queueDir = this.queueManager.getQueueDir(id);
118
+ if (queueDir) {
119
+ fs.rmSync(queueDir, { recursive: true, force: true });
120
+ }
121
+ const offsetPath = this.queueManager.getOffsetPath(id);
122
+ if (offsetPath) fs.rmSync(offsetPath, { force: true });
123
+ } catch {
124
+ // ignore cleanup errors
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ /**
131
+ * 加入总线
132
+ */
133
+ async join(sessionId, agentType, nickname = null, options = {}) {
134
+ // Special case: ufoo-agent uses fixed ID without suffix
135
+ const subscriber = (sessionId === "ufoo-agent")
136
+ ? "ufoo-agent"
137
+ : `${agentType}:${sessionId}`;
138
+
139
+ if (!this.busData.agents) {
140
+ this.busData.agents = {};
141
+ }
142
+
143
+ const nicknameManager = new NicknameManager(this.busData);
144
+
145
+ // 检查是否是重新加入(rejoin)
146
+ const existingMeta = this.busData.agents[subscriber];
147
+ let finalNickname = nickname;
148
+
149
+ if (existingMeta && existingMeta.nickname) {
150
+ // 重新加入,保留原昵称
151
+ finalNickname = existingMeta.nickname;
152
+ } else if (nickname) {
153
+ // 新昵称,检查冲突
154
+ if (nicknameManager.nicknameExists(nickname, subscriber)) {
155
+ throw new Error(`Nickname "${nickname}" already exists`);
156
+ }
157
+ finalNickname = nickname;
158
+ } else {
159
+ // 自动生成昵称(并标记占用,避免并发重复)
160
+ finalNickname = nicknameManager.generateAutoNickname(agentType);
161
+ nicknameManager.setNickname(subscriber, finalNickname);
162
+ }
163
+
164
+ const launchMode = options.launchMode || process.env.UFOO_LAUNCH_MODE || "";
165
+ const overridePid = Number.isFinite(options.parentPid) && options.parentPid > 0
166
+ ? options.parentPid
167
+ : null;
168
+ const hasOverrideTty = Object.prototype.hasOwnProperty.call(options, "tty");
169
+ const overrideTty = (typeof options.tty === "string" && isValidTty(options.tty.trim()))
170
+ ? options.tty.trim()
171
+ : "";
172
+ const detectedTty = hasOverrideTty ? overrideTty : getTtyPath();
173
+ const tty = overrideTty || (isValidTty(detectedTty) ? detectedTty : "");
174
+ const preservedTty = !tty && launchMode !== "internal" && isValidTty(existingMeta?.tty)
175
+ ? existingMeta.tty
176
+ : "";
177
+ const finalTty = tty || preservedTty;
178
+ const ttyInfo = finalTty ? getTtyProcessInfo(finalTty) : null;
179
+
180
+ // 清理同一 tty 的旧订阅者(避免重复启动污染)
181
+ await this.cleanupDuplicateTty(subscriber, finalTty);
182
+
183
+ // 更新订阅者信息(保留已有字段,如 provider_session_*)
184
+ const preserved = existingMeta && typeof existingMeta === "object"
185
+ ? { ...existingMeta }
186
+ : {};
187
+ this.busData.agents[subscriber] = {
188
+ ...preserved,
189
+ agent_type: agentType,
190
+ nickname: finalNickname,
191
+ status: "active",
192
+ joined_at: existingMeta?.joined_at || getTimestamp(),
193
+ last_seen: getTimestamp(),
194
+ pid: overridePid || getJoinedPid(),
195
+ tty: finalTty,
196
+ tty_shell_pid: ttyInfo?.shellPid || 0,
197
+ tmux_pane: options.tmuxPane || process.env.TMUX_PANE || "",
198
+ launch_mode: launchMode,
199
+ };
200
+
201
+ const terminalApp = options.terminalApp || detectTerminalAppFromEnv();
202
+ if (terminalApp) {
203
+ this.busData.agents[subscriber].terminal_app = terminalApp;
204
+ }
205
+
206
+ // 如果传入了 providerSessionId(从旧 session 恢复),设置它
207
+ if (options.providerSessionId) {
208
+ this.busData.agents[subscriber].provider_session_id = options.providerSessionId;
209
+ }
210
+
211
+ // 保存 tty 信息
212
+ if (this.busData.agents[subscriber].tty) {
213
+ await this.queueManager.saveTty(
214
+ subscriber,
215
+ this.busData.agents[subscriber].tty
216
+ );
217
+ } else {
218
+ // 清理旧 tty 文件,避免错误注入
219
+ try {
220
+ const ttyPath = this.queueManager.getTtyPath(subscriber);
221
+ if (ttyPath && fs.existsSync(ttyPath)) {
222
+ fs.rmSync(ttyPath, { force: true });
223
+ }
224
+ } catch {
225
+ // ignore
226
+ }
227
+ }
228
+
229
+ // 创建队列目录
230
+ this.queueManager.ensureQueueDir(subscriber);
231
+
232
+ return { subscriber, nickname: finalNickname };
233
+ }
234
+
235
+ /**
236
+ * 离开总线
237
+ */
238
+ async leave(subscriber) {
239
+ if (!this.busData.agents || !this.busData.agents[subscriber]) {
240
+ return false;
241
+ }
242
+
243
+ this.busData.agents[subscriber].status = "inactive";
244
+ this.busData.agents[subscriber].last_seen = getTimestamp();
245
+
246
+ return true;
247
+ }
248
+
249
+ /**
250
+ * 重命名订阅者
251
+ */
252
+ async rename(subscriber, newNickname) {
253
+ if (!this.busData.agents || !this.busData.agents[subscriber]) {
254
+ throw new Error(`Subscriber "${subscriber}" not found`);
255
+ }
256
+
257
+ const nicknameManager = new NicknameManager(this.busData);
258
+
259
+ // 检查昵称冲突
260
+ if (nicknameManager.nicknameExists(newNickname, subscriber)) {
261
+ throw new Error(`Nickname "${newNickname}" already exists`);
262
+ }
263
+
264
+ const oldNickname = this.busData.agents[subscriber].nickname;
265
+ this.busData.agents[subscriber].nickname = newNickname;
266
+
267
+ return { subscriber, oldNickname, newNickname };
268
+ }
269
+
270
+ /**
271
+ * 获取所有在线订阅者
272
+ */
273
+ getActiveSubscribers() {
274
+ if (!this.busData.agents) return [];
275
+
276
+ return Object.entries(this.busData.agents)
277
+ .filter(([, meta]) => isMetaActive(meta))
278
+ .map(([id, meta]) => ({ id, ...meta }));
279
+ }
280
+
281
+ /**
282
+ * 获取订阅者信息
283
+ */
284
+ getSubscriber(subscriber) {
285
+ return this.busData.agents?.[subscriber] || null;
286
+ }
287
+
288
+ /**
289
+ * 更新订阅者的最后活动时间
290
+ */
291
+ updateLastSeen(subscriber) {
292
+ if (this.busData.agents && this.busData.agents[subscriber]) {
293
+ this.busData.agents[subscriber].last_seen = getTimestamp();
294
+ }
295
+ }
296
+
297
+ /**
298
+ * 清理不活跃的订阅者
299
+ */
300
+ cleanupInactive() {
301
+ if (!this.busData.agents) return;
302
+
303
+ for (const [id, meta] of Object.entries(this.busData.agents)) {
304
+ if (meta.status === "active" && !isMetaActive(meta)) {
305
+ meta.status = "inactive";
306
+ meta.last_seen = getTimestamp();
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ module.exports = SubscriberManager;