u-foo 1.0.2 → 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.2",
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) {
@@ -29,6 +29,14 @@ function startDaemon(projectRoot) {
29
29
  child.unref();
30
30
  }
31
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
+
32
40
  async function connectWithRetry(sockPath, retries, delayMs) {
33
41
  for (let i = 0; i < retries; i += 1) {
34
42
  try {
@@ -54,15 +62,23 @@ async function runChat(projectRoot) {
54
62
  startDaemon(projectRoot);
55
63
  }
56
64
 
65
+ const daemonBin = resolveProjectFile(projectRoot, path.join("bin", "ufoo.js"), path.join("bin", "ufoo.js"));
57
66
  const sock = socketPath(projectRoot);
58
- let client = await connectWithRetry(sock, 25, 200);
59
- if (!client) {
60
- // Retry once with a fresh daemon start and longer wait.
61
- if (!isRunning(projectRoot)) {
62
- 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);
63
77
  }
64
- client = await connectWithRetry(sock, 50, 200);
65
- }
78
+ return newClient;
79
+ };
80
+
81
+ client = await connectClient();
66
82
  if (!client) {
67
83
  // Check if daemon failed to start
68
84
  if (!isRunning(projectRoot)) {
@@ -88,6 +104,7 @@ async function runChat(projectRoot) {
88
104
 
89
105
  const config = loadConfig(projectRoot);
90
106
  let launchMode = config.launchMode;
107
+ let agentProvider = config.agentProvider;
91
108
 
92
109
  // Dynamic input height settings
93
110
  // Layout: topLine(1) + content + bottomLine(1) + dashboard(1)
@@ -1107,6 +1124,12 @@ async function runChat(projectRoot) {
1107
1124
  let focusMode = "input"; // "input" or "dashboard"
1108
1125
  let dashboardView = "agents"; // "agents" or "mode"
1109
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;
1110
1133
 
1111
1134
  function getAgentLabel(agentId) {
1112
1135
  return activeAgentLabelMap.get(agentId) || agentId;
@@ -1131,6 +1154,7 @@ async function runChat(projectRoot) {
1131
1154
  }
1132
1155
 
1133
1156
  function send(req) {
1157
+ if (!client || client.destroyed) return;
1134
1158
  client.write(`${JSON.stringify(req)}\n`);
1135
1159
  }
1136
1160
 
@@ -1177,6 +1201,49 @@ async function runChat(projectRoot) {
1177
1201
  screen.render();
1178
1202
  }
1179
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
+
1180
1247
  function clearLog() {
1181
1248
  logBox.setContent("");
1182
1249
  if (typeof logBox.scrollTo === "function") {
@@ -1197,6 +1264,15 @@ async function runChat(projectRoot) {
1197
1264
  return `{cyan-fg}${mode}{/cyan-fg}`;
1198
1265
  });
1199
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(" ")}`;
1200
1276
  content += " {gray-fg}│ ←/→ select, Enter confirm, ↑ back{/gray-fg}";
1201
1277
  } else {
1202
1278
  if (activeAgents.length > 0) {
@@ -1230,6 +1306,7 @@ async function runChat(projectRoot) {
1230
1306
  : "none";
1231
1307
  content += `{gray-fg}Agents:{/gray-fg} {cyan-fg}${agents}{/cyan-fg}`;
1232
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}`;
1233
1310
  }
1234
1311
  dashboard.setContent(content);
1235
1312
  }
@@ -1280,6 +1357,7 @@ async function runChat(projectRoot) {
1280
1357
  agentListWindowStart = 0;
1281
1358
  clampAgentWindow();
1282
1359
  selectedModeIndex = launchMode === "internal" ? 1 : 0;
1360
+ selectedProviderIndex = agentProvider === "claude-cli" ? 1 : 0;
1283
1361
  screen.grabKeys = true;
1284
1362
  renderDashboard();
1285
1363
  screen.program.hideCursor();
@@ -1301,6 +1379,13 @@ async function runChat(projectRoot) {
1301
1379
  screen.render();
1302
1380
  return true;
1303
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
+ }
1304
1389
  if (key.name === "up") {
1305
1390
  dashboardView = "agents";
1306
1391
  renderDashboard();
@@ -1319,6 +1404,37 @@ async function runChat(projectRoot) {
1319
1404
  }
1320
1405
  return true;
1321
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
+ }
1322
1438
 
1323
1439
  if (key.name === "left") {
1324
1440
  if (activeAgents.length > 0 && selectedAgentIndex > 0) {
@@ -1380,13 +1496,29 @@ async function runChat(projectRoot) {
1380
1496
  send({ type: "status" });
1381
1497
  }
1382
1498
 
1383
- let buffer = "";
1384
- client.on("data", (data) => {
1385
- buffer += data.toString("utf8");
1386
- const lines = buffer.split(/\r?\n/);
1387
- buffer = lines.pop() || "";
1388
- for (const line of lines.filter((l) => l.trim())) {
1389
- 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 {
1390
1522
  const msg = JSON.parse(line);
1391
1523
  if (msg.type === "status") {
1392
1524
  const data = msg.data || {};
@@ -1491,6 +1623,12 @@ async function runChat(projectRoot) {
1491
1623
  }
1492
1624
  }
1493
1625
  });
1626
+ client.on("close", () => {
1627
+ client = null;
1628
+ });
1629
+ };
1630
+
1631
+ attachClient(client);
1494
1632
 
1495
1633
  input.on("submit", (value) => {
1496
1634
  const text = value.trim();
@@ -1618,6 +1756,7 @@ async function runChat(projectRoot) {
1618
1756
  }
1619
1757
  loadHistory();
1620
1758
  loadInputHistory();
1759
+ renderDashboard();
1621
1760
  resizeInput();
1622
1761
  requestStatus();
1623
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;