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,754 @@
1
+ const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
2
+ const { PTY_SOCKET_MESSAGE_TYPES } = require("../shared/ptySocketContract");
3
+ const { spawn, spawnSync } = require("child_process");
4
+ const fs = require("fs");
5
+ const net = require("net");
6
+ const path = require("path");
7
+ const EventBus = require("../bus");
8
+ const { isAgentPidAlive } = require("../bus/utils");
9
+ const { showBanner } = require("../utils/banner");
10
+ const AgentNotifier = require("./notifier");
11
+ const { getUfooPaths } = require("../ufoo/paths");
12
+ const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
13
+ const PtyWrapper = require("./ptyWrapper");
14
+ const ReadyDetector = require("./readyDetector");
15
+
16
+ function connectSocket(sockPath) {
17
+ return new Promise((resolve, reject) => {
18
+ const client = net.createConnection(sockPath, () => resolve(client));
19
+ client.on("error", reject);
20
+ });
21
+ }
22
+
23
+ async function connectWithRetry(sockPath, retries, delayMs) {
24
+ for (let i = 0; i < retries; i += 1) {
25
+ try {
26
+ // eslint-disable-next-line no-await-in-loop
27
+ const client = await connectSocket(sockPath);
28
+ return client;
29
+ } catch {
30
+ // eslint-disable-next-line no-await-in-loop
31
+ await new Promise((r) => setTimeout(r, delayMs));
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ async function probeDaemonSocket(sockPath) {
38
+ try {
39
+ const client = await connectSocket(sockPath);
40
+ try {
41
+ client.end();
42
+ client.destroy();
43
+ } catch {
44
+ // ignore cleanup errors
45
+ }
46
+ return { ok: true, code: "" };
47
+ } catch (err) {
48
+ return { ok: false, code: err && err.code ? err.code : "" };
49
+ }
50
+ }
51
+
52
+ function normalizeTty(ttyPath) {
53
+ if (!ttyPath) return "";
54
+ const trimmed = String(ttyPath).trim();
55
+ if (!trimmed || trimmed === "not a tty") return "";
56
+ if (trimmed === "/dev/tty") return "";
57
+ return trimmed;
58
+ }
59
+
60
+ function getEnvTtyOverride() {
61
+ const override = normalizeTty(process.env.UFOO_TTY_OVERRIDE || "");
62
+ return override;
63
+ }
64
+
65
+ function detectTtyOnce() {
66
+ try {
67
+ const res = spawnSync("tty", {
68
+ stdio: [0, "pipe", "ignore"],
69
+ encoding: "utf8",
70
+ });
71
+ if (res && res.status === 0) {
72
+ const tty = normalizeTty(res.stdout || "");
73
+ if (tty) return tty;
74
+ }
75
+ } catch {
76
+ // ignore
77
+ }
78
+ return "";
79
+ }
80
+
81
+ async function detectTtyWithRetry(retries = 3, delayMs = 50) {
82
+ for (let i = 0; i < retries; i += 1) {
83
+ const tty = detectTtyOnce();
84
+ if (tty) return tty;
85
+ // eslint-disable-next-line no-await-in-loop
86
+ await new Promise((r) => setTimeout(r, delayMs));
87
+ }
88
+ return "";
89
+ }
90
+
91
+ /**
92
+ * 查找当前 TTY/tmux pane 对应的旧 session
93
+ * 用于在同一终端重启时自动恢复之前的会话
94
+ *
95
+ * 匹配规则:
96
+ * - tmux 模式:优先匹配 tmux_pane(每个 pane 有唯一 ID 如 %0, %1)
97
+ * - terminal 模式:匹配 tty(如 /dev/ttys001)
98
+ */
99
+ function findPreviousSession(cwd, agentType, tty, tmuxPane) {
100
+ if (!tty && !tmuxPane) return null;
101
+
102
+ try {
103
+ const agentsFile = getUfooPaths(cwd).agentsFile;
104
+ if (!fs.existsSync(agentsFile)) return null;
105
+
106
+ const data = JSON.parse(fs.readFileSync(agentsFile, "utf8"));
107
+ const agents = data.agents || {};
108
+
109
+ // 查找匹配的旧 session
110
+ for (const [id, meta] of Object.entries(agents)) {
111
+ if (!meta) continue;
112
+
113
+ // 必须是同类型 agent
114
+ if (meta.agent_type !== agentType) continue;
115
+
116
+ // tmux 模式:必须匹配 tmux_pane(更精确)
117
+ if (tmuxPane) {
118
+ if (meta.tmux_pane !== tmuxPane) continue;
119
+ } else if (tty) {
120
+ // terminal 模式:匹配 tty
121
+ if (meta.tty !== tty) continue;
122
+ } else {
123
+ continue;
124
+ }
125
+
126
+ // 检查旧进程是否已经退出
127
+ if (meta.pid && isAgentPidAlive(meta.pid)) {
128
+ // 旧进程还在运行,不能复用
129
+ continue;
130
+ }
131
+
132
+ // 找到了可以复用的旧 session
133
+ const parts = id.split(":");
134
+ if (parts.length !== 2) continue;
135
+
136
+ return {
137
+ sessionId: parts[1],
138
+ subscriberId: id,
139
+ nickname: meta.nickname || "",
140
+ providerSessionId: meta.provider_session_id || "",
141
+ };
142
+ }
143
+ } catch {
144
+ // ignore errors
145
+ }
146
+
147
+ return null;
148
+ }
149
+
150
+ function resolveLaunchMode() {
151
+ const explicit = process.env.UFOO_LAUNCH_MODE || "";
152
+ if (explicit) return explicit;
153
+ if (process.env.TMUX_PANE) return "tmux";
154
+ return "terminal";
155
+ }
156
+
157
+ function shouldShowLaunchBanner(agentType = "") {
158
+ if (process.env.UFOO_SUPPRESS_LAUNCHER_BANNER === "1") return false;
159
+ void agentType;
160
+ return true;
161
+ }
162
+
163
+ /**
164
+ * Agent 启动器
165
+ * 统一处理 agent 启动流程:初始化、daemon 注册、banner、命令执行
166
+ */
167
+ class AgentLauncher {
168
+ constructor(agentType, command) {
169
+ this.agentType = agentType;
170
+ this.command = command;
171
+ this.cwd = process.cwd();
172
+ }
173
+
174
+ /**
175
+ * 确保 .ufoo 目录已初始化
176
+ */
177
+ async ensureInit() {
178
+ const paths = getUfooPaths(this.cwd);
179
+ const busDir = paths.busDir;
180
+
181
+ if (!fs.existsSync(busDir)) {
182
+ // 调用 ufoo init
183
+ spawnSync("ufoo", ["init", "--modules", "context,bus"], {
184
+ cwd: this.cwd,
185
+ stdio: "ignore",
186
+ });
187
+ }
188
+
189
+ // 检查 AGENTS.md 是否有 ufoo template
190
+ const agentsFile = path.join(this.cwd, "AGENTS.md");
191
+ if (fs.existsSync(agentsFile)) {
192
+ const content = fs.readFileSync(agentsFile, "utf8");
193
+ if (!content.includes("<!-- ufoo -->")) {
194
+ spawnSync("ufoo", ["init", "--modules", "context,bus"], {
195
+ cwd: this.cwd,
196
+ stdio: "ignore",
197
+ });
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * 解析已预注册的 subscriber(daemon 启动的情况)
204
+ */
205
+ async getPreRegistered() {
206
+ const subscriberId = process.env.UFOO_SUBSCRIBER_ID || "";
207
+ if (!subscriberId) return null;
208
+ const parts = subscriberId.split(":");
209
+ if (parts.length !== 2) return null;
210
+ if (parts[0] !== this.agentType) return null;
211
+ try {
212
+ const bus = new EventBus(this.cwd);
213
+ bus.loadBusData();
214
+ const meta = bus.subscriberManager.getSubscriber(subscriberId);
215
+ if (!meta || meta.status !== "active") return null;
216
+ const pidValue = Number.parseInt(meta.pid, 10);
217
+ if (Number.isFinite(pidValue) && pidValue > 0 && !isAgentPidAlive(pidValue)) {
218
+ return null;
219
+ }
220
+ if (meta.agent_type && meta.agent_type !== this.agentType) return null;
221
+ return {
222
+ subscriberId,
223
+ sessionId: parts[1],
224
+ nickname: meta.nickname || process.env.UFOO_NICKNAME || "",
225
+ preRegistered: true,
226
+ };
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * 确保 daemon 正在运行
234
+ */
235
+ async ensureDaemon() {
236
+ const paths = getUfooPaths(this.cwd);
237
+ const pidFile = paths.ufooDaemonPid;
238
+ const sockFile = paths.ufooSock;
239
+
240
+ const existingProbe = await probeDaemonSocket(sockFile);
241
+ if (existingProbe.ok) return "running";
242
+ if (existingProbe.code === "EPERM" && fs.existsSync(pidFile)) {
243
+ // Sandbox may deny socket connect probes; keep prior behavior.
244
+ return "running";
245
+ }
246
+
247
+ // Stale daemon runtime markers can block restart with false-positive "running".
248
+ // Clean local runtime markers only when socket probe failed.
249
+ try {
250
+ if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile);
251
+ } catch {
252
+ // ignore
253
+ }
254
+ try {
255
+ if (fs.existsSync(sockFile)) fs.unlinkSync(sockFile);
256
+ } catch {
257
+ // ignore
258
+ }
259
+ try {
260
+ const lockFile = path.join(paths.runDir, "daemon.lock");
261
+ if (fs.existsSync(lockFile)) fs.unlinkSync(lockFile);
262
+ } catch {
263
+ // ignore
264
+ }
265
+
266
+ // Start daemon using correct command
267
+ spawnSync("ufoo", ["daemon", "start"], {
268
+ cwd: this.cwd,
269
+ stdio: "ignore",
270
+ detached: true,
271
+ });
272
+
273
+ // Wait for daemon socket to be ready and reachable
274
+ let lastProbeCode = "";
275
+ for (let i = 0; i < 30; i++) {
276
+ const probe = await probeDaemonSocket(sockFile);
277
+ if (probe.ok) {
278
+ return "started";
279
+ }
280
+ if (probe.code === "EPERM" && fs.existsSync(pidFile)) {
281
+ return "started";
282
+ }
283
+ lastProbeCode = probe.code || lastProbeCode;
284
+ await new Promise((resolve) => setTimeout(resolve, 100));
285
+ }
286
+
287
+ throw new Error(
288
+ `Failed to start ufoo daemon${lastProbeCode ? ` (${lastProbeCode})` : ""}`
289
+ );
290
+ }
291
+
292
+ /**
293
+ * 通过 daemon socket 注册 agent
294
+ */
295
+ async registerWithDaemon(nickname) {
296
+ const sockFile = getUfooPaths(this.cwd).ufooSock;
297
+ const client = await connectWithRetry(sockFile, 25, 200);
298
+ if (!client) {
299
+ throw new Error("Failed to connect to ufoo daemon");
300
+ }
301
+
302
+ const ttyOverride = getEnvTtyOverride();
303
+ const tty = ttyOverride || await detectTtyWithRetry();
304
+ const tmuxPane = process.env.TMUX_PANE || "";
305
+ const launchMode = resolveLaunchMode();
306
+
307
+ // 只在支持 session reuse 的模式下查找旧 session(可见终端才需要恢复)
308
+ // internal 模式由 daemon 管理,不需要自动恢复
309
+ const adapterRouter = createTerminalAdapterRouter();
310
+ const adapter = adapterRouter.getAdapter({ launchMode, agentId: "" });
311
+ const shouldReuse = adapter.capabilities.supportsSessionReuse;
312
+ const previousSession = shouldReuse
313
+ ? findPreviousSession(this.cwd, this.agentType, tty, tmuxPane)
314
+ : null;
315
+
316
+ const req = {
317
+ type: IPC_REQUEST_TYPES.REGISTER_AGENT,
318
+ agentType: this.agentType,
319
+ nickname: nickname || (previousSession?.nickname) || "",
320
+ parentPid: process.pid,
321
+ launchMode,
322
+ tmuxPane,
323
+ tty,
324
+ skipProbe: process.env.UFOO_SKIP_SESSION_PROBE === "1",
325
+ // 传递旧 session 信息用于复用(仅 terminal/tmux 模式)
326
+ reuseSession: previousSession ? {
327
+ sessionId: previousSession.sessionId,
328
+ subscriberId: previousSession.subscriberId,
329
+ providerSessionId: previousSession.providerSessionId,
330
+ } : null,
331
+ };
332
+
333
+ return new Promise((resolve, reject) => {
334
+ let buffer = "";
335
+ let settled = false;
336
+ const timeout = setTimeout(() => {
337
+ if (settled) return;
338
+ settled = true;
339
+ try {
340
+ client.destroy();
341
+ } catch {
342
+ // ignore
343
+ }
344
+ reject(new Error("register_agent timeout"));
345
+ }, 8000);
346
+
347
+ const cleanup = () => {
348
+ clearTimeout(timeout);
349
+ client.removeAllListeners();
350
+ try {
351
+ client.end();
352
+ } catch {
353
+ // ignore
354
+ }
355
+ };
356
+
357
+ client.on("error", (err) => {
358
+ if (settled) return;
359
+ settled = true;
360
+ cleanup();
361
+ reject(err);
362
+ });
363
+
364
+ client.on("data", (data) => {
365
+ buffer += data.toString("utf8");
366
+ const lines = buffer.split(/\r?\n/);
367
+ buffer = lines.pop() || "";
368
+ for (const line of lines) {
369
+ if (!line.trim()) continue;
370
+ let payload;
371
+ try {
372
+ payload = JSON.parse(line);
373
+ } catch {
374
+ continue;
375
+ }
376
+ if (payload.type === "register_ok") {
377
+ if (settled) return;
378
+ settled = true;
379
+ cleanup();
380
+ resolve({
381
+ subscriberId: payload.subscriberId,
382
+ nickname: payload.nickname || nickname || "",
383
+ sessionId: (payload.subscriberId || "").split(":")[1] || "",
384
+ preRegistered: false,
385
+ });
386
+ return;
387
+ }
388
+ if (payload.type === "error") {
389
+ if (settled) return;
390
+ settled = true;
391
+ cleanup();
392
+ reject(new Error(payload.error || "register_agent failed"));
393
+ return;
394
+ }
395
+ }
396
+ });
397
+
398
+ client.write(`${JSON.stringify(req)}\n`);
399
+ });
400
+ }
401
+
402
+ /**
403
+ * 直接spawn启动(回退逻辑)
404
+ * @private
405
+ */
406
+ _spawnDirect(args, subscriberId) {
407
+ const child = spawn(this.command, args, {
408
+ cwd: this.cwd,
409
+ stdio: "inherit",
410
+ env: process.env,
411
+ });
412
+
413
+ child.on("error", (err) => {
414
+ console.error(`[${this.command}] Failed to start:`, err.message);
415
+ process.exit(1);
416
+ });
417
+
418
+ child.on("exit", async (code, signal) => {
419
+ // 清理 bus 状态
420
+ try {
421
+ const bus = new EventBus(this.cwd);
422
+ bus.loadBusData();
423
+ await bus.subscriberManager.leave(subscriberId);
424
+ bus.saveBusData();
425
+ } catch {
426
+ // ignore cleanup errors
427
+ }
428
+
429
+ if (signal) {
430
+ console.log(`\n[${this.command}] killed by signal ${signal}`);
431
+ }
432
+ process.exit(code || 0);
433
+ });
434
+
435
+ return child;
436
+ }
437
+
438
+ /**
439
+ * 启动 agent
440
+ */
441
+ async launch(args) {
442
+ try {
443
+ // 1. 确保初始化
444
+ await this.ensureInit();
445
+
446
+ // 2. 确保 daemon 运行
447
+ const daemonStatus = await this.ensureDaemon();
448
+
449
+ // 3. 使用 daemon 注册(或复用预注册)
450
+ const preRegistered = await this.getPreRegistered();
451
+ const nickname = process.env.UFOO_NICKNAME || "";
452
+ const result = preRegistered || await this.registerWithDaemon(nickname);
453
+
454
+ const subscriberId = result.subscriberId;
455
+ const sessionId = result.sessionId || (subscriberId.split(":")[1] || "");
456
+ const finalNickname = result.nickname || nickname || "";
457
+
458
+ // 4. 更新环境变量(供子进程/后续使用)
459
+ if (subscriberId) process.env.UFOO_SUBSCRIBER_ID = subscriberId;
460
+ if (finalNickname) process.env.UFOO_NICKNAME = finalNickname;
461
+ process.env.UFOO_AGENT_TYPE = this.agentType;
462
+
463
+ // 5. 显示 banner(ucode 自带 TUI banner,这里避免重复)
464
+ if (shouldShowLaunchBanner(this.agentType)) {
465
+ showBanner({
466
+ agentType: this.agentType,
467
+ sessionId,
468
+ nickname: finalNickname,
469
+ daemonStatus,
470
+ });
471
+ }
472
+
473
+ // 6. 启动消息通知监听器(ufoo-code 改为内部 bus 轮询消费)
474
+ const shouldStartNotifier = String(this.agentType || "").trim().toLowerCase() !== "ufoo-code";
475
+ if (shouldStartNotifier) {
476
+ const notifier = new AgentNotifier(this.cwd, subscriberId);
477
+ notifier.start();
478
+ }
479
+
480
+ // 7. 启动命令(PTY wrapper或直接spawn)
481
+
482
+ // 7.1 PTY启用条件(显式开关 + 自动检测)
483
+ let shouldUsePty = false;
484
+
485
+ // 显式开关(优先级最高)
486
+ if (process.env.UFOO_DISABLE_PTY === "1") {
487
+ shouldUsePty = false; // 强制回退spawn (CI/回滚)
488
+ } else if (process.env.UFOO_FORCE_PTY === "1") {
489
+ shouldUsePty = true; // 强制使用PTY (测试/调试)
490
+ } else {
491
+ // 自动检测:Terminal模式 + 非tmux + 非internal
492
+ shouldUsePty =
493
+ process.stdin.isTTY &&
494
+ process.stdout.isTTY &&
495
+ !process.env.TMUX && // tmux已有PTY,避免套嵌
496
+ !process.env.UFOO_INTERNAL_AGENT; // internal有专用runner(当前阶段)
497
+ }
498
+
499
+ // 7.2 使用PTY wrapper或回退到spawn
500
+ if (shouldUsePty) {
501
+ // 使用PTY wrapper
502
+ try {
503
+ const wrapper = new PtyWrapper(this.command, args, {
504
+ cwd: this.cwd,
505
+ env: process.env,
506
+ // 未来扩展:ioAdapter: new TerminalIOAdapter()
507
+ });
508
+
509
+ // 启用日志记录(JSONL)
510
+ const logFile = path.join(
511
+ getUfooPaths(this.cwd).runDir,
512
+ `${this.agentType}-${sessionId}-io.jsonl`
513
+ );
514
+ wrapper.enableLogging(logFile);
515
+
516
+ // 启用Ready检测(监控agent初始化状态)
517
+ const readyDetector = new ReadyDetector(this.agentType);
518
+ wrapper.enableMonitoring((data) => {
519
+ readyDetector.processOutput(data);
520
+ });
521
+
522
+ // 当检测到agent ready时,通知daemon可以提前inject probe
523
+ const daemonSockPath = getUfooPaths(this.cwd).ufooSock;
524
+ readyDetector.onReady(async () => {
525
+ const startTime = Date.now();
526
+ try {
527
+ const daemonSock = await connectWithRetry(daemonSockPath, 3, 100);
528
+ if (daemonSock) {
529
+ daemonSock.write(`${JSON.stringify({
530
+ type: IPC_REQUEST_TYPES.AGENT_READY,
531
+ subscriberId,
532
+ })}\n`);
533
+ daemonSock.end();
534
+
535
+ const notifyTime = Date.now() - startTime;
536
+ if (process.env.UFOO_DEBUG) {
537
+ console.error(`[ready] notified daemon in ${notifyTime}ms`);
538
+ }
539
+ } else {
540
+ if (process.env.UFOO_DEBUG) {
541
+ console.error(`[ready] failed to connect to daemon after retries, will use fallback delay`);
542
+ }
543
+ }
544
+ } catch (err) {
545
+ // 忽略通知失败(probe会通过fallback延迟执行)
546
+ if (process.env.UFOO_DEBUG) {
547
+ console.error(`[ready] daemon notification error: ${err.message}, will use fallback delay`);
548
+ }
549
+ }
550
+ });
551
+
552
+ // Fallback:如果10秒后还没检测到ready,强制标记为ready
553
+ const forceReadyTimer = setTimeout(() => {
554
+ readyDetector.forceReady();
555
+ }, 10000);
556
+
557
+ // 设置退出回调(复用清理逻辑)
558
+ wrapper.onExit = async ({ exitCode, signal }) => {
559
+ // 清理forceReady timer
560
+ clearTimeout(forceReadyTimer);
561
+
562
+ // 清理 bus 状态
563
+ try {
564
+ const bus = new EventBus(this.cwd);
565
+ bus.loadBusData();
566
+ await bus.subscriberManager.leave(subscriberId);
567
+ bus.saveBusData();
568
+ } catch {
569
+ // ignore cleanup errors
570
+ }
571
+
572
+ if (signal) {
573
+ console.log(`\n[${this.command}] killed by signal ${signal}`);
574
+ }
575
+ process.exit(exitCode || 0);
576
+ };
577
+
578
+ // 启动PTY
579
+ wrapper.spawn();
580
+ wrapper.attachStreams(process.stdin, process.stdout, process.stderr);
581
+
582
+ // 启动inject监听socket(用于外部注入命令到PTY)
583
+ const injectSockPath = path.join(
584
+ getUfooPaths(this.cwd).busQueuesDir,
585
+ subscriberId.replace(/:/g, "_"),
586
+ "inject.sock"
587
+ );
588
+ // 确保目录存在
589
+ const injectSockDir = path.dirname(injectSockPath);
590
+ if (!fs.existsSync(injectSockDir)) {
591
+ fs.mkdirSync(injectSockDir, { recursive: true });
592
+ }
593
+ // 清理旧socket
594
+ if (fs.existsSync(injectSockPath)) {
595
+ fs.unlinkSync(injectSockPath);
596
+ }
597
+
598
+ // Output subscribers for TTY view streaming
599
+ const outputSubscribers = new Set();
600
+
601
+ // In-memory ring buffer of recent PTY output for replay on subscribe
602
+ const OUTPUT_BUFFER_MAX = 256 * 1024; // 256KB
603
+ let outputRingBuffer = "";
604
+
605
+ // Chain monitor callback to forward output to subscribers
606
+ const originalMonitor = wrapper.monitor;
607
+ wrapper.monitor = {
608
+ onOutput: (data) => {
609
+ // Call original monitor (ReadyDetector)
610
+ if (originalMonitor && originalMonitor.onOutput) {
611
+ originalMonitor.onOutput(data);
612
+ }
613
+ // Accumulate in ring buffer
614
+ const text = Buffer.from(data).toString("utf8");
615
+ outputRingBuffer += text;
616
+ if (outputRingBuffer.length > OUTPUT_BUFFER_MAX) {
617
+ outputRingBuffer = outputRingBuffer.slice(-OUTPUT_BUFFER_MAX);
618
+ }
619
+ // Forward to all output subscribers
620
+ if (outputSubscribers.size > 0) {
621
+ const msg = JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.OUTPUT, data: text, encoding: "utf8" }) + "\n";
622
+ for (const sub of outputSubscribers) {
623
+ try {
624
+ sub.write(msg);
625
+ } catch {
626
+ outputSubscribers.delete(sub);
627
+ }
628
+ }
629
+ }
630
+ },
631
+ };
632
+
633
+ const injectServer = net.createServer((client) => {
634
+ let buffer = "";
635
+ client.on("data", (data) => {
636
+ buffer += data.toString("utf8");
637
+ const lines = buffer.split("\n");
638
+ buffer = lines.pop() || "";
639
+
640
+ for (const line of lines) {
641
+ if (!line.trim()) continue;
642
+ try {
643
+ const req = JSON.parse(line);
644
+ if (req.type === "inject" && req.command) {
645
+ const normalizedAgentType = String(this.agentType || "").trim().toLowerCase();
646
+ if (normalizedAgentType === "ufoo-code" || normalizedAgentType === "ucode" || normalizedAgentType === "ufoo") {
647
+ client.write(JSON.stringify({ ok: false, error: "inject disabled for ufoo-code (internal bus loop)" }) + "\n");
648
+ continue;
649
+ }
650
+ // 注入命令到PTY(带延迟确保输入完成)
651
+ wrapper.write(req.command);
652
+ setTimeout(() => {
653
+ wrapper.write("\x1b");
654
+ setTimeout(() => {
655
+ wrapper.write("\r");
656
+ }, 100);
657
+ }, 200);
658
+ client.write(JSON.stringify({ ok: true }) + "\n");
659
+ if (wrapper.logger) {
660
+ const logEntry = {
661
+ ts: Date.now(),
662
+ dir: "in",
663
+ data: { text: req.command, encoding: "utf8", size: req.command.length },
664
+ source: "inject",
665
+ };
666
+ wrapper.logger.write(JSON.stringify(logEntry) + "\n");
667
+ }
668
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RAW && req.data) {
669
+ // Raw PTY write (no Enter appended) - for TTY view passthrough
670
+ wrapper.write(req.data);
671
+ client.write(JSON.stringify({ ok: true }) + "\n");
672
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RESIZE && req.cols && req.rows) {
673
+ // Resize PTY - for TTY view viewport adjustment
674
+ if (wrapper.pty && !wrapper.pty._closed) {
675
+ wrapper.pty.resize(req.cols, req.rows);
676
+ }
677
+ client.write(JSON.stringify({ ok: true }) + "\n");
678
+ } else if (req.type === PTY_SOCKET_MESSAGE_TYPES.SUBSCRIBE) {
679
+ // Subscribe to PTY output stream for TTY view
680
+ outputSubscribers.add(client);
681
+ client.write(JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.SUBSCRIBED, ok: true }) + "\n");
682
+ // Replay from in-memory ring buffer
683
+ if (outputRingBuffer.length > 0) {
684
+ client.write(JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.REPLAY, data: outputRingBuffer, encoding: "utf8" }) + "\n");
685
+ }
686
+ } else {
687
+ client.write(JSON.stringify({ ok: false, error: "invalid request" }) + "\n");
688
+ }
689
+ } catch (err) {
690
+ client.write(JSON.stringify({ ok: false, error: err.message }) + "\n");
691
+ }
692
+ }
693
+ });
694
+ client.on("error", () => {
695
+ outputSubscribers.delete(client);
696
+ });
697
+ client.on("close", () => {
698
+ outputSubscribers.delete(client);
699
+ });
700
+ });
701
+
702
+ injectServer.listen(injectSockPath, () => {
703
+ if (process.env.UFOO_DEBUG) {
704
+ console.error(`[inject] listening on ${injectSockPath}`);
705
+ }
706
+ });
707
+
708
+ injectServer.on("error", (err) => {
709
+ if (process.env.UFOO_DEBUG) {
710
+ console.error(`[inject] server error: ${err.message}`);
711
+ }
712
+ });
713
+
714
+ // 记录inject socket路径到cleanup
715
+ const cleanupInjectServer = () => {
716
+ // Close all output subscribers
717
+ for (const sub of outputSubscribers) {
718
+ try { sub.destroy(); } catch { /* ignore */ }
719
+ }
720
+ outputSubscribers.clear();
721
+ try {
722
+ injectServer.close();
723
+ if (fs.existsSync(injectSockPath)) {
724
+ fs.unlinkSync(injectSockPath);
725
+ }
726
+ } catch {
727
+ // ignore
728
+ }
729
+ };
730
+
731
+ // 更新onExit以清理inject server
732
+ const originalOnExit = wrapper.onExit;
733
+ wrapper.onExit = async (exitInfo) => {
734
+ cleanupInjectServer();
735
+ if (originalOnExit) {
736
+ await originalOnExit(exitInfo);
737
+ }
738
+ };
739
+ } catch (err) {
740
+ console.error(`[PTY] Failed to start, falling back to spawn:`, err.message);
741
+ this._spawnDirect(args, subscriberId);
742
+ }
743
+ } else {
744
+ // 非PTY环境:tmux、internal、管道、显式禁用等
745
+ this._spawnDirect(args, subscriberId);
746
+ }
747
+ } catch (err) {
748
+ console.error(`[${this.command}] Error:`, err.message);
749
+ process.exit(1);
750
+ }
751
+ }
752
+ }
753
+
754
+ module.exports = AgentLauncher;