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.
- package/README.md +247 -23
- package/SKILLS/ufoo/SKILL.md +17 -2
- package/SKILLS/uinit/SKILL.md +8 -3
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +4 -0
- package/modules/AGENTS.template.md +14 -4
- package/modules/bus/README.md +8 -5
- package/modules/bus/SKILLS/ubus/SKILL.md +5 -4
- package/modules/context/SKILLS/uctx/SKILL.md +3 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +12 -3
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +20 -49
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +524 -31
- package/src/agent/internalRunner.js +76 -9
- package/src/agent/launcher.js +97 -45
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +144 -4
- package/src/agent/ptyRunner.js +480 -10
- package/src/agent/ptyWrapper.js +28 -3
- package/src/agent/readyDetector.js +16 -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 +168 -28
- 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 +27 -11
- package/src/bus/daemon.js +133 -5
- package/src/bus/index.js +137 -80
- package/src/bus/inject.js +47 -17
- package/src/bus/message.js +145 -17
- package/src/bus/nickname.js +3 -1
- package/src/bus/queue.js +6 -1
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +20 -4
- package/src/bus/utils.js +9 -3
- 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 +157 -0
- package/src/chat/index.js +938 -2910
- 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 +133 -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 +741 -238
- 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 +1587 -0
- package/src/config.js +50 -2
- package/src/context/decisions.js +12 -2
- package/src/context/index.js +18 -1
- 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 +662 -489
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +417 -179
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +32 -17
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +2 -5
- package/src/daemon/status.js +24 -1
- package/src/init/index.js +68 -14
- 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/status/index.js +50 -17
- 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/ufoo/agentsStore.js +69 -3
- package/src/utils/banner.js +5 -2
- package/scripts/.archived/bash-to-js-migration/README.md +0 -46
- package/scripts/.archived/bash-to-js-migration/banner.sh +0 -89
- package/scripts/.archived/bash-to-js-migration/bus-alert.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-autotrigger.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus-daemon.sh +0 -231
- package/scripts/.archived/bash-to-js-migration/bus-inject.sh +0 -176
- package/scripts/.archived/bash-to-js-migration/bus-listen.sh +0 -6
- package/scripts/.archived/bash-to-js-migration/bus.sh +0 -986
- package/scripts/.archived/bash-to-js-migration/context-decisions.sh +0 -167
- package/scripts/.archived/bash-to-js-migration/context-doctor.sh +0 -72
- package/scripts/.archived/bash-to-js-migration/context-lint.sh +0 -110
- package/scripts/.archived/bash-to-js-migration/doctor.sh +0 -22
- package/scripts/.archived/bash-to-js-migration/init.sh +0 -247
- package/scripts/.archived/bash-to-js-migration/skills.sh +0 -113
- package/scripts/.archived/bash-to-js-migration/status.sh +0 -125
- package/scripts/banner.sh +0 -2
- package/src/bus/API_DESIGN.md +0 -204
|
@@ -2,6 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
|
+
const EventBus = require("../bus");
|
|
5
6
|
const { runCliAgent } = require("./cliRunner");
|
|
6
7
|
const { normalizeCliOutput } = require("./normalizeOutput");
|
|
7
8
|
|
|
@@ -37,6 +38,30 @@ function safeSubscriber(subscriber) {
|
|
|
37
38
|
return subscriber.replace(/:/g, "_");
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function createBusSender(projectRoot, subscriber) {
|
|
42
|
+
const eventBus = new EventBus(projectRoot);
|
|
43
|
+
let sendQueue = Promise.resolve();
|
|
44
|
+
|
|
45
|
+
function enqueue(target, message) {
|
|
46
|
+
if (!target || !message) return;
|
|
47
|
+
sendQueue = sendQueue
|
|
48
|
+
.then(() => eventBus.send(target, message, subscriber))
|
|
49
|
+
.catch(() => {
|
|
50
|
+
// ignore per-message bus send errors to keep runner loop alive
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function flush() {
|
|
55
|
+
try {
|
|
56
|
+
await sendQueue;
|
|
57
|
+
} catch {
|
|
58
|
+
// ignore flush errors
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { enqueue, flush };
|
|
63
|
+
}
|
|
64
|
+
|
|
40
65
|
function drainQueue(queueFile) {
|
|
41
66
|
if (!fs.existsSync(queueFile)) return [];
|
|
42
67
|
const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
|
|
@@ -70,11 +95,20 @@ function drainQueue(queueFile) {
|
|
|
70
95
|
return content.split(/\r?\n/).filter(Boolean);
|
|
71
96
|
}
|
|
72
97
|
|
|
73
|
-
async function handleEvent(projectRoot, agentType, provider, model, subscriber, nickname, evt, cliSessionState) {
|
|
98
|
+
async function handleEvent(projectRoot, agentType, provider, model, subscriber, nickname, evt, cliSessionState, busSender) {
|
|
74
99
|
if (!evt || !evt.data || !evt.data.message) return;
|
|
75
100
|
const prompt = evt.data.message;
|
|
76
101
|
const publisher = evt.publisher || "unknown";
|
|
77
102
|
const sandbox = "workspace-write";
|
|
103
|
+
const streamState = { emitted: false, lastChar: "" };
|
|
104
|
+
|
|
105
|
+
const emitStreamDelta = (delta) => {
|
|
106
|
+
const text = String(delta || "");
|
|
107
|
+
if (!text) return;
|
|
108
|
+
streamState.emitted = true;
|
|
109
|
+
streamState.lastChar = text.slice(-1);
|
|
110
|
+
busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: text }));
|
|
111
|
+
};
|
|
78
112
|
|
|
79
113
|
let res = await runCliAgent({
|
|
80
114
|
provider,
|
|
@@ -83,6 +117,7 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
83
117
|
sessionId: cliSessionState.cliSessionId,
|
|
84
118
|
sandbox,
|
|
85
119
|
cwd: projectRoot,
|
|
120
|
+
onStreamDelta: emitStreamDelta,
|
|
86
121
|
});
|
|
87
122
|
|
|
88
123
|
// Handle session errors with immediate retry (only for claude)
|
|
@@ -100,6 +135,7 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
100
135
|
sessionId: null, // Let runCliAgent generate new session
|
|
101
136
|
sandbox,
|
|
102
137
|
cwd: projectRoot,
|
|
138
|
+
onStreamDelta: emitStreamDelta,
|
|
103
139
|
});
|
|
104
140
|
}
|
|
105
141
|
}
|
|
@@ -117,13 +153,25 @@ async function handleEvent(projectRoot, agentType, provider, model, subscriber,
|
|
|
117
153
|
reply = `[internal:${agentType}] error: ${res.error || "unknown error"}`;
|
|
118
154
|
}
|
|
119
155
|
|
|
156
|
+
if (streamState.emitted) {
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
if (streamState.lastChar !== "\n") {
|
|
159
|
+
busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: "\n" }));
|
|
160
|
+
}
|
|
161
|
+
busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: reply }));
|
|
162
|
+
}
|
|
163
|
+
busSender.enqueue(
|
|
164
|
+
publisher,
|
|
165
|
+
JSON.stringify({ stream: true, done: true, reason: res.ok ? "complete" : "error" })
|
|
166
|
+
);
|
|
167
|
+
await busSender.flush();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
120
171
|
if (!reply) return;
|
|
121
172
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
env: { ...process.env, AI_BUS_PUBLISHER: subscriber },
|
|
125
|
-
stdio: "ignore",
|
|
126
|
-
});
|
|
173
|
+
busSender.enqueue(publisher, reply);
|
|
174
|
+
await busSender.flush();
|
|
127
175
|
}
|
|
128
176
|
|
|
129
177
|
async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
@@ -133,8 +181,13 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
133
181
|
|
|
134
182
|
const queueDir = path.join(getUfooPaths(projectRoot).busQueuesDir, safeSubscriber(subscriber));
|
|
135
183
|
const queueFile = path.join(queueDir, "pending.jsonl");
|
|
136
|
-
const
|
|
184
|
+
const normalizedAgentType = String(agentType || "").trim().toLowerCase();
|
|
185
|
+
if (normalizedAgentType === "ufoo" || normalizedAgentType === "ucode" || normalizedAgentType === "ufoo-code") {
|
|
186
|
+
throw new Error("ufoo core is not supported by headless internal runner; use internal-pty");
|
|
187
|
+
}
|
|
188
|
+
const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
|
|
137
189
|
const model = process.env.UFOO_AGENT_MODEL || "";
|
|
190
|
+
const busSender = createBusSender(projectRoot, subscriber);
|
|
138
191
|
|
|
139
192
|
// Session state management for CLI continuity
|
|
140
193
|
// Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
|
|
@@ -206,7 +259,17 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
206
259
|
|
|
207
260
|
for (const evt of events) {
|
|
208
261
|
// eslint-disable-next-line no-await-in-loop
|
|
209
|
-
await handleEvent(
|
|
262
|
+
await handleEvent(
|
|
263
|
+
projectRoot,
|
|
264
|
+
parsedAgentType,
|
|
265
|
+
provider,
|
|
266
|
+
model,
|
|
267
|
+
subscriber,
|
|
268
|
+
nickname,
|
|
269
|
+
evt,
|
|
270
|
+
cliSessionState,
|
|
271
|
+
busSender
|
|
272
|
+
);
|
|
210
273
|
}
|
|
211
274
|
|
|
212
275
|
// Persist CLI session state after processing (only if changed and for claude)
|
|
@@ -236,4 +299,8 @@ async function runInternalRunner({ projectRoot, agentType = "codex" }) {
|
|
|
236
299
|
}
|
|
237
300
|
}
|
|
238
301
|
|
|
239
|
-
module.exports = {
|
|
302
|
+
module.exports = {
|
|
303
|
+
runInternalRunner,
|
|
304
|
+
createBusSender,
|
|
305
|
+
handleEvent,
|
|
306
|
+
};
|
package/src/agent/launcher.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
|
|
2
|
+
const { PTY_SOCKET_MESSAGE_TYPES } = require("../shared/ptySocketContract");
|
|
1
3
|
const { spawn, spawnSync } = require("child_process");
|
|
2
4
|
const fs = require("fs");
|
|
3
5
|
const net = require("net");
|
|
@@ -7,6 +9,7 @@ const { isAgentPidAlive } = require("../bus/utils");
|
|
|
7
9
|
const { showBanner } = require("../utils/banner");
|
|
8
10
|
const AgentNotifier = require("./notifier");
|
|
9
11
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
12
|
+
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
10
13
|
const PtyWrapper = require("./ptyWrapper");
|
|
11
14
|
const ReadyDetector = require("./readyDetector");
|
|
12
15
|
|
|
@@ -31,6 +34,21 @@ async function connectWithRetry(sockPath, retries, delayMs) {
|
|
|
31
34
|
return null;
|
|
32
35
|
}
|
|
33
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
|
+
|
|
34
52
|
function normalizeTty(ttyPath) {
|
|
35
53
|
if (!ttyPath) return "";
|
|
36
54
|
const trimmed = String(ttyPath).trim();
|
|
@@ -136,6 +154,12 @@ function resolveLaunchMode() {
|
|
|
136
154
|
return "terminal";
|
|
137
155
|
}
|
|
138
156
|
|
|
157
|
+
function shouldShowLaunchBanner(agentType = "") {
|
|
158
|
+
if (process.env.UFOO_SUPPRESS_LAUNCHER_BANNER === "1") return false;
|
|
159
|
+
void agentType;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
139
163
|
/**
|
|
140
164
|
* Agent 启动器
|
|
141
165
|
* 统一处理 agent 启动流程:初始化、daemon 注册、banner、命令执行
|
|
@@ -209,19 +233,34 @@ class AgentLauncher {
|
|
|
209
233
|
* 确保 daemon 正在运行
|
|
210
234
|
*/
|
|
211
235
|
async ensureDaemon() {
|
|
212
|
-
const
|
|
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
|
+
}
|
|
213
246
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
225
264
|
}
|
|
226
265
|
|
|
227
266
|
// Start daemon using correct command
|
|
@@ -231,23 +270,23 @@ class AgentLauncher {
|
|
|
231
270
|
detached: true,
|
|
232
271
|
});
|
|
233
272
|
|
|
234
|
-
// Wait for daemon socket to be ready
|
|
235
|
-
|
|
273
|
+
// Wait for daemon socket to be ready and reachable
|
|
274
|
+
let lastProbeCode = "";
|
|
236
275
|
for (let i = 0; i < 30; i++) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
} catch {
|
|
244
|
-
// Continue waiting
|
|
245
|
-
}
|
|
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";
|
|
246
282
|
}
|
|
283
|
+
lastProbeCode = probe.code || lastProbeCode;
|
|
247
284
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
248
285
|
}
|
|
249
286
|
|
|
250
|
-
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Failed to start ufoo daemon${lastProbeCode ? ` (${lastProbeCode})` : ""}`
|
|
289
|
+
);
|
|
251
290
|
}
|
|
252
291
|
|
|
253
292
|
/**
|
|
@@ -265,15 +304,17 @@ class AgentLauncher {
|
|
|
265
304
|
const tmuxPane = process.env.TMUX_PANE || "";
|
|
266
305
|
const launchMode = resolveLaunchMode();
|
|
267
306
|
|
|
268
|
-
//
|
|
307
|
+
// 只在支持 session reuse 的模式下查找旧 session(可见终端才需要恢复)
|
|
269
308
|
// internal 模式由 daemon 管理,不需要自动恢复
|
|
270
|
-
const
|
|
309
|
+
const adapterRouter = createTerminalAdapterRouter();
|
|
310
|
+
const adapter = adapterRouter.getAdapter({ launchMode, agentId: "" });
|
|
311
|
+
const shouldReuse = adapter.capabilities.supportsSessionReuse;
|
|
271
312
|
const previousSession = shouldReuse
|
|
272
313
|
? findPreviousSession(this.cwd, this.agentType, tty, tmuxPane)
|
|
273
314
|
: null;
|
|
274
315
|
|
|
275
316
|
const req = {
|
|
276
|
-
type:
|
|
317
|
+
type: IPC_REQUEST_TYPES.REGISTER_AGENT,
|
|
277
318
|
agentType: this.agentType,
|
|
278
319
|
nickname: nickname || (previousSession?.nickname) || "",
|
|
279
320
|
parentPid: process.pid,
|
|
@@ -417,18 +458,24 @@ class AgentLauncher {
|
|
|
417
458
|
// 4. 更新环境变量(供子进程/后续使用)
|
|
418
459
|
if (subscriberId) process.env.UFOO_SUBSCRIBER_ID = subscriberId;
|
|
419
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
|
+
}
|
|
420
472
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
// 6. 启动消息通知监听器
|
|
430
|
-
const notifier = new AgentNotifier(this.cwd, subscriberId);
|
|
431
|
-
notifier.start();
|
|
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
|
+
}
|
|
432
479
|
|
|
433
480
|
// 7. 启动命令(PTY wrapper或直接spawn)
|
|
434
481
|
|
|
@@ -480,7 +527,7 @@ class AgentLauncher {
|
|
|
480
527
|
const daemonSock = await connectWithRetry(daemonSockPath, 3, 100);
|
|
481
528
|
if (daemonSock) {
|
|
482
529
|
daemonSock.write(`${JSON.stringify({
|
|
483
|
-
type:
|
|
530
|
+
type: IPC_REQUEST_TYPES.AGENT_READY,
|
|
484
531
|
subscriberId,
|
|
485
532
|
})}\n`);
|
|
486
533
|
daemonSock.end();
|
|
@@ -571,7 +618,7 @@ class AgentLauncher {
|
|
|
571
618
|
}
|
|
572
619
|
// Forward to all output subscribers
|
|
573
620
|
if (outputSubscribers.size > 0) {
|
|
574
|
-
const msg = JSON.stringify({ type:
|
|
621
|
+
const msg = JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.OUTPUT, data: text, encoding: "utf8" }) + "\n";
|
|
575
622
|
for (const sub of outputSubscribers) {
|
|
576
623
|
try {
|
|
577
624
|
sub.write(msg);
|
|
@@ -595,6 +642,11 @@ class AgentLauncher {
|
|
|
595
642
|
try {
|
|
596
643
|
const req = JSON.parse(line);
|
|
597
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
|
+
}
|
|
598
650
|
// 注入命令到PTY(带延迟确保输入完成)
|
|
599
651
|
wrapper.write(req.command);
|
|
600
652
|
setTimeout(() => {
|
|
@@ -613,23 +665,23 @@ class AgentLauncher {
|
|
|
613
665
|
};
|
|
614
666
|
wrapper.logger.write(JSON.stringify(logEntry) + "\n");
|
|
615
667
|
}
|
|
616
|
-
} else if (req.type ===
|
|
668
|
+
} else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RAW && req.data) {
|
|
617
669
|
// Raw PTY write (no Enter appended) - for TTY view passthrough
|
|
618
670
|
wrapper.write(req.data);
|
|
619
671
|
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
620
|
-
} else if (req.type ===
|
|
672
|
+
} else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RESIZE && req.cols && req.rows) {
|
|
621
673
|
// Resize PTY - for TTY view viewport adjustment
|
|
622
674
|
if (wrapper.pty && !wrapper.pty._closed) {
|
|
623
675
|
wrapper.pty.resize(req.cols, req.rows);
|
|
624
676
|
}
|
|
625
677
|
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
626
|
-
} else if (req.type ===
|
|
678
|
+
} else if (req.type === PTY_SOCKET_MESSAGE_TYPES.SUBSCRIBE) {
|
|
627
679
|
// Subscribe to PTY output stream for TTY view
|
|
628
680
|
outputSubscribers.add(client);
|
|
629
|
-
client.write(JSON.stringify({ type:
|
|
681
|
+
client.write(JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.SUBSCRIBED, ok: true }) + "\n");
|
|
630
682
|
// Replay from in-memory ring buffer
|
|
631
683
|
if (outputRingBuffer.length > 0) {
|
|
632
|
-
client.write(JSON.stringify({ type:
|
|
684
|
+
client.write(JSON.stringify({ type: PTY_SOCKET_MESSAGE_TYPES.REPLAY, data: outputRingBuffer, encoding: "utf8" }) + "\n");
|
|
633
685
|
}
|
|
634
686
|
} else {
|
|
635
687
|
client.write(JSON.stringify({ ok: false, error: "invalid request" }) + "\n");
|
|
@@ -3,7 +3,7 @@ function extractTextFromObject(obj) {
|
|
|
3
3
|
if (obj.structured_output && typeof obj.structured_output === "object") {
|
|
4
4
|
return JSON.stringify(obj.structured_output);
|
|
5
5
|
}
|
|
6
|
-
const candidates = ["output", "text", "message", "content", "output_text", "result"];
|
|
6
|
+
const candidates = ["reply", "output", "text", "message", "content", "output_text", "result"];
|
|
7
7
|
for (const key of candidates) {
|
|
8
8
|
const val = obj[key];
|
|
9
9
|
if (typeof val === "string") return val;
|
package/src/agent/notifier.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const EventBus = require("../bus");
|
|
3
4
|
const Injector = require("../bus/inject");
|
|
4
5
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
5
6
|
const { shakeTerminalByTty } = require("../bus/shake");
|
|
@@ -20,6 +21,7 @@ class AgentNotifier {
|
|
|
20
21
|
this.stopped = false;
|
|
21
22
|
this.autoTrigger = process.env.UFOO_AUTO_TRIGGER !== "0"; // 默认启用自动触发
|
|
22
23
|
this.lastNickname = "";
|
|
24
|
+
this.lastUbusWakeCount = -1;
|
|
23
25
|
|
|
24
26
|
// 计算队列文件路径
|
|
25
27
|
const safeSub = subscriber.replace(/:/g, "_");
|
|
@@ -34,6 +36,11 @@ class AgentNotifier {
|
|
|
34
36
|
// 初始化 injector
|
|
35
37
|
const busDir = paths.busDir;
|
|
36
38
|
this.injector = new Injector(busDir, paths.agentsFile);
|
|
39
|
+
this.eventBus = new EventBus(projectRoot);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isUfooCodeSubscriber() {
|
|
43
|
+
return String(this.subscriber || "").startsWith("ufoo-code:");
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/**
|
|
@@ -104,6 +111,116 @@ class AgentNotifier {
|
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
|
|
114
|
+
drainPending() {
|
|
115
|
+
if (!fs.existsSync(this.queueFile)) return [];
|
|
116
|
+
const processingFile = `${this.queueFile}.processing.${process.pid}.${Date.now()}`;
|
|
117
|
+
let content = "";
|
|
118
|
+
let readOk = false;
|
|
119
|
+
try {
|
|
120
|
+
fs.renameSync(this.queueFile, processingFile);
|
|
121
|
+
content = fs.readFileSync(processingFile, "utf8");
|
|
122
|
+
readOk = true;
|
|
123
|
+
} catch {
|
|
124
|
+
try {
|
|
125
|
+
if (fs.existsSync(processingFile)) {
|
|
126
|
+
fs.renameSync(processingFile, this.queueFile);
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore rollback errors
|
|
130
|
+
}
|
|
131
|
+
return [];
|
|
132
|
+
} finally {
|
|
133
|
+
if (readOk) {
|
|
134
|
+
try {
|
|
135
|
+
if (fs.existsSync(processingFile)) {
|
|
136
|
+
fs.rmSync(processingFile, { force: true });
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore cleanup errors
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!content.trim()) return [];
|
|
144
|
+
return content.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(line);
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}).filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
normalizePublisher(publisher) {
|
|
154
|
+
if (!publisher) return "";
|
|
155
|
+
if (typeof publisher === "string") return publisher;
|
|
156
|
+
if (typeof publisher === "object") {
|
|
157
|
+
return publisher.subscriber || publisher.nickname || "";
|
|
158
|
+
}
|
|
159
|
+
return String(publisher);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async emitDelivery(evt, status, errorMessage = "") {
|
|
163
|
+
const publisher = this.normalizePublisher(evt.publisher);
|
|
164
|
+
if (!publisher) return;
|
|
165
|
+
const data = {
|
|
166
|
+
target: this.subscriber,
|
|
167
|
+
seq: evt.seq,
|
|
168
|
+
status,
|
|
169
|
+
};
|
|
170
|
+
if (errorMessage) data.error = errorMessage;
|
|
171
|
+
// Provide a human-readable message for chat UI
|
|
172
|
+
if (status === "ok") {
|
|
173
|
+
data.message = `delivered to ${this.lastNickname || this.subscriber}`;
|
|
174
|
+
} else {
|
|
175
|
+
data.message = `delivery failed to ${this.lastNickname || this.subscriber}: ${errorMessage || "unknown error"}`;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
await this.eventBus.send(publisher, "", this.subscriber, { event: "delivery", data });
|
|
179
|
+
} catch {
|
|
180
|
+
// ignore delivery emit failures
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async deliverPending() {
|
|
185
|
+
if (this.isUfooCodeSubscriber()) {
|
|
186
|
+
// ufoo-code consumes bus queue internally; notifier must not inject text/commands.
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const events = this.drainPending();
|
|
191
|
+
if (events.length === 0) return 0;
|
|
192
|
+
const failed = [];
|
|
193
|
+
let delivered = 0;
|
|
194
|
+
for (const evt of events) {
|
|
195
|
+
if (!evt || evt.event !== "message" || !evt.data || typeof evt.data.message !== "string") {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const message = String(evt.data.message);
|
|
199
|
+
try {
|
|
200
|
+
// Inject the actual message text into the terminal/tmux agent
|
|
201
|
+
// (Bus is the source of truth; inject is the delivery adapter.)
|
|
202
|
+
// eslint-disable-next-line no-await-in-loop
|
|
203
|
+
await this.injector.inject(this.subscriber, message);
|
|
204
|
+
delivered += 1;
|
|
205
|
+
// eslint-disable-next-line no-await-in-loop
|
|
206
|
+
await this.emitDelivery(evt, "ok");
|
|
207
|
+
} catch (err) {
|
|
208
|
+
failed.push(evt);
|
|
209
|
+
// eslint-disable-next-line no-await-in-loop
|
|
210
|
+
await this.emitDelivery(evt, "error", err.message || "inject failed");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (failed.length > 0) {
|
|
214
|
+
try {
|
|
215
|
+
const content = failed.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
216
|
+
fs.appendFileSync(this.queueFile, content, "utf8");
|
|
217
|
+
} catch {
|
|
218
|
+
// ignore requeue failures
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return delivered;
|
|
222
|
+
}
|
|
223
|
+
|
|
107
224
|
/**
|
|
108
225
|
* 发送终端通知
|
|
109
226
|
* iTerm2: 使用 OSC 9 原生通知
|
|
@@ -126,7 +243,7 @@ class AgentNotifier {
|
|
|
126
243
|
if (!this.autoTrigger) return;
|
|
127
244
|
|
|
128
245
|
try {
|
|
129
|
-
await this.
|
|
246
|
+
await this.deliverPending();
|
|
130
247
|
} catch (err) {
|
|
131
248
|
// 自动触发失败时静默忽略,用户仍可手动输入
|
|
132
249
|
// console.error("[notifier] Auto-trigger failed:", err.message);
|
|
@@ -136,7 +253,7 @@ class AgentNotifier {
|
|
|
136
253
|
/**
|
|
137
254
|
* 轮询检查队列
|
|
138
255
|
*/
|
|
139
|
-
poll() {
|
|
256
|
+
async poll() {
|
|
140
257
|
if (this.stopped) return;
|
|
141
258
|
|
|
142
259
|
const currentCount = this.getMessageCount();
|
|
@@ -152,7 +269,30 @@ class AgentNotifier {
|
|
|
152
269
|
});
|
|
153
270
|
}
|
|
154
271
|
|
|
155
|
-
|
|
272
|
+
// Ensure pending delivery happens even if count doesn't change
|
|
273
|
+
if (this.autoTrigger && currentCount > 0) {
|
|
274
|
+
if (this.isUfooCodeSubscriber()) {
|
|
275
|
+
if (this.lastUbusWakeCount !== currentCount) {
|
|
276
|
+
try {
|
|
277
|
+
await this.autoTriggerInput();
|
|
278
|
+
this.lastUbusWakeCount = currentCount;
|
|
279
|
+
} catch {
|
|
280
|
+
// ignore delivery errors
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
try {
|
|
285
|
+
await this.deliverPending();
|
|
286
|
+
} catch {
|
|
287
|
+
// ignore delivery errors
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (currentCount <= 0) {
|
|
292
|
+
this.lastUbusWakeCount = -1;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.lastCount = this.getMessageCount();
|
|
156
296
|
this.refreshTitle();
|
|
157
297
|
this.updateHeartbeat();
|
|
158
298
|
}
|
|
@@ -170,7 +310,7 @@ class AgentNotifier {
|
|
|
170
310
|
|
|
171
311
|
// 启动轮询
|
|
172
312
|
this.timer = setInterval(() => {
|
|
173
|
-
this.poll();
|
|
313
|
+
this.poll().catch(() => {});
|
|
174
314
|
}, this.interval);
|
|
175
315
|
|
|
176
316
|
// 注册清理
|