u-foo 1.2.16 → 1.4.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.
@@ -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.2.16",
3
+ "version": "1.4.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/",
@@ -678,7 +678,7 @@ async function runCliAgent(params) {
678
678
  cwd: params.cwd,
679
679
  env,
680
680
  input: retryStdin,
681
- timeoutMs: params.timeoutMs || 60000,
681
+ timeoutMs: params.timeoutMs || 300000,
682
682
  onStdout: retryParser ? (chunk) => retryParser.onChunk(chunk) : null,
683
683
  signal: params.signal,
684
684
  });
@@ -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
- setTimeout(() => {
653
- wrapper.write("\x1b");
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
- }, 100);
657
- }, 200);
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 = {
@@ -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
- setTimeout(() => {
337
- if (!ptyProcess || !ptyAlive) return;
338
- ptyProcess.write("\x1b");
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
- }, 100);
344
- }, 200);
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
- // Send ESC first to dismiss any auto-complete/suggestion overlay
748
- // in Ink-based TUIs (Claude Code, Codex), then CR to submit.
749
- // This matches the inject socket pattern in launcher.js.
750
- ptyProcess.write("\x1b");
751
- setTimeout(() => {
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
- }, 100);
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
  }
@@ -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
- ' "assistant_call": {"kind":"explore|bash|mixed","task":"string","context":"optional","expect":"optional","provider":"codex|claude|ufoo (optional)","model":"optional","timeout_ms":60000},',
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"}]}',
@@ -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 = Number.isFinite(payload.timeout_ms) ? payload.timeout_ms : 60000;
40
+ const timeoutMs = normalizeAssistantTimeoutMs(payload.timeout_ms, DEFAULT_ASSISTANT_TIMEOUT_MS);
40
41
 
41
42
  return {
42
43
  projectRoot,
@@ -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 = 60000,
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: timeoutMs,
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
- }, timeoutMs);
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
+ };
@@ -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 = 60000,
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
- }, timeoutMs);
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 = Number.isFinite(taskInput.timeoutMs) ? taskInput.timeoutMs : 60000;
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: 60000,
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: Number.isFinite(payload.timeout_ms) ? payload.timeout_ms : 60000,
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 = parseUcodeConfigKv(startArgs);
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
- const parsed = {};
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,
@@ -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",