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 +1 -1
- package/src/chat/index.js +164 -19
- package/src/config.js +14 -2
- package/src/daemon/run.js +4 -2
package/package.json
CHANGED
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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 =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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 {
|
|
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
|
|
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;
|