u-foo 2.5.6 → 2.5.7

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.
@@ -0,0 +1,504 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { execSync } = require("child_process");
4
+ const { createBusProgressReporter } = require("./taskDecomposer");
5
+ const { DeliveryQueue } = require("../coordination/bus/deliveryQueue");
6
+
7
+ function shellQuote(value = "") {
8
+ const text = String(value == null ? "" : value);
9
+ return `'${text.replace(/'/g, `'\"'\"'`)}'`;
10
+ }
11
+
12
+ function toText(value = "") {
13
+ if (typeof value === "string") return value;
14
+ if (Buffer.isBuffer(value)) return value.toString("utf8");
15
+ return String(value == null ? "" : value);
16
+ }
17
+
18
+ function stripAnsi(text = "") {
19
+ const raw = String(text || "");
20
+ if (!raw) return "";
21
+ // CSI + OSC sequences (best-effort).
22
+ return raw
23
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
24
+ .replace(/\x1b\][^\x07]*\x07/g, "")
25
+ .replace(/\x1b\][^\x1b]*(?:\x1b\\)/g, "");
26
+ }
27
+
28
+ // Bound every shell capture so a hung `ufoo bus` CLI cannot freeze the
29
+ // agent loop (autoBus re-invokes this every 800ms).
30
+ const SHELL_CAPTURE_TIMEOUT_MS = 15000;
31
+
32
+ function runShellCapture(command = "", workspaceRoot = process.cwd()) {
33
+ try {
34
+ const output = execSync(String(command || ""), {
35
+ cwd: workspaceRoot,
36
+ encoding: "utf8",
37
+ stdio: ["pipe", "pipe", "pipe"],
38
+ timeout: SHELL_CAPTURE_TIMEOUT_MS,
39
+ });
40
+ return {
41
+ ok: true,
42
+ output: toText(output),
43
+ error: "",
44
+ };
45
+ } catch (err) {
46
+ const stdout = toText(err && err.stdout);
47
+ const stderr = toText(err && err.stderr);
48
+ const detail = [stdout, stderr].filter(Boolean).join("\n").trim();
49
+ return {
50
+ ok: false,
51
+ output: detail,
52
+ error: detail || (err && err.message ? err.message : "shell command failed"),
53
+ };
54
+ }
55
+ }
56
+
57
+ function safeSubscriberName(subscriberId = "") {
58
+ return String(subscriberId || "").replace(/:/g, "_");
59
+ }
60
+
61
+ function resolvePendingQueueFile(workspaceRoot = process.cwd(), subscriberId = "") {
62
+ const root = String(workspaceRoot || process.cwd()).trim() || process.cwd();
63
+ const sub = String(subscriberId || "").trim();
64
+ if (!sub) return "";
65
+ return path.join(root, ".ufoo", "bus", "queues", safeSubscriberName(sub), "pending.jsonl");
66
+ }
67
+
68
+ function resolveUfooProjectRoot(preferredRoot = "", env = process.env) {
69
+ const candidates = [
70
+ String(preferredRoot || "").trim(),
71
+ String((env && env.UFOO_UCODE_PROJECT_ROOT) || "").trim(),
72
+ String((env && env.UFOO_PROJECT_ROOT) || "").trim(),
73
+ process.cwd(),
74
+ ].filter(Boolean);
75
+
76
+ for (const root of candidates) {
77
+ try {
78
+ const busDir = path.join(root, ".ufoo", "bus");
79
+ if (fs.existsSync(busDir)) return root;
80
+ } catch {
81
+ // ignore
82
+ }
83
+ }
84
+
85
+ return candidates[0] || process.cwd();
86
+ }
87
+
88
+ function countPendingQueueLines(filePath = "") {
89
+ const target = String(filePath || "").trim();
90
+ if (!target) return 0;
91
+ try {
92
+ if (!fs.existsSync(target)) return 0;
93
+ const content = String(fs.readFileSync(target, "utf8") || "");
94
+ if (!content.trim()) return 0;
95
+ return content.split(/\r?\n/).filter((line) => line.trim()).length;
96
+ } catch {
97
+ return 0;
98
+ }
99
+ }
100
+
101
+ function isPidAlive(pid) {
102
+ const p = parseInt(String(pid || "").trim(), 10);
103
+ if (!Number.isFinite(p) || p <= 0) return false;
104
+ try {
105
+ process.kill(p, 0);
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ function listProcessingFiles(pendingFilePath = "") {
113
+ const pendingFile = String(pendingFilePath || "").trim();
114
+ if (!pendingFile) return [];
115
+ const dir = path.dirname(pendingFile);
116
+ const base = path.basename(pendingFile);
117
+ const prefix = `${base}.processing.`;
118
+ try {
119
+ if (!fs.existsSync(dir)) return [];
120
+ return fs.readdirSync(dir)
121
+ .filter((name) => name && name.startsWith(prefix))
122
+ .map((name) => path.join(dir, name));
123
+ } catch {
124
+ return [];
125
+ }
126
+ }
127
+
128
+ function countRecoverableProcessingFiles(pendingFilePath = "", options = {}) {
129
+ const pendingFile = String(pendingFilePath || "").trim();
130
+ if (!pendingFile) return 0;
131
+ const maxAgeMs = Number.isFinite(options.maxAgeMs) ? options.maxAgeMs : 60000;
132
+ const now = Date.now();
133
+ const files = listProcessingFiles(pendingFile);
134
+ let count = 0;
135
+
136
+ for (const file of files) {
137
+ const name = path.basename(file);
138
+ const m = name.match(/\.processing\.(\d+)\./);
139
+ const pid = m ? parseInt(m[1], 10) : NaN;
140
+
141
+ if (Number.isFinite(pid) && pid > 0 && !isPidAlive(pid)) {
142
+ count += 1;
143
+ continue;
144
+ }
145
+
146
+ if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) continue;
147
+ try {
148
+ const stat = fs.statSync(file);
149
+ if (stat && stat.isFile() && (now - stat.mtimeMs > maxAgeMs)) {
150
+ count += 1;
151
+ }
152
+ } catch {
153
+ // ignore
154
+ }
155
+ }
156
+
157
+ return count;
158
+ }
159
+
160
+ function getPendingBusCount(workspaceRoot = process.cwd(), subscriberId = "") {
161
+ const pendingFile = resolvePendingQueueFile(workspaceRoot, subscriberId);
162
+ const pendingLines = countPendingQueueLines(pendingFile);
163
+ if (!pendingFile) return pendingLines;
164
+ // If a prior crash left `.processing.*` behind, count it so autoBus can self-heal.
165
+ const recoverable = countRecoverableProcessingFiles(pendingFile, { maxAgeMs: 60000 });
166
+ return pendingLines + recoverable;
167
+ }
168
+
169
+ function drainJsonlFile(filePath = "") {
170
+ const target = String(filePath || "").trim();
171
+ if (!target) return { drained: [], rawLines: [], error: "" };
172
+ const queue = new DeliveryQueue(target);
173
+ const drained = [];
174
+ const rawLines = [];
175
+ const claims = [];
176
+ try {
177
+ queue.recover();
178
+ while (true) {
179
+ const claim = queue.claimNext();
180
+ if (!claim) break;
181
+ claims.push(claim);
182
+ drained.push(claim.event);
183
+ rawLines.push(JSON.stringify(claim.event));
184
+ queue.completeClaim(claim);
185
+ }
186
+ } catch (err) {
187
+ for (const claim of claims) queue.restoreClaim(claim);
188
+ return { drained: [], rawLines: [], error: err && err.message ? err.message : "drain failed" };
189
+ }
190
+ return {
191
+ drained,
192
+ rawLines,
193
+ error: "",
194
+ claims,
195
+ processingFile: claims[0] ? claims[0].processingFile : "",
196
+ };
197
+ }
198
+
199
+ function extractTaskFromBusEvent(evt) {
200
+ if (!evt || typeof evt !== "object") return null;
201
+ if (String(evt.event || "").trim().toLowerCase() !== "message") return null;
202
+ let publisher = "";
203
+ if (typeof evt.publisher === "string") {
204
+ publisher = String(evt.publisher || "").trim();
205
+ } else if (evt.publisher && typeof evt.publisher === "object") {
206
+ publisher = String(evt.publisher.subscriber || evt.publisher.nickname || "").trim();
207
+ } else {
208
+ publisher = String(evt.publisher || "").trim();
209
+ }
210
+ if (publisher === "[object Object]") publisher = "";
211
+ if (!publisher) return null;
212
+ const data = evt.data && typeof evt.data === "object" ? evt.data : {};
213
+ const message = typeof data.message === "string"
214
+ ? data.message
215
+ : (typeof data.text === "string" ? data.text : "");
216
+ const task = String(message || "").trim();
217
+ if (!task) return null;
218
+ return { publisher, task };
219
+ }
220
+
221
+ function shouldAutoConsumeBus(subscriberId = "") {
222
+ const id = String(subscriberId || "").trim().toLowerCase();
223
+ if (!id) return false;
224
+ return id.startsWith("ufoo-code:")
225
+ || id.startsWith("ucode:")
226
+ || id.startsWith("ufoo:");
227
+ }
228
+
229
+ function extractBusMessageTask(contentRaw = "") {
230
+ const raw = String(contentRaw || "").trim();
231
+ if (!raw) return "";
232
+ try {
233
+ const parsed = JSON.parse(raw);
234
+ if (parsed && typeof parsed === "object") {
235
+ if (typeof parsed.message === "string" && parsed.message.trim()) return parsed.message.trim();
236
+ if (typeof parsed.text === "string" && parsed.text.trim()) return parsed.text.trim();
237
+ if (typeof parsed.prompt === "string" && parsed.prompt.trim()) return parsed.prompt.trim();
238
+ }
239
+ } catch {
240
+ // treat as plain text below
241
+ }
242
+ return raw;
243
+ }
244
+
245
+ function busCheckOutputIndicatesPending(raw = "") {
246
+ const text = stripAnsi(String(raw || ""));
247
+ if (!text.trim()) return false;
248
+ if (/no pending messages/i.test(text)) return false;
249
+ if (/you have\s+\d+\s+pending/i.test(text)) return true;
250
+ if (/after handling,\s*run:\s*ufoo bus ack/i.test(text)) return true;
251
+ if (/pending event/i.test(text)) return true;
252
+ return false;
253
+ }
254
+
255
+ function parseBusCheckOutput(raw = "") {
256
+ const text = stripAnsi(String(raw || ""));
257
+ if (!text.trim()) return [];
258
+ if (/no pending messages/i.test(text)) return [];
259
+
260
+ const lines = text.split(/\r?\n/);
261
+ const rows = [];
262
+ let current = null;
263
+
264
+ for (const line of lines) {
265
+ const trimmed = String(line || "").trim();
266
+ if (!trimmed) continue;
267
+
268
+ const header = trimmed.match(/^@.+\s+from\s+([^\s]+)\s*$/i);
269
+ if (header) {
270
+ if (current && current.publisher) rows.push(current);
271
+ current = {
272
+ publisher: String(header[1] || "").trim(),
273
+ content: "",
274
+ };
275
+ continue;
276
+ }
277
+
278
+ if (!current) continue;
279
+
280
+ const contentMatch = trimmed.match(/^content:\s*(.*)$/i);
281
+ if (contentMatch) {
282
+ current.content = String(contentMatch[1] || "").trim();
283
+ continue;
284
+ }
285
+
286
+ if (
287
+ current.content
288
+ && !/^(type|event|seq|target|timestamp):\s*/i.test(trimmed)
289
+ && !trimmed.startsWith("@")
290
+ ) {
291
+ current.content = `${current.content}\n${trimmed}`;
292
+ }
293
+ }
294
+
295
+ if (current && current.publisher) rows.push(current);
296
+
297
+ return rows
298
+ .map((entry) => {
299
+ const publisher = String(entry.publisher || "").trim();
300
+ const content = String(entry.content || "").trim();
301
+ const task = extractBusMessageTask(content);
302
+ if (!publisher || !task) return null;
303
+ return {
304
+ publisher,
305
+ content,
306
+ task,
307
+ };
308
+ })
309
+ .filter(Boolean);
310
+ }
311
+
312
+ async function runUbusCommand(state = {}, options = {}) {
313
+ const runtimeWorkspace = resolveUfooProjectRoot(String(
314
+ options.workspaceRoot
315
+ || (state && state.workspaceRoot)
316
+ || ""
317
+ ));
318
+ const shell = typeof options.execShell === "function"
319
+ ? options.execShell
320
+ : (command) => runShellCapture(command, runtimeWorkspace);
321
+ const runNl = typeof options.runNaturalLanguageTaskImpl === "function"
322
+ ? options.runNaturalLanguageTaskImpl
323
+ : require("./agent").runNaturalLanguageTask;
324
+ const formatNl = typeof options.formatNlResultImpl === "function"
325
+ ? options.formatNlResultImpl
326
+ : require("./agent").formatNlResult;
327
+ const onMessageReceived = typeof options.onMessageReceived === "function"
328
+ ? options.onMessageReceived
329
+ : null;
330
+
331
+ const explicitSubscriber = String(options.subscriberId || "").trim();
332
+ const envSubscriber = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
333
+ let subscriberId = explicitSubscriber || envSubscriber;
334
+ if (!subscriberId) {
335
+ const whoami = shell("ufoo bus whoami 2>/dev/null || true");
336
+ subscriberId = String((whoami && whoami.output) || "").trim();
337
+ }
338
+ if (!subscriberId) {
339
+ const joined = shell("ufoo bus join | tail -1");
340
+ subscriberId = String((joined && joined.output) || "").trim();
341
+ }
342
+ if (!subscriberId) {
343
+ return {
344
+ ok: false,
345
+ summary: "",
346
+ error: "failed to resolve bus subscriber id",
347
+ handled: 0,
348
+ subscriberId: "",
349
+ };
350
+ }
351
+
352
+ // Prefer consuming pending.jsonl directly (stable, ANSI/wrapping-proof).
353
+ const pendingFile = resolvePendingQueueFile(runtimeWorkspace, subscriberId);
354
+ const queue = pendingFile ? new DeliveryQueue(pendingFile) : null;
355
+ if (queue) queue.recover();
356
+ const hasPendingFile = Boolean(pendingFile && fs.existsSync(pendingFile));
357
+ let handled = 0;
358
+ const sendErrors = [];
359
+ const messageExchanges = [];
360
+
361
+ if (queue && hasPendingFile) {
362
+ while (fs.existsSync(pendingFile)) {
363
+ const claim = queue.claimNext();
364
+ if (!claim) break;
365
+ const message = extractTaskFromBusEvent(claim.event);
366
+ if (!message) {
367
+ queue.completeClaim(claim);
368
+ continue;
369
+ }
370
+ let nlResult;
371
+
372
+ // Notify that we received the message (for immediate display)
373
+ if (onMessageReceived) {
374
+ onMessageReceived({
375
+ from: message.publisher,
376
+ task: message.task,
377
+ });
378
+ }
379
+
380
+ // Create progress reporter for this message
381
+ const progressReporter = createBusProgressReporter(shell, message.publisher);
382
+
383
+ try {
384
+ // Send initial acknowledgment
385
+ shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote("🚀 Starting task...")}`);
386
+
387
+ // eslint-disable-next-line no-await-in-loop
388
+ nlResult = await runNl(message.task, state, {
389
+ onProgress: progressReporter,
390
+ signal: options.signal,
391
+ });
392
+ } catch (err) {
393
+ const errorMessage = err && err.message ? err.message : "task failed";
394
+ sendErrors.push(`task from ${message.publisher} failed: ${errorMessage}`);
395
+ queue.restoreClaim(claim);
396
+ // Send error notification
397
+ shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(`❌ Error: ${errorMessage}`)}`);
398
+ break;
399
+ }
400
+ const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
401
+ const sendRes = shell(`ufoo bus send ${shellQuote(message.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
402
+ if (!sendRes.ok) {
403
+ sendErrors.push(`reply to ${message.publisher} failed: ${sendRes.error || "send failed"}`);
404
+ queue.restoreClaim(claim);
405
+ break;
406
+ }
407
+ handled += 1;
408
+ queue.completeClaim(claim);
409
+ messageExchanges.push({
410
+ from: message.publisher,
411
+ task: message.task,
412
+ reply,
413
+ });
414
+ }
415
+ }
416
+
417
+ // Fallback: if there is no pending file, fall back to CLI `bus check` parsing.
418
+ if (!hasPendingFile) {
419
+ const checked = shell(`ufoo bus check ${shellQuote(subscriberId)}`);
420
+ if (!checked.ok) {
421
+ return {
422
+ ok: false,
423
+ summary: "",
424
+ error: checked.error || "ufoo bus check failed",
425
+ handled: 0,
426
+ subscriberId,
427
+ };
428
+ }
429
+ const parsed = parseBusCheckOutput(checked.output);
430
+ if (parsed.length === 0 && busCheckOutputIndicatesPending(checked.output)) {
431
+ return {
432
+ ok: false,
433
+ summary: "",
434
+ error: "failed to parse ufoo bus check output (pending events detected).",
435
+ handled: 0,
436
+ subscriberId,
437
+ };
438
+ }
439
+ for (const item of parsed) {
440
+ // Notify that we received the message (for immediate display)
441
+ if (onMessageReceived) {
442
+ onMessageReceived({
443
+ from: item.publisher,
444
+ task: item.task,
445
+ });
446
+ }
447
+
448
+ const nlResult = await runNl(item.task, state, {
449
+ signal: options.signal,
450
+ });
451
+ const reply = String(formatNl(nlResult, false) || "").replace(/\s+/g, " ").trim() || "Done.";
452
+ const sendRes = shell(`ufoo bus send ${shellQuote(item.publisher)} ${shellQuote(reply.slice(0, 2000))}`);
453
+ if (!sendRes.ok) {
454
+ sendErrors.push(`reply to ${item.publisher} failed: ${sendRes.error || "send failed"}`);
455
+ continue;
456
+ }
457
+ handled += 1;
458
+ messageExchanges.push({
459
+ from: item.publisher,
460
+ task: item.task,
461
+ reply,
462
+ });
463
+ }
464
+ }
465
+
466
+ if (sendErrors.length > 0) {
467
+ return {
468
+ ok: false,
469
+ summary: "",
470
+ error: sendErrors.join("; "),
471
+ handled,
472
+ subscriberId,
473
+ messageExchanges,
474
+ };
475
+ }
476
+
477
+ const summary = handled > 0
478
+ ? `ubus: handled ${handled} message${handled === 1 ? "" : "s"} for ${subscriberId}.`
479
+ : `ubus: no pending messages for ${subscriberId}.`;
480
+ return {
481
+ ok: true,
482
+ summary,
483
+ error: "",
484
+ handled,
485
+ subscriberId,
486
+ messageExchanges,
487
+ };
488
+ }
489
+
490
+ module.exports = {
491
+ runUbusCommand,
492
+ parseBusCheckOutput,
493
+ extractBusMessageTask,
494
+ runShellCapture,
495
+ stripAnsi,
496
+ busCheckOutputIndicatesPending,
497
+ resolvePendingQueueFile,
498
+ resolveUfooProjectRoot,
499
+ countPendingQueueLines,
500
+ getPendingBusCount,
501
+ drainJsonlFile,
502
+ extractTaskFromBusEvent,
503
+ shouldAutoConsumeBus,
504
+ };
@@ -27,12 +27,7 @@ function runToolCall(input = {}, options = {}) {
27
27
  if (tool === "read") return runReadTool(args, options);
28
28
  if (tool === "write") return runWriteTool(args, options);
29
29
  if (tool === "edit") return runEditTool(args, options);
30
- if (tool === "bash") return runBashTool(args, options);
31
- return {
32
- ok: false,
33
- error: "unknown tool",
34
- supported_tools: TOOL_NAMES.slice(),
35
- };
30
+ return runBashTool(args, options);
36
31
  }
37
32
 
38
33
  module.exports = {