u-foo 1.0.1 → 1.0.3

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.0.1",
3
+ "version": "1.0.3",
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/chat/index.js CHANGED
@@ -3,7 +3,7 @@ const path = require("path");
3
3
  const blessed = require("blessed");
4
4
  const { spawn, spawnSync } = require("child_process");
5
5
  const fs = require("fs");
6
- const { loadConfig, saveConfig, normalizeLaunchMode } = require("../config");
6
+ const { loadConfig, saveConfig, normalizeLaunchMode, normalizeAgentProvider } = require("../config");
7
7
  const { socketPath, isRunning } = require("../daemon");
8
8
 
9
9
  function connectSocket(sockPath) {
@@ -13,10 +13,15 @@ function connectSocket(sockPath) {
13
13
  });
14
14
  }
15
15
 
16
+ function resolveProjectFile(projectRoot, relativePath, fallbackRelativePath) {
17
+ const local = path.join(projectRoot, relativePath);
18
+ if (fs.existsSync(local)) return local;
19
+ return path.join(__dirname, "..", "..", fallbackRelativePath);
20
+ }
21
+
16
22
  function startDaemon(projectRoot) {
17
- // eslint-disable-next-line no-console
18
- console.log("Starting ufoo daemon...");
19
- const child = spawn(process.execPath, [path.join(projectRoot, "bin", "ufoo.js"), "daemon", "--start"], {
23
+ const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
24
+ const child = spawn(process.execPath, [daemonBin, "daemon", "--start"], {
20
25
  detached: true,
21
26
  stdio: "ignore",
22
27
  cwd: projectRoot,
@@ -24,6 +29,14 @@ function startDaemon(projectRoot) {
24
29
  child.unref();
25
30
  }
26
31
 
32
+ function stopDaemon(projectRoot) {
33
+ const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
34
+ spawnSync(process.execPath, [daemonBin, "daemon", "--stop"], {
35
+ stdio: "ignore",
36
+ cwd: projectRoot,
37
+ });
38
+ }
39
+
27
40
  async function connectWithRetry(sockPath, retries, delayMs) {
28
41
  for (let i = 0; i < retries; i += 1) {
29
42
  try {
@@ -40,7 +53,8 @@ async function connectWithRetry(sockPath, retries, delayMs) {
40
53
 
41
54
  async function runChat(projectRoot) {
42
55
  if (!fs.existsSync(path.join(projectRoot, ".ufoo"))) {
43
- spawnSync("bash", [path.join(projectRoot, "scripts", "init.sh"), "--modules", "context,bus", "--project", projectRoot], {
56
+ const initScript = resolveProjectFile(projectRoot, path.join("scripts", "init.sh"), path.join("scripts", "init.sh"));
57
+ spawnSync("bash", [initScript, "--modules", "context,bus", "--project", projectRoot], {
44
58
  stdio: "inherit",
45
59
  });
46
60
  }
@@ -48,15 +62,23 @@ async function runChat(projectRoot) {
48
62
  startDaemon(projectRoot);
49
63
  }
50
64
 
65
+ const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
51
66
  const sock = socketPath(projectRoot);
52
- let client = await connectWithRetry(sock, 25, 200);
53
- if (!client) {
54
- // Retry once with a fresh daemon start and longer wait.
55
- if (!isRunning(projectRoot)) {
56
- startDaemon(projectRoot);
67
+ let client = null;
68
+
69
+ const connectClient = async () => {
70
+ let newClient = await connectWithRetry(sock, 25, 200);
71
+ if (!newClient) {
72
+ // Retry once with a fresh daemon start and longer wait.
73
+ if (!isRunning(projectRoot)) {
74
+ startDaemon(projectRoot);
75
+ }
76
+ newClient = await connectWithRetry(sock, 50, 200);
57
77
  }
58
- client = await connectWithRetry(sock, 50, 200);
59
- }
78
+ return newClient;
79
+ };
80
+
81
+ client = await connectClient();
60
82
  if (!client) {
61
83
  // Check if daemon failed to start
62
84
  if (!isRunning(projectRoot)) {
@@ -82,6 +104,7 @@ async function runChat(projectRoot) {
82
104
 
83
105
  const config = loadConfig(projectRoot);
84
106
  let launchMode = config.launchMode;
107
+ let agentProvider = config.agentProvider;
85
108
 
86
109
  // Dynamic input height settings
87
110
  // Layout: topLine(1) + content + bottomLine(1) + dashboard(1)
@@ -1101,6 +1124,12 @@ async function runChat(projectRoot) {
1101
1124
  let focusMode = "input"; // "input" or "dashboard"
1102
1125
  let dashboardView = "agents"; // "agents" or "mode"
1103
1126
  let selectedModeIndex = launchMode === "internal" ? 1 : 0;
1127
+ const providerOptions = [
1128
+ { label: "codex", value: "codex-cli" },
1129
+ { label: "claude", value: "claude-cli" },
1130
+ ];
1131
+ let selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
1132
+ let restartInProgress = false;
1104
1133
 
1105
1134
  function getAgentLabel(agentId) {
1106
1135
  return activeAgentLabelMap.get(agentId) || agentId;
@@ -1125,6 +1154,7 @@ async function runChat(projectRoot) {
1125
1154
  }
1126
1155
 
1127
1156
  function send(req) {
1157
+ if (!client || client.destroyed) return;
1128
1158
  client.write(`${JSON.stringify(req)}\n`);
1129
1159
  }
1130
1160
 
@@ -1171,6 +1201,49 @@ async function runChat(projectRoot) {
1171
1201
  screen.render();
1172
1202
  }
1173
1203
 
1204
+ function providerLabel(value) {
1205
+ return value === "claude-cli" ? "claude" : "codex";
1206
+ }
1207
+
1208
+ function setAgentProvider(provider) {
1209
+ const next = normalizeAgentProvider(provider);
1210
+ if (next === agentProvider) return;
1211
+ agentProvider = next;
1212
+ selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
1213
+ saveConfig(projectRoot, { agentProvider });
1214
+ logMessage("status", `{magenta-fg}⚙{/magenta-fg} ufoo-agent: ${providerLabel(agentProvider)}`);
1215
+ renderDashboard();
1216
+ screen.render();
1217
+ void restartDaemon();
1218
+ }
1219
+
1220
+ async function restartDaemon() {
1221
+ if (restartInProgress) return;
1222
+ restartInProgress = true;
1223
+ logMessage("status", "{magenta-fg}⚙{/magenta-fg} Restarting daemon...");
1224
+ try {
1225
+ if (client) {
1226
+ client.removeAllListeners();
1227
+ try {
1228
+ client.end();
1229
+ } catch {
1230
+ // ignore
1231
+ }
1232
+ }
1233
+ stopDaemon(projectRoot);
1234
+ startDaemon(projectRoot);
1235
+ const newClient = await connectClient();
1236
+ if (newClient) {
1237
+ attachClient(newClient);
1238
+ logMessage("status", "{green-fg}✓{/green-fg} Daemon reconnected");
1239
+ } else {
1240
+ logMessage("error", "{red-fg}✗{/red-fg} Failed to reconnect to daemon");
1241
+ }
1242
+ } finally {
1243
+ restartInProgress = false;
1244
+ }
1245
+ }
1246
+
1174
1247
  function clearLog() {
1175
1248
  logBox.setContent("");
1176
1249
  if (typeof logBox.scrollTo === "function") {
@@ -1191,6 +1264,15 @@ async function runChat(projectRoot) {
1191
1264
  return `{cyan-fg}${mode}{/cyan-fg}`;
1192
1265
  });
1193
1266
  content += `{gray-fg}Mode:{/gray-fg} ${modeParts.join(" ")}`;
1267
+ content += " {gray-fg}│ ←/→ select, Enter confirm, ↓ agent, ↑ back{/gray-fg}";
1268
+ } else if (dashboardView === "provider") {
1269
+ const providerParts = providerOptions.map((opt, i) => {
1270
+ if (i === selectedProviderIndex) {
1271
+ return `{inverse}${opt.label}{/inverse}`;
1272
+ }
1273
+ return `{cyan-fg}${opt.label}{/cyan-fg}`;
1274
+ });
1275
+ content += `{gray-fg}Agent:{/gray-fg} ${providerParts.join(" ")}`;
1194
1276
  content += " {gray-fg}│ ←/→ select, Enter confirm, ↑ back{/gray-fg}";
1195
1277
  } else {
1196
1278
  if (activeAgents.length > 0) {
@@ -1224,6 +1306,7 @@ async function runChat(projectRoot) {
1224
1306
  : "none";
1225
1307
  content += `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`;
1226
1308
  content += ` {gray-fg}Mode:{/gray-fg} {cyan-fg}${launchMode}{/cyan-fg}`;
1309
+ content += ` {gray-fg}Agent:{/gray-fg} {cyan-fg}${providerLabel(agentProvider)}{/cyan-fg}`;
1227
1310
  }
1228
1311
  dashboard.setContent(content);
1229
1312
  }
@@ -1274,6 +1357,7 @@ async function runChat(projectRoot) {
1274
1357
  agentListWindowStart = 0;
1275
1358
  clampAgentWindow();
1276
1359
  selectedModeIndex = launchMode === "internal" ? 1 : 0;
1360
+ selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
1277
1361
  screen.grabKeys = true;
1278
1362
  renderDashboard();
1279
1363
  screen.program.hideCursor();
@@ -1295,6 +1379,13 @@ async function runChat(projectRoot) {
1295
1379
  screen.render();
1296
1380
  return true;
1297
1381
  }
1382
+ if (key.name === "down") {
1383
+ dashboardView = "provider";
1384
+ selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
1385
+ renderDashboard();
1386
+ screen.render();
1387
+ return true;
1388
+ }
1298
1389
  if (key.name === "up") {
1299
1390
  dashboardView = "agents";
1300
1391
  renderDashboard();
@@ -1313,6 +1404,37 @@ async function runChat(projectRoot) {
1313
1404
  }
1314
1405
  return true;
1315
1406
  }
1407
+ if (dashboardView === "provider") {
1408
+ if (key.name === "left") {
1409
+ selectedProviderIndex = selectedProviderIndex <= 0 ? providerOptions.length - 1 : selectedProviderIndex - 1;
1410
+ renderDashboard();
1411
+ screen.render();
1412
+ return true;
1413
+ }
1414
+ if (key.name === "right") {
1415
+ selectedProviderIndex = selectedProviderIndex >= providerOptions.length - 1 ? 0 : selectedProviderIndex + 1;
1416
+ renderDashboard();
1417
+ screen.render();
1418
+ return true;
1419
+ }
1420
+ if (key.name === "up") {
1421
+ dashboardView = "mode";
1422
+ renderDashboard();
1423
+ screen.render();
1424
+ return true;
1425
+ }
1426
+ if (key.name === "enter" || key.name === "return") {
1427
+ const selected = providerOptions[selectedProviderIndex];
1428
+ if (selected) setAgentProvider(selected.value);
1429
+ exitDashboardMode(false);
1430
+ return true;
1431
+ }
1432
+ if (key.name === "escape") {
1433
+ exitDashboardMode(false);
1434
+ return true;
1435
+ }
1436
+ return true;
1437
+ }
1316
1438
 
1317
1439
  if (key.name === "left") {
1318
1440
  if (activeAgents.length > 0 && selectedAgentIndex > 0) {
@@ -1374,13 +1496,29 @@ async function runChat(projectRoot) {
1374
1496
  send({ type: "status" });
1375
1497
  }
1376
1498
 
1377
- let buffer = "";
1378
- client.on("data", (data) => {
1379
- buffer += data.toString("utf8");
1380
- const lines = buffer.split(/\r?\n/);
1381
- buffer = lines.pop() || "";
1382
- for (const line of lines.filter((l) => l.trim())) {
1383
- try {
1499
+ const detachClient = () => {
1500
+ if (!client) return;
1501
+ client.removeAllListeners("data");
1502
+ client.removeAllListeners("close");
1503
+ try {
1504
+ client.end();
1505
+ client.destroy();
1506
+ } catch {
1507
+ // ignore
1508
+ }
1509
+ };
1510
+
1511
+ const attachClient = (newClient) => {
1512
+ if (!newClient) return;
1513
+ detachClient();
1514
+ client = newClient;
1515
+ let buffer = "";
1516
+ client.on("data", (data) => {
1517
+ buffer += data.toString("utf8");
1518
+ const lines = buffer.split(/\r?\n/);
1519
+ buffer = lines.pop() || "";
1520
+ for (const line of lines.filter((l) => l.trim())) {
1521
+ try {
1384
1522
  const msg = JSON.parse(line);
1385
1523
  if (msg.type === "status") {
1386
1524
  const data = msg.data || {};
@@ -1485,6 +1623,12 @@ async function runChat(projectRoot) {
1485
1623
  }
1486
1624
  }
1487
1625
  });
1626
+ client.on("close", () => {
1627
+ client = null;
1628
+ });
1629
+ };
1630
+
1631
+ attachClient(client);
1488
1632
 
1489
1633
  input.on("submit", (value) => {
1490
1634
  const text = value.trim();
@@ -1612,6 +1756,7 @@ async function runChat(projectRoot) {
1612
1756
  }
1613
1757
  loadHistory();
1614
1758
  loadInputHistory();
1759
+ renderDashboard();
1615
1760
  resizeInput();
1616
1761
  requestStatus();
1617
1762
  setInterval(requestStatus, 2000);
package/src/config.js CHANGED
@@ -3,12 +3,18 @@ const path = require("path");
3
3
 
4
4
  const DEFAULT_CONFIG = {
5
5
  launchMode: "terminal",
6
+ agentProvider: "codex-cli",
7
+ agentModel: "",
6
8
  };
7
9
 
8
10
  function normalizeLaunchMode(value) {
9
11
  return value === "internal" ? "internal" : "terminal";
10
12
  }
11
13
 
14
+ function normalizeAgentProvider(value) {
15
+ return value === "claude-cli" ? "claude-cli" : "codex-cli";
16
+ }
17
+
12
18
  function configPath(projectRoot) {
13
19
  return path.join(projectRoot, ".ufoo", "config.json");
14
20
  }
@@ -16,7 +22,12 @@ function configPath(projectRoot) {
16
22
  function loadConfig(projectRoot) {
17
23
  try {
18
24
  const raw = JSON.parse(fs.readFileSync(configPath(projectRoot), "utf8"));
19
- return { ...DEFAULT_CONFIG, ...raw, launchMode: normalizeLaunchMode(raw.launchMode) };
25
+ return {
26
+ ...DEFAULT_CONFIG,
27
+ ...raw,
28
+ launchMode: normalizeLaunchMode(raw.launchMode),
29
+ agentProvider: normalizeAgentProvider(raw.agentProvider),
30
+ };
20
31
  } catch {
21
32
  return { ...DEFAULT_CONFIG };
22
33
  }
@@ -30,8 +41,9 @@ function saveConfig(projectRoot, config) {
30
41
  ...config,
31
42
  };
32
43
  merged.launchMode = normalizeLaunchMode(merged.launchMode);
44
+ merged.agentProvider = normalizeAgentProvider(merged.agentProvider);
33
45
  fs.writeFileSync(target, JSON.stringify(merged, null, 2));
34
46
  return merged;
35
47
  }
36
48
 
37
- module.exports = { loadConfig, saveConfig, normalizeLaunchMode };
49
+ module.exports = { loadConfig, saveConfig, normalizeLaunchMode, normalizeAgentProvider };
package/src/daemon/run.js CHANGED
@@ -1,12 +1,14 @@
1
1
  const path = require("path");
2
2
  const { startDaemon, stopDaemon, isRunning } = require("./index");
3
+ const { loadConfig } = require("../config");
3
4
 
4
5
  function runDaemonCli(argv) {
5
6
  const cmd = argv[1] || "start";
6
7
  const projectRoot = process.cwd();
7
- const provider = process.env.UFOO_AGENT_PROVIDER || "codex-cli";
8
+ const config = loadConfig(projectRoot);
9
+ const provider = process.env.UFOO_AGENT_PROVIDER || config.agentProvider || "codex-cli";
8
10
  const model =
9
- process.env.UFOO_AGENT_MODEL || (provider === "claude-cli" ? "opus" : "");
11
+ process.env.UFOO_AGENT_MODEL || config.agentModel || (provider === "claude-cli" ? "opus" : "");
10
12
 
11
13
  if (cmd === "start" || cmd === "--start") {
12
14
  if (isRunning(projectRoot)) return;