u-foo 1.2.14 → 1.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "1.2.14",
3
+ "version": "1.2.16",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
package/src/code/agent.js CHANGED
@@ -43,6 +43,14 @@ function normalizeLine(input = "") {
43
43
  return String(input || "").trim();
44
44
  }
45
45
 
46
+ function parseProbeMarkerCommand(input = "") {
47
+ const text = String(input || "").trim();
48
+ if (!text) return "";
49
+ // Accept only strict probe markers: "<prefix> <single-marker-token>".
50
+ const match = text.match(/^(?:\$ufoo|\/ufoo|ufoo)\s+([A-Za-z0-9][A-Za-z0-9._:-]{0,63})$/);
51
+ return match ? String(match[1] || "").trim() : "";
52
+ }
53
+
46
54
  function parseJson(text = "") {
47
55
  const raw = String(text || "").trim();
48
56
  if (!raw) return {};
@@ -1209,16 +1217,18 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
1209
1217
  " help",
1210
1218
  " exit|quit",
1211
1219
  " ubus|/ubus",
1220
+ " bg|/bg <task>",
1212
1221
  " resume <session-id>",
1213
1222
  " tool <read|write|edit|bash> <args-json>",
1214
1223
  " run <read|write|edit|bash> <args-json>",
1215
1224
  ].join("\n"),
1216
1225
  };
1217
1226
  }
1218
- if (text.startsWith("$ufoo ") || text.startsWith("/ufoo ") || text.startsWith("ufoo ")) {
1227
+ const probeMarker = parseProbeMarkerCommand(text);
1228
+ if (probeMarker) {
1219
1229
  return {
1220
1230
  kind: "probe",
1221
- output: text.split(/\s+/).slice(1).join(" ").trim(),
1231
+ marker: probeMarker,
1222
1232
  };
1223
1233
  }
1224
1234
  if (text === "ubus" || text === "/ubus") {
@@ -1226,6 +1236,26 @@ function runSingleCommand(line = "", workspaceRoot = process.cwd()) {
1226
1236
  kind: "ubus",
1227
1237
  };
1228
1238
  }
1239
+ if (text === "bg" || text === "/bg") {
1240
+ return {
1241
+ kind: "error",
1242
+ output: "usage: bg <task>",
1243
+ };
1244
+ }
1245
+ const bgMatch = text.match(/^(?:\/bg|bg)\s+(.+)$/i);
1246
+ if (bgMatch) {
1247
+ const task = String(bgMatch[1] || "").trim();
1248
+ if (!task) {
1249
+ return {
1250
+ kind: "error",
1251
+ output: "usage: bg <task>",
1252
+ };
1253
+ }
1254
+ return {
1255
+ kind: "nl_bg",
1256
+ task,
1257
+ };
1258
+ }
1229
1259
  const resumeMatch = text.match(/^resume(?:\s+(.+))?$/i);
1230
1260
  if (resumeMatch) {
1231
1261
  const session = String(resumeMatch[1] || "").trim();
@@ -1345,6 +1375,8 @@ async function runUcodeCoreAgent({
1345
1375
  });
1346
1376
  return new Promise((resolve) => {
1347
1377
  let chain = Promise.resolve();
1378
+ let backgroundSeq = 0;
1379
+ const backgroundRuns = new Map();
1348
1380
  const subscriberId = String(process.env.UFOO_SUBSCRIBER_ID || "").trim();
1349
1381
  const autoBusEnabled = shouldAutoConsumeBus(subscriberId);
1350
1382
  let autoBusTimer = null;
@@ -1400,6 +1432,38 @@ async function runUcodeCoreAgent({
1400
1432
  scheduleAutoBus();
1401
1433
  }
1402
1434
 
1435
+ const startBackgroundTask = (task = "") => {
1436
+ backgroundSeq += 1;
1437
+ const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
1438
+ const bgState = {
1439
+ workspaceRoot: state.workspaceRoot,
1440
+ provider: state.provider,
1441
+ model: state.model,
1442
+ engine: state.engine,
1443
+ context: state.context,
1444
+ nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
1445
+ sessionId: "",
1446
+ timeoutMs: state.timeoutMs,
1447
+ jsonOutput: false,
1448
+ };
1449
+ const run = runNaturalLanguageTask(task, bgState)
1450
+ .then((nlResult) => {
1451
+ const summary = String(formatNlResult(nlResult, false) || "").trim();
1452
+ const title = nlResult && nlResult.ok ? "done" : "failed";
1453
+ stdout.write(`[${jobId}] ${title}: ${summary || "no summary"}\n`);
1454
+ printPrompt();
1455
+ })
1456
+ .catch((err) => {
1457
+ stdout.write(`[${jobId}] failed: ${err && err.message ? err.message : "background task failed"}\n`);
1458
+ printPrompt();
1459
+ })
1460
+ .finally(() => {
1461
+ backgroundRuns.delete(jobId);
1462
+ });
1463
+ backgroundRuns.set(jobId, run);
1464
+ return jobId;
1465
+ };
1466
+
1403
1467
  const handleLine = async (line) => {
1404
1468
  const runtimeWorkspace = String(state.workspaceRoot || workspaceRoot || process.cwd());
1405
1469
  const result = runSingleCommand(line, runtimeWorkspace);
@@ -1407,7 +1471,10 @@ async function runUcodeCoreAgent({
1407
1471
  rl.close();
1408
1472
  return;
1409
1473
  }
1410
- if (result.kind === "help" || result.kind === "probe" || result.kind === "tool" || result.kind === "error") {
1474
+ if (result.kind === "probe") {
1475
+ return;
1476
+ }
1477
+ if (result.kind === "help" || result.kind === "tool" || result.kind === "error") {
1411
1478
  stdout.write(`${result.output}\n`);
1412
1479
  }
1413
1480
  if (result.kind === "ubus") {
@@ -1442,6 +1509,10 @@ async function runUcodeCoreAgent({
1442
1509
  stdout.write(`Resumed session ${resumed.sessionId} (${resumed.restoredMessages} messages).\n`);
1443
1510
  }
1444
1511
  }
1512
+ if (result.kind === "nl_bg") {
1513
+ const jobId = startBackgroundTask(result.task);
1514
+ stdout.write(`[${jobId}] started in background.\n`);
1515
+ }
1445
1516
  if (result.kind === "nl") {
1446
1517
  let streamBuffer = null;
1447
1518
  let streamedVisible = false;
package/src/code/tui.js CHANGED
@@ -468,7 +468,7 @@ function runUcodeTui({
468
468
  } = {}) {
469
469
  return new Promise((resolve) => {
470
470
  const blessed = require("blessed");
471
- const { execSync } = require("child_process");
471
+ const { execFileSync } = require("child_process");
472
472
  const { createChatLayout } = require("../chat/layout");
473
473
  const { computeDashboardContent } = require("../chat/dashboardView");
474
474
  const { escapeBlessed, stripBlessedTags } = require("../chat/text");
@@ -490,6 +490,8 @@ function runUcodeTui({
490
490
  let agentListWindowStart = 0;
491
491
  let agentSelectionMode = false;
492
492
  let pendingTask = null;
493
+ const backgroundTasks = new Map();
494
+ let backgroundSeq = 0;
493
495
  const logRenderState = { inCodeBlock: false };
494
496
  const inputHistory = [];
495
497
  let historyIndex = -1;
@@ -900,12 +902,27 @@ function runUcodeTui({
900
902
  };
901
903
 
902
904
  const updateStatus = (message = "", type = "thinking", options = {}) => {
905
+ const getBackgroundSuffix = () => {
906
+ if (!backgroundTasks || backgroundTasks.size === 0) return "";
907
+ let running = 0;
908
+ let done = 0;
909
+ let failed = 0;
910
+ for (const task of backgroundTasks.values()) {
911
+ const status = String(task && task.status || "").trim().toLowerCase();
912
+ if (status === "running") running += 1;
913
+ else if (status === "done") done += 1;
914
+ else if (status === "failed") failed += 1;
915
+ }
916
+ const total = running + done + failed;
917
+ if (total <= 0) return "";
918
+ return ` · BG ${running}/${done}/${failed}`;
919
+ };
903
920
  if (statusInterval) {
904
921
  clearInterval(statusInterval);
905
922
  statusInterval = null;
906
923
  }
907
924
  if (!message) {
908
- statusLine.setContent("{bold}UCODE{/bold} · Ready");
925
+ statusLine.setContent(escapeBlessed(`UCODE · Ready${getBackgroundSuffix()}`));
909
926
  screen.render();
910
927
  return;
911
928
  }
@@ -918,7 +935,7 @@ function runUcodeTui({
918
935
  const timerText = showTimer
919
936
  ? ` (${formatPendingElapsed(Date.now() - startedAt)},esc cancel)`
920
937
  : "";
921
- statusLine.setContent(escapeBlessed(`${indicator} ${message}${timerText}`));
938
+ statusLine.setContent(escapeBlessed(`${indicator} ${message}${timerText}${getBackgroundSuffix()}`));
922
939
  statusIndex += 1;
923
940
  screen.render();
924
941
  };
@@ -1082,7 +1099,7 @@ function runUcodeTui({
1082
1099
  if (isBusMessage && targetAgent) {
1083
1100
  updateStatus("Sending message...", "typing");
1084
1101
  try {
1085
- execSync(`ufoo bus send "${targetAgent}" "${actualLine.replace(/"/g, '\\"')}"`, {
1102
+ execFileSync("ufoo", ["bus", "send", targetAgent, actualLine], {
1086
1103
  cwd: workspaceRoot,
1087
1104
  encoding: "utf8",
1088
1105
  });
@@ -1118,7 +1135,10 @@ function runUcodeTui({
1118
1135
  }, payload);
1119
1136
  return;
1120
1137
  }
1121
- if (result.kind === "help" || result.kind === "probe" || result.kind === "error") {
1138
+ if (result.kind === "probe") {
1139
+ return;
1140
+ }
1141
+ if (result.kind === "help" || result.kind === "error") {
1122
1142
  logText(result.output || "");
1123
1143
  return;
1124
1144
  }
@@ -1166,6 +1186,54 @@ function runUcodeTui({
1166
1186
  return;
1167
1187
  }
1168
1188
 
1189
+ if (result.kind === "nl_bg") {
1190
+ backgroundSeq += 1;
1191
+ const jobId = `bg-${Date.now().toString(36)}-${backgroundSeq.toString(36)}`;
1192
+ const taskRecord = {
1193
+ id: jobId,
1194
+ task: result.task,
1195
+ status: "running",
1196
+ startedAt: Date.now(),
1197
+ summary: "",
1198
+ };
1199
+ backgroundTasks.set(jobId, taskRecord);
1200
+ updateStatus("", "none");
1201
+ logText(`[${jobId}] started in background.`);
1202
+
1203
+ const bgState = {
1204
+ workspaceRoot: state.workspaceRoot,
1205
+ provider: state.provider,
1206
+ model: state.model,
1207
+ engine: state.engine,
1208
+ context: state.context,
1209
+ nlMessages: Array.isArray(state.nlMessages) ? state.nlMessages.slice() : [],
1210
+ sessionId: "",
1211
+ timeoutMs: state.timeoutMs,
1212
+ jsonOutput: false,
1213
+ };
1214
+
1215
+ Promise.resolve()
1216
+ .then(() => runNaturalLanguageTask(result.task, bgState))
1217
+ .then((nlResult) => {
1218
+ taskRecord.status = nlResult && nlResult.ok ? "done" : "failed";
1219
+ taskRecord.finishedAt = Date.now();
1220
+ taskRecord.summary = String(formatNlResult(nlResult, false) || "").trim();
1221
+ const title = taskRecord.status === "done" ? "done" : "failed";
1222
+ logText(`[${jobId}] ${title}: ${taskRecord.summary || "no summary"}`);
1223
+ })
1224
+ .catch((err) => {
1225
+ taskRecord.status = "failed";
1226
+ taskRecord.finishedAt = Date.now();
1227
+ taskRecord.summary = err && err.message ? String(err.message) : "background task failed";
1228
+ logText(`[${jobId}] failed: ${taskRecord.summary}`);
1229
+ })
1230
+ .finally(() => {
1231
+ updateStatus("", "none");
1232
+ screen.render();
1233
+ });
1234
+ return;
1235
+ }
1236
+
1169
1237
  if (result.kind === "nl") {
1170
1238
  const statusMessages = [
1171
1239
  "Thinking...",
@@ -129,6 +129,12 @@ function messageSource(msg) {
129
129
  return "channel";
130
130
  }
131
131
 
132
+ function normalizeOnlineSender(from = "") {
133
+ const text = String(from || "").trim();
134
+ if (!text) return "remote";
135
+ return text;
136
+ }
137
+
132
138
  function appendToInbox(nickname, msg) {
133
139
  const dir = inboxDir();
134
140
  fs.mkdirSync(dir, { recursive: true });
@@ -322,7 +328,8 @@ class OnlineConnect {
322
328
  if (!this.eventBus) return;
323
329
  const from = msg.from || "remote";
324
330
  const text = msg.payload.message || "";
325
- const decorated = `[${from}] ${text}`.trim();
331
+ const sender = normalizeOnlineSender(from);
332
+ const decorated = `[ufoo-online] ${sender}: ${String(text || "").trim()}`;
326
333
  try {
327
334
  this.eventBus.send("*", decorated, "remote:online");
328
335
  } catch {