u-foo 1.8.9 → 1.9.1
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/bin/uclaude.js +12 -2
- package/bin/ucodex.js +12 -2
- package/bin/ufoo.js +1 -1
- package/package.json +1 -1
- package/src/agent/defaultBootstrap.js +144 -0
- package/src/agent/launcher.js +45 -31
- package/src/agent/ufooAgent.js +1 -2
- package/src/bus/daemon.js +26 -1
- package/src/bus/index.js +23 -3
- package/src/chat/commandExecutor.js +103 -4
- package/src/chat/commands.js +20 -2
- package/src/chat/completionController.js +43 -4
- package/src/chat/daemonReconnect.js +6 -5
- package/src/chat/daemonTransport.js +7 -1
- package/src/chat/index.js +13 -5
- package/src/cli.js +191 -0
- package/src/daemon/groupOrchestrator.js +7 -0
- package/src/daemon/index.js +64 -39
- package/src/daemon/promptRequest.js +1 -1
- package/src/group/bootstrap.js +14 -0
- package/src/history/inputTimeline.js +601 -0
- package/src/{globalMode.js → projects/identity.js} +1 -1
- package/src/projects/index.js +11 -0
- package/src/solo/commands.js +31 -0
- package/src/ufoo/paths.js +2 -0
- package/templates/groups/build-ultra.json +8 -2
- /package/src/{chat/projectRuntimes.js → projects/runtimes.js} +0 -0
package/bin/uclaude.js
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const AgentLauncher = require("../src/agent/launcher");
|
|
9
|
+
const { resolveDefaultManualBootstrap } = require("../src/agent/defaultBootstrap");
|
|
9
10
|
|
|
10
11
|
const launcher = new AgentLauncher("claude-code", "claude");
|
|
11
|
-
const
|
|
12
|
+
const resolved = resolveDefaultManualBootstrap({
|
|
13
|
+
projectRoot: process.cwd(),
|
|
14
|
+
agentType: "claude-code",
|
|
15
|
+
args: process.argv.slice(2),
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
for (const [key, value] of Object.entries(resolved.env || {})) {
|
|
20
|
+
process.env[key] = String(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
launcher.launch(resolved.args);
|
package/bin/ucodex.js
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const AgentLauncher = require("../src/agent/launcher");
|
|
9
|
+
const { resolveDefaultManualBootstrap } = require("../src/agent/defaultBootstrap");
|
|
9
10
|
|
|
10
11
|
const launcher = new AgentLauncher("codex", "codex");
|
|
11
|
-
const
|
|
12
|
+
const resolved = resolveDefaultManualBootstrap({
|
|
13
|
+
projectRoot: process.cwd(),
|
|
14
|
+
agentType: "codex",
|
|
15
|
+
args: process.argv.slice(2),
|
|
16
|
+
env: process.env,
|
|
17
|
+
});
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
for (const [key, value] of Object.entries(resolved.env || {})) {
|
|
20
|
+
process.env[key] = String(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
launcher.launch(resolved.args);
|
package/bin/ufoo.js
CHANGED
|
@@ -5,7 +5,7 @@ const { runDaemonCli } = require("../src/daemon/run");
|
|
|
5
5
|
const { runChat } = require("../src/chat");
|
|
6
6
|
const { runInternalRunner } = require("../src/agent/internalRunner");
|
|
7
7
|
const { runPtyRunner } = require("../src/agent/ptyRunner");
|
|
8
|
-
const { resolveGlobalControllerProjectRoot } = require("../src/
|
|
8
|
+
const { resolveGlobalControllerProjectRoot } = require("../src/projects");
|
|
9
9
|
|
|
10
10
|
const rawArgv = process.argv.slice(2);
|
|
11
11
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { SHARED_UFOO_PROTOCOL } = require("../group/bootstrap");
|
|
6
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
|
+
|
|
8
|
+
function asTrimmedString(value) {
|
|
9
|
+
return typeof value === "string" ? value.trim() : "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function hasArg(args = [], names = []) {
|
|
13
|
+
if (!Array.isArray(args) || args.length === 0) return false;
|
|
14
|
+
const known = new Set((Array.isArray(names) ? names : []).map((item) => asTrimmedString(item)).filter(Boolean));
|
|
15
|
+
return args.some((item) => {
|
|
16
|
+
const text = asTrimmedString(item);
|
|
17
|
+
if (!text) return false;
|
|
18
|
+
if (known.has(text)) return true;
|
|
19
|
+
const eqIndex = text.indexOf("=");
|
|
20
|
+
if (eqIndex <= 0) return false;
|
|
21
|
+
return known.has(text.slice(0, eqIndex).trim());
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasMetaCommandArgs(args = []) {
|
|
26
|
+
return hasArg(args, ["-h", "--help", "-v", "--version"]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load the team activity timeline for prompt injection.
|
|
31
|
+
* The daemon syncs manual inputs every ~30s; bus messages are appended in real-time.
|
|
32
|
+
* Agent startup only reads — no build triggered here.
|
|
33
|
+
*/
|
|
34
|
+
function loadTeamActivityContext(projectRoot) {
|
|
35
|
+
try {
|
|
36
|
+
const { renderTimelineForPrompt } = require("../history/inputTimeline");
|
|
37
|
+
return renderTimelineForPrompt(projectRoot, 20) || "";
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildDefaultStartupBootstrapPrompt({ agentType = "", projectRoot = "" } = {}) {
|
|
44
|
+
const normalizedAgent = asTrimmedString(agentType).toLowerCase();
|
|
45
|
+
const displayAgent = normalizedAgent === "claude-code"
|
|
46
|
+
? "Claude"
|
|
47
|
+
: (normalizedAgent === "codex" ? "Codex" : "agent");
|
|
48
|
+
|
|
49
|
+
const segments = [
|
|
50
|
+
`Session bootstrap for ${displayAgent}.`,
|
|
51
|
+
"Adopt the following ufoo coordination protocol silently.",
|
|
52
|
+
"Do not reply to this bootstrap message unless the user explicitly asks about it. After applying it, continue the active task or wait for user input.",
|
|
53
|
+
SHARED_UFOO_PROTOCOL,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const root = asTrimmedString(projectRoot) || process.cwd();
|
|
57
|
+
const teamActivity = loadTeamActivityContext(root);
|
|
58
|
+
if (teamActivity) {
|
|
59
|
+
segments.push(teamActivity);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return segments.join("\n\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultBootstrapFile(projectRoot, agentType = "") {
|
|
66
|
+
const safeAgentType = asTrimmedString(agentType).replace(/[^a-zA-Z0-9._-]/g, "-") || "agent";
|
|
67
|
+
return path.join(getUfooPaths(projectRoot).agentDir, safeAgentType, "default-bootstrap.md");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function prepareDefaultBootstrapFile({
|
|
71
|
+
projectRoot,
|
|
72
|
+
agentType = "",
|
|
73
|
+
promptText = "",
|
|
74
|
+
targetFile = "",
|
|
75
|
+
} = {}) {
|
|
76
|
+
const root = asTrimmedString(projectRoot) || process.cwd();
|
|
77
|
+
const file = asTrimmedString(targetFile) || defaultBootstrapFile(root, agentType);
|
|
78
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
79
|
+
fs.writeFileSync(file, String(promptText || ""), "utf8");
|
|
80
|
+
return { ok: true, file };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveDefaultManualBootstrap({
|
|
84
|
+
projectRoot,
|
|
85
|
+
agentType = "",
|
|
86
|
+
args = [],
|
|
87
|
+
env = process.env,
|
|
88
|
+
} = {}) {
|
|
89
|
+
const normalizedAgent = asTrimmedString(agentType).toLowerCase();
|
|
90
|
+
const currentEnv = env && typeof env === "object" ? env : {};
|
|
91
|
+
const currentArgs = Array.isArray(args) ? args.slice() : [];
|
|
92
|
+
if (
|
|
93
|
+
currentEnv.UFOO_SKIP_DEFAULT_BOOTSTRAP === "1"
|
|
94
|
+
|| currentEnv.UFOO_STARTUP_BOOTSTRAP_TEXT
|
|
95
|
+
|| hasMetaCommandArgs(currentArgs)
|
|
96
|
+
) {
|
|
97
|
+
return { args: currentArgs, env: {}, mode: "skip" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (normalizedAgent === "claude-code") {
|
|
101
|
+
if (hasArg(currentArgs, ["--append-system-prompt", "--system-prompt"])) {
|
|
102
|
+
return { args: currentArgs, env: {}, mode: "skip" };
|
|
103
|
+
}
|
|
104
|
+
const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
|
|
105
|
+
const prepared = prepareDefaultBootstrapFile({
|
|
106
|
+
projectRoot,
|
|
107
|
+
agentType: normalizedAgent,
|
|
108
|
+
promptText,
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
args: [...currentArgs, "--append-system-prompt", prepared.file],
|
|
112
|
+
env: {},
|
|
113
|
+
mode: "system-prompt-file",
|
|
114
|
+
file: prepared.file,
|
|
115
|
+
promptText,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (normalizedAgent === "codex") {
|
|
120
|
+
if (currentArgs.length > 0) {
|
|
121
|
+
return { args: currentArgs, env: {}, mode: "skip" };
|
|
122
|
+
}
|
|
123
|
+
const promptText = buildDefaultStartupBootstrapPrompt({ agentType: normalizedAgent, projectRoot });
|
|
124
|
+
return {
|
|
125
|
+
args: currentArgs,
|
|
126
|
+
env: {
|
|
127
|
+
UFOO_STARTUP_BOOTSTRAP_TEXT: promptText,
|
|
128
|
+
},
|
|
129
|
+
mode: "post-launch-inject",
|
|
130
|
+
promptText,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { args: currentArgs, env: {}, mode: "skip" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
hasArg,
|
|
139
|
+
hasMetaCommandArgs,
|
|
140
|
+
buildDefaultStartupBootstrapPrompt,
|
|
141
|
+
defaultBootstrapFile,
|
|
142
|
+
prepareDefaultBootstrapFile,
|
|
143
|
+
resolveDefaultManualBootstrap,
|
|
144
|
+
};
|
package/src/agent/launcher.js
CHANGED
|
@@ -224,6 +224,42 @@ function computeInjectedSubmitDelayMs(agentType, text) {
|
|
|
224
224
|
return delayMs;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function sleep(ms) {
|
|
228
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function logInjectedCommand(wrapper, source, commandText) {
|
|
232
|
+
if (!wrapper || !wrapper.logger) return;
|
|
233
|
+
const text = String(commandText || "");
|
|
234
|
+
if (!text) return;
|
|
235
|
+
const logEntry = {
|
|
236
|
+
ts: Date.now(),
|
|
237
|
+
dir: "in",
|
|
238
|
+
data: { text, encoding: "utf8", size: text.length },
|
|
239
|
+
source,
|
|
240
|
+
};
|
|
241
|
+
wrapper.logger.write(JSON.stringify(logEntry) + "\n");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function injectPtyCommand(wrapper, agentType, commandText, source = "inject") {
|
|
245
|
+
const text = String(commandText || "");
|
|
246
|
+
if (!wrapper || !text) return false;
|
|
247
|
+
const normalizedAgentType = String(agentType || "").trim().toLowerCase();
|
|
248
|
+
const submitDelayMs = computeInjectedSubmitDelayMs(agentType, text);
|
|
249
|
+
wrapper.write(text);
|
|
250
|
+
if (normalizedAgentType === "claude-code") {
|
|
251
|
+
await sleep(submitDelayMs);
|
|
252
|
+
wrapper.write("\r");
|
|
253
|
+
} else {
|
|
254
|
+
await sleep(submitDelayMs);
|
|
255
|
+
wrapper.write("\x1b");
|
|
256
|
+
await sleep(100);
|
|
257
|
+
wrapper.write("\r");
|
|
258
|
+
}
|
|
259
|
+
logInjectedCommand(wrapper, source, text);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
227
263
|
async function resolveHostRegistrationData(launchMode) {
|
|
228
264
|
if (launchMode !== "host") {
|
|
229
265
|
return {
|
|
@@ -451,6 +487,7 @@ class AgentLauncher {
|
|
|
451
487
|
return new Promise((resolve, reject) => {
|
|
452
488
|
let buffer = "";
|
|
453
489
|
let settled = false;
|
|
490
|
+
const registerTimeoutMs = parseInt(process.env.UFOO_REGISTER_TIMEOUT_MS, 10) || 8000;
|
|
454
491
|
const timeout = setTimeout(() => {
|
|
455
492
|
if (settled) return;
|
|
456
493
|
settled = true;
|
|
@@ -460,7 +497,7 @@ class AgentLauncher {
|
|
|
460
497
|
// ignore
|
|
461
498
|
}
|
|
462
499
|
reject(new Error("register_agent timeout"));
|
|
463
|
-
},
|
|
500
|
+
}, registerTimeoutMs);
|
|
464
501
|
|
|
465
502
|
const cleanup = () => {
|
|
466
503
|
clearTimeout(timeout);
|
|
@@ -607,6 +644,7 @@ class AgentLauncher {
|
|
|
607
644
|
}
|
|
608
645
|
|
|
609
646
|
// 7. 启动命令(PTY wrapper或直接spawn)
|
|
647
|
+
const startupBootstrapText = String(process.env.UFOO_STARTUP_BOOTSTRAP_TEXT || "").trim();
|
|
610
648
|
|
|
611
649
|
// 7.1 PTY启用条件(显式开关 + 自动检测)
|
|
612
650
|
let shouldUsePty = false;
|
|
@@ -704,6 +742,10 @@ class AgentLauncher {
|
|
|
704
742
|
}
|
|
705
743
|
}
|
|
706
744
|
|
|
745
|
+
if (startupBootstrapText && wrapper.pty) {
|
|
746
|
+
await injectPtyCommand(wrapper, this.agentType, startupBootstrapText, "startup-bootstrap");
|
|
747
|
+
}
|
|
748
|
+
|
|
707
749
|
await notifyDaemonAgentReady(daemonSockPath, subscriberId, wrapper.pty ? wrapper.pty.pid : 0);
|
|
708
750
|
});
|
|
709
751
|
|
|
@@ -812,37 +854,8 @@ class AgentLauncher {
|
|
|
812
854
|
continue;
|
|
813
855
|
}
|
|
814
856
|
// 注入命令到PTY(带延迟确保输入完成)
|
|
815
|
-
|
|
816
|
-
// Alt+Enter (newline) instead of two separate keys. Use a
|
|
817
|
-
// longer gap so the escape sequence parser times out.
|
|
818
|
-
const commandText = String(req.command);
|
|
819
|
-
const submitDelayMs = computeInjectedSubmitDelayMs(this.agentType, commandText);
|
|
820
|
-
wrapper.write(commandText);
|
|
821
|
-
if (normalizedAgentType === "claude-code") {
|
|
822
|
-
// Claude Code: send CR directly without ESC.
|
|
823
|
-
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
824
|
-
setTimeout(() => {
|
|
825
|
-
wrapper.write("\r");
|
|
826
|
-
}, submitDelayMs);
|
|
827
|
-
} else {
|
|
828
|
-
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
829
|
-
setTimeout(() => {
|
|
830
|
-
wrapper.write("\x1b");
|
|
831
|
-
setTimeout(() => {
|
|
832
|
-
wrapper.write("\r");
|
|
833
|
-
}, 100);
|
|
834
|
-
}, submitDelayMs);
|
|
835
|
-
}
|
|
857
|
+
void injectPtyCommand(wrapper, this.agentType, req.command, "inject");
|
|
836
858
|
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
837
|
-
if (wrapper.logger) {
|
|
838
|
-
const logEntry = {
|
|
839
|
-
ts: Date.now(),
|
|
840
|
-
dir: "in",
|
|
841
|
-
data: { text: req.command, encoding: "utf8", size: req.command.length },
|
|
842
|
-
source: "inject",
|
|
843
|
-
};
|
|
844
|
-
wrapper.logger.write(JSON.stringify(logEntry) + "\n");
|
|
845
|
-
}
|
|
846
859
|
} else if (req.type === PTY_SOCKET_MESSAGE_TYPES.RAW && req.data) {
|
|
847
860
|
// Raw PTY write (no Enter appended) - for TTY view passthrough
|
|
848
861
|
wrapper.write(req.data);
|
|
@@ -952,5 +965,6 @@ class AgentLauncher {
|
|
|
952
965
|
AgentLauncher._sanitizeNickname = (nick) => nick.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
953
966
|
AgentLauncher._findPreviousSession = findPreviousSession;
|
|
954
967
|
AgentLauncher._notifyDaemonAgentReady = notifyDaemonAgentReady;
|
|
968
|
+
AgentLauncher._injectPtyCommand = injectPtyCommand;
|
|
955
969
|
|
|
956
970
|
module.exports = AgentLauncher;
|
package/src/agent/ufooAgent.js
CHANGED
|
@@ -11,8 +11,7 @@ const {
|
|
|
11
11
|
} = require("../code/nativeRunner");
|
|
12
12
|
const { DEFAULT_ASSISTANT_TIMEOUT_MS } = require("../assistant/constants");
|
|
13
13
|
const { normalizeAgentTypeAlias } = require("../bus/utils");
|
|
14
|
-
const { listProjectRuntimes } = require("../projects
|
|
15
|
-
const { isGlobalControllerProjectRoot } = require("../globalMode");
|
|
14
|
+
const { listProjectRuntimes, isGlobalControllerProjectRoot } = require("../projects");
|
|
16
15
|
|
|
17
16
|
function loadSessionState(projectRoot) {
|
|
18
17
|
const dir = getUfooPaths(projectRoot).agentDir;
|
package/src/bus/daemon.js
CHANGED
|
@@ -19,17 +19,21 @@ function isBusyActivityState(value = "") {
|
|
|
19
19
|
* Bus Daemon - 监控消息并自动注入命令
|
|
20
20
|
*/
|
|
21
21
|
class BusDaemon {
|
|
22
|
-
constructor(busDir, agentsFile, daemonDir, interval = 2000) {
|
|
22
|
+
constructor(busDir, agentsFile, daemonDir, interval = 2000, projectRoot = "") {
|
|
23
23
|
this.busDir = busDir;
|
|
24
24
|
this.agentsFile = agentsFile;
|
|
25
25
|
this.interval = interval;
|
|
26
26
|
this.daemonDir = daemonDir;
|
|
27
|
+
this.projectRoot = projectRoot || path.resolve(busDir, "..", "..");
|
|
27
28
|
this.pidFile = path.join(this.daemonDir, "daemon.pid");
|
|
28
29
|
this.logFile = path.join(this.daemonDir, "daemon.log");
|
|
29
30
|
this.countsDir = path.join(this.daemonDir, "counts", `${process.pid}`);
|
|
30
31
|
this.running = false;
|
|
31
32
|
this.cleanupCounter = 0;
|
|
32
33
|
this.cleanupInterval = 5; // 每 5 个周期清理一次
|
|
34
|
+
this.timelineSyncCounter = 0;
|
|
35
|
+
// 每 15 个周期同步一次 manual inputs (~15 × interval, default ~30s)
|
|
36
|
+
this.timelineSyncInterval = 15;
|
|
33
37
|
|
|
34
38
|
this.queueManager = new QueueManager(busDir);
|
|
35
39
|
this.injector = new Injector(busDir, agentsFile);
|
|
@@ -233,6 +237,13 @@ class BusDaemon {
|
|
|
233
237
|
this.cleanupCounter = 0;
|
|
234
238
|
}
|
|
235
239
|
|
|
240
|
+
// 定期同步 timeline(manual inputs from session files, ~15 × interval)
|
|
241
|
+
this.timelineSyncCounter++;
|
|
242
|
+
if (this.timelineSyncCounter >= this.timelineSyncInterval) {
|
|
243
|
+
this.syncTimeline();
|
|
244
|
+
this.timelineSyncCounter = 0;
|
|
245
|
+
}
|
|
246
|
+
|
|
236
247
|
// 检查所有订阅者的队列
|
|
237
248
|
await this.checkQueues();
|
|
238
249
|
} catch (err) {
|
|
@@ -244,6 +255,20 @@ class BusDaemon {
|
|
|
244
255
|
}
|
|
245
256
|
}
|
|
246
257
|
|
|
258
|
+
/**
|
|
259
|
+
* 增量同步 timeline — 捕获 manual inputs(bus 消息已在 send() 时实时追加)
|
|
260
|
+
*/
|
|
261
|
+
syncTimeline() {
|
|
262
|
+
try {
|
|
263
|
+
const { buildTimeline } = require("../history/inputTimeline");
|
|
264
|
+
buildTimeline(this.projectRoot);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
if (process.env.UFOO_HISTORY_DEBUG === "1") {
|
|
267
|
+
console.error("[daemon][history] syncTimeline failed:", err.message);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
247
272
|
/**
|
|
248
273
|
* 检查所有队列
|
|
249
274
|
*/
|
package/src/bus/index.js
CHANGED
|
@@ -328,6 +328,23 @@ class EventBus {
|
|
|
328
328
|
`Event sent: event=${eventName} seq=${result.seq} -> ${result.targets.join(", ")}`
|
|
329
329
|
);
|
|
330
330
|
}
|
|
331
|
+
|
|
332
|
+
// Real-time timeline append for message events
|
|
333
|
+
if (eventName === "message" && message) {
|
|
334
|
+
try {
|
|
335
|
+
const { appendBusEntry } = require("../history/inputTimeline");
|
|
336
|
+
appendBusEntry(this.projectRoot, {
|
|
337
|
+
seq: result.seq,
|
|
338
|
+
timestamp: new Date().toISOString(),
|
|
339
|
+
publisher,
|
|
340
|
+
target,
|
|
341
|
+
message,
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
// non-critical
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
331
348
|
return result;
|
|
332
349
|
} catch (err) {
|
|
333
350
|
logError(err.message);
|
|
@@ -364,7 +381,10 @@ class EventBus {
|
|
|
364
381
|
console.log();
|
|
365
382
|
|
|
366
383
|
for (const event of pending) {
|
|
367
|
-
|
|
384
|
+
const publisherMeta = this.busData.agents?.[event.publisher];
|
|
385
|
+
const nick = publisherMeta?.nickname;
|
|
386
|
+
const fromLabel = nick ? `${event.publisher}(${nick})` : event.publisher;
|
|
387
|
+
console.log(` ${colors.yellow}[ufoo]<from:${fromLabel}>${colors.reset}`);
|
|
368
388
|
console.log(` Type: ${event.type}/${event.event}`);
|
|
369
389
|
console.log(` Content: ${JSON.stringify(event.data)}`);
|
|
370
390
|
console.log();
|
|
@@ -731,7 +751,7 @@ class EventBus {
|
|
|
731
751
|
|
|
732
752
|
if (countAfter > countBefore) {
|
|
733
753
|
await sleep(50);
|
|
734
|
-
const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, 2000);
|
|
754
|
+
const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, 2000, this.projectRoot);
|
|
735
755
|
await daemon.injector.inject(target, options.command || "");
|
|
736
756
|
if (options.shake !== false) {
|
|
737
757
|
const tty = daemon.injector.readTty(target);
|
|
@@ -829,7 +849,7 @@ class EventBus {
|
|
|
829
849
|
*/
|
|
830
850
|
async daemon(action, options = {}) {
|
|
831
851
|
const interval = options.interval || 2000;
|
|
832
|
-
const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, interval);
|
|
852
|
+
const daemon = new BusDaemon(this.busDir, this.agentsFile, this.paths.busDaemonDir, interval, this.projectRoot);
|
|
833
853
|
|
|
834
854
|
switch (action) {
|
|
835
855
|
case "start":
|
|
@@ -6,8 +6,9 @@ const { runGroupCoreCommand } = require("../cli/groupCoreCommands");
|
|
|
6
6
|
const { loadConfig: loadProjectConfig, saveConfig: saveProjectConfig, loadGlobalUcodeConfig, saveGlobalUcodeConfig } = require("../config");
|
|
7
7
|
const { resolveTransport } = require("../code/nativeRunner");
|
|
8
8
|
const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
|
|
9
|
-
const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../
|
|
9
|
+
const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../projects");
|
|
10
10
|
const { loadPromptProfileRegistry } = require("../group/promptProfiles");
|
|
11
|
+
const { resolveSoloAgentType } = require("../solo/commands");
|
|
11
12
|
|
|
12
13
|
function defaultCreateDoctor(projectRoot) {
|
|
13
14
|
const UfooDoctor = require("../doctor");
|
|
@@ -528,10 +529,15 @@ function createCommandExecutor(options = {}) {
|
|
|
528
529
|
return;
|
|
529
530
|
}
|
|
530
531
|
|
|
531
|
-
const target =
|
|
532
|
-
|
|
532
|
+
const target = action === "assign"
|
|
533
|
+
? String(args[1] || "").trim()
|
|
534
|
+
: String(args[0] || "").trim();
|
|
535
|
+
const profile = action === "assign"
|
|
536
|
+
? String(args[2] || "").trim()
|
|
537
|
+
: String(args[1] || "").trim();
|
|
533
538
|
if (!target || !profile) {
|
|
534
|
-
logMessage("error", "{white-fg}✗{/white-fg} Usage: /role <agent-id|nickname> <prompt-profile>");
|
|
539
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /role assign <agent-id|nickname> <prompt-profile>");
|
|
540
|
+
logMessage("error", " /role <agent-id|nickname> <prompt-profile>");
|
|
535
541
|
logMessage("error", " /role list");
|
|
536
542
|
return;
|
|
537
543
|
}
|
|
@@ -548,6 +554,95 @@ function createCommandExecutor(options = {}) {
|
|
|
548
554
|
}
|
|
549
555
|
}
|
|
550
556
|
|
|
557
|
+
async function handleSoloCommand(args = []) {
|
|
558
|
+
const subcommand = String(args[0] || "").trim().toLowerCase();
|
|
559
|
+
if (!subcommand) {
|
|
560
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /solo <run|list> ...");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (subcommand === "list" || subcommand === "ls") {
|
|
565
|
+
try {
|
|
566
|
+
const registry = loadPromptProfileRegistry(projectRoot);
|
|
567
|
+
const profiles = registry.profiles || [];
|
|
568
|
+
if (profiles.length === 0) {
|
|
569
|
+
logMessage("system", "{white-fg}⚙{/white-fg} No solo roles found.");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
logMessage("system", `{white-fg}⚙{/white-fg} Available solo roles (${profiles.length}):`);
|
|
573
|
+
for (const p of profiles) {
|
|
574
|
+
const aliases = p.aliases && p.aliases.length > 0 ? ` {gray-fg}(${p.aliases.join(", ")}){/gray-fg}` : "";
|
|
575
|
+
const source = p.source ? ` {cyan-fg}[${p.source}]{/cyan-fg}` : "";
|
|
576
|
+
const summary = p.summary ? ` ${p.summary}` : "";
|
|
577
|
+
logMessage("system", ` {bold}${escapeBlessed(p.id)}{/bold}${aliases}${source}`);
|
|
578
|
+
if (summary) {
|
|
579
|
+
logMessage("system", ` ${escapeBlessed(summary)}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
} catch (err) {
|
|
583
|
+
logMessage("error", `{white-fg}✗{/white-fg} Failed to list solo roles: ${escapeBlessed(err.message)}`);
|
|
584
|
+
}
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (subcommand !== "run") {
|
|
589
|
+
logMessage("error", `{white-fg}✗{/white-fg} Unknown solo action: ${escapeBlessed(subcommand)}`);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const profile = String(args[1] || "").trim();
|
|
594
|
+
if (!profile) {
|
|
595
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /solo run <prompt-profile> [agent=codex|claude|ucode] [nickname=<name>] [scope=inplace|window]");
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const parsedOptions = {};
|
|
600
|
+
for (let i = 2; i < args.length; i += 1) {
|
|
601
|
+
const arg = args[i];
|
|
602
|
+
if (arg.includes("=")) {
|
|
603
|
+
const [key, value] = arg.split("=", 2);
|
|
604
|
+
parsedOptions[key] = value;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function normalizeLaunchScopeOption(value, fallback = "inplace") {
|
|
609
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
610
|
+
if (!raw) return fallback;
|
|
611
|
+
if (raw === "inplace" || raw === "same" || raw === "current" || raw === "tab" || raw === "pane") {
|
|
612
|
+
return "inplace";
|
|
613
|
+
}
|
|
614
|
+
if (raw === "window" || raw === "separate" || raw === "new" || raw === "new-window" || raw === "external") {
|
|
615
|
+
return "window";
|
|
616
|
+
}
|
|
617
|
+
return "";
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const config = loadConfig(projectRoot);
|
|
621
|
+
const agent = resolveSoloAgentType(config, parsedOptions.agent || parsedOptions.type || "");
|
|
622
|
+
const nickname = String(parsedOptions.nickname || "").trim();
|
|
623
|
+
const scopeRaw = parsedOptions.scope || parsedOptions.launch_scope || "";
|
|
624
|
+
const launchScope = normalizeLaunchScopeOption(scopeRaw, "inplace");
|
|
625
|
+
if (scopeRaw && !launchScope) {
|
|
626
|
+
logMessage("error", "{white-fg}✗{/white-fg} scope must be inplace|window");
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
send({
|
|
632
|
+
type: IPC_REQUEST_TYPES.LAUNCH_AGENT,
|
|
633
|
+
agent: agent === "ucode" ? "ufoo" : agent,
|
|
634
|
+
count: 1,
|
|
635
|
+
nickname,
|
|
636
|
+
prompt_profile: profile,
|
|
637
|
+
launch_scope: launchScope,
|
|
638
|
+
...collectHostLaunchRequestContext(),
|
|
639
|
+
});
|
|
640
|
+
schedule(requestStatus, 1000);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
logMessage("error", `{white-fg}✗{/white-fg} Solo launch failed: ${escapeBlessed(err.message)}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
551
646
|
async function handleResumeCommand(args = []) {
|
|
552
647
|
const action = String(args[0] || "").toLowerCase();
|
|
553
648
|
if (action === "list" || action === "ls") {
|
|
@@ -1218,6 +1313,9 @@ function createCommandExecutor(options = {}) {
|
|
|
1218
1313
|
case "role":
|
|
1219
1314
|
await handleRoleCommand(args);
|
|
1220
1315
|
return true;
|
|
1316
|
+
case "solo":
|
|
1317
|
+
await handleSoloCommand(args);
|
|
1318
|
+
return true;
|
|
1221
1319
|
case "cron":
|
|
1222
1320
|
await handleCronCommand(args);
|
|
1223
1321
|
return true;
|
|
@@ -1250,6 +1348,7 @@ function createCommandExecutor(options = {}) {
|
|
|
1250
1348
|
handleResumeCommand,
|
|
1251
1349
|
handleProjectCommand,
|
|
1252
1350
|
handleRoleCommand,
|
|
1351
|
+
handleSoloCommand,
|
|
1253
1352
|
handleCronCommand,
|
|
1254
1353
|
handleGroupCommand,
|
|
1255
1354
|
handleSettingsCommand,
|
package/src/chat/commands.js
CHANGED
|
@@ -67,7 +67,15 @@ const COMMAND_TREE = {
|
|
|
67
67
|
"/role": {
|
|
68
68
|
desc: "Assign preset role to an existing agent",
|
|
69
69
|
children: {
|
|
70
|
-
|
|
70
|
+
assign: { desc: "Assign a role to an existing agent", order: 1 },
|
|
71
|
+
list: { desc: "List available prompt profiles", order: 2 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
"/solo": {
|
|
75
|
+
desc: "Solo role agent operations",
|
|
76
|
+
children: {
|
|
77
|
+
run: { desc: "Launch a solo role agent", order: 1 },
|
|
78
|
+
list: { desc: "List available solo roles", order: 2 },
|
|
71
79
|
},
|
|
72
80
|
},
|
|
73
81
|
"/resume": {
|
|
@@ -111,10 +119,20 @@ function buildCommandRegistry(tree) {
|
|
|
111
119
|
const entry = { cmd, desc: node.desc || "" };
|
|
112
120
|
if (node.children) {
|
|
113
121
|
entry.subcommands = Object.keys(node.children)
|
|
114
|
-
.sort((a, b) =>
|
|
122
|
+
.sort((a, b) => {
|
|
123
|
+
const aNode = node.children[a] || {};
|
|
124
|
+
const bNode = node.children[b] || {};
|
|
125
|
+
const aOrder = Number.isFinite(aNode.order) ? aNode.order : Number.POSITIVE_INFINITY;
|
|
126
|
+
const bOrder = Number.isFinite(bNode.order) ? bNode.order : Number.POSITIVE_INFINITY;
|
|
127
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
128
|
+
return a.localeCompare(b, "en", { sensitivity: "base" });
|
|
129
|
+
})
|
|
115
130
|
.map((sub) => ({
|
|
116
131
|
cmd: sub,
|
|
117
132
|
desc: (node.children[sub] && node.children[sub].desc) || "",
|
|
133
|
+
order: Number.isFinite(node.children[sub] && node.children[sub].order)
|
|
134
|
+
? node.children[sub].order
|
|
135
|
+
: undefined,
|
|
118
136
|
}));
|
|
119
137
|
}
|
|
120
138
|
return entry;
|