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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
|
|
6
|
+
function defaultTokensPath() {
|
|
7
|
+
return path.join(os.homedir(), ".ufoo", "online", "tokens.json");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizeTokensData(raw) {
|
|
11
|
+
if (!raw || typeof raw !== "object") {
|
|
12
|
+
return { agents: {} };
|
|
13
|
+
}
|
|
14
|
+
if (raw.agents && typeof raw.agents === "object") {
|
|
15
|
+
return { agents: raw.agents };
|
|
16
|
+
}
|
|
17
|
+
// Legacy: flat object mapping id -> token
|
|
18
|
+
return { agents: raw };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function generateToken(bytes = 24) {
|
|
22
|
+
return crypto.randomBytes(bytes).toString("base64url");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hashToken(token) {
|
|
26
|
+
if (!token) return "";
|
|
27
|
+
return crypto.createHash("sha256").update(String(token)).digest("hex");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadTokens(filePath = defaultTokensPath()) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
return normalizeTokensData(parsed);
|
|
35
|
+
} catch {
|
|
36
|
+
return { agents: {} };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function saveTokens(filePath = defaultTokensPath(), data = { agents: {} }) {
|
|
41
|
+
const dir = path.dirname(filePath);
|
|
42
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
43
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setToken(filePath, agentId, token, server = "", extra = {}) {
|
|
47
|
+
if (!agentId || !token) throw new Error("agentId and token are required");
|
|
48
|
+
const data = loadTokens(filePath);
|
|
49
|
+
data.agents[agentId] = {
|
|
50
|
+
token,
|
|
51
|
+
token_hash: hashToken(token),
|
|
52
|
+
server,
|
|
53
|
+
nickname: extra.nickname || data.agents[agentId]?.nickname || "",
|
|
54
|
+
updated_at: new Date().toISOString(),
|
|
55
|
+
};
|
|
56
|
+
saveTokens(filePath, data);
|
|
57
|
+
return data.agents[agentId];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function removeToken(filePath, agentId) {
|
|
61
|
+
const data = loadTokens(filePath);
|
|
62
|
+
if (data.agents[agentId]) {
|
|
63
|
+
delete data.agents[agentId];
|
|
64
|
+
saveTokens(filePath, data);
|
|
65
|
+
}
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getToken(filePath, agentId) {
|
|
70
|
+
const data = loadTokens(filePath);
|
|
71
|
+
return data.agents[agentId] || null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getTokenByNickname(filePath, nickname) {
|
|
75
|
+
const data = loadTokens(filePath);
|
|
76
|
+
let best = null;
|
|
77
|
+
for (const entry of Object.values(data.agents)) {
|
|
78
|
+
if (entry.nickname === nickname) {
|
|
79
|
+
if (!best || (entry.updated_at || "") > (best.updated_at || "")) {
|
|
80
|
+
best = entry;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return best;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function listTokens(filePath) {
|
|
88
|
+
const data = loadTokens(filePath);
|
|
89
|
+
return Object.entries(data.agents).map(([id, entry]) => ({ id, ...entry }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
defaultTokensPath,
|
|
94
|
+
generateToken,
|
|
95
|
+
hashToken,
|
|
96
|
+
loadTokens,
|
|
97
|
+
saveTokens,
|
|
98
|
+
setToken,
|
|
99
|
+
removeToken,
|
|
100
|
+
getToken,
|
|
101
|
+
getTokenByNickname,
|
|
102
|
+
listTokens,
|
|
103
|
+
};
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
4
|
+
|
|
5
|
+
const REPORT_PHASES = {
|
|
6
|
+
START: "start",
|
|
7
|
+
PROGRESS: "progress",
|
|
8
|
+
DONE: "done",
|
|
9
|
+
ERROR: "error",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function getReportPaths(projectRoot) {
|
|
13
|
+
const { agentDir } = getUfooPaths(projectRoot);
|
|
14
|
+
return {
|
|
15
|
+
reportsFile: path.join(agentDir, "reports.jsonl"),
|
|
16
|
+
stateFile: path.join(agentDir, "report-state.json"),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ensureReportDir(projectRoot) {
|
|
21
|
+
const { agentDir } = getUfooPaths(projectRoot);
|
|
22
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizePhase(value = "") {
|
|
26
|
+
const phase = String(value || "").trim().toLowerCase();
|
|
27
|
+
if (phase === REPORT_PHASES.START) return REPORT_PHASES.START;
|
|
28
|
+
if (phase === REPORT_PHASES.PROGRESS) return REPORT_PHASES.PROGRESS;
|
|
29
|
+
if (phase === REPORT_PHASES.ERROR) return REPORT_PHASES.ERROR;
|
|
30
|
+
return REPORT_PHASES.DONE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeScope(value = "") {
|
|
34
|
+
const scope = String(value || "").trim().toLowerCase();
|
|
35
|
+
if (scope === "private") return "private";
|
|
36
|
+
return "public";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeReportInput(input = {}, options = {}) {
|
|
40
|
+
const ts = options.ts || new Date().toISOString();
|
|
41
|
+
let phase = normalizePhase(input.phase || options.phase);
|
|
42
|
+
const entryId = String(
|
|
43
|
+
input.entry_id
|
|
44
|
+
|| input.entryId
|
|
45
|
+
|| options.entry_id
|
|
46
|
+
|| `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`
|
|
47
|
+
).trim();
|
|
48
|
+
|
|
49
|
+
const taskId = String(
|
|
50
|
+
input.task_id
|
|
51
|
+
|| input.taskId
|
|
52
|
+
|| input.task
|
|
53
|
+
|| options.task_id
|
|
54
|
+
|| options.taskId
|
|
55
|
+
|| `task-${Date.now()}`
|
|
56
|
+
).trim();
|
|
57
|
+
const agentId = String(
|
|
58
|
+
input.agent_id
|
|
59
|
+
|| input.agentId
|
|
60
|
+
|| options.agent_id
|
|
61
|
+
|| options.agentId
|
|
62
|
+
|| "unknown-agent"
|
|
63
|
+
).trim() || "unknown-agent";
|
|
64
|
+
const message = String(input.message || "").trim();
|
|
65
|
+
const summary = String(input.summary || "").trim();
|
|
66
|
+
const error = String(input.error || "").trim();
|
|
67
|
+
const source = String(input.source || options.source || "cli").trim() || "cli";
|
|
68
|
+
const scope = normalizeScope(input.scope || options.scope);
|
|
69
|
+
const controllerId = String(
|
|
70
|
+
input.controller_id
|
|
71
|
+
|| input.controllerId
|
|
72
|
+
|| options.controller_id
|
|
73
|
+
|| options.controllerId
|
|
74
|
+
|| "ufoo-agent"
|
|
75
|
+
).trim() || "ufoo-agent";
|
|
76
|
+
const ok = input.ok !== false && phase !== REPORT_PHASES.ERROR && !error;
|
|
77
|
+
if (!ok && phase !== REPORT_PHASES.START && phase !== REPORT_PHASES.PROGRESS) {
|
|
78
|
+
phase = REPORT_PHASES.ERROR;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const meta = input.meta && typeof input.meta === "object" ? input.meta : {};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
entry_id: entryId,
|
|
85
|
+
ts,
|
|
86
|
+
phase,
|
|
87
|
+
task_id: taskId,
|
|
88
|
+
agent_id: agentId,
|
|
89
|
+
message,
|
|
90
|
+
summary,
|
|
91
|
+
error,
|
|
92
|
+
ok,
|
|
93
|
+
source,
|
|
94
|
+
scope,
|
|
95
|
+
controller_id: controllerId,
|
|
96
|
+
meta,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function appendReport(projectRoot, entry) {
|
|
101
|
+
ensureReportDir(projectRoot);
|
|
102
|
+
const { reportsFile } = getReportPaths(projectRoot);
|
|
103
|
+
fs.appendFileSync(reportsFile, `${JSON.stringify(entry)}\n`, "utf8");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseJsonLines(file) {
|
|
107
|
+
if (!fs.existsSync(file)) return [];
|
|
108
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
109
|
+
if (!raw.trim()) return [];
|
|
110
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
111
|
+
const rows = [];
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
try {
|
|
114
|
+
rows.push(JSON.parse(line));
|
|
115
|
+
} catch {
|
|
116
|
+
// ignore malformed lines
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return rows;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function listReports(projectRoot, options = {}) {
|
|
123
|
+
const { reportsFile } = getReportPaths(projectRoot);
|
|
124
|
+
const num = Number.isFinite(options.num) && options.num > 0 ? options.num : 20;
|
|
125
|
+
const filterAgent = String(options.agent || options.agent_id || "").trim();
|
|
126
|
+
let rows = parseJsonLines(reportsFile);
|
|
127
|
+
if (filterAgent) {
|
|
128
|
+
rows = rows.filter((row) => String(row.agent_id || "").trim() === filterAgent);
|
|
129
|
+
}
|
|
130
|
+
rows.sort((a, b) => {
|
|
131
|
+
const left = new Date(a.ts || 0).getTime();
|
|
132
|
+
const right = new Date(b.ts || 0).getTime();
|
|
133
|
+
return right - left;
|
|
134
|
+
});
|
|
135
|
+
return rows.slice(0, num);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function loadReportState(projectRoot) {
|
|
139
|
+
const { stateFile } = getReportPaths(projectRoot);
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(fs.readFileSync(stateFile, "utf8"));
|
|
142
|
+
if (!parsed || typeof parsed !== "object") return { updated_at: "", agents: {} };
|
|
143
|
+
if (!parsed.agents || typeof parsed.agents !== "object") parsed.agents = {};
|
|
144
|
+
if (typeof parsed.updated_at !== "string") parsed.updated_at = "";
|
|
145
|
+
return parsed;
|
|
146
|
+
} catch {
|
|
147
|
+
return { updated_at: "", agents: {} };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function saveReportState(projectRoot, state) {
|
|
152
|
+
ensureReportDir(projectRoot);
|
|
153
|
+
const { stateFile } = getReportPaths(projectRoot);
|
|
154
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function updateReportState(projectRoot, entry) {
|
|
158
|
+
const state = loadReportState(projectRoot);
|
|
159
|
+
const current = state.agents[entry.agent_id] && typeof state.agents[entry.agent_id] === "object"
|
|
160
|
+
? state.agents[entry.agent_id]
|
|
161
|
+
: {};
|
|
162
|
+
const pending = current.pending && typeof current.pending === "object"
|
|
163
|
+
? { ...current.pending }
|
|
164
|
+
: {};
|
|
165
|
+
|
|
166
|
+
if (entry.phase === REPORT_PHASES.START || entry.phase === REPORT_PHASES.PROGRESS) {
|
|
167
|
+
pending[entry.task_id] = entry;
|
|
168
|
+
} else {
|
|
169
|
+
delete pending[entry.task_id];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
state.agents[entry.agent_id] = {
|
|
173
|
+
...current,
|
|
174
|
+
pending,
|
|
175
|
+
pending_count: Object.keys(pending).length,
|
|
176
|
+
last: entry,
|
|
177
|
+
updated_at: entry.ts,
|
|
178
|
+
};
|
|
179
|
+
state.updated_at = entry.ts;
|
|
180
|
+
saveReportState(projectRoot, state);
|
|
181
|
+
return state;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isSummaryHiddenEntry(entry) {
|
|
185
|
+
if (!entry || typeof entry !== "object") return false;
|
|
186
|
+
const scope = normalizeScope(entry.scope);
|
|
187
|
+
if (scope !== "private") return false;
|
|
188
|
+
const controllerId = String(entry.controller_id || "").trim();
|
|
189
|
+
return controllerId === "ufoo-agent";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function controllerToSafeName(controllerId = "") {
|
|
193
|
+
return String(controllerId || "ufoo-agent")
|
|
194
|
+
.trim()
|
|
195
|
+
.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getControllerInboxFile(projectRoot, controllerId = "ufoo-agent") {
|
|
199
|
+
const { agentDir } = getUfooPaths(projectRoot);
|
|
200
|
+
const inboxDir = path.join(agentDir, "private-inbox");
|
|
201
|
+
const safe = controllerToSafeName(controllerId);
|
|
202
|
+
return path.join(inboxDir, `${safe}.jsonl`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function appendControllerInboxEntry(projectRoot, controllerId, entry) {
|
|
206
|
+
const file = getControllerInboxFile(projectRoot, controllerId);
|
|
207
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
208
|
+
fs.appendFileSync(file, `${JSON.stringify(entry)}\n`, "utf8");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function listControllerInboxEntries(projectRoot, controllerId = "ufoo-agent", options = {}) {
|
|
212
|
+
const file = getControllerInboxFile(projectRoot, controllerId);
|
|
213
|
+
const rows = parseJsonLines(file);
|
|
214
|
+
const num = Number.isFinite(options.num) && options.num > 0 ? options.num : rows.length;
|
|
215
|
+
if (rows.length <= num) return rows;
|
|
216
|
+
return rows.slice(rows.length - num);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function clearControllerInbox(projectRoot, controllerId = "ufoo-agent") {
|
|
220
|
+
const file = getControllerInboxFile(projectRoot, controllerId);
|
|
221
|
+
try {
|
|
222
|
+
if (fs.existsSync(file)) fs.rmSync(file, { force: true });
|
|
223
|
+
} catch {
|
|
224
|
+
// ignore clear errors
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function consumeControllerInboxEntries(projectRoot, controllerId = "ufoo-agent", consumed = []) {
|
|
229
|
+
const file = getControllerInboxFile(projectRoot, controllerId);
|
|
230
|
+
if (!fs.existsSync(file)) return { removed: 0, remaining: 0 };
|
|
231
|
+
const list = Array.isArray(consumed) ? consumed : [];
|
|
232
|
+
if (list.length === 0) {
|
|
233
|
+
const current = parseJsonLines(file);
|
|
234
|
+
return { removed: 0, remaining: current.length };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const idSet = new Set();
|
|
238
|
+
const legacySerialized = new Set();
|
|
239
|
+
for (const item of list) {
|
|
240
|
+
if (!item) continue;
|
|
241
|
+
if (typeof item === "string") {
|
|
242
|
+
idSet.add(item);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (typeof item === "object") {
|
|
246
|
+
if (item.entry_id) {
|
|
247
|
+
idSet.add(String(item.entry_id));
|
|
248
|
+
} else {
|
|
249
|
+
legacySerialized.add(JSON.stringify(item));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
255
|
+
const lines = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
256
|
+
if (lines.length === 0) {
|
|
257
|
+
return { removed: 0, remaining: 0 };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const keptLines = [];
|
|
261
|
+
let removed = 0;
|
|
262
|
+
for (const line of lines) {
|
|
263
|
+
let parsed = null;
|
|
264
|
+
try {
|
|
265
|
+
parsed = JSON.parse(line);
|
|
266
|
+
} catch {
|
|
267
|
+
parsed = null;
|
|
268
|
+
}
|
|
269
|
+
if (parsed && typeof parsed === "object") {
|
|
270
|
+
const id = parsed.entry_id ? String(parsed.entry_id) : "";
|
|
271
|
+
if (id && idSet.has(id)) {
|
|
272
|
+
removed += 1;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!id && legacySerialized.has(JSON.stringify(parsed))) {
|
|
276
|
+
removed += 1;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
keptLines.push(line);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (keptLines.length === 0) {
|
|
284
|
+
fs.rmSync(file, { force: true });
|
|
285
|
+
return { removed, remaining: 0 };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fs.writeFileSync(file, `${keptLines.join("\n")}\n`, "utf8");
|
|
289
|
+
return { removed, remaining: keptLines.length };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function readReportSummary(projectRoot) {
|
|
293
|
+
const state = loadReportState(projectRoot);
|
|
294
|
+
const agents = Object.entries(state.agents || {}).map(([agentId, value]) => {
|
|
295
|
+
const pending = value && value.pending && typeof value.pending === "object"
|
|
296
|
+
? Object.values(value.pending).filter((entry) => !isSummaryHiddenEntry(entry))
|
|
297
|
+
: [];
|
|
298
|
+
return {
|
|
299
|
+
agent_id: agentId,
|
|
300
|
+
pending_count: pending.length,
|
|
301
|
+
pending,
|
|
302
|
+
last: value && value.last ? value.last : null,
|
|
303
|
+
updated_at: value && value.updated_at ? value.updated_at : "",
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
updated_at: state.updated_at || "",
|
|
308
|
+
pending_total: agents.reduce((sum, item) => sum + item.pending_count, 0),
|
|
309
|
+
agents,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = {
|
|
314
|
+
REPORT_PHASES,
|
|
315
|
+
getReportPaths,
|
|
316
|
+
normalizePhase,
|
|
317
|
+
normalizeScope,
|
|
318
|
+
normalizeReportInput,
|
|
319
|
+
appendReport,
|
|
320
|
+
listReports,
|
|
321
|
+
loadReportState,
|
|
322
|
+
saveReportState,
|
|
323
|
+
updateReportState,
|
|
324
|
+
readReportSummary,
|
|
325
|
+
controllerToSafeName,
|
|
326
|
+
getControllerInboxFile,
|
|
327
|
+
appendControllerInboxEntry,
|
|
328
|
+
listControllerInboxEntries,
|
|
329
|
+
clearControllerInbox,
|
|
330
|
+
consumeControllerInboxEntries,
|
|
331
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const IPC_REQUEST_TYPES = {
|
|
4
|
+
STATUS: "status",
|
|
5
|
+
PROMPT: "prompt",
|
|
6
|
+
BUS_SEND: "bus_send",
|
|
7
|
+
CLOSE_AGENT: "close_agent",
|
|
8
|
+
LAUNCH_AGENT: "launch_agent",
|
|
9
|
+
RESUME_AGENTS: "resume_agents",
|
|
10
|
+
LIST_RECOVERABLE_AGENTS: "list_recoverable_agents",
|
|
11
|
+
REGISTER_AGENT: "register_agent",
|
|
12
|
+
AGENT_READY: "agent_ready",
|
|
13
|
+
AGENT_REPORT: "agent_report",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const IPC_RESPONSE_TYPES = {
|
|
17
|
+
STATUS: "status",
|
|
18
|
+
RESPONSE: "response",
|
|
19
|
+
BUS: "bus",
|
|
20
|
+
ERROR: "error",
|
|
21
|
+
BUS_SEND_OK: "bus_send_ok",
|
|
22
|
+
REGISTER_OK: "register_ok",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const BUS_STATUS_PHASES = {
|
|
26
|
+
START: "start",
|
|
27
|
+
DONE: "done",
|
|
28
|
+
ERROR: "error",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
IPC_REQUEST_TYPES,
|
|
33
|
+
IPC_RESPONSE_TYPES,
|
|
34
|
+
BUS_STATUS_PHASES,
|
|
35
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const PTY_SOCKET_MESSAGE_TYPES = {
|
|
4
|
+
OUTPUT: "output",
|
|
5
|
+
REPLAY: "replay",
|
|
6
|
+
SNAPSHOT: "snapshot",
|
|
7
|
+
SUBSCRIBED: "subscribed",
|
|
8
|
+
SUBSCRIBE: "subscribe",
|
|
9
|
+
RAW: "raw",
|
|
10
|
+
RESIZE: "resize",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const PTY_SOCKET_SUBSCRIBE_MODES = {
|
|
14
|
+
FULL: "full",
|
|
15
|
+
SCREEN: "screen",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
PTY_SOCKET_MESSAGE_TYPES,
|
|
20
|
+
PTY_SOCKET_SUBSCRIBE_MODES,
|
|
21
|
+
};
|
package/src/status/index.js
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const childProcess = require("child_process");
|
|
3
4
|
const { readJSON } = require("../bus/utils");
|
|
4
5
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
5
6
|
|
|
7
|
+
function normalizeTty(ttyPath) {
|
|
8
|
+
if (!ttyPath) return "";
|
|
9
|
+
const trimmed = String(ttyPath).trim();
|
|
10
|
+
if (!trimmed || trimmed === "not a tty") return "";
|
|
11
|
+
if (trimmed === "/dev/tty") return "";
|
|
12
|
+
return trimmed;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function tryTtyWithFd(fd) {
|
|
16
|
+
try {
|
|
17
|
+
const res = childProcess.spawnSync("tty", {
|
|
18
|
+
stdio: [fd, "pipe", "ignore"],
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
});
|
|
21
|
+
if (res && res.status === 0) {
|
|
22
|
+
const tty = normalizeTty(res.stdout || "");
|
|
23
|
+
if (tty) return tty;
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// ignore
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function detectCurrentTty() {
|
|
32
|
+
const stdinTtyPath = normalizeTty(process.stdin?.ttyPath || "");
|
|
33
|
+
if (stdinTtyPath) return stdinTtyPath;
|
|
34
|
+
|
|
35
|
+
const fromStdin = tryTtyWithFd(0);
|
|
36
|
+
if (fromStdin) return fromStdin;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const fd = fs.openSync("/dev/tty", "r");
|
|
40
|
+
const fromTty = tryTtyWithFd(fd);
|
|
41
|
+
fs.closeSync(fd);
|
|
42
|
+
if (fromTty) return fromTty;
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
6
50
|
/**
|
|
7
51
|
* 显示项目状态
|
|
8
52
|
*/
|
|
@@ -38,12 +82,7 @@ class StatusDisplay {
|
|
|
38
82
|
}
|
|
39
83
|
|
|
40
84
|
// 尝试通过 tty 查找订阅者
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
currentTty = fs.readFileSync("/dev/tty", "utf8").trim();
|
|
44
|
-
} catch {
|
|
45
|
-
// tty 不可用
|
|
46
|
-
}
|
|
85
|
+
const currentTty = detectCurrentTty();
|
|
47
86
|
|
|
48
87
|
if (currentTty && currentTty.startsWith("/dev/")) {
|
|
49
88
|
const busData = readJSON(agentsFile);
|
|
@@ -198,21 +237,15 @@ class StatusDisplay {
|
|
|
198
237
|
}
|
|
199
238
|
|
|
200
239
|
/**
|
|
201
|
-
*
|
|
240
|
+
* 显示横幅
|
|
202
241
|
*/
|
|
203
242
|
showBanner(subscriber) {
|
|
204
|
-
|
|
205
|
-
|
|
243
|
+
console.log("=== ufoo status ===");
|
|
244
|
+
if (subscriber) {
|
|
245
|
+
console.log(`Agent: ${subscriber}`);
|
|
246
|
+
} else {
|
|
206
247
|
console.log();
|
|
207
|
-
return;
|
|
208
248
|
}
|
|
209
|
-
|
|
210
|
-
const { showBanner } = require("../utils/banner");
|
|
211
|
-
const agentType = subscriber.startsWith("codex:") ? "codex" : "claude";
|
|
212
|
-
const sessionId = subscriber.split(":")[1] || "unknown";
|
|
213
|
-
const nickname = this.getSubscriberNickname(subscriber);
|
|
214
|
-
|
|
215
|
-
showBanner({ agentType, sessionId, nickname });
|
|
216
249
|
}
|
|
217
250
|
|
|
218
251
|
/**
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const TERMINAL_CAPABILITY_KEYS = [
|
|
2
|
+
"supportsActivate",
|
|
3
|
+
"supportsSubscribeFull",
|
|
4
|
+
"supportsSubscribeScreen",
|
|
5
|
+
"supportsSnapshot",
|
|
6
|
+
"supportsReplay",
|
|
7
|
+
"supportsWindowClose",
|
|
8
|
+
"supportsSocketProtocol",
|
|
9
|
+
"supportsNotifierInjector",
|
|
10
|
+
"supportsInternalQueueLoop",
|
|
11
|
+
"supportsRestartFallback",
|
|
12
|
+
"supportsSessionReuse",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const TERMINAL_ADAPTER_METHODS = [
|
|
16
|
+
"connect",
|
|
17
|
+
"disconnect",
|
|
18
|
+
"send",
|
|
19
|
+
"sendRaw",
|
|
20
|
+
"resize",
|
|
21
|
+
"snapshot",
|
|
22
|
+
"subscribe",
|
|
23
|
+
"activate",
|
|
24
|
+
"getState",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function createTerminalCapabilities(overrides = {}) {
|
|
28
|
+
const capabilities = {};
|
|
29
|
+
for (const key of TERMINAL_CAPABILITY_KEYS) {
|
|
30
|
+
capabilities[key] = false;
|
|
31
|
+
}
|
|
32
|
+
return { ...capabilities, ...overrides };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertTerminalCapabilities(capabilities) {
|
|
36
|
+
if (!capabilities || typeof capabilities !== "object") {
|
|
37
|
+
throw new Error("TerminalAdapter capabilities must be an object");
|
|
38
|
+
}
|
|
39
|
+
for (const key of TERMINAL_CAPABILITY_KEYS) {
|
|
40
|
+
if (!(key in capabilities)) {
|
|
41
|
+
throw new Error(`TerminalAdapter capabilities missing: ${key}`);
|
|
42
|
+
}
|
|
43
|
+
if (typeof capabilities[key] !== "boolean") {
|
|
44
|
+
throw new Error(`TerminalAdapter capability must be boolean: ${key}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function createUnsupportedCapabilityError(capability, operation) {
|
|
51
|
+
const suffix = operation ? ` (operation: ${operation})` : "";
|
|
52
|
+
const err = new Error(`TerminalAdapter capability unsupported: ${capability}${suffix}`);
|
|
53
|
+
err.code = "UFOO_UNSUPPORTED_CAPABILITY";
|
|
54
|
+
err.capability = capability;
|
|
55
|
+
err.operation = operation || null;
|
|
56
|
+
return err;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function requireCapability(capabilities, capability, operation) {
|
|
60
|
+
if (!capabilities || !capabilities[capability]) {
|
|
61
|
+
throw createUnsupportedCapabilityError(capability, operation);
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertTerminalAdapterContract(adapter) {
|
|
67
|
+
if (!adapter || typeof adapter !== "object") {
|
|
68
|
+
throw new Error("TerminalAdapter must be an object");
|
|
69
|
+
}
|
|
70
|
+
for (const method of TERMINAL_ADAPTER_METHODS) {
|
|
71
|
+
if (typeof adapter[method] !== "function") {
|
|
72
|
+
throw new Error(`TerminalAdapter missing method: ${method}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
assertTerminalCapabilities(adapter.capabilities);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
TERMINAL_CAPABILITY_KEYS,
|
|
81
|
+
TERMINAL_ADAPTER_METHODS,
|
|
82
|
+
createTerminalCapabilities,
|
|
83
|
+
assertTerminalCapabilities,
|
|
84
|
+
createUnsupportedCapabilityError,
|
|
85
|
+
requireCapability,
|
|
86
|
+
assertTerminalAdapterContract,
|
|
87
|
+
};
|