u-foo 1.4.1 → 1.6.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 +21 -0
- package/README.zh-CN.md +21 -0
- package/bin/ufoo.js +15 -7
- package/modules/AGENTS.template.md +4 -102
- package/package.json +3 -2
- package/scripts/global-chat-switch-benchmark.js +406 -0
- package/src/agent/activityDetector.js +328 -0
- package/src/agent/activityStatePublisher.js +67 -0
- package/src/agent/activityStateWriter.js +40 -0
- package/src/agent/internalRunner.js +13 -0
- package/src/agent/launcher.js +47 -7
- package/src/agent/notifier.js +73 -4
- package/src/agent/ptyRunner.js +81 -34
- package/src/agent/ufooAgent.js +192 -6
- package/src/bus/message.js +1 -9
- package/src/bus/subscriber.js +2 -0
- package/src/bus/utils.js +10 -0
- package/src/chat/agentBar.js +21 -3
- package/src/chat/agentViewController.js +2 -0
- package/src/chat/chatLogController.js +28 -5
- package/src/chat/commandExecutor.js +127 -3
- package/src/chat/commands.js +8 -0
- package/src/chat/daemonConnection.js +77 -4
- package/src/chat/daemonCoordinator.js +36 -0
- package/src/chat/daemonMessageRouter.js +22 -0
- package/src/chat/daemonTransport.js +47 -5
- package/src/chat/daemonTransportDefaults.js +1 -0
- package/src/chat/dashboardKeyController.js +89 -1
- package/src/chat/dashboardView.js +312 -93
- package/src/chat/index.js +683 -41
- package/src/chat/inputHistoryController.js +33 -3
- package/src/chat/inputListenerController.js +22 -12
- package/src/chat/layout.js +12 -7
- package/src/chat/projectCloseController.js +119 -0
- package/src/chat/projectRuntimes.js +55 -0
- package/src/chat/statusLineController.js +52 -6
- package/src/chat/streamTracker.js +6 -0
- package/src/chat/transport.js +41 -5
- package/src/cli.js +167 -4
- package/src/daemon/index.js +54 -5
- package/src/daemon/ipcServer.js +6 -1
- package/src/daemon/ops.js +245 -35
- package/src/daemon/status.js +3 -1
- package/src/init/index.js +32 -3
- package/src/projects/projectId.js +29 -0
- package/src/projects/registry.js +279 -0
- package/src/ufoo/agentsStore.js +44 -0
package/src/cli.js
CHANGED
|
@@ -7,6 +7,9 @@ const { runBusCoreCommand } = require("./cli/busCoreCommands");
|
|
|
7
7
|
const { runCtxCommand } = require("./cli/ctxCoreCommands");
|
|
8
8
|
const { runOnlineCommand } = require("./cli/onlineCoreCommands");
|
|
9
9
|
const { runGroupCoreCommand } = require("./cli/groupCoreCommands");
|
|
10
|
+
const { listProjectRuntimes, getCurrentProjectRuntime } = require("./projects/registry");
|
|
11
|
+
const { canonicalProjectRoot, buildProjectId } = require("./projects/projectId");
|
|
12
|
+
const { getUfooPaths } = require("./ufoo/paths");
|
|
10
13
|
|
|
11
14
|
function getPackageRoot() {
|
|
12
15
|
return path.resolve(__dirname, "..");
|
|
@@ -161,6 +164,111 @@ function parseJsonObject(text, fallback = {}) {
|
|
|
161
164
|
return parsed;
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
function formatProjectRuntimeLine(row, index = 0) {
|
|
168
|
+
const idx = String(index + 1).padStart(2, " ");
|
|
169
|
+
const name = String(row.project_name || "-");
|
|
170
|
+
const status = String(row.status || "-");
|
|
171
|
+
const seen = row.last_seen || "-";
|
|
172
|
+
const projectRoot = String(row.project_root || "-");
|
|
173
|
+
return `${idx}. ${name} [${status}] last_seen=${seen} ${projectRoot}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function printProjectList(rows = [], write = (line) => console.log(line)) {
|
|
177
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
178
|
+
write("No projects found.");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
write("=== Projects ===");
|
|
182
|
+
rows.forEach((row, index) => {
|
|
183
|
+
write(formatProjectRuntimeLine(row, index));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildCurrentProjectFallback(projectRoot) {
|
|
188
|
+
let canonical = "";
|
|
189
|
+
let projectId = "";
|
|
190
|
+
try {
|
|
191
|
+
canonical = canonicalProjectRoot(projectRoot);
|
|
192
|
+
} catch {
|
|
193
|
+
canonical = path.resolve(projectRoot || process.cwd());
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
projectId = buildProjectId(canonical);
|
|
197
|
+
} catch {
|
|
198
|
+
projectId = "";
|
|
199
|
+
}
|
|
200
|
+
const paths = getUfooPaths(canonical);
|
|
201
|
+
return {
|
|
202
|
+
version: 1,
|
|
203
|
+
project_id: projectId || null,
|
|
204
|
+
project_root: canonical,
|
|
205
|
+
project_name: path.basename(canonical) || canonical,
|
|
206
|
+
daemon_pid: null,
|
|
207
|
+
socket_path: paths.ufooSock,
|
|
208
|
+
status: "untracked",
|
|
209
|
+
last_seen: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function printCurrentProject(runtime, write = (line) => console.log(line)) {
|
|
214
|
+
const row = runtime || {};
|
|
215
|
+
write("=== Current Project ===");
|
|
216
|
+
write(`name: ${row.project_name || "-"}`);
|
|
217
|
+
write(`status: ${row.status || "-"}`);
|
|
218
|
+
write(`path: ${row.project_root || "-"}`);
|
|
219
|
+
write(`daemon_pid: ${row.daemon_pid || "-"}`);
|
|
220
|
+
write(`socket: ${row.socket_path || "-"}`);
|
|
221
|
+
write(`last_seen: ${row.last_seen || "-"}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function projectSwitchV1Error() {
|
|
225
|
+
const err = new Error("project switch is chat-only in v1");
|
|
226
|
+
err.code = "UFOO_PROJECT_SWITCH_CHAT_ONLY";
|
|
227
|
+
err.exitCode = 2;
|
|
228
|
+
return err;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function runProjectCommand({
|
|
232
|
+
subcommand = "list",
|
|
233
|
+
outputJson = false,
|
|
234
|
+
cwd = process.cwd(),
|
|
235
|
+
write = (line) => console.log(line),
|
|
236
|
+
writeError = (line) => console.error(line),
|
|
237
|
+
} = {}) {
|
|
238
|
+
const sub = String(subcommand || "list").trim().toLowerCase();
|
|
239
|
+
try {
|
|
240
|
+
if (sub === "list") {
|
|
241
|
+
const rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
|
|
242
|
+
if (outputJson) {
|
|
243
|
+
write(JSON.stringify(rows, null, 2));
|
|
244
|
+
return 0;
|
|
245
|
+
}
|
|
246
|
+
printProjectList(rows, write);
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
if (sub === "current") {
|
|
250
|
+
const current = getCurrentProjectRuntime(cwd, { validate: true })
|
|
251
|
+
|| buildCurrentProjectFallback(cwd);
|
|
252
|
+
if (outputJson) {
|
|
253
|
+
write(JSON.stringify(current, null, 2));
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
printCurrentProject(current, write);
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
if (sub === "switch") {
|
|
260
|
+
const err = projectSwitchV1Error();
|
|
261
|
+
writeError(err.message);
|
|
262
|
+
return err.exitCode || 2;
|
|
263
|
+
}
|
|
264
|
+
writeError("project requires list|current|switch subcommand");
|
|
265
|
+
return 1;
|
|
266
|
+
} catch (err) {
|
|
267
|
+
writeError(err.message || String(err));
|
|
268
|
+
return 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
164
272
|
function normalizeReportPhase(action = "") {
|
|
165
273
|
const value = String(action || "").trim().toLowerCase();
|
|
166
274
|
if (value === "start") return "start";
|
|
@@ -244,9 +352,46 @@ async function runCli(argv) {
|
|
|
244
352
|
program
|
|
245
353
|
.command("chat")
|
|
246
354
|
.description("Launch ufoo chat UI")
|
|
247
|
-
.
|
|
355
|
+
.option("-g, --global", "Launch in global multi-project mode")
|
|
356
|
+
.action((opts) => {
|
|
248
357
|
const repoRoot = getPackageRoot();
|
|
249
|
-
|
|
358
|
+
const args = ["chat"];
|
|
359
|
+
if (opts.global === true) args.push("-g");
|
|
360
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), ...args]);
|
|
361
|
+
});
|
|
362
|
+
const project = program.command("project").description("Project runtime commands");
|
|
363
|
+
project
|
|
364
|
+
.command("list")
|
|
365
|
+
.description("List runtime projects discovered from global registry")
|
|
366
|
+
.option("--json", "Output as JSON")
|
|
367
|
+
.action((opts) => {
|
|
368
|
+
process.exitCode = runProjectCommand({
|
|
369
|
+
subcommand: "list",
|
|
370
|
+
outputJson: opts.json === true,
|
|
371
|
+
cwd: process.cwd(),
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
project
|
|
375
|
+
.command("current")
|
|
376
|
+
.description("Show current project runtime context")
|
|
377
|
+
.option("--json", "Output as JSON")
|
|
378
|
+
.action((opts) => {
|
|
379
|
+
process.exitCode = runProjectCommand({
|
|
380
|
+
subcommand: "current",
|
|
381
|
+
outputJson: opts.json === true,
|
|
382
|
+
cwd: process.cwd(),
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
project
|
|
386
|
+
.command("switch")
|
|
387
|
+
.description("Switch active project (chat-only in v1)")
|
|
388
|
+
.argument("<indexOrPath>", "Project index from list output or absolute path")
|
|
389
|
+
.action(() => {
|
|
390
|
+
process.exitCode = runProjectCommand({
|
|
391
|
+
subcommand: "switch",
|
|
392
|
+
outputJson: false,
|
|
393
|
+
cwd: process.cwd(),
|
|
394
|
+
});
|
|
250
395
|
});
|
|
251
396
|
program
|
|
252
397
|
.command("launch")
|
|
@@ -1112,7 +1257,11 @@ async function runCli(argv) {
|
|
|
1112
1257
|
console.log(" ufoo doctor");
|
|
1113
1258
|
console.log(" ufoo status");
|
|
1114
1259
|
console.log(" ufoo daemon --start|--stop|--status");
|
|
1115
|
-
console.log(" ufoo
|
|
1260
|
+
console.log(" ufoo -g");
|
|
1261
|
+
console.log(" ufoo chat [-g]");
|
|
1262
|
+
console.log(" ufoo project list [--json]");
|
|
1263
|
+
console.log(" ufoo project current [--json]");
|
|
1264
|
+
console.log(" ufoo project switch <index|path>");
|
|
1116
1265
|
console.log(" ufoo resume [nickname]");
|
|
1117
1266
|
console.log(" ufoo recover [list [target] | run <target>] [--json]");
|
|
1118
1267
|
console.log(" ufoo report <start|progress|done|error|list> [message] [--task <id>] [--agent <id>]");
|
|
@@ -1175,7 +1324,21 @@ async function runCli(argv) {
|
|
|
1175
1324
|
return;
|
|
1176
1325
|
}
|
|
1177
1326
|
if (cmd === "chat") {
|
|
1178
|
-
|
|
1327
|
+
const chatArgs = ["chat"];
|
|
1328
|
+
if (rest.includes("-g") || rest.includes("--global")) {
|
|
1329
|
+
chatArgs.push("-g");
|
|
1330
|
+
}
|
|
1331
|
+
run(process.execPath, [path.join(repoRoot, "bin", "ufoo.js"), ...chatArgs]);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (cmd === "project") {
|
|
1335
|
+
const sub = String(rest[0] || "list").trim().toLowerCase();
|
|
1336
|
+
const outputJson = rest.includes("--json");
|
|
1337
|
+
process.exitCode = runProjectCommand({
|
|
1338
|
+
subcommand: sub,
|
|
1339
|
+
outputJson,
|
|
1340
|
+
cwd: process.cwd(),
|
|
1341
|
+
});
|
|
1179
1342
|
return;
|
|
1180
1343
|
}
|
|
1181
1344
|
if (cmd === "resume") {
|
package/src/daemon/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const { generateInstanceId, subscriberToSafeName } = require("../bus/utils");
|
|
|
11
11
|
const { createDaemonIpcServer } = require("./ipcServer");
|
|
12
12
|
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
|
|
13
13
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
14
|
+
const { upsertProjectRuntime, markProjectStopped } = require("../projects/registry");
|
|
14
15
|
const { scheduleProviderSessionProbe, loadProviderSessionCache } = require("./providerSessions");
|
|
15
16
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
16
17
|
const { createDaemonCronController } = require("./cronOps");
|
|
@@ -25,6 +26,7 @@ let providerSessions = null;
|
|
|
25
26
|
let probeHandles = new Map();
|
|
26
27
|
let daemonCronController = null;
|
|
27
28
|
let daemonGroupOrchestrator = null;
|
|
29
|
+
const PROJECT_RUNTIME_HEARTBEAT_MS = 10 * 1000;
|
|
28
30
|
|
|
29
31
|
function sleep(ms) {
|
|
30
32
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -323,7 +325,10 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
323
325
|
continue;
|
|
324
326
|
}
|
|
325
327
|
// eslint-disable-next-line no-await-in-loop
|
|
326
|
-
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager
|
|
328
|
+
const launchResult = await launchAgent(projectRoot, agent, count, nickname, processManager, {
|
|
329
|
+
launchScope: op.launch_scope || "",
|
|
330
|
+
terminalApp: op.terminal_app || "",
|
|
331
|
+
});
|
|
327
332
|
if (launchResult.mode === "internal" && launchResult.subscriberIds && launchResult.subscriberIds.length > 0) {
|
|
328
333
|
const probeAgentType = agent === "codex"
|
|
329
334
|
? "codex"
|
|
@@ -359,6 +364,7 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
359
364
|
agent,
|
|
360
365
|
count,
|
|
361
366
|
nickname: nickname || undefined,
|
|
367
|
+
launch_scope: launchResult.launchScope || undefined,
|
|
362
368
|
subscriber_ids: Array.isArray(launchResult.subscriberIds) ? launchResult.subscriberIds.slice() : [],
|
|
363
369
|
});
|
|
364
370
|
if (nickname) {
|
|
@@ -372,8 +378,15 @@ async function handleOps(projectRoot, ops = [], processManager = null) {
|
|
|
372
378
|
results.push({ action: "launch", ok: false, agent, count, error: err.message });
|
|
373
379
|
}
|
|
374
380
|
} else if (op.action === "close") {
|
|
375
|
-
const
|
|
376
|
-
|
|
381
|
+
const closeResult = await closeAgent(projectRoot, op.agent_id);
|
|
382
|
+
const normalizedClose = closeResult && typeof closeResult === "object"
|
|
383
|
+
? closeResult
|
|
384
|
+
: { ok: Boolean(closeResult) };
|
|
385
|
+
results.push({
|
|
386
|
+
action: "close",
|
|
387
|
+
agent_id: op.agent_id,
|
|
388
|
+
...normalizedClose,
|
|
389
|
+
});
|
|
377
390
|
} else if (op.action === "rename") {
|
|
378
391
|
const agentId = op.agent_id || "";
|
|
379
392
|
const nickname = op.nickname || "";
|
|
@@ -669,6 +682,19 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
669
682
|
const log = (msg) => {
|
|
670
683
|
logFile.write(`[daemon] ${new Date().toISOString()} ${msg}\n`);
|
|
671
684
|
};
|
|
685
|
+
const publishProjectRuntime = (status = "running") => {
|
|
686
|
+
try {
|
|
687
|
+
upsertProjectRuntime({
|
|
688
|
+
projectRoot,
|
|
689
|
+
daemonPid: process.pid,
|
|
690
|
+
socketPath: socketPath(projectRoot),
|
|
691
|
+
status,
|
|
692
|
+
lastSeen: new Date().toISOString(),
|
|
693
|
+
});
|
|
694
|
+
} catch (err) {
|
|
695
|
+
log(`project runtime update failed (${status}): ${err.message || err}`);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
672
698
|
|
|
673
699
|
// 创建进程管理器 - daemon 作为父进程监控所有 internal agents
|
|
674
700
|
const processManager = new AgentProcessManager(projectRoot);
|
|
@@ -924,7 +950,9 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
924
950
|
const closeResult = opsResults.find((r) => r.action === "close");
|
|
925
951
|
const ok = closeResult ? closeResult.ok !== false : true;
|
|
926
952
|
const reply = ok
|
|
927
|
-
?
|
|
953
|
+
? (closeResult && closeResult.already_stopped
|
|
954
|
+
? `Closed ${agent_id} (already stopped)`
|
|
955
|
+
: `Closed ${agent_id}`)
|
|
928
956
|
: `Close failed: ${closeResult?.error || "unknown error"}`;
|
|
929
957
|
socket.write(
|
|
930
958
|
`${JSON.stringify({
|
|
@@ -952,7 +980,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
952
980
|
}
|
|
953
981
|
if (req.type === IPC_REQUEST_TYPES.LAUNCH_AGENT) {
|
|
954
982
|
log(`launch_agent received: agent=${req.agent} count=${req.count}`);
|
|
955
|
-
const { agent, count, nickname } = req;
|
|
983
|
+
const { agent, count, nickname, launch_scope, terminal_app } = req;
|
|
956
984
|
const normalizedAgent = normalizeLaunchAgent(agent);
|
|
957
985
|
if (!normalizedAgent) {
|
|
958
986
|
socket.write(
|
|
@@ -971,6 +999,8 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
971
999
|
agent: normalizedAgent,
|
|
972
1000
|
count: finalCount,
|
|
973
1001
|
nickname: nickname || "",
|
|
1002
|
+
launch_scope: launch_scope || "",
|
|
1003
|
+
terminal_app: terminal_app || "",
|
|
974
1004
|
};
|
|
975
1005
|
try {
|
|
976
1006
|
const opsResults = await handleOps(projectRoot, [op], processManager);
|
|
@@ -1515,6 +1545,10 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1515
1545
|
};
|
|
1516
1546
|
|
|
1517
1547
|
ipcServer.listen(socketPath(projectRoot));
|
|
1548
|
+
publishProjectRuntime("running");
|
|
1549
|
+
const runtimeHeartbeat = setInterval(() => {
|
|
1550
|
+
publishProjectRuntime("running");
|
|
1551
|
+
}, PROJECT_RUNTIME_HEARTBEAT_MS);
|
|
1518
1552
|
|
|
1519
1553
|
log(`Started pid=${process.pid}`);
|
|
1520
1554
|
|
|
@@ -1619,8 +1653,17 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1619
1653
|
}, 1500);
|
|
1620
1654
|
}
|
|
1621
1655
|
|
|
1656
|
+
let cleanedUp = false;
|
|
1622
1657
|
const cleanup = () => {
|
|
1658
|
+
if (cleanedUp) return;
|
|
1659
|
+
cleanedUp = true;
|
|
1623
1660
|
log(`Shutting down daemon (managed agents: ${processManager.count()})`);
|
|
1661
|
+
clearInterval(runtimeHeartbeat);
|
|
1662
|
+
try {
|
|
1663
|
+
markProjectStopped(projectRoot);
|
|
1664
|
+
} catch {
|
|
1665
|
+
// ignore cleanup errors
|
|
1666
|
+
}
|
|
1624
1667
|
|
|
1625
1668
|
if (daemonCronController) {
|
|
1626
1669
|
daemonCronController.stopAll();
|
|
@@ -1706,6 +1749,12 @@ function stopDaemon(projectRoot) {
|
|
|
1706
1749
|
// ignore
|
|
1707
1750
|
}
|
|
1708
1751
|
|
|
1752
|
+
try {
|
|
1753
|
+
markProjectStopped(projectRoot);
|
|
1754
|
+
} catch {
|
|
1755
|
+
// ignore
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1709
1758
|
return killed;
|
|
1710
1759
|
}
|
|
1711
1760
|
|
package/src/daemon/ipcServer.js
CHANGED
|
@@ -28,6 +28,7 @@ function createDaemonIpcServer(options = {}) {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
let lastActiveJson = "";
|
|
31
|
+
let lastMetaJson = "";
|
|
31
32
|
const statusSyncInterval = setInterval(() => {
|
|
32
33
|
if (sockets.size === 0) return;
|
|
33
34
|
try {
|
|
@@ -38,8 +39,12 @@ function createDaemonIpcServer(options = {}) {
|
|
|
38
39
|
try {
|
|
39
40
|
const status = buildStatus(projectRoot);
|
|
40
41
|
const currentActiveJson = JSON.stringify(status.active);
|
|
41
|
-
|
|
42
|
+
const currentMetaJson = JSON.stringify(
|
|
43
|
+
(status.active_meta || []).map((m) => `${m.id}:${m.activity_state || ""}`)
|
|
44
|
+
);
|
|
45
|
+
if (currentActiveJson !== lastActiveJson || currentMetaJson !== lastMetaJson) {
|
|
42
46
|
lastActiveJson = currentActiveJson;
|
|
47
|
+
lastMetaJson = currentMetaJson;
|
|
43
48
|
sendToSockets({ type: IPC_RESPONSE_TYPES.STATUS, data: status });
|
|
44
49
|
log(`status sync: active agents changed to ${status.active.length}`);
|
|
45
50
|
}
|