u-foo 1.7.1 → 1.7.3
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/package.json +1 -1
- package/src/bus/daemon.js +5 -4
- package/src/bus/utils.js +29 -8
- package/src/chat/commandExecutor.js +1 -0
- package/src/chat/dashboardKeyController.js +3 -2
- package/src/chat/dashboardView.js +1 -1
- package/src/chat/index.js +7 -2
- package/src/chat/settingsController.js +3 -1
- package/src/cli.js +2 -0
- package/src/daemon/groupOrchestrator.js +20 -0
- package/src/daemon/index.js +15 -0
- package/src/init/index.js +7 -6
package/package.json
CHANGED
package/src/bus/daemon.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
-
const { readJSON, writeJSON, isPidAlive, isAgentPidAlive, ensureDir, safeNameToSubscriber, subscriberToSafeName } = require("./utils");
|
|
3
|
+
const { readJSON, writeJSON, isPidAlive, isAgentPidAlive, isMetaActive, ensureDir, safeNameToSubscriber, subscriberToSafeName } = require("./utils");
|
|
4
4
|
const Injector = require("./inject");
|
|
5
5
|
const QueueManager = require("./queue");
|
|
6
6
|
const MessageManager = require("./message");
|
|
@@ -405,12 +405,13 @@ class BusDaemon {
|
|
|
405
405
|
continue;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
// 检查 PID
|
|
409
|
-
if (
|
|
408
|
+
// 检查 agent 是否仍然存活(PID + TTY 交叉检查)
|
|
409
|
+
if (!isMetaActive(meta)) {
|
|
410
410
|
const now = new Date().toISOString().split("T")[1].slice(0, 8);
|
|
411
|
-
console.log(`[daemon] ${now} Agent ${subscriber} (pid=${meta.pid}) is dead, marking inactive`);
|
|
411
|
+
console.log(`[daemon] ${now} Agent ${subscriber} (pid=${meta.pid || 0}) is dead, marking inactive`);
|
|
412
412
|
|
|
413
413
|
meta.status = "inactive";
|
|
414
|
+
meta.activity_state = "";
|
|
414
415
|
changed = true;
|
|
415
416
|
|
|
416
417
|
// 清理队列目录和 offset
|
package/src/bus/utils.js
CHANGED
|
@@ -310,23 +310,44 @@ function isMetaActive(meta) {
|
|
|
310
310
|
// 2. PID 存活(最可靠)
|
|
311
311
|
if (meta.pid && isAgentPidAlive(meta.pid)) return true;
|
|
312
312
|
|
|
313
|
-
// 3.
|
|
313
|
+
// 3. PID 已记录但进程已死 → 确定离线
|
|
314
|
+
if (meta.pid) return false;
|
|
315
|
+
|
|
316
|
+
// 4. 无 PID(如 codex)— TTY 交叉校验
|
|
317
|
+
// 仅当 tty_shell_pid 也还活着时才信任 TTY 检查,
|
|
318
|
+
// 防止 TTY 上残留的僵尸进程导致误判存活
|
|
314
319
|
if (meta.tty) {
|
|
315
320
|
const ttyInfo = getTtyProcessInfo(meta.tty);
|
|
316
|
-
if (ttyInfo && ttyInfo.hasAgent)
|
|
321
|
+
if (ttyInfo && ttyInfo.hasAgent) {
|
|
322
|
+
// 如果记录了 tty_shell_pid,验证它还在
|
|
323
|
+
if (meta.tty_shell_pid) {
|
|
324
|
+
if (isPidAlive(meta.tty_shell_pid)) return true;
|
|
325
|
+
// shell pid 已死,TTY 上的进程是残留
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
// 无 tty_shell_pid,用 last_seen 超时兜底
|
|
329
|
+
if (meta.last_seen) {
|
|
330
|
+
const age = Date.now() - new Date(meta.last_seen).getTime();
|
|
331
|
+
return age <= HEARTBEAT_TIMEOUT_MS;
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
317
335
|
}
|
|
318
336
|
|
|
319
|
-
//
|
|
320
|
-
if (meta.pid) return false;
|
|
321
|
-
|
|
322
|
-
// 5. 无 PID,用 last_seen 心跳超时兜底
|
|
337
|
+
// 5. 无 PID 无 TTY agent,用 last_seen 心跳超时兜底
|
|
323
338
|
if (meta.status === "active" && meta.last_seen) {
|
|
324
339
|
const age = Date.now() - new Date(meta.last_seen).getTime();
|
|
325
340
|
return age <= HEARTBEAT_TIMEOUT_MS;
|
|
326
341
|
}
|
|
327
342
|
|
|
328
|
-
// 6. status=active
|
|
329
|
-
if (meta.status === "active")
|
|
343
|
+
// 6. status=active 但无任何可靠信息 → 超时后判定离线
|
|
344
|
+
if (meta.status === "active") {
|
|
345
|
+
if (meta.joined_at) {
|
|
346
|
+
const age = Date.now() - new Date(meta.joined_at).getTime();
|
|
347
|
+
return age <= HEARTBEAT_TIMEOUT_MS;
|
|
348
|
+
}
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
330
351
|
|
|
331
352
|
return false;
|
|
332
353
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const DEFAULT_MODE_OPTIONS = ["terminal", "tmux", "internal"];
|
|
1
|
+
const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
|
|
2
2
|
|
|
3
3
|
function createDashboardKeyController(options = {}) {
|
|
4
4
|
const {
|
|
@@ -468,7 +468,8 @@ function createDashboardKeyController(options = {}) {
|
|
|
468
468
|
if (key.name === "down") {
|
|
469
469
|
clearTargetAgent();
|
|
470
470
|
state.dashboardView = "mode";
|
|
471
|
-
|
|
471
|
+
const launchModeIndex = modeOptions.indexOf(state.launchMode);
|
|
472
|
+
state.selectedModeIndex = launchModeIndex >= 0 ? launchModeIndex : 0;
|
|
472
473
|
renderDashboardAndScreen();
|
|
473
474
|
return true;
|
|
474
475
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { clampAgentWindowWithSelection } = require("./agentDirectory");
|
|
2
2
|
|
|
3
|
-
const DEFAULT_MODE_OPTIONS = ["terminal", "tmux", "internal"];
|
|
3
|
+
const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
|
|
4
4
|
|
|
5
5
|
function providerLabel(value) {
|
|
6
6
|
if (value === "claude-cli") return "claude";
|
package/src/chat/index.js
CHANGED
|
@@ -52,6 +52,8 @@ const {
|
|
|
52
52
|
filterVisibleProjectRuntimes,
|
|
53
53
|
} = require("./projectRuntimes");
|
|
54
54
|
|
|
55
|
+
const MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
|
|
56
|
+
|
|
55
57
|
async function runChat(projectRoot, options = {}) {
|
|
56
58
|
const globalMode = options && options.globalMode === true;
|
|
57
59
|
const DASHBOARD_HEIGHT = globalMode ? 2 : 1;
|
|
@@ -673,7 +675,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
673
675
|
let focusMode = "input"; // "input" or "dashboard"
|
|
674
676
|
let dashboardView = "agents"; // "projects" | "agents" | "mode" | "provider" | "assistant" | "cron"
|
|
675
677
|
let reportPendingTotal = 0;
|
|
676
|
-
let selectedModeIndex =
|
|
678
|
+
let selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
677
679
|
const providerOptions = [
|
|
678
680
|
{ label: "codex", value: "codex-cli" },
|
|
679
681
|
{ label: "claude", value: "claude-cli" },
|
|
@@ -1129,6 +1131,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1129
1131
|
},
|
|
1130
1132
|
assistantOptions,
|
|
1131
1133
|
providerOptions,
|
|
1134
|
+
modeOptions: MODE_OPTIONS,
|
|
1132
1135
|
getAutoResume: () => autoResume,
|
|
1133
1136
|
setAutoResumeState: (value) => {
|
|
1134
1137
|
autoResume = value;
|
|
@@ -1187,6 +1190,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1187
1190
|
resumeOptions,
|
|
1188
1191
|
pendingReports: reportPendingTotal,
|
|
1189
1192
|
dashHints: DASH_HINTS,
|
|
1193
|
+
modeOptions: MODE_OPTIONS,
|
|
1190
1194
|
});
|
|
1191
1195
|
if (globalMode && (focusMode !== "dashboard" || dashboardView === "projects")) {
|
|
1192
1196
|
projectListWindowStart = computed.windowStart;
|
|
@@ -1329,7 +1333,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1329
1333
|
agentListWindowStart = 0;
|
|
1330
1334
|
clampAgentWindow();
|
|
1331
1335
|
}
|
|
1332
|
-
selectedModeIndex =
|
|
1336
|
+
selectedModeIndex = Math.max(0, MODE_OPTIONS.indexOf(launchMode));
|
|
1333
1337
|
selectedProviderIndex = Math.max(0, providerOptions.findIndex((opt) => opt.value === agentProvider));
|
|
1334
1338
|
selectedAssistantIndex = Math.max(
|
|
1335
1339
|
0,
|
|
@@ -1421,6 +1425,7 @@ async function runChat(projectRoot, options = {}) {
|
|
|
1421
1425
|
setScreenGrabKeys: (value) => {
|
|
1422
1426
|
screen.grabKeys = Boolean(value);
|
|
1423
1427
|
},
|
|
1428
|
+
modeOptions: MODE_OPTIONS,
|
|
1424
1429
|
});
|
|
1425
1430
|
|
|
1426
1431
|
function handleDashboardKey(key) {
|
|
@@ -24,6 +24,7 @@ function createSettingsController(options = {}) {
|
|
|
24
24
|
setSelectedAssistantIndex = () => {},
|
|
25
25
|
assistantOptions = [],
|
|
26
26
|
providerOptions = [],
|
|
27
|
+
modeOptions = [],
|
|
27
28
|
getAutoResume = () => true,
|
|
28
29
|
setAutoResumeState = () => {},
|
|
29
30
|
setSelectedResumeIndex = () => {},
|
|
@@ -68,7 +69,8 @@ function createSettingsController(options = {}) {
|
|
|
68
69
|
const next = normalizeLaunchMode(mode);
|
|
69
70
|
if (next === getLaunchMode()) return false;
|
|
70
71
|
setLaunchModeState(next);
|
|
71
|
-
|
|
72
|
+
const nextIndex = modeOptions.findIndex((opt) => opt === next);
|
|
73
|
+
setSelectedModeIndex(nextIndex >= 0 ? nextIndex : 0);
|
|
72
74
|
saveConfig(projectRoot, { launchMode: next });
|
|
73
75
|
logMessage("status", `{white-fg}⚙{/white-fg} Launch mode: ${next}`);
|
|
74
76
|
renderDashboard();
|
package/src/cli.js
CHANGED
|
@@ -791,6 +791,7 @@ async function runCli(argv) {
|
|
|
791
791
|
alias,
|
|
792
792
|
instance: opts.instance || "",
|
|
793
793
|
dry_run: opts.dryRun === true,
|
|
794
|
+
...collectHostLaunchRequestContext(),
|
|
794
795
|
});
|
|
795
796
|
if (opts.json) {
|
|
796
797
|
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
@@ -1684,6 +1685,7 @@ async function runCli(argv) {
|
|
|
1684
1685
|
alias,
|
|
1685
1686
|
instance,
|
|
1686
1687
|
dry_run: dryRun,
|
|
1688
|
+
...collectHostLaunchRequestContext(),
|
|
1687
1689
|
});
|
|
1688
1690
|
if (outputJson) {
|
|
1689
1691
|
console.log(JSON.stringify(resp?.data || {}, null, 2));
|
|
@@ -190,6 +190,24 @@ function nowIso() {
|
|
|
190
190
|
return new Date().toISOString();
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
function buildLaunchHostContext(params = {}) {
|
|
194
|
+
const hostInjectSock = asTrimmedString(params.host_inject_sock || params.hostInjectSock);
|
|
195
|
+
const hostDaemonSock = asTrimmedString(params.host_daemon_sock || params.hostDaemonSock);
|
|
196
|
+
const hostName = asTrimmedString(params.host_name || params.hostName);
|
|
197
|
+
const hostSessionId = asTrimmedString(params.host_session_id || params.hostSessionId);
|
|
198
|
+
const context = {};
|
|
199
|
+
if (hostInjectSock) context.host_inject_sock = hostInjectSock;
|
|
200
|
+
if (hostDaemonSock) context.host_daemon_sock = hostDaemonSock;
|
|
201
|
+
if (hostName) context.host_name = hostName;
|
|
202
|
+
if (hostSessionId) context.host_session_id = hostSessionId;
|
|
203
|
+
if (params.host_capabilities && typeof params.host_capabilities === "object") {
|
|
204
|
+
context.host_capabilities = { ...params.host_capabilities };
|
|
205
|
+
} else if (params.hostCapabilities && typeof params.hostCapabilities === "object") {
|
|
206
|
+
context.host_capabilities = { ...params.hostCapabilities };
|
|
207
|
+
}
|
|
208
|
+
return context;
|
|
209
|
+
}
|
|
210
|
+
|
|
193
211
|
function buildDefaultRuntime({
|
|
194
212
|
groupId,
|
|
195
213
|
instance,
|
|
@@ -309,6 +327,7 @@ function createGroupOrchestrator(options = {}) {
|
|
|
309
327
|
const alias = asTrimmedString(params.alias);
|
|
310
328
|
const instance = asTrimmedString(params.instance);
|
|
311
329
|
const dryRun = params.dry_run === true || params.dryRun === true;
|
|
330
|
+
const launchHostContext = buildLaunchHostContext(params);
|
|
312
331
|
|
|
313
332
|
if (!alias) {
|
|
314
333
|
return { ok: false, error: "group run requires alias", status: "failed" };
|
|
@@ -377,6 +396,7 @@ function createGroupOrchestrator(options = {}) {
|
|
|
377
396
|
agent: item.type,
|
|
378
397
|
count: 1,
|
|
379
398
|
nickname: item.nickname,
|
|
399
|
+
...launchHostContext,
|
|
380
400
|
};
|
|
381
401
|
|
|
382
402
|
// eslint-disable-next-line no-await-in-loop
|
package/src/daemon/index.js
CHANGED
|
@@ -1080,11 +1080,26 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1080
1080
|
const alias = req.alias || req.template || "";
|
|
1081
1081
|
const instance = req.instance || req.group_id || "";
|
|
1082
1082
|
const dryRun = req.dry_run === true || req.dryRun === true;
|
|
1083
|
+
const hostInjectSock = req.host_inject_sock || req.hostInjectSock || "";
|
|
1084
|
+
const hostDaemonSock = req.host_daemon_sock || req.hostDaemonSock || "";
|
|
1085
|
+
const hostName = req.host_name || req.hostName || "";
|
|
1086
|
+
const hostSessionId = req.host_session_id || req.hostSessionId || "";
|
|
1087
|
+
const hostCapabilities =
|
|
1088
|
+
req.host_capabilities && typeof req.host_capabilities === "object"
|
|
1089
|
+
? req.host_capabilities
|
|
1090
|
+
: ((req.hostCapabilities && typeof req.hostCapabilities === "object")
|
|
1091
|
+
? req.hostCapabilities
|
|
1092
|
+
: null);
|
|
1083
1093
|
try {
|
|
1084
1094
|
const result = await daemonGroupOrchestrator.runGroup({
|
|
1085
1095
|
alias,
|
|
1086
1096
|
instance,
|
|
1087
1097
|
dry_run: dryRun,
|
|
1098
|
+
host_inject_sock: hostInjectSock,
|
|
1099
|
+
host_daemon_sock: hostDaemonSock,
|
|
1100
|
+
host_name: hostName,
|
|
1101
|
+
host_session_id: hostSessionId,
|
|
1102
|
+
host_capabilities: hostCapabilities,
|
|
1088
1103
|
});
|
|
1089
1104
|
const ok = result && result.ok !== false;
|
|
1090
1105
|
let reply = "";
|
package/src/init/index.js
CHANGED
|
@@ -90,16 +90,17 @@ class UfooInit {
|
|
|
90
90
|
fs.mkdirSync(ufooDir, { recursive: true });
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
// 创建 docs
|
|
93
|
+
// 创建 docs 符号链接:项目的 docs/ -> .ufoo/docs
|
|
94
94
|
const docsLink = path.join(ufooDir, "docs");
|
|
95
|
-
const
|
|
95
|
+
const projectDocs = path.join(project, "docs");
|
|
96
96
|
|
|
97
|
-
if (fs.existsSync(
|
|
98
|
-
|
|
97
|
+
if (fs.existsSync(projectDocs)) {
|
|
98
|
+
const linkStat = this.safeLstat(docsLink);
|
|
99
|
+
if (linkStat) {
|
|
99
100
|
fs.unlinkSync(docsLink);
|
|
100
101
|
}
|
|
101
|
-
fs.symlinkSync(
|
|
102
|
-
console.log(`[core] Created docs symlink: .ufoo/docs ->
|
|
102
|
+
fs.symlinkSync(projectDocs, docsLink);
|
|
103
|
+
console.log(`[core] Created docs symlink: .ufoo/docs -> docs/`);
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
console.log("[core] Done");
|