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.
- package/README.md +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +46 -42
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +1011 -1392
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +1162 -96
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- package/scripts/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- 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;
|