u-foo 1.0.3 → 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 +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -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 +46 -42
- 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 +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- 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 +1011 -1392
- 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 +1162 -96
- 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 +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- 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 +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- 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/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- 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/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- package/scripts/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- package/scripts/status.sh +0 -125
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const EventBus = require("../bus");
|
|
5
|
+
const { loadAgentsData, saveAgentsData } = require("../ufoo/agentsStore");
|
|
6
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build probe marker using nickname (e.g., "claude-47")
|
|
10
|
+
* Simpler than the old token format, easier to search
|
|
11
|
+
*/
|
|
12
|
+
function buildProbeMarker(nickname) {
|
|
13
|
+
return nickname || "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build probe command:
|
|
18
|
+
* - claude-code: /ufoo <nickname>
|
|
19
|
+
* - codex: $ufoo <nickname>
|
|
20
|
+
*/
|
|
21
|
+
function buildProbeCommand(agentType, nickname) {
|
|
22
|
+
const marker = String(nickname || "").trim();
|
|
23
|
+
if (agentType === "claude-code") {
|
|
24
|
+
return `/ufoo ${marker}`;
|
|
25
|
+
}
|
|
26
|
+
return `$ufoo ${marker}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readLines(filePath) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
32
|
+
return raw.split(/\r?\n/).filter(Boolean);
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function escapeRegExp(value = "") {
|
|
39
|
+
return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function containsProbeCommand(text, marker) {
|
|
43
|
+
if (!text || !marker) return false;
|
|
44
|
+
const escapedMarker = escapeRegExp(marker);
|
|
45
|
+
const pattern = `(?:^|[\\s"'\\\`])(?:\\/ufoo|\\$ufoo|ufoo)\\s+${escapedMarker}(?=$|[\\s"'\\\`.,:;!?\\]\\)\\}])`;
|
|
46
|
+
const re = new RegExp(pattern);
|
|
47
|
+
return re.test(String(text));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a history record contains our probe marker
|
|
52
|
+
* Searches for probe marker command patterns:
|
|
53
|
+
* - "/ufoo <marker>" (claude)
|
|
54
|
+
* - "$ufoo <marker>" (codex)
|
|
55
|
+
* - "ufoo <marker>" (legacy compatibility)
|
|
56
|
+
*/
|
|
57
|
+
function recordContainsMarker(record, marker, rawLine) {
|
|
58
|
+
if (!marker) return false;
|
|
59
|
+
|
|
60
|
+
// Check raw line first (fastest)
|
|
61
|
+
if (containsProbeCommand(rawLine, marker)) return true;
|
|
62
|
+
|
|
63
|
+
if (!record || typeof record !== "object") return false;
|
|
64
|
+
|
|
65
|
+
// Check common fields where user input might appear
|
|
66
|
+
const fields = [
|
|
67
|
+
record.display, // history.jsonl uses "display" for user input
|
|
68
|
+
record.text,
|
|
69
|
+
record.prompt,
|
|
70
|
+
record.input,
|
|
71
|
+
record.message,
|
|
72
|
+
record.query,
|
|
73
|
+
record.content,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
for (const field of fields) {
|
|
77
|
+
if (containsProbeCommand(field, marker)) return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractSessionId(record, rawLine) {
|
|
83
|
+
if (record && typeof record === "object") {
|
|
84
|
+
return record.session_id || record.sessionId || record.session || "";
|
|
85
|
+
}
|
|
86
|
+
if (typeof rawLine === "string") {
|
|
87
|
+
const match = rawLine.match(/"session(?:_id|Id)"\s*:\s*"([^"]+)"/);
|
|
88
|
+
if (match && match[1]) return match[1];
|
|
89
|
+
}
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Find session ID in a history file by searching for the probe marker
|
|
95
|
+
*/
|
|
96
|
+
function findSessionInFile(filePath, marker) {
|
|
97
|
+
if (!filePath || !fs.existsSync(filePath)) return null;
|
|
98
|
+
const lines = readLines(filePath);
|
|
99
|
+
|
|
100
|
+
// Search from end (most recent first)
|
|
101
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
102
|
+
const line = lines[i];
|
|
103
|
+
|
|
104
|
+
// Quick check: line must contain the marker string
|
|
105
|
+
if (!line.includes(marker)) continue;
|
|
106
|
+
|
|
107
|
+
let record = null;
|
|
108
|
+
try {
|
|
109
|
+
record = JSON.parse(line);
|
|
110
|
+
} catch {
|
|
111
|
+
record = null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!recordContainsMarker(record, marker, line)) continue;
|
|
115
|
+
|
|
116
|
+
const sessionId = extractSessionId(record, line);
|
|
117
|
+
if (sessionId) {
|
|
118
|
+
return { sessionId, source: filePath };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getClaudeHistoryPath() {
|
|
125
|
+
return path.join(os.homedir(), ".claude", "history.jsonl");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getCodexHistoryPath() {
|
|
129
|
+
return path.join(os.homedir(), ".codex", "history.jsonl");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Search provider history for the probe marker and return session ID
|
|
134
|
+
*/
|
|
135
|
+
function resolveProviderSession(agentType, marker) {
|
|
136
|
+
if (agentType === "codex") {
|
|
137
|
+
return findSessionInFile(getCodexHistoryPath(), marker);
|
|
138
|
+
}
|
|
139
|
+
if (agentType === "claude-code") {
|
|
140
|
+
return findSessionInFile(getClaudeHistoryPath(), marker);
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Save probe marker to agents data (for debugging/tracking)
|
|
147
|
+
*/
|
|
148
|
+
function persistProbeMarker(projectRoot, subscriberId, marker) {
|
|
149
|
+
const filePath = getUfooPaths(projectRoot).agentsFile;
|
|
150
|
+
const data = loadAgentsData(filePath);
|
|
151
|
+
const meta = data.agents[subscriberId] || {};
|
|
152
|
+
data.agents[subscriberId] = {
|
|
153
|
+
...meta,
|
|
154
|
+
provider_session_probe: marker,
|
|
155
|
+
provider_session_updated_at: new Date().toISOString(),
|
|
156
|
+
};
|
|
157
|
+
saveAgentsData(filePath, data);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function persistProviderSession(projectRoot, subscriberId, payload) {
|
|
161
|
+
const filePath = getUfooPaths(projectRoot).agentsFile;
|
|
162
|
+
const data = loadAgentsData(filePath);
|
|
163
|
+
const meta = data.agents[subscriberId] || {};
|
|
164
|
+
data.agents[subscriberId] = {
|
|
165
|
+
...meta,
|
|
166
|
+
provider_session_id: payload.sessionId || "",
|
|
167
|
+
provider_session_source: payload.source || "",
|
|
168
|
+
provider_session_updated_at: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
saveAgentsData(filePath, data);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Retry searching for session ID with the given marker
|
|
175
|
+
*/
|
|
176
|
+
async function resolveWithRetries(agentType, marker, attempts = 12, intervalMs = 2000) {
|
|
177
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
178
|
+
const resolved = resolveProviderSession(agentType, marker);
|
|
179
|
+
if (resolved && resolved.sessionId) return resolved;
|
|
180
|
+
// eslint-disable-next-line no-await-in-loop
|
|
181
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
function loadProviderSessionCache(projectRoot) {
|
|
188
|
+
const filePath = getUfooPaths(projectRoot).agentsFile;
|
|
189
|
+
const data = loadAgentsData(filePath);
|
|
190
|
+
const cache = new Map();
|
|
191
|
+
for (const [id, meta] of Object.entries(data.agents || {})) {
|
|
192
|
+
if (meta && meta.provider_session_id) {
|
|
193
|
+
cache.set(id, {
|
|
194
|
+
sessionId: meta.provider_session_id,
|
|
195
|
+
source: meta.provider_session_source || "",
|
|
196
|
+
updated_at: meta.provider_session_updated_at || "",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return cache;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Execute probe: inject command and search for session ID
|
|
205
|
+
*/
|
|
206
|
+
async function executeProbe({
|
|
207
|
+
projectRoot,
|
|
208
|
+
subscriberId,
|
|
209
|
+
agentType,
|
|
210
|
+
nickname,
|
|
211
|
+
attempts = 15,
|
|
212
|
+
intervalMs = 2000,
|
|
213
|
+
onResolved = null,
|
|
214
|
+
}) {
|
|
215
|
+
const marker = buildProbeMarker(nickname);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const command = buildProbeCommand(agentType, nickname);
|
|
219
|
+
const bus = new EventBus(projectRoot);
|
|
220
|
+
bus.ensureBus();
|
|
221
|
+
await bus.inject(subscriberId, command);
|
|
222
|
+
} catch {
|
|
223
|
+
// ignore injection failures
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const resolved = await resolveWithRetries(agentType, marker, attempts, intervalMs);
|
|
227
|
+
if (resolved && resolved.sessionId) {
|
|
228
|
+
persistProviderSession(projectRoot, subscriberId, resolved);
|
|
229
|
+
if (typeof onResolved === "function") {
|
|
230
|
+
onResolved(subscriberId, resolved);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Schedule a provider session probe
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} options
|
|
239
|
+
* @param {string} options.projectRoot - Project root directory
|
|
240
|
+
* @param {string} options.subscriberId - Subscriber ID (e.g., "claude-code:abc123")
|
|
241
|
+
* @param {string} options.agentType - Agent type ("claude-code" or "codex")
|
|
242
|
+
* @param {string} options.nickname - Agent nickname (e.g., "claude-47")
|
|
243
|
+
* @param {number} options.delayMs - Delay before injection
|
|
244
|
+
* @param {number} options.attempts - Number of search attempts
|
|
245
|
+
* @param {number} options.intervalMs - Interval between attempts
|
|
246
|
+
* @param {Function} options.onResolved - Callback when session ID is found
|
|
247
|
+
*/
|
|
248
|
+
function scheduleProviderSessionProbe({
|
|
249
|
+
projectRoot,
|
|
250
|
+
subscriberId,
|
|
251
|
+
agentType,
|
|
252
|
+
nickname,
|
|
253
|
+
delayMs = 8000,
|
|
254
|
+
attempts = 15,
|
|
255
|
+
intervalMs = 2000,
|
|
256
|
+
onResolved = null,
|
|
257
|
+
}) {
|
|
258
|
+
if (!subscriberId || !agentType) return null;
|
|
259
|
+
if (agentType !== "codex" && agentType !== "claude-code") return null;
|
|
260
|
+
if (!nickname) return null;
|
|
261
|
+
|
|
262
|
+
const marker = buildProbeMarker(nickname);
|
|
263
|
+
persistProbeMarker(projectRoot, subscriberId, marker);
|
|
264
|
+
|
|
265
|
+
let executed = false;
|
|
266
|
+
let timer = null;
|
|
267
|
+
|
|
268
|
+
const execute = async () => {
|
|
269
|
+
if (executed) return;
|
|
270
|
+
executed = true;
|
|
271
|
+
if (timer) {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
timer = null;
|
|
274
|
+
}
|
|
275
|
+
await executeProbe({
|
|
276
|
+
projectRoot,
|
|
277
|
+
subscriberId,
|
|
278
|
+
agentType,
|
|
279
|
+
nickname,
|
|
280
|
+
attempts,
|
|
281
|
+
intervalMs,
|
|
282
|
+
onResolved,
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Schedule delayed execution (fallback)
|
|
287
|
+
timer = setTimeout(execute, delayMs);
|
|
288
|
+
|
|
289
|
+
// Return handle for early trigger
|
|
290
|
+
return {
|
|
291
|
+
subscriberId,
|
|
292
|
+
marker,
|
|
293
|
+
triggerNow: execute,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
scheduleProviderSessionProbe,
|
|
299
|
+
loadProviderSessionCache,
|
|
300
|
+
__private: {
|
|
301
|
+
buildProbeCommand,
|
|
302
|
+
recordContainsMarker,
|
|
303
|
+
containsProbeCommand,
|
|
304
|
+
escapeRegExp,
|
|
305
|
+
},
|
|
306
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const { BUS_STATUS_PHASES } = require("../shared/eventContract");
|
|
3
|
+
const {
|
|
4
|
+
REPORT_PHASES,
|
|
5
|
+
normalizeReportInput,
|
|
6
|
+
appendReport,
|
|
7
|
+
updateReportState,
|
|
8
|
+
appendControllerInboxEntry,
|
|
9
|
+
} = require("../report/store");
|
|
10
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
11
|
+
|
|
12
|
+
function resolveAgentDisplayName(projectRoot, agentId) {
|
|
13
|
+
if (!agentId) return "unknown-agent";
|
|
14
|
+
try {
|
|
15
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
16
|
+
const bus = JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
17
|
+
const meta = bus && bus.agents ? bus.agents[agentId] : null;
|
|
18
|
+
if (meta && typeof meta.nickname === "string" && meta.nickname.trim()) {
|
|
19
|
+
return meta.nickname.trim();
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// ignore
|
|
23
|
+
}
|
|
24
|
+
return agentId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toStatusPhase(reportPhase) {
|
|
28
|
+
if (reportPhase === REPORT_PHASES.START || reportPhase === REPORT_PHASES.PROGRESS) {
|
|
29
|
+
return BUS_STATUS_PHASES.START;
|
|
30
|
+
}
|
|
31
|
+
if (reportPhase === REPORT_PHASES.ERROR) return BUS_STATUS_PHASES.ERROR;
|
|
32
|
+
return BUS_STATUS_PHASES.DONE;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatStatusText(displayName, entry) {
|
|
36
|
+
if (entry.phase === REPORT_PHASES.START) {
|
|
37
|
+
const detail = entry.message || entry.summary || entry.task_id;
|
|
38
|
+
return `${displayName} ${detail}`;
|
|
39
|
+
}
|
|
40
|
+
if (entry.phase === REPORT_PHASES.PROGRESS) {
|
|
41
|
+
const detail = entry.message || entry.summary || entry.task_id;
|
|
42
|
+
return `${displayName} progress: ${detail}`;
|
|
43
|
+
}
|
|
44
|
+
if (entry.phase === REPORT_PHASES.ERROR) {
|
|
45
|
+
const detail = entry.error || entry.summary || entry.message || entry.task_id;
|
|
46
|
+
return `${displayName} failed: ${detail}`;
|
|
47
|
+
}
|
|
48
|
+
const detail = entry.summary || entry.message || entry.task_id;
|
|
49
|
+
return `${displayName} done: ${detail}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildReportStatus(entry, displayName) {
|
|
53
|
+
return {
|
|
54
|
+
phase: toStatusPhase(entry.phase),
|
|
55
|
+
key: `report:${entry.agent_id}:${entry.task_id}`,
|
|
56
|
+
text: formatStatusText(displayName, entry),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function publishToPrivateController(projectRoot, entry) {
|
|
61
|
+
if (!entry || !entry.controller_id) return;
|
|
62
|
+
appendControllerInboxEntry(projectRoot, entry.controller_id, entry);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function recordAgentReport({
|
|
66
|
+
projectRoot,
|
|
67
|
+
report,
|
|
68
|
+
onStatus = () => {},
|
|
69
|
+
log = () => {},
|
|
70
|
+
}) {
|
|
71
|
+
const entry = normalizeReportInput(report);
|
|
72
|
+
appendReport(projectRoot, entry);
|
|
73
|
+
const state = updateReportState(projectRoot, entry);
|
|
74
|
+
publishToPrivateController(projectRoot, entry);
|
|
75
|
+
const displayName = resolveAgentDisplayName(projectRoot, entry.agent_id);
|
|
76
|
+
if (entry.scope !== "private") {
|
|
77
|
+
onStatus(buildReportStatus(entry, displayName));
|
|
78
|
+
}
|
|
79
|
+
log(`report ${entry.phase} scope=${entry.scope} agent=${entry.agent_id} task=${entry.task_id}`);
|
|
80
|
+
return { entry, state };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
recordAgentReport,
|
|
85
|
+
resolveAgentDisplayName,
|
|
86
|
+
toStatusPhase,
|
|
87
|
+
formatStatusText,
|
|
88
|
+
buildReportStatus,
|
|
89
|
+
publishToPrivateController,
|
|
90
|
+
};
|
package/src/daemon/run.js
CHANGED
|
@@ -9,6 +9,8 @@ function runDaemonCli(argv) {
|
|
|
9
9
|
const provider = process.env.UFOO_AGENT_PROVIDER || config.agentProvider || "codex-cli";
|
|
10
10
|
const model =
|
|
11
11
|
process.env.UFOO_AGENT_MODEL || config.agentModel || (provider === "claude-cli" ? "opus" : "");
|
|
12
|
+
const resumeMode = process.env.UFOO_FORCE_RESUME === "1" ? "force" : "auto";
|
|
13
|
+
const launchMode = config.launchMode || "terminal";
|
|
12
14
|
|
|
13
15
|
if (cmd === "start" || cmd === "--start") {
|
|
14
16
|
if (isRunning(projectRoot)) return;
|
|
@@ -23,13 +25,41 @@ function runDaemonCli(argv) {
|
|
|
23
25
|
child.unref();
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
26
|
-
startDaemon({ projectRoot, provider, model });
|
|
28
|
+
startDaemon({ projectRoot, provider, model, resumeMode });
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
if (cmd === "stop" || cmd === "--stop") {
|
|
30
32
|
stopDaemon(projectRoot);
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
35
|
+
if (cmd === "restart" || cmd === "--restart") {
|
|
36
|
+
// Stop if running
|
|
37
|
+
if (isRunning(projectRoot)) {
|
|
38
|
+
stopDaemon(projectRoot);
|
|
39
|
+
// Wait for clean shutdown
|
|
40
|
+
let attempts = 0;
|
|
41
|
+
while (isRunning(projectRoot) && attempts < 50) {
|
|
42
|
+
attempts++;
|
|
43
|
+
require("child_process").spawnSync("sleep", ["0.1"]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Start fresh daemon
|
|
47
|
+
if (!process.env.UFOO_DAEMON_CHILD) {
|
|
48
|
+
const { spawn } = require("child_process");
|
|
49
|
+
const childEnv = { ...process.env, UFOO_DAEMON_CHILD: "1" };
|
|
50
|
+
const child = spawn(process.execPath, [path.join(__dirname, "..", "..", "bin", "ufoo.js"), "daemon", "start"], {
|
|
51
|
+
detached: true,
|
|
52
|
+
stdio: "ignore",
|
|
53
|
+
env: childEnv,
|
|
54
|
+
cwd: projectRoot,
|
|
55
|
+
});
|
|
56
|
+
child.unref();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Manual restart does not auto-resume; crash-recovery is handled on next auto start with stale lock detection.
|
|
60
|
+
startDaemon({ projectRoot, provider, model, resumeMode: "none" });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
33
63
|
if (cmd === "status" || cmd === "--status") {
|
|
34
64
|
const running = isRunning(projectRoot);
|
|
35
65
|
// eslint-disable-next-line no-console
|
package/src/daemon/status.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
4
|
+
const { isMetaActive } = require("../bus/utils");
|
|
5
|
+
const { readReportSummary } = require("../report/store");
|
|
3
6
|
|
|
4
7
|
function readBus(projectRoot) {
|
|
5
|
-
const busPath =
|
|
8
|
+
const busPath = getUfooPaths(projectRoot).agentsFile;
|
|
6
9
|
try {
|
|
7
10
|
return JSON.parse(fs.readFileSync(busPath, "utf8"));
|
|
8
11
|
} catch {
|
|
@@ -11,7 +14,9 @@ function readBus(projectRoot) {
|
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
function readDecisions(projectRoot) {
|
|
14
|
-
const
|
|
17
|
+
const DecisionsManager = require("../context/decisions");
|
|
18
|
+
const manager = new DecisionsManager(projectRoot);
|
|
19
|
+
const dir = manager.decisionsDir;
|
|
15
20
|
let open = 0;
|
|
16
21
|
try {
|
|
17
22
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
@@ -28,7 +33,7 @@ function readDecisions(projectRoot) {
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
function readUnread(projectRoot) {
|
|
31
|
-
const queuesDir =
|
|
36
|
+
const queuesDir = getUfooPaths(projectRoot).busQueuesDir;
|
|
32
37
|
let total = 0;
|
|
33
38
|
const perSubscriber = {};
|
|
34
39
|
try {
|
|
@@ -48,21 +53,51 @@ function readUnread(projectRoot) {
|
|
|
48
53
|
return { total, perSubscriber };
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
function
|
|
56
|
+
function isHiddenSubscriber(id, meta) {
|
|
57
|
+
if (!id) return false;
|
|
58
|
+
if (id === "ufoo-agent") return true;
|
|
59
|
+
if (meta && meta.nickname === "ufoo-agent") return true;
|
|
60
|
+
if (meta && meta.agent_type === "ufoo-agent") return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeCronTasks(raw = []) {
|
|
65
|
+
const items = Array.isArray(raw) ? raw : [];
|
|
66
|
+
return items.map((task) => ({
|
|
67
|
+
id: String(task && task.id ? task.id : ""),
|
|
68
|
+
intervalMs: Number(task && task.intervalMs ? task.intervalMs : 0) || 0,
|
|
69
|
+
interval: String(task && task.interval ? task.interval : ""),
|
|
70
|
+
targets: Array.isArray(task && task.targets) ? task.targets.slice() : [],
|
|
71
|
+
prompt: String(task && task.prompt ? task.prompt : ""),
|
|
72
|
+
summary: String(task && task.summary ? task.summary : ""),
|
|
73
|
+
createdAt: Number(task && task.createdAt ? task.createdAt : 0) || 0,
|
|
74
|
+
lastRunAt: Number(task && task.lastRunAt ? task.lastRunAt : 0) || 0,
|
|
75
|
+
tickCount: Number(task && task.tickCount ? task.tickCount : 0) || 0,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildStatus(projectRoot, options = {}) {
|
|
52
80
|
const bus = readBus(projectRoot);
|
|
53
81
|
const decisions = readDecisions(projectRoot);
|
|
54
82
|
const unread = readUnread(projectRoot);
|
|
55
|
-
const
|
|
83
|
+
const reports = readReportSummary(projectRoot);
|
|
84
|
+
const subscribers = bus ? Object.keys(bus.agents || {}) : [];
|
|
85
|
+
const cronTasks = normalizeCronTasks(options.cronTasks || []);
|
|
86
|
+
|
|
56
87
|
const activeEntries = bus
|
|
57
|
-
? Object.entries(bus.
|
|
58
|
-
.filter(([, meta]) => meta
|
|
88
|
+
? Object.entries(bus.agents || {})
|
|
89
|
+
.filter(([, meta]) => isMetaActive(meta))
|
|
90
|
+
.filter(([id, meta]) => !isHiddenSubscriber(id, meta))
|
|
59
91
|
.map(([id, meta]) => ({ id, meta }))
|
|
60
92
|
: [];
|
|
61
93
|
const active = activeEntries.map(({ id }) => id);
|
|
62
94
|
const activeMeta = activeEntries.map(({ id, meta }) => {
|
|
63
95
|
const nickname = meta?.nickname || "";
|
|
64
96
|
const display = nickname ? nickname : id;
|
|
65
|
-
|
|
97
|
+
const launch_mode = meta?.launch_mode || "unknown";
|
|
98
|
+
const tmux_pane = meta?.tmux_pane || "";
|
|
99
|
+
const tty = meta?.tty || "";
|
|
100
|
+
return { id, nickname, display, launch_mode, tmux_pane, tty };
|
|
66
101
|
});
|
|
67
102
|
|
|
68
103
|
return {
|
|
@@ -72,6 +107,11 @@ function buildStatus(projectRoot) {
|
|
|
72
107
|
active_meta: activeMeta,
|
|
73
108
|
unread,
|
|
74
109
|
decisions,
|
|
110
|
+
reports,
|
|
111
|
+
cron: {
|
|
112
|
+
count: cronTasks.length,
|
|
113
|
+
tasks: cronTasks,
|
|
114
|
+
},
|
|
75
115
|
};
|
|
76
116
|
}
|
|
77
117
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const ContextDoctor = require("../context/doctor");
|
|
4
|
+
|
|
5
|
+
class RepoDoctor {
|
|
6
|
+
constructor(repoRoot) {
|
|
7
|
+
this.repoRoot = repoRoot;
|
|
8
|
+
this.failed = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
fail(message) {
|
|
12
|
+
console.error(`FAIL: ${message}`);
|
|
13
|
+
this.failed = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
run() {
|
|
17
|
+
const contextMod = path.join(this.repoRoot, "modules", "context");
|
|
18
|
+
|
|
19
|
+
const contextExists = fs.existsSync(contextMod);
|
|
20
|
+
if (!contextExists) {
|
|
21
|
+
this.fail(`missing ${contextMod}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (contextExists) {
|
|
25
|
+
const contextDoctor = new ContextDoctor(this.repoRoot);
|
|
26
|
+
const ok = contextDoctor.lintProtocol();
|
|
27
|
+
if (!ok) this.failed = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log("=== ufoo doctor ===");
|
|
31
|
+
console.log(`Monorepo: ${this.repoRoot}`);
|
|
32
|
+
console.log("Modules:");
|
|
33
|
+
if (contextExists) {
|
|
34
|
+
console.log(`- context: ${contextMod}`);
|
|
35
|
+
}
|
|
36
|
+
const resources = path.join(this.repoRoot, "modules", "resources");
|
|
37
|
+
if (fs.existsSync(resources)) {
|
|
38
|
+
console.log(`- resources: ${resources}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (this.failed) {
|
|
42
|
+
console.log("Status: FAILED");
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
console.log("Status: OK");
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = RepoDoctor;
|