u-foo 1.2.16 → 1.3.0
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/modules/online/README.md +18 -0
- package/package.json +2 -1
- package/src/agent/cliRunner.js +1 -1
- package/src/agent/launcher.js +23 -4
- package/src/agent/ptyRunner.js +39 -16
- package/src/agent/ufooAgent.js +2 -1
- package/src/assistant/agent.js +2 -1
- package/src/assistant/bridge.js +9 -3
- package/src/assistant/constants.js +15 -0
- package/src/assistant/engine.js +7 -2
- package/src/assistant/ufooEngineCli.js +9 -3
- package/src/chat/commandExecutor.js +188 -13
- package/src/chat/commands.js +11 -0
- package/src/chat/daemonMessageRouter.js +107 -0
- package/src/cli/groupCoreCommands.js +246 -0
- package/src/cli/onlineCoreCommands.js +8 -0
- package/src/cli.js +325 -2
- package/src/daemon/groupOrchestrator.js +557 -0
- package/src/daemon/index.js +319 -1
- package/src/daemon/status.js +48 -0
- package/src/group/diagram.js +222 -0
- package/src/group/templates.js +280 -0
- package/src/group/validateTemplate.js +234 -0
- package/src/online/server.js +193 -14
- package/src/shared/eventContract.js +5 -0
- package/src/ufoo/paths.js +2 -0
- package/templates/groups/dev-basic.json +78 -0
- package/templates/groups/research-quick.json +49 -0
package/modules/online/README.md
CHANGED
|
@@ -55,6 +55,24 @@ In private room mode, agents automatically sync:
|
|
|
55
55
|
- **Decisions** — new `.md` files synced across team
|
|
56
56
|
- **Wake events** — remote agent can wake local agent via bus
|
|
57
57
|
|
|
58
|
+
## HTTP APIs (for web preview)
|
|
59
|
+
|
|
60
|
+
Auth-required management APIs:
|
|
61
|
+
|
|
62
|
+
- `GET/POST /ufoo/online/channels`
|
|
63
|
+
- `GET/POST /ufoo/online/rooms`
|
|
64
|
+
|
|
65
|
+
Public read-only preview APIs (no bearer token required):
|
|
66
|
+
|
|
67
|
+
- `GET /ufoo/online/public/channels`
|
|
68
|
+
- `GET /ufoo/online/public/rooms?type=private`
|
|
69
|
+
- `GET /ufoo/online/public/channels/:channel/messages?limit=120`
|
|
70
|
+
|
|
71
|
+
Notes:
|
|
72
|
+
|
|
73
|
+
- Channel history is in-memory (rolling buffer) on relay server.
|
|
74
|
+
- Private room public API only exposes metadata (`room_id`, `name`, `created_by`, `password_required`).
|
|
75
|
+
|
|
58
76
|
## Storage
|
|
59
77
|
|
|
60
78
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"bin/",
|
|
29
29
|
"src/",
|
|
30
|
+
"templates/",
|
|
30
31
|
"online/",
|
|
31
32
|
"scripts/",
|
|
32
33
|
"SKILLS/",
|
package/src/agent/cliRunner.js
CHANGED
|
@@ -678,7 +678,7 @@ async function runCliAgent(params) {
|
|
|
678
678
|
cwd: params.cwd,
|
|
679
679
|
env,
|
|
680
680
|
input: retryStdin,
|
|
681
|
-
timeoutMs: params.timeoutMs ||
|
|
681
|
+
timeoutMs: params.timeoutMs || 300000,
|
|
682
682
|
onStdout: retryParser ? (chunk) => retryParser.onChunk(chunk) : null,
|
|
683
683
|
signal: params.signal,
|
|
684
684
|
});
|
package/src/agent/launcher.js
CHANGED
|
@@ -522,6 +522,13 @@ class AgentLauncher {
|
|
|
522
522
|
// 当检测到agent ready时,通知daemon可以提前inject probe
|
|
523
523
|
const daemonSockPath = getUfooPaths(this.cwd).ufooSock;
|
|
524
524
|
readyDetector.onReady(async () => {
|
|
525
|
+
// Claude Code's Ink TUI renders ❯ prompt before the input handler
|
|
526
|
+
// is fully mounted. Wait a short period for the TUI to be ready to
|
|
527
|
+
// accept injected text, otherwise only the trailing CR is processed
|
|
528
|
+
// and the probe command is lost.
|
|
529
|
+
if (this.agentType === "claude-code") {
|
|
530
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
531
|
+
}
|
|
525
532
|
const startTime = Date.now();
|
|
526
533
|
try {
|
|
527
534
|
const daemonSock = await connectWithRetry(daemonSockPath, 3, 100);
|
|
@@ -648,13 +655,25 @@ class AgentLauncher {
|
|
|
648
655
|
continue;
|
|
649
656
|
}
|
|
650
657
|
// 注入命令到PTY(带延迟确保输入完成)
|
|
658
|
+
// Claude Code (Ink TUI) interprets ESC+CR within ~100ms as
|
|
659
|
+
// Alt+Enter (newline) instead of two separate keys. Use a
|
|
660
|
+
// longer gap so the escape sequence parser times out.
|
|
651
661
|
wrapper.write(req.command);
|
|
652
|
-
|
|
653
|
-
|
|
662
|
+
if (normalizedAgentType === "claude-code") {
|
|
663
|
+
// Claude Code: send CR directly without ESC.
|
|
664
|
+
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
654
665
|
setTimeout(() => {
|
|
655
666
|
wrapper.write("\r");
|
|
656
|
-
},
|
|
657
|
-
}
|
|
667
|
+
}, 200);
|
|
668
|
+
} else {
|
|
669
|
+
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
670
|
+
setTimeout(() => {
|
|
671
|
+
wrapper.write("\x1b");
|
|
672
|
+
setTimeout(() => {
|
|
673
|
+
wrapper.write("\r");
|
|
674
|
+
}, 100);
|
|
675
|
+
}, 200);
|
|
676
|
+
}
|
|
658
677
|
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
659
678
|
if (wrapper.logger) {
|
|
660
679
|
const logEntry = {
|
package/src/agent/ptyRunner.js
CHANGED
|
@@ -332,16 +332,28 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
332
332
|
const req = JSON.parse(line);
|
|
333
333
|
if (req.type === "inject" && req.command) {
|
|
334
334
|
if (ptyProcess && ptyAlive) {
|
|
335
|
+
const isClaude = agentType === "claude-code";
|
|
335
336
|
ptyProcess.write(String(req.command));
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
337
|
+
if (isClaude) {
|
|
338
|
+
// Claude Code: send CR directly without ESC.
|
|
339
|
+
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
339
340
|
setTimeout(() => {
|
|
340
341
|
if (ptyProcess && ptyAlive) {
|
|
341
342
|
ptyProcess.write("\r");
|
|
342
343
|
}
|
|
343
|
-
},
|
|
344
|
-
}
|
|
344
|
+
}, 200);
|
|
345
|
+
} else {
|
|
346
|
+
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
if (!ptyProcess || !ptyAlive) return;
|
|
349
|
+
ptyProcess.write("\x1b");
|
|
350
|
+
setTimeout(() => {
|
|
351
|
+
if (ptyProcess && ptyAlive) {
|
|
352
|
+
ptyProcess.write("\r");
|
|
353
|
+
}
|
|
354
|
+
}, 100);
|
|
355
|
+
}, 200);
|
|
356
|
+
}
|
|
345
357
|
client.write(JSON.stringify({ ok: true }) + "\n");
|
|
346
358
|
} else {
|
|
347
359
|
client.write(JSON.stringify({ ok: false, error: "pty not ready" }) + "\n");
|
|
@@ -744,16 +756,11 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
744
756
|
setTimeout(() => {
|
|
745
757
|
if (ptyProcess && ptyAlive) {
|
|
746
758
|
outputBuffer = "";
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (ptyProcess && ptyAlive) {
|
|
753
|
-
ptyProcess.write("\r");
|
|
754
|
-
}
|
|
755
|
-
// Fallback: if we never observe the marker in echoed output,
|
|
756
|
-
// stop suppressing after a short delay to avoid freezing output.
|
|
759
|
+
const isClaude = agentType === "claude-code";
|
|
760
|
+
if (isClaude) {
|
|
761
|
+
// Claude Code: send CR directly without ESC.
|
|
762
|
+
// ESC before CR is interpreted as Alt+Enter (newline).
|
|
763
|
+
ptyProcess.write("\r");
|
|
757
764
|
suppressTimer = setTimeout(() => {
|
|
758
765
|
suppressTimer = null;
|
|
759
766
|
if (!suppressEcho) return;
|
|
@@ -762,7 +769,23 @@ async function runPtyRunner({ projectRoot, agentType = "codex" }) {
|
|
|
762
769
|
currentMarker = savedMarker;
|
|
763
770
|
outputBuffer = "";
|
|
764
771
|
}, 1500);
|
|
765
|
-
}
|
|
772
|
+
} else {
|
|
773
|
+
// Codex/others: ESC dismisses autocomplete, then CR submits.
|
|
774
|
+
ptyProcess.write("\x1b");
|
|
775
|
+
setTimeout(() => {
|
|
776
|
+
if (ptyProcess && ptyAlive) {
|
|
777
|
+
ptyProcess.write("\r");
|
|
778
|
+
}
|
|
779
|
+
suppressTimer = setTimeout(() => {
|
|
780
|
+
suppressTimer = null;
|
|
781
|
+
if (!suppressEcho) return;
|
|
782
|
+
suppressEcho = false;
|
|
783
|
+
echoMarker = "";
|
|
784
|
+
currentMarker = savedMarker;
|
|
785
|
+
outputBuffer = "";
|
|
786
|
+
}, 1500);
|
|
787
|
+
}, 100);
|
|
788
|
+
}
|
|
766
789
|
}
|
|
767
790
|
}, 200);
|
|
768
791
|
}
|
package/src/agent/ufooAgent.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
resolveCompletionUrl,
|
|
10
10
|
resolveAnthropicMessagesUrl,
|
|
11
11
|
} = require("../code/nativeRunner");
|
|
12
|
+
const { DEFAULT_ASSISTANT_TIMEOUT_MS } = require("../assistant/constants");
|
|
12
13
|
|
|
13
14
|
function loadSessionState(projectRoot) {
|
|
14
15
|
const dir = getUfooPaths(projectRoot).agentDir;
|
|
@@ -89,7 +90,7 @@ function buildSystemPrompt(context) {
|
|
|
89
90
|
"Schema:",
|
|
90
91
|
"{",
|
|
91
92
|
' "reply": "string",',
|
|
92
|
-
|
|
93
|
+
` "assistant_call": {"kind":"explore|bash|mixed","task":"string","context":"optional","expect":"optional","provider":"codex|claude|ufoo (optional)","model":"optional","timeout_ms":${DEFAULT_ASSISTANT_TIMEOUT_MS}},`,
|
|
93
94
|
' "dispatch": [{"target":"broadcast|<agent-id>|<nickname>","message":"string"}],',
|
|
94
95
|
' "ops": [{"action":"launch|close|rename|cron","agent":"codex|claude|ucode","count":1,"agent_id":"id","nickname":"optional","operation":"start|list|stop","every":"30m","interval_ms":1800000,"target":"agent-id|nickname|csv","targets":["agent-id"],"prompt":"message","id":"task-id|all"}],',
|
|
95
96
|
' "disambiguate": {"prompt":"string","candidates":[{"agent_id":"id","reason":"string"}]}',
|
package/src/assistant/agent.js
CHANGED
|
@@ -3,6 +3,7 @@ const path = require("path");
|
|
|
3
3
|
const { runCliAgent } = require("../agent/cliRunner");
|
|
4
4
|
const { normalizeCliOutput } = require("../agent/normalizeOutput");
|
|
5
5
|
const { resolveAssistantEngine, runExternalAssistantEngine } = require("./engine");
|
|
6
|
+
const { DEFAULT_ASSISTANT_TIMEOUT_MS, normalizeAssistantTimeoutMs } = require("./constants");
|
|
6
7
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
8
|
|
|
8
9
|
const ASSISTANT_JSON_SCHEMA = {
|
|
@@ -36,7 +37,7 @@ function parseTaskPayload(payload = {}) {
|
|
|
36
37
|
const kind = typeof payload.kind === "string" && payload.kind ? payload.kind : "mixed";
|
|
37
38
|
const context = typeof payload.context === "string" ? payload.context : "";
|
|
38
39
|
const expectText = typeof payload.expect === "string" ? payload.expect : "";
|
|
39
|
-
const timeoutMs =
|
|
40
|
+
const timeoutMs = normalizeAssistantTimeoutMs(payload.timeout_ms, DEFAULT_ASSISTANT_TIMEOUT_MS);
|
|
40
41
|
|
|
41
42
|
return {
|
|
42
43
|
projectRoot,
|
package/src/assistant/bridge.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
const { spawn } = require("child_process");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
DEFAULT_ASSISTANT_TIMEOUT_MS,
|
|
5
|
+
DEFAULT_ASSISTANT_TIMEOUT_GRACE_MS,
|
|
6
|
+
normalizeAssistantTimeoutMs,
|
|
7
|
+
} = require("./constants");
|
|
3
8
|
|
|
4
9
|
function resolveAssistantCommand() {
|
|
5
10
|
const raw = String(process.env.UFOO_ASSISTANT_CMD || "ufoo-assistant-agent").trim();
|
|
@@ -70,10 +75,11 @@ async function runAssistantTask({
|
|
|
70
75
|
kind = "mixed",
|
|
71
76
|
context = "",
|
|
72
77
|
expect = "",
|
|
73
|
-
timeoutMs =
|
|
78
|
+
timeoutMs = DEFAULT_ASSISTANT_TIMEOUT_MS,
|
|
74
79
|
} = {}) {
|
|
75
80
|
return new Promise((resolve) => {
|
|
76
81
|
const startedAt = Date.now();
|
|
82
|
+
const effectiveTimeoutMs = normalizeAssistantTimeoutMs(timeoutMs, DEFAULT_ASSISTANT_TIMEOUT_MS);
|
|
77
83
|
const { command, args } = resolveAssistantCommand();
|
|
78
84
|
const payload = {
|
|
79
85
|
request_id: `assistant-${startedAt}`,
|
|
@@ -85,7 +91,7 @@ async function runAssistantTask({
|
|
|
85
91
|
kind,
|
|
86
92
|
context,
|
|
87
93
|
expect,
|
|
88
|
-
timeout_ms:
|
|
94
|
+
timeout_ms: effectiveTimeoutMs,
|
|
89
95
|
};
|
|
90
96
|
|
|
91
97
|
const child = spawn(command, args, {
|
|
@@ -117,7 +123,7 @@ async function runAssistantTask({
|
|
|
117
123
|
error: "assistant timeout",
|
|
118
124
|
metrics: { duration_ms: Date.now() - startedAt },
|
|
119
125
|
});
|
|
120
|
-
},
|
|
126
|
+
}, effectiveTimeoutMs + DEFAULT_ASSISTANT_TIMEOUT_GRACE_MS);
|
|
121
127
|
|
|
122
128
|
child.on("error", (err) => {
|
|
123
129
|
clearTimeout(timer);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const DEFAULT_ASSISTANT_TIMEOUT_MS = 300000; // 5 minutes
|
|
2
|
+
const DEFAULT_ASSISTANT_TIMEOUT_GRACE_MS = 5000;
|
|
3
|
+
|
|
4
|
+
function normalizeAssistantTimeoutMs(value, fallback = DEFAULT_ASSISTANT_TIMEOUT_MS) {
|
|
5
|
+
const parsed = Number(value);
|
|
6
|
+
const base = Number.isFinite(parsed) ? parsed : fallback;
|
|
7
|
+
if (!Number.isFinite(base)) return DEFAULT_ASSISTANT_TIMEOUT_MS;
|
|
8
|
+
return Math.max(1000, Math.floor(base));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
DEFAULT_ASSISTANT_TIMEOUT_MS,
|
|
13
|
+
DEFAULT_ASSISTANT_TIMEOUT_GRACE_MS,
|
|
14
|
+
normalizeAssistantTimeoutMs,
|
|
15
|
+
};
|
package/src/assistant/engine.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { spawn } = require("child_process");
|
|
2
2
|
const { loadConfig, normalizeAssistantEngine } = require("../config");
|
|
3
|
+
const { DEFAULT_ASSISTANT_TIMEOUT_MS, normalizeAssistantTimeoutMs } = require("./constants");
|
|
3
4
|
|
|
4
5
|
function splitCommand(raw, fallback = "ufoo-engine") {
|
|
5
6
|
const text = String(raw || "").trim();
|
|
@@ -103,6 +104,9 @@ function buildExternalEngineArgs(engine = {}, payload = {}) {
|
|
|
103
104
|
if (payload.kind) args.push("--kind", String(payload.kind));
|
|
104
105
|
if (payload.context) args.push("--context", String(payload.context));
|
|
105
106
|
if (payload.expect) args.push("--expect", String(payload.expect));
|
|
107
|
+
if (Number.isFinite(payload.timeout_ms)) {
|
|
108
|
+
args.push("--timeout-ms", String(normalizeAssistantTimeoutMs(payload.timeout_ms)));
|
|
109
|
+
}
|
|
106
110
|
args.push(String(payload.task || ""));
|
|
107
111
|
return args;
|
|
108
112
|
}
|
|
@@ -115,9 +119,10 @@ function extractSessionId(parsed) {
|
|
|
115
119
|
async function runExternalAssistantEngine({
|
|
116
120
|
engine,
|
|
117
121
|
payload,
|
|
118
|
-
timeoutMs =
|
|
122
|
+
timeoutMs = DEFAULT_ASSISTANT_TIMEOUT_MS,
|
|
119
123
|
}) {
|
|
120
124
|
const startedAt = Date.now();
|
|
125
|
+
const effectiveTimeoutMs = normalizeAssistantTimeoutMs(timeoutMs, DEFAULT_ASSISTANT_TIMEOUT_MS);
|
|
121
126
|
|
|
122
127
|
const runAttempt = (attempt = {}) => new Promise((resolve) => {
|
|
123
128
|
const child = spawn(engine.command, attempt.args || [], {
|
|
@@ -149,7 +154,7 @@ async function runExternalAssistantEngine({
|
|
|
149
154
|
stderr,
|
|
150
155
|
error: "assistant engine timeout",
|
|
151
156
|
});
|
|
152
|
-
},
|
|
157
|
+
}, effectiveTimeoutMs);
|
|
153
158
|
|
|
154
159
|
child.on("error", (err) => {
|
|
155
160
|
clearTimeout(timer);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { runCliAgent } = require("../agent/cliRunner");
|
|
2
2
|
const { normalizeCliOutput } = require("../agent/normalizeOutput");
|
|
3
3
|
const { loadConfig } = require("../config");
|
|
4
|
+
const { DEFAULT_ASSISTANT_TIMEOUT_MS, normalizeAssistantTimeoutMs } = require("./constants");
|
|
4
5
|
|
|
5
6
|
function normalizeProvider(value, fallback = "codex-cli") {
|
|
6
7
|
const raw = String(value || "").trim().toLowerCase();
|
|
@@ -21,6 +22,7 @@ function parseAssistantTaskArgs(argv = []) {
|
|
|
21
22
|
kind: "mixed",
|
|
22
23
|
context: "",
|
|
23
24
|
expect: "",
|
|
25
|
+
timeoutMs: DEFAULT_ASSISTANT_TIMEOUT_MS,
|
|
24
26
|
task: "",
|
|
25
27
|
};
|
|
26
28
|
|
|
@@ -64,6 +66,10 @@ function parseAssistantTaskArgs(argv = []) {
|
|
|
64
66
|
options.expect = args[++i] || "";
|
|
65
67
|
continue;
|
|
66
68
|
}
|
|
69
|
+
if (arg === "--timeout-ms") {
|
|
70
|
+
options.timeoutMs = normalizeAssistantTimeoutMs(args[++i], DEFAULT_ASSISTANT_TIMEOUT_MS);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
67
73
|
rest.push(arg);
|
|
68
74
|
}
|
|
69
75
|
options.task = rest.join(" ").trim();
|
|
@@ -187,7 +193,7 @@ async function runEngineTask(taskInput, deps = {}) {
|
|
|
187
193
|
task: taskInput.task,
|
|
188
194
|
expect: taskInput.expect,
|
|
189
195
|
});
|
|
190
|
-
const timeoutMs =
|
|
196
|
+
const timeoutMs = normalizeAssistantTimeoutMs(taskInput.timeoutMs, DEFAULT_ASSISTANT_TIMEOUT_MS);
|
|
191
197
|
|
|
192
198
|
const runOnce = async (sessionId) => runCliAgentImpl({
|
|
193
199
|
provider,
|
|
@@ -257,7 +263,7 @@ async function runUfooEngineCli({ argv = [], stdinText = "", deps = {} } = {}) {
|
|
|
257
263
|
model: options.model,
|
|
258
264
|
sessionId: options.sessionId,
|
|
259
265
|
cwd: options.cwd,
|
|
260
|
-
timeoutMs:
|
|
266
|
+
timeoutMs: normalizeAssistantTimeoutMs(options.timeoutMs, DEFAULT_ASSISTANT_TIMEOUT_MS),
|
|
261
267
|
};
|
|
262
268
|
} else {
|
|
263
269
|
const payload = parseStdinPayload(stdinText);
|
|
@@ -281,7 +287,7 @@ async function runUfooEngineCli({ argv = [], stdinText = "", deps = {} } = {}) {
|
|
|
281
287
|
model: typeof payload.model === "string" ? payload.model : "",
|
|
282
288
|
sessionId: typeof payload.session_id === "string" ? payload.session_id : "",
|
|
283
289
|
cwd: typeof payload.project_root === "string" ? payload.project_root : "",
|
|
284
|
-
timeoutMs:
|
|
290
|
+
timeoutMs: normalizeAssistantTimeoutMs(payload.timeout_ms, DEFAULT_ASSISTANT_TIMEOUT_MS),
|
|
285
291
|
};
|
|
286
292
|
}
|
|
287
293
|
|
|
@@ -2,6 +2,7 @@ const path = require("path");
|
|
|
2
2
|
const EventBus = require("../bus");
|
|
3
3
|
const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
|
|
4
4
|
const UfooInit = require("../init");
|
|
5
|
+
const { runGroupCoreCommand } = require("../cli/groupCoreCommands");
|
|
5
6
|
const { loadConfig: loadProjectConfig, saveConfig: saveProjectConfig, loadGlobalUcodeConfig, saveGlobalUcodeConfig } = require("../config");
|
|
6
7
|
const { resolveTransport } = require("../code/nativeRunner");
|
|
7
8
|
const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
|
|
@@ -69,6 +70,7 @@ function createCommandExecutor(options = {}) {
|
|
|
69
70
|
createCronTask = () => null,
|
|
70
71
|
listCronTasks = () => [],
|
|
71
72
|
stopCronTask = () => false,
|
|
73
|
+
runGroupCore = runGroupCoreCommand,
|
|
72
74
|
requestCron = null,
|
|
73
75
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
74
76
|
schedule = (fn, ms) => setTimeout(fn, ms),
|
|
@@ -381,8 +383,6 @@ function createCommandExecutor(options = {}) {
|
|
|
381
383
|
}
|
|
382
384
|
|
|
383
385
|
try {
|
|
384
|
-
const label = nickname ? ` (${nickname})` : "";
|
|
385
|
-
logMessage("system", `{white-fg}⚙{/white-fg} Launching ${normalizedAgent}${label}...`);
|
|
386
386
|
send({
|
|
387
387
|
type: IPC_REQUEST_TYPES.LAUNCH_AGENT,
|
|
388
388
|
agent: normalizedAgent,
|
|
@@ -413,6 +413,19 @@ function createCommandExecutor(options = {}) {
|
|
|
413
413
|
schedule(requestStatus, 1000);
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
function parseKeyValueArgs(args = []) {
|
|
417
|
+
const parsed = {};
|
|
418
|
+
for (const raw of args) {
|
|
419
|
+
if (!raw || !String(raw).includes("=")) continue;
|
|
420
|
+
const [keyRaw, ...valueParts] = String(raw).split("=");
|
|
421
|
+
const key = String(keyRaw || "").trim().toLowerCase();
|
|
422
|
+
const value = valueParts.join("=").trim();
|
|
423
|
+
if (!key) continue;
|
|
424
|
+
parsed[key] = value;
|
|
425
|
+
}
|
|
426
|
+
return parsed;
|
|
427
|
+
}
|
|
428
|
+
|
|
416
429
|
function parseCronTargets(raw = "") {
|
|
417
430
|
return String(raw || "")
|
|
418
431
|
.split(",")
|
|
@@ -499,7 +512,7 @@ function createCommandExecutor(options = {}) {
|
|
|
499
512
|
}
|
|
500
513
|
|
|
501
514
|
const startArgs = action === "start" ? args.slice(1) : args;
|
|
502
|
-
const kv =
|
|
515
|
+
const kv = parseKeyValueArgs(startArgs);
|
|
503
516
|
const nonKvParts = startArgs.filter((item) => !String(item || "").includes("="));
|
|
504
517
|
|
|
505
518
|
const intervalRaw = String(
|
|
@@ -598,6 +611,173 @@ function createCommandExecutor(options = {}) {
|
|
|
598
611
|
);
|
|
599
612
|
}
|
|
600
613
|
|
|
614
|
+
function parseBooleanOption(value, fallback = false) {
|
|
615
|
+
const text = String(value || "").trim().toLowerCase();
|
|
616
|
+
if (!text) return fallback;
|
|
617
|
+
if (text === "1" || text === "true" || text === "yes" || text === "y" || text === "on") return true;
|
|
618
|
+
if (text === "0" || text === "false" || text === "no" || text === "n" || text === "off") return false;
|
|
619
|
+
return fallback;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function logGroupCoreOutput(text) {
|
|
623
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
624
|
+
lines.forEach((line) => {
|
|
625
|
+
logMessage("system", escapeBlessed(line));
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function handleGroupCommand(args = []) {
|
|
630
|
+
const subcommand = String(args[0] || "").trim().toLowerCase();
|
|
631
|
+
if (!subcommand) {
|
|
632
|
+
logMessage(
|
|
633
|
+
"error",
|
|
634
|
+
"{white-fg}✗{/white-fg} Usage: /group <templates|template|run|status|stop|diagram> ..."
|
|
635
|
+
);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (subcommand === "templates") {
|
|
640
|
+
const action = String(args[1] || "list").trim().toLowerCase();
|
|
641
|
+
if (action !== "list" && action !== "ls") {
|
|
642
|
+
logMessage("error", `{white-fg}✗{/white-fg} Unknown group templates action: ${escapeBlessed(action)}`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
await runGroupCore("templates", [action], {
|
|
647
|
+
cwd: projectRoot,
|
|
648
|
+
write: logGroupCoreOutput,
|
|
649
|
+
});
|
|
650
|
+
} catch (err) {
|
|
651
|
+
logMessage("error", `{white-fg}✗{/white-fg} Group templates failed: ${escapeBlessed(err.message)}`);
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (subcommand === "template") {
|
|
657
|
+
const action = String(args[1] || "list").trim().toLowerCase();
|
|
658
|
+
if (action === "validate") {
|
|
659
|
+
const target = String(args[2] || "").trim();
|
|
660
|
+
if (!target) {
|
|
661
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /group template validate <alias|path>");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
send({
|
|
665
|
+
type: IPC_REQUEST_TYPES.GROUP_TEMPLATE_VALIDATE,
|
|
666
|
+
target,
|
|
667
|
+
alias: target,
|
|
668
|
+
path: target,
|
|
669
|
+
});
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
await runGroupCore("template", [action, ...args.slice(2)], {
|
|
674
|
+
cwd: projectRoot,
|
|
675
|
+
write: logGroupCoreOutput,
|
|
676
|
+
});
|
|
677
|
+
} catch (err) {
|
|
678
|
+
logMessage("error", `{white-fg}✗{/white-fg} Group template failed: ${escapeBlessed(err.message)}`);
|
|
679
|
+
}
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (subcommand === "run") {
|
|
684
|
+
const alias = String(args[1] || "").trim();
|
|
685
|
+
if (!alias) {
|
|
686
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /group run <alias> [instance=<name>] [dry_run=true]");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const runArgs = args.slice(2);
|
|
690
|
+
const kv = parseKeyValueArgs(runArgs);
|
|
691
|
+
let instance = String(kv.instance || kv.group_id || "").trim();
|
|
692
|
+
const instanceIndex = runArgs.indexOf("--instance");
|
|
693
|
+
if (instanceIndex !== -1) {
|
|
694
|
+
instance = String(runArgs[instanceIndex + 1] || "").trim();
|
|
695
|
+
}
|
|
696
|
+
let dryRun = runArgs.includes("--dry-run");
|
|
697
|
+
if (!dryRun && Object.prototype.hasOwnProperty.call(kv, "dry_run")) {
|
|
698
|
+
dryRun = parseBooleanOption(kv.dry_run, false);
|
|
699
|
+
}
|
|
700
|
+
if (!dryRun && Object.prototype.hasOwnProperty.call(kv, "dryrun")) {
|
|
701
|
+
dryRun = parseBooleanOption(kv.dryrun, false);
|
|
702
|
+
}
|
|
703
|
+
send({
|
|
704
|
+
type: IPC_REQUEST_TYPES.LAUNCH_GROUP,
|
|
705
|
+
alias,
|
|
706
|
+
instance,
|
|
707
|
+
dry_run: dryRun,
|
|
708
|
+
});
|
|
709
|
+
schedule(requestStatus, 1000);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if (subcommand === "status") {
|
|
714
|
+
const statusArgs = args.slice(1);
|
|
715
|
+
const kv = parseKeyValueArgs(statusArgs);
|
|
716
|
+
const groupId = String(
|
|
717
|
+
kv.group_id ||
|
|
718
|
+
kv.group ||
|
|
719
|
+
(statusArgs[0] && !String(statusArgs[0]).includes("=") ? statusArgs[0] : "")
|
|
720
|
+
).trim();
|
|
721
|
+
send({
|
|
722
|
+
type: IPC_REQUEST_TYPES.GROUP_STATUS,
|
|
723
|
+
group_id: groupId,
|
|
724
|
+
});
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (subcommand === "stop") {
|
|
729
|
+
const stopArgs = args.slice(1);
|
|
730
|
+
const kv = parseKeyValueArgs(stopArgs);
|
|
731
|
+
const groupId = String(
|
|
732
|
+
kv.group_id ||
|
|
733
|
+
kv.group ||
|
|
734
|
+
(stopArgs[0] && !String(stopArgs[0]).includes("=") ? stopArgs[0] : "")
|
|
735
|
+
).trim();
|
|
736
|
+
if (!groupId) {
|
|
737
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /group stop <groupId>");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
send({
|
|
741
|
+
type: IPC_REQUEST_TYPES.STOP_GROUP,
|
|
742
|
+
group_id: groupId,
|
|
743
|
+
});
|
|
744
|
+
schedule(requestStatus, 1000);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (subcommand === "diagram") {
|
|
749
|
+
const diagramArgs = args.slice(1);
|
|
750
|
+
const kv = parseKeyValueArgs(diagramArgs);
|
|
751
|
+
const target = String(
|
|
752
|
+
kv.group_id ||
|
|
753
|
+
kv.group ||
|
|
754
|
+
kv.alias ||
|
|
755
|
+
(diagramArgs[0] && !String(diagramArgs[0]).includes("=") ? diagramArgs[0] : "")
|
|
756
|
+
).trim();
|
|
757
|
+
if (!target) {
|
|
758
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /group diagram <alias|groupId> [format=ascii|mermaid]");
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const format = diagramArgs.includes("--mermaid")
|
|
762
|
+
? "mermaid"
|
|
763
|
+
: (diagramArgs.includes("--ascii")
|
|
764
|
+
? "ascii"
|
|
765
|
+
: String(kv.format || "ascii").trim().toLowerCase());
|
|
766
|
+
send({
|
|
767
|
+
type: IPC_REQUEST_TYPES.GROUP_DIAGRAM,
|
|
768
|
+
alias: target,
|
|
769
|
+
group_id: target,
|
|
770
|
+
format: format === "mermaid" ? "mermaid" : "ascii",
|
|
771
|
+
});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
logMessage(
|
|
776
|
+
"error",
|
|
777
|
+
"{white-fg}✗{/white-fg} Unknown group command. Use: templates, template, run, status, stop, diagram"
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
601
781
|
async function handleSettingsCommand(args = []) {
|
|
602
782
|
const section = String(args[0] || "").trim().toLowerCase();
|
|
603
783
|
if (!section) {
|
|
@@ -619,16 +799,7 @@ function createCommandExecutor(options = {}) {
|
|
|
619
799
|
}
|
|
620
800
|
|
|
621
801
|
function parseUcodeConfigKv(args = []) {
|
|
622
|
-
|
|
623
|
-
for (const raw of args) {
|
|
624
|
-
if (!raw || !String(raw).includes("=")) continue;
|
|
625
|
-
const [keyRaw, ...valueParts] = String(raw).split("=");
|
|
626
|
-
const key = String(keyRaw || "").trim().toLowerCase();
|
|
627
|
-
const value = valueParts.join("=").trim();
|
|
628
|
-
if (!key) continue;
|
|
629
|
-
parsed[key] = value;
|
|
630
|
-
}
|
|
631
|
-
return parsed;
|
|
802
|
+
return parseKeyValueArgs(args);
|
|
632
803
|
}
|
|
633
804
|
|
|
634
805
|
function maskSecret(value = "") {
|
|
@@ -795,6 +966,9 @@ function createCommandExecutor(options = {}) {
|
|
|
795
966
|
case "cron":
|
|
796
967
|
await handleCronCommand(args);
|
|
797
968
|
return true;
|
|
969
|
+
case "group":
|
|
970
|
+
await handleGroupCommand(args);
|
|
971
|
+
return true;
|
|
798
972
|
case "settings":
|
|
799
973
|
await handleSettingsCommand(args);
|
|
800
974
|
return true;
|
|
@@ -819,6 +993,7 @@ function createCommandExecutor(options = {}) {
|
|
|
819
993
|
handleLaunchCommand,
|
|
820
994
|
handleResumeCommand,
|
|
821
995
|
handleCronCommand,
|
|
996
|
+
handleGroupCommand,
|
|
822
997
|
handleSettingsCommand,
|
|
823
998
|
handleUcodeConfigCommand,
|
|
824
999
|
handleUfooCommand,
|
package/src/chat/commands.js
CHANGED
|
@@ -35,6 +35,17 @@ const COMMAND_TREE = {
|
|
|
35
35
|
stop: { desc: "Stop cron task by id or all" },
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
|
+
"/group": {
|
|
39
|
+
desc: "Agent group orchestration",
|
|
40
|
+
children: {
|
|
41
|
+
diagram: { desc: "Render group diagram (ascii|mermaid)" },
|
|
42
|
+
run: { desc: "Launch a group template" },
|
|
43
|
+
status: { desc: "Show group runtime status" },
|
|
44
|
+
stop: { desc: "Stop a running group" },
|
|
45
|
+
template: { desc: "Template ops (list/show/validate/new)" },
|
|
46
|
+
templates: { desc: "List available templates" },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
38
49
|
"/init": { desc: "Initialize modules" },
|
|
39
50
|
"/launch": {
|
|
40
51
|
desc: "Launch new agent",
|