u-foo 1.0.6 → 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 +44 -4
- 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 +11 -2
- 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 +154 -0
- package/src/chat/index.js +935 -2909
- 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 +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 +1580 -0
- package/src/config.js +47 -1
- 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 +661 -488
- 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
package/src/bus/queue.js
CHANGED
|
@@ -15,6 +15,7 @@ class QueueManager {
|
|
|
15
15
|
constructor(busDir) {
|
|
16
16
|
this.busDir = busDir;
|
|
17
17
|
this.queuesDir = path.join(busDir, "queues");
|
|
18
|
+
this.offsetsDir = path.join(busDir, "offsets");
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -38,7 +39,7 @@ class QueueManager {
|
|
|
38
39
|
* 获取 offset 文件路径
|
|
39
40
|
*/
|
|
40
41
|
getOffsetPath(subscriber) {
|
|
41
|
-
return path.join(this.
|
|
42
|
+
return path.join(this.offsetsDir, `${subscriberToSafeName(subscriber)}.offset`);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
/**
|
|
@@ -91,6 +92,10 @@ class QueueManager {
|
|
|
91
92
|
this.ensureQueueDir(subscriber);
|
|
92
93
|
const pendingPath = this.getPendingPath(subscriber);
|
|
93
94
|
appendJSONL(pendingPath, event);
|
|
95
|
+
if (event && event.event === "wake") {
|
|
96
|
+
const wakePath = path.join(this.getQueueDir(subscriber), "wake");
|
|
97
|
+
fs.writeFileSync(wakePath, String(event.seq || Date.now()), "utf8");
|
|
98
|
+
}
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
/**
|
package/src/bus/store.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { getTimestamp, ensureDir, safeNameToSubscriber, getTtyProcessInfo } = require("./utils");
|
|
6
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
|
+
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
8
|
+
|
|
9
|
+
function readQueueTty(queueDir) {
|
|
10
|
+
try {
|
|
11
|
+
const value = fs.readFileSync(path.join(queueDir, "tty"), "utf8").trim();
|
|
12
|
+
return value || "";
|
|
13
|
+
} catch {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function nicknamePrefixForType(agentType = "") {
|
|
19
|
+
return agentType === "claude-code" ? "claude"
|
|
20
|
+
: agentType === "ufoo-code" ? "ucode"
|
|
21
|
+
: String(agentType || "agent");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRecoverableSessionId(sessionId = "") {
|
|
25
|
+
const text = String(sessionId || "").trim();
|
|
26
|
+
if (!text) return false;
|
|
27
|
+
if (text.includes(":") || text.includes("_")) return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function buildUsedNicknameSet(agents = {}) {
|
|
32
|
+
const set = new Set();
|
|
33
|
+
for (const meta of Object.values(agents || {})) {
|
|
34
|
+
if (!meta || meta.status !== "active") continue;
|
|
35
|
+
const nick = meta && typeof meta.nickname === "string" ? meta.nickname : "";
|
|
36
|
+
if (nick) set.add(nick);
|
|
37
|
+
}
|
|
38
|
+
return set;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function allocateRecoveredNickname(agentType, used) {
|
|
42
|
+
const prefix = nicknamePrefixForType(agentType);
|
|
43
|
+
let idx = 1;
|
|
44
|
+
while (used.has(`${prefix}-${idx}`)) idx += 1;
|
|
45
|
+
const nick = `${prefix}-${idx}`;
|
|
46
|
+
used.add(nick);
|
|
47
|
+
return nick;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class BusStore {
|
|
51
|
+
constructor(projectRoot) {
|
|
52
|
+
this.projectRoot = projectRoot;
|
|
53
|
+
this.paths = getUfooPaths(projectRoot);
|
|
54
|
+
this.busDir = this.paths.busDir;
|
|
55
|
+
this.agentsFile = this.paths.agentsFile;
|
|
56
|
+
this.eventsDir = this.paths.busEventsDir;
|
|
57
|
+
this.logsDir = this.paths.busLogsDir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ensure() {
|
|
61
|
+
if (!fs.existsSync(this.busDir) || !fs.existsSync(this.paths.agentDir)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Event bus not initialized. Please run: ufoo bus init or /uinit"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
load() {
|
|
69
|
+
const data = loadAgentsData(this.agentsFile);
|
|
70
|
+
if (!data.agents || typeof data.agents !== "object") {
|
|
71
|
+
data.agents = {};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const queueRoot = path.join(this.busDir, "queues");
|
|
75
|
+
if (!fs.existsSync(queueRoot)) return data;
|
|
76
|
+
|
|
77
|
+
const usedNicknames = buildUsedNicknameSet(data.agents);
|
|
78
|
+
const now = getTimestamp();
|
|
79
|
+
let recovered = false;
|
|
80
|
+
|
|
81
|
+
for (const entry of fs.readdirSync(queueRoot)) {
|
|
82
|
+
const queueDir = path.join(queueRoot, entry);
|
|
83
|
+
let stat;
|
|
84
|
+
try {
|
|
85
|
+
stat = fs.statSync(queueDir);
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!stat.isDirectory()) continue;
|
|
90
|
+
|
|
91
|
+
const subscriber = safeNameToSubscriber(entry);
|
|
92
|
+
const parts = subscriber.split(":");
|
|
93
|
+
if (parts.length !== 2) continue;
|
|
94
|
+
const [agentType, sessionId] = parts;
|
|
95
|
+
if (!agentType || !sessionId) continue;
|
|
96
|
+
if (!isRecoverableSessionId(sessionId)) continue;
|
|
97
|
+
if (data.agents[subscriber]) continue;
|
|
98
|
+
|
|
99
|
+
const tty = readQueueTty(queueDir);
|
|
100
|
+
const ttyInfo = tty ? getTtyProcessInfo(tty) : null;
|
|
101
|
+
const activeByTty = Boolean(ttyInfo && ttyInfo.alive && ttyInfo.hasAgent);
|
|
102
|
+
const nickname = activeByTty ? allocateRecoveredNickname(agentType, usedNicknames) : "";
|
|
103
|
+
|
|
104
|
+
data.agents[subscriber] = {
|
|
105
|
+
agent_type: agentType,
|
|
106
|
+
nickname,
|
|
107
|
+
status: activeByTty ? "active" : "inactive",
|
|
108
|
+
joined_at: now,
|
|
109
|
+
last_seen: now,
|
|
110
|
+
pid: 0,
|
|
111
|
+
tty,
|
|
112
|
+
tty_shell_pid: ttyInfo && ttyInfo.shellPid ? ttyInfo.shellPid : 0,
|
|
113
|
+
tmux_pane: "",
|
|
114
|
+
launch_mode: "",
|
|
115
|
+
};
|
|
116
|
+
recovered = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (recovered) {
|
|
120
|
+
saveAgentsData(this.agentsFile, data);
|
|
121
|
+
}
|
|
122
|
+
return data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
save(busData) {
|
|
126
|
+
if (busData) {
|
|
127
|
+
saveAgentsData(this.agentsFile, busData);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
init() {
|
|
132
|
+
ensureDir(this.busDir);
|
|
133
|
+
ensureDir(this.paths.agentDir);
|
|
134
|
+
ensureDir(this.eventsDir);
|
|
135
|
+
ensureDir(path.join(this.busDir, "queues"));
|
|
136
|
+
ensureDir(this.logsDir);
|
|
137
|
+
ensureDir(path.join(this.busDir, "offsets"));
|
|
138
|
+
ensureDir(this.paths.busDaemonDir);
|
|
139
|
+
ensureDir(this.paths.busDaemonCountsDir);
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(this.agentsFile)) {
|
|
142
|
+
const busData = {
|
|
143
|
+
created_at: getTimestamp(),
|
|
144
|
+
agents: {},
|
|
145
|
+
};
|
|
146
|
+
saveAgentsData(this.agentsFile, busData);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getCurrentSubscriber(busData) {
|
|
151
|
+
if (process.env.UFOO_SUBSCRIBER_ID) {
|
|
152
|
+
return process.env.UFOO_SUBSCRIBER_ID;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!fs.existsSync(this.agentsFile)) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sessionFile = path.join(this.paths.agentDir, "session.txt");
|
|
160
|
+
if (fs.existsSync(sessionFile)) {
|
|
161
|
+
const sessionId = fs.readFileSync(sessionFile, "utf8").trim();
|
|
162
|
+
if (sessionId) {
|
|
163
|
+
return sessionId;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let currentTty = null;
|
|
168
|
+
try {
|
|
169
|
+
const ttyPath = fs.realpathSync("/dev/tty");
|
|
170
|
+
if (ttyPath && ttyPath.startsWith("/dev/")) {
|
|
171
|
+
currentTty = ttyPath;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// tty not available
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (currentTty && busData && busData.agents) {
|
|
178
|
+
for (const [id, meta] of Object.entries(busData.agents)) {
|
|
179
|
+
if (meta.tty === currentTty) {
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { BusStore };
|
package/src/bus/subscriber.js
CHANGED
|
@@ -3,6 +3,13 @@ const { getTimestamp, isAgentPidAlive, isMetaActive, isValidTty, getTtyProcessIn
|
|
|
3
3
|
const NicknameManager = require("./nickname");
|
|
4
4
|
const { spawnSync } = require("child_process");
|
|
5
5
|
|
|
6
|
+
function detectTerminalAppFromEnv() {
|
|
7
|
+
const termProgram = process.env.TERM_PROGRAM || "";
|
|
8
|
+
if (process.env.ITERM_SESSION_ID || termProgram === "iTerm.app") return "iterm2";
|
|
9
|
+
if (termProgram === "Apple_Terminal") return "terminal";
|
|
10
|
+
return termProgram || "";
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
/**
|
|
7
14
|
* 获取当前终端的 tty 路径
|
|
8
15
|
*/
|
|
@@ -43,6 +50,10 @@ function tryTtyWithStdin(fd) {
|
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
function getTtyPath() {
|
|
53
|
+
// 0) Honor explicit ttyPath from node stdio if present (useful for tests)
|
|
54
|
+
const stdinTtyPath = normalizeTty(process.stdin?.ttyPath || "");
|
|
55
|
+
if (stdinTtyPath) return stdinTtyPath;
|
|
56
|
+
|
|
46
57
|
// 1) Try stdin directly (inherits real tty if present)
|
|
47
58
|
let ttyPath = tryTtyWithStdin(0);
|
|
48
59
|
if (ttyPath) return ttyPath;
|
|
@@ -145,8 +156,9 @@ class SubscriberManager {
|
|
|
145
156
|
}
|
|
146
157
|
finalNickname = nickname;
|
|
147
158
|
} else {
|
|
148
|
-
//
|
|
159
|
+
// 自动生成昵称(并标记占用,避免并发重复)
|
|
149
160
|
finalNickname = nicknameManager.generateAutoNickname(agentType);
|
|
161
|
+
nicknameManager.setNickname(subscriber, finalNickname);
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
const launchMode = options.launchMode || process.env.UFOO_LAUNCH_MODE || "";
|
|
@@ -186,6 +198,11 @@ class SubscriberManager {
|
|
|
186
198
|
launch_mode: launchMode,
|
|
187
199
|
};
|
|
188
200
|
|
|
201
|
+
const terminalApp = options.terminalApp || detectTerminalAppFromEnv();
|
|
202
|
+
if (terminalApp) {
|
|
203
|
+
this.busData.agents[subscriber].terminal_app = terminalApp;
|
|
204
|
+
}
|
|
205
|
+
|
|
189
206
|
// 如果传入了 providerSessionId(从旧 session 恢复),设置它
|
|
190
207
|
if (options.providerSessionId) {
|
|
191
208
|
this.busData.agents[subscriber].provider_session_id = options.providerSessionId;
|
|
@@ -284,10 +301,9 @@ class SubscriberManager {
|
|
|
284
301
|
if (!this.busData.agents) return;
|
|
285
302
|
|
|
286
303
|
for (const [id, meta] of Object.entries(this.busData.agents)) {
|
|
287
|
-
if (meta.status
|
|
288
|
-
// PID 已死则直接标记 inactive(不依赖 tty 检测,因为 tty 可能被新 agent 复用)
|
|
289
|
-
if (meta.pid && !isAgentPidAlive(meta.pid)) {
|
|
304
|
+
if (meta.status === "active" && !isMetaActive(meta)) {
|
|
290
305
|
meta.status = "inactive";
|
|
306
|
+
meta.last_seen = getTimestamp();
|
|
291
307
|
}
|
|
292
308
|
}
|
|
293
309
|
}
|
package/src/bus/utils.js
CHANGED
|
@@ -86,7 +86,7 @@ function isAgentPidAlive(pid) {
|
|
|
86
86
|
if (!isPidAlive(pid)) return false;
|
|
87
87
|
const cmd = getPidCommand(pid);
|
|
88
88
|
if (!cmd) return true;
|
|
89
|
-
return /(claude|codex|node)/i.test(cmd);
|
|
89
|
+
return /(claude|codex|node|pi-mono|ufoo|ucode)/i.test(cmd);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
@@ -150,7 +150,7 @@ function getTtyProcessInfo(ttyPath) {
|
|
|
150
150
|
return { alive: false, idle: false, hasAgent: false, shellPid: 0, processes: [] };
|
|
151
151
|
}
|
|
152
152
|
const shellNames = new Set(["zsh", "bash", "fish", "sh", "login"]);
|
|
153
|
-
const hasAgent = processes.some((p) => /(codex|claude|node)/i.test(p.comm));
|
|
153
|
+
const hasAgent = processes.some((p) => /(codex|claude|node|pi-mono|ufoo|ucode)/i.test(p.comm));
|
|
154
154
|
const nonShell = processes.filter((p) => !shellNames.has(p.comm));
|
|
155
155
|
const idle = !hasAgent && nonShell.length === 0;
|
|
156
156
|
const shell = processes.find((p) => shellNames.has(p.comm));
|
|
@@ -184,7 +184,8 @@ function appendFileAtomic(filePath, content) {
|
|
|
184
184
|
*/
|
|
185
185
|
function writeFileAtomic(filePath, content) {
|
|
186
186
|
ensureDir(path.dirname(filePath));
|
|
187
|
-
|
|
187
|
+
// Include pid + random suffix to avoid collisions across concurrent writers in the same millisecond.
|
|
188
|
+
const tmpFile = `${filePath}.tmp.${process.pid}.${Date.now()}.${crypto.randomBytes(4).toString("hex")}`;
|
|
188
189
|
fs.writeFileSync(tmpFile, content, "utf8");
|
|
189
190
|
fs.renameSync(tmpFile, filePath);
|
|
190
191
|
}
|
|
@@ -254,6 +255,10 @@ function truncateFile(filePath) {
|
|
|
254
255
|
}
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
function sleep(ms) {
|
|
259
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
260
|
+
}
|
|
261
|
+
|
|
257
262
|
/**
|
|
258
263
|
* 日志输出(带颜色)
|
|
259
264
|
*/
|
|
@@ -349,6 +354,7 @@ module.exports = {
|
|
|
349
354
|
appendJSONL,
|
|
350
355
|
readLastLine,
|
|
351
356
|
truncateFile,
|
|
357
|
+
sleep,
|
|
352
358
|
logInfo,
|
|
353
359
|
logOk,
|
|
354
360
|
logWarn,
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const { stripAnsi, truncateAnsi } = require("./text");
|
|
2
|
+
|
|
3
|
+
function computeAgentBar(options = {}) {
|
|
4
|
+
const {
|
|
5
|
+
cols = 80,
|
|
6
|
+
hintText = "",
|
|
7
|
+
focusMode = "input",
|
|
8
|
+
selectedAgentIndex = -1,
|
|
9
|
+
activeAgents = [],
|
|
10
|
+
viewingAgent = null,
|
|
11
|
+
agentListWindowStart = 0,
|
|
12
|
+
maxAgentWindow = 4,
|
|
13
|
+
getAgentLabel = (id) => id,
|
|
14
|
+
} = options;
|
|
15
|
+
|
|
16
|
+
const hintAnsi = `\x1b[90m│ ${hintText}\x1b[0m`;
|
|
17
|
+
const selectionIndex = focusMode === "dashboard"
|
|
18
|
+
? (selectedAgentIndex > 0 ? selectedAgentIndex - 1 : -1)
|
|
19
|
+
: activeAgents.indexOf(viewingAgent);
|
|
20
|
+
const maxAgentLen = Math.max(0, cols - 1 - 2 - stripAnsi(hintAnsi).length);
|
|
21
|
+
let windowItems = Math.max(1, Math.min(maxAgentWindow, activeAgents.length));
|
|
22
|
+
let start = agentListWindowStart;
|
|
23
|
+
const ufooItem = focusMode === "dashboard" && selectedAgentIndex === 0
|
|
24
|
+
? "\x1b[90;7mufoo\x1b[0m"
|
|
25
|
+
: "\x1b[36mufoo\x1b[0m";
|
|
26
|
+
const ufooLen = stripAnsi(ufooItem).length;
|
|
27
|
+
|
|
28
|
+
const computeStart = (items) => {
|
|
29
|
+
if (activeAgents.length === 0) return 0;
|
|
30
|
+
let s = start;
|
|
31
|
+
if (selectionIndex >= 0) {
|
|
32
|
+
if (selectionIndex < s) {
|
|
33
|
+
s = selectionIndex;
|
|
34
|
+
} else if (selectionIndex >= s + items) {
|
|
35
|
+
s = selectionIndex - items + 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const maxStart = Math.max(0, activeAgents.length - items);
|
|
39
|
+
if (s > maxStart) s = maxStart;
|
|
40
|
+
if (s < 0) s = 0;
|
|
41
|
+
return s;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const truncateLabel = (label, maxLen) => {
|
|
45
|
+
const text = String(label || "");
|
|
46
|
+
if (maxLen <= 0) return "";
|
|
47
|
+
if (text.length <= maxLen) return text;
|
|
48
|
+
if (maxLen <= 3) return text.slice(0, maxLen);
|
|
49
|
+
return `${text.slice(0, maxLen - 3)}...`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const buildAgentSegment = (items, maxLabelLen) => {
|
|
53
|
+
const s = computeStart(items);
|
|
54
|
+
const e = s + items;
|
|
55
|
+
const visible = activeAgents.slice(s, e);
|
|
56
|
+
const leftMore = s > 0 ? "\x1b[90m<\x1b[0m " : "";
|
|
57
|
+
const rightMore = e < activeAgents.length ? " \x1b[90m>\x1b[0m" : "";
|
|
58
|
+
let agentParts = [];
|
|
59
|
+
if (activeAgents.length > 0) {
|
|
60
|
+
agentParts = visible.map((agent, i) => {
|
|
61
|
+
const rawLabel = getAgentLabel(agent);
|
|
62
|
+
const label = maxLabelLen ? truncateLabel(rawLabel, maxLabelLen) : rawLabel;
|
|
63
|
+
const idx = s + i + 1; // +1 for ufoo at index 0
|
|
64
|
+
if (focusMode === "dashboard" && idx === selectedAgentIndex) {
|
|
65
|
+
return `\x1b[90;7m${label}\x1b[0m`;
|
|
66
|
+
}
|
|
67
|
+
if (agent === viewingAgent) return `\x1b[1;36m${label}\x1b[0m`;
|
|
68
|
+
return `\x1b[36m${label}\x1b[0m`;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const agentsText = activeAgents.length > 0
|
|
72
|
+
? `${leftMore}${agentParts.join(" ")}${rightMore}`
|
|
73
|
+
: "\x1b[36mnone\x1b[0m";
|
|
74
|
+
return { segment: `${ufooItem} ${agentsText}`, start: s };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let segmentInfo = buildAgentSegment(windowItems, 0);
|
|
78
|
+
while (windowItems > 0) {
|
|
79
|
+
const s = computeStart(windowItems);
|
|
80
|
+
const e = s + windowItems;
|
|
81
|
+
const hasLeft = s > 0;
|
|
82
|
+
const hasRight = e < activeAgents.length;
|
|
83
|
+
const spacingLen = windowItems > 1 ? (windowItems - 1) * 2 : 0;
|
|
84
|
+
const overhead = ufooLen + 2 + (hasLeft ? 2 : 0) + (hasRight ? 2 : 0) + spacingLen;
|
|
85
|
+
const available = maxAgentLen - overhead;
|
|
86
|
+
let maxLabelLen = windowItems > 0 ? Math.floor(available / windowItems) : 0;
|
|
87
|
+
if (windowItems > 1 && maxLabelLen < 3) {
|
|
88
|
+
windowItems -= 1;
|
|
89
|
+
segmentInfo = buildAgentSegment(windowItems, 0);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (maxLabelLen < 1) maxLabelLen = 1;
|
|
93
|
+
segmentInfo = buildAgentSegment(windowItems, maxLabelLen);
|
|
94
|
+
if (stripAnsi(segmentInfo.segment).length <= maxAgentLen || windowItems === 1) break;
|
|
95
|
+
windowItems -= 1;
|
|
96
|
+
segmentInfo = buildAgentSegment(windowItems, 0);
|
|
97
|
+
}
|
|
98
|
+
start = segmentInfo.start;
|
|
99
|
+
const agentSegment = segmentInfo.segment;
|
|
100
|
+
|
|
101
|
+
let bar = ` ${agentSegment} ${hintAnsi}`;
|
|
102
|
+
let barLen = stripAnsi(bar).length;
|
|
103
|
+
if (barLen > cols) {
|
|
104
|
+
bar = truncateAnsi(bar, cols);
|
|
105
|
+
barLen = stripAnsi(bar).length;
|
|
106
|
+
}
|
|
107
|
+
const pad = Math.max(0, cols - barLen);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
bar: `${bar}${" ".repeat(pad)}`,
|
|
111
|
+
windowStart: start,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
computeAgentBar,
|
|
117
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function buildAgentMaps(activeAgents = [], metaList = [], fallbackMap = null) {
|
|
2
|
+
const labelMap = new Map();
|
|
3
|
+
const metaMap = new Map();
|
|
4
|
+
const metaById = new Map();
|
|
5
|
+
|
|
6
|
+
for (const meta of metaList) {
|
|
7
|
+
if (!meta || !meta.id) continue;
|
|
8
|
+
metaById.set(meta.id, meta);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
for (const id of activeAgents) {
|
|
12
|
+
const meta = metaById.get(id);
|
|
13
|
+
const label = meta && meta.nickname
|
|
14
|
+
? meta.nickname
|
|
15
|
+
: (fallbackMap && fallbackMap.get(id)) || id;
|
|
16
|
+
labelMap.set(id, label);
|
|
17
|
+
if (meta) {
|
|
18
|
+
metaMap.set(id, meta);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { labelMap, metaMap };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getAgentLabel(labelMap, agentId) {
|
|
26
|
+
return labelMap.get(agentId) || agentId;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveAgentId({ label, activeAgents = [], labelMap = new Map(), lookupNickname = null }) {
|
|
30
|
+
if (!label) return null;
|
|
31
|
+
if (activeAgents.includes(label)) return label;
|
|
32
|
+
|
|
33
|
+
for (const [id, name] of labelMap.entries()) {
|
|
34
|
+
if (name === label) return id;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof lookupNickname === "function") {
|
|
38
|
+
const resolved = lookupNickname(label);
|
|
39
|
+
if (resolved) return resolved;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveAgentDisplayName({ publisher, labelMap = new Map(), lookupNicknameById = null }) {
|
|
46
|
+
let displayName = publisher;
|
|
47
|
+
if (publisher && publisher.includes(":")) {
|
|
48
|
+
if (labelMap && labelMap.has(publisher)) {
|
|
49
|
+
displayName = labelMap.get(publisher);
|
|
50
|
+
} else if (typeof lookupNicknameById === "function") {
|
|
51
|
+
const resolved = lookupNicknameById(publisher);
|
|
52
|
+
if (resolved) displayName = resolved;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return displayName;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clampAgentWindowWithSelection({
|
|
59
|
+
activeCount = 0,
|
|
60
|
+
maxWindow = 4,
|
|
61
|
+
windowStart = 0,
|
|
62
|
+
selectionIndex = -1,
|
|
63
|
+
}) {
|
|
64
|
+
if (activeCount <= 0) {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const maxItems = Math.max(1, Math.min(maxWindow, activeCount));
|
|
68
|
+
let nextStart = windowStart;
|
|
69
|
+
if (selectionIndex >= 0) {
|
|
70
|
+
if (selectionIndex < nextStart) {
|
|
71
|
+
nextStart = selectionIndex;
|
|
72
|
+
} else if (selectionIndex >= nextStart + maxItems) {
|
|
73
|
+
nextStart = selectionIndex - maxItems + 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const maxStart = Math.max(0, activeCount - maxItems);
|
|
77
|
+
if (nextStart > maxStart) nextStart = maxStart;
|
|
78
|
+
if (nextStart < 0) nextStart = 0;
|
|
79
|
+
return nextStart;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
buildAgentMaps,
|
|
84
|
+
getAgentLabel,
|
|
85
|
+
resolveAgentId,
|
|
86
|
+
resolveAgentDisplayName,
|
|
87
|
+
clampAgentWindowWithSelection,
|
|
88
|
+
};
|