u-foo 1.2.13 → 1.2.16

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/ufoo.js CHANGED
@@ -42,6 +42,155 @@ async function main() {
42
42
  await runChat(process.cwd());
43
43
  return;
44
44
  }
45
+
46
+ // Handle resume command to resume/launch agent sessions
47
+ if (cmd === "resume") {
48
+ const target = process.argv[3];
49
+ if (!target) {
50
+ console.error("Error: resume requires an agent type or nickname");
51
+ console.error("Usage: ufoo resume <ucode|uclaude|ucodex|nickname>");
52
+ console.error("");
53
+ console.error("Examples:");
54
+ console.error(" ufoo resume ucode # Start new ucode agent");
55
+ console.error(" ufoo resume ucode-1 # Resume agent with nickname ucode-1");
56
+ console.error(" ufoo resume uclaude # Start new uclaude agent");
57
+ process.exitCode = 1;
58
+ return;
59
+ }
60
+
61
+ const { execSync } = require("child_process");
62
+ const path = require("path");
63
+ const { spawn } = require("child_process");
64
+
65
+ // First check if it's a nickname for an existing online agent
66
+ let targetAgent = null;
67
+ try {
68
+ const statusOutput = execSync("ufoo bus status", { encoding: "utf8", cwd: process.cwd() });
69
+
70
+ // Parse online agents
71
+ const onlineAgents = [];
72
+ const lines = statusOutput.split("\n");
73
+ let inOnlineSection = false;
74
+
75
+ for (const line of lines) {
76
+ if (line.includes("Online agents:")) {
77
+ inOnlineSection = true;
78
+ continue;
79
+ }
80
+ if (inOnlineSection) {
81
+ const trimmedLine = line.trim();
82
+ if (!trimmedLine) continue;
83
+ if (trimmedLine.includes("Event statistics:") || trimmedLine.includes("===")) {
84
+ inOnlineSection = false;
85
+ continue;
86
+ }
87
+ // Parse agent line: "ufoo-code:abc123 (nickname)"
88
+ const agentMatch = trimmedLine.match(/^[-\s]*([a-z-]+:[a-f0-9]+|[a-z-]+)(?:\s+\(([^)]+)\))?/i);
89
+ if (agentMatch) {
90
+ const subscriberId = agentMatch[1];
91
+ const nickname = agentMatch[2] || "";
92
+ onlineAgents.push({ subscriberId, nickname });
93
+ }
94
+ }
95
+ }
96
+
97
+ // Check if target matches any online agent's nickname
98
+ for (const agent of onlineAgents) {
99
+ if (agent.nickname && agent.nickname === target) {
100
+ targetAgent = agent;
101
+ break;
102
+ }
103
+ }
104
+ } catch (err) {
105
+ // Ignore errors from bus status check
106
+ }
107
+
108
+ if (targetAgent) {
109
+ // Found an online agent with this nickname
110
+ // Determine the agent type from subscriber ID
111
+ const subscriberId = targetAgent.subscriberId;
112
+ const [agentType, sessionId] = subscriberId.split(":");
113
+
114
+ let scriptName = "";
115
+ let displayName = "";
116
+
117
+ if (agentType === "ufoo-code") {
118
+ scriptName = "ucode.js";
119
+ displayName = "ucode";
120
+ } else if (agentType === "claude-code") {
121
+ scriptName = "uclaude.js";
122
+ displayName = "uclaude";
123
+ } else if (agentType === "codex") {
124
+ scriptName = "ucodex.js";
125
+ displayName = "ucodex";
126
+ } else {
127
+ console.error(`Error: Unable to determine agent type for ${subscriberId}`);
128
+ process.exitCode = 1;
129
+ return;
130
+ }
131
+
132
+ console.log(`Resuming ${displayName} session for ${targetAgent.nickname} (${subscriberId})`);
133
+
134
+ // Set environment variable to indicate this is a resume
135
+ const scriptPath = path.join(__dirname, scriptName);
136
+ const env = { ...process.env };
137
+
138
+ // Pass the subscriber ID so the agent can reuse the same identity
139
+ env.UFOO_SUBSCRIBER_ID = subscriberId;
140
+
141
+ // Spawn the agent process - it will reuse the subscriber ID
142
+ const child = spawn(process.execPath, [scriptPath], {
143
+ stdio: "inherit",
144
+ cwd: process.cwd(),
145
+ env,
146
+ });
147
+
148
+ child.on("exit", (code) => {
149
+ process.exit(code || 0);
150
+ });
151
+
152
+ return;
153
+ }
154
+
155
+ // Not an existing online agent - check if it's an agent type to launch
156
+ const targetLower = target.toLowerCase();
157
+ let scriptName = "";
158
+
159
+ if (targetLower === "ucode" || targetLower === "ufoo-code" || targetLower === "ufoo") {
160
+ scriptName = "ucode.js";
161
+ } else if (targetLower === "uclaude" || targetLower === "claude-code" || targetLower === "claude") {
162
+ scriptName = "uclaude.js";
163
+ } else if (targetLower === "ucodex" || targetLower === "codex" || targetLower === "openai") {
164
+ scriptName = "ucodex.js";
165
+ } else {
166
+ // Not a valid agent type - might be an offline agent nickname
167
+ console.error(`Error: Agent '${target}' is not online and is not a valid agent type`);
168
+ console.error("");
169
+ console.error("Valid agent types: ucode, uclaude, ucodex");
170
+ console.error("");
171
+ console.error("To see online agents, run: ufoo bus status");
172
+ process.exitCode = 1;
173
+ return;
174
+ }
175
+
176
+ // Run the agent script directly for a new session
177
+ const scriptPath = path.join(__dirname, scriptName);
178
+ console.log(`Starting new ${target} session...`);
179
+
180
+ // Spawn the agent process and inherit stdio for interactive mode
181
+ const child = spawn(process.execPath, [scriptPath], {
182
+ stdio: "inherit",
183
+ cwd: process.cwd(),
184
+ env: process.env,
185
+ });
186
+
187
+ child.on("exit", (code) => {
188
+ process.exit(code || 0);
189
+ });
190
+
191
+ return;
192
+ }
193
+
45
194
  await runCli(process.argv);
46
195
  }
47
196
 
@@ -62,7 +62,7 @@ Inbox retention: channel messages 7 days, room messages 30 days.
62
62
  ## Full Connect Options
63
63
 
64
64
  ```bash
65
- ufoo online connect --nickname <name> [--url <ws://...>] [--subscriber <id>]
65
+ ufoo online connect --nickname <name> [--url <wss://...>] [--subscriber <id>]
66
66
  [--token <tok>] [--token-hash <hash>] [--world <name>] [--ping-ms <ms>]
67
67
  [--join <channel>] [--room <room-id> --room-password <pwd>]
68
68
  [--interval <ms>] [--allow-insecure-ws]
@@ -131,7 +131,7 @@ ufoo online inbox agent-b # See agent-a's message
131
131
  ### 2. Private room collaboration
132
132
 
133
133
  ```bash
134
- ufoo online room create --type private --password secret --server http://127.0.0.1:8787
134
+ ufoo online room create --type private --password secret --server https://online.ufoo.dev
135
135
  # → returns room_id
136
136
 
137
137
  ufoo online connect --nickname dev-1 --room room_001 --room-password secret
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "1.2.13",
3
+ "version": "1.2.16",
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",
@@ -74,31 +74,49 @@ function forceSymlink(target, linkPath) {
74
74
  fs.rmSync(linkPath, { recursive: true, force: true });
75
75
  }
76
76
  } catch {
77
- // doesn't exist fine
77
+ // doesn't exist - fine
78
78
  }
79
79
  fs.symlinkSync(target, linkPath);
80
80
  }
81
81
 
82
- // Install ufoo skills as Claude Code slash commands (~/.claude/commands/<name>.md)
83
- // and as skill directories (~/.claude/skills/<name>/)
82
+ function installClaudeCommands(home, sources) {
83
+ const commandsDir = path.join(home, ".claude", "commands");
84
+ fs.mkdirSync(commandsDir, { recursive: true });
85
+
86
+ for (const { name, md } of sources) {
87
+ forceSymlink(md, path.join(commandsDir, `${name}.md`));
88
+ }
89
+
90
+ console.log(`[postinstall] Installed ${sources.length} ufoo command(s) to ${commandsDir}`);
91
+ }
92
+
93
+ function installSkillDirs(targetDir, sources, label) {
94
+ fs.mkdirSync(targetDir, { recursive: true });
95
+
96
+ for (const { name, dir } of sources) {
97
+ forceSymlink(dir, path.join(targetDir, name));
98
+ }
99
+
100
+ console.log(`[postinstall] Installed ${sources.length} ufoo skill(s) to ${label}`);
101
+ }
102
+
103
+ // Install ufoo skills for Claude and Codex at npm install time.
104
+ // - Claude slash commands: ~/.claude/commands/<name>.md -> SKILL.md
105
+ // - Claude skills: ~/.claude/skills/<name> -> skill dir
106
+ // - Codex skills: ${CODEX_HOME:-~/.codex}/skills/<name> -> skill dir
84
107
  try {
85
108
  const pkgRoot = path.resolve(__dirname, "..");
86
109
  const home = os.homedir();
87
110
  const sources = collectSkillSources(pkgRoot);
88
111
 
89
112
  if (sources.length > 0) {
90
- // Slash commands: ~/.claude/commands/<name>.md -> SKILL.md
91
- const commandsDir = path.join(home, ".claude", "commands");
92
- fs.mkdirSync(commandsDir, { recursive: true });
93
-
94
- let installed = 0;
95
- for (const { name, md } of sources) {
96
- forceSymlink(md, path.join(commandsDir, `${name}.md`));
97
- installed += 1;
98
- }
99
- console.log(`[postinstall] Installed ${installed} ufoo command(s) to ${commandsDir}`);
113
+ installClaudeCommands(home, sources);
114
+ installSkillDirs(path.join(home, ".claude", "skills"), sources, "~/.claude/skills");
115
+
116
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
117
+ installSkillDirs(path.join(codexHome, "skills"), sources, `${codexHome}/skills`);
100
118
  }
101
119
  } catch (err) {
102
- // Non-fatal skills can be installed manually via `ufoo skills install`
120
+ // Non-fatal - skills can be installed manually via `ufoo skills install`
103
121
  console.log(`[postinstall] Skipped skills install: ${err.message}`);
104
122
  }
@@ -175,7 +175,11 @@ class AgentNotifier {
175
175
  data.message = `delivery failed to ${this.lastNickname || this.subscriber}: ${errorMessage || "unknown error"}`;
176
176
  }
177
177
  try {
178
- await this.eventBus.send(publisher, "", this.subscriber, { event: "delivery", data });
178
+ await this.eventBus.send(publisher, "", this.subscriber, {
179
+ event: "delivery",
180
+ data,
181
+ silent: true,
182
+ });
179
183
  } catch {
180
184
  // ignore delivery emit failures
181
185
  }
@@ -99,7 +99,7 @@ function buildSystemPrompt(context) {
99
99
  "- If multiple possible agents, use disambiguate with candidates and no dispatch.",
100
100
  "- If user specifies a nickname for a new agent, include ops.launch with nickname so daemon can rename.",
101
101
  "- If user requests rename, use ops.rename with agent_id and nickname (do NOT launch).",
102
- "- For scheduled follow-up (cron/corn), use ops.cron with operation=start and include every+target(s)+prompt.",
102
+ "- For scheduled follow-up (cron), use ops.cron with operation=start and include every+target(s)+prompt (or at for one-time).",
103
103
  "- To check scheduled tasks, use ops.cron with operation=list.",
104
104
  "- To stop scheduled tasks, use ops.cron with operation=stop and id (or id=all).",
105
105
  "- Use top-level assistant_call for project exploration, temporary shell tasks, and quick execution support.",
package/src/bus/index.js CHANGED
@@ -308,9 +308,16 @@ class EventBus {
308
308
  const result = eventName === "message"
309
309
  ? await this.messageManager.send(target, message, publisher)
310
310
  : await this.messageManager.emit(target, eventName, data, publisher);
311
- logOk(
312
- `Message sent: seq=${result.seq} -> ${result.targets.join(", ")}`
313
- );
311
+ const silent = options.silent === true;
312
+ if (!silent && eventName === "message") {
313
+ logOk(
314
+ `Message sent: seq=${result.seq} -> ${result.targets.join(", ")}`
315
+ );
316
+ } else if (!silent && process.env.UFOO_BUS_VERBOSE_EVENTS === "1") {
317
+ logInfo(
318
+ `Event sent: event=${eventName} seq=${result.seq} -> ${result.targets.join(", ")}`
319
+ );
320
+ }
314
321
  return result;
315
322
  } catch (err) {
316
323
  logError(err.message);
package/src/bus/inject.js CHANGED
@@ -284,8 +284,11 @@ class Injector {
284
284
  // 读取 tty(tmux 需要)
285
285
  const tty = this.readTty(subscriber);
286
286
 
287
- if (supportsNotifier) {
288
- // 2. 尝试 tmux(无需权限)
287
+ // 2. 尝试 tmux(无需权限)
288
+ // Launch mode may be temporarily missing/stale (e.g. rejoin from non-interactive context).
289
+ // In that case still try tmux fallback by pane/tty.
290
+ const allowTmuxFallback = supportsNotifier || !launchMode || launchMode === "terminal" || launchMode === "tmux";
291
+ if (allowTmuxFallback) {
289
292
  const tmuxPane = meta.tmux_pane || this.getTmuxPane(subscriber);
290
293
  if (tmuxPane) {
291
294
  const paneExists = await this.checkTmuxPane(tmuxPane);
@@ -296,7 +299,7 @@ class Injector {
296
299
  }
297
300
  }
298
301
 
299
- // 尝试通过 tty 查找 tmux pane
302
+ // Try resolving pane via tty when tmux pane metadata is missing.
300
303
  if (tty && isValidTty(tty)) {
301
304
  const fallbackPane = await this.findTmuxPaneByTty(tty);
302
305
  if (fallbackPane) {
@@ -161,7 +161,15 @@ class SubscriberManager {
161
161
  nicknameManager.setNickname(subscriber, finalNickname);
162
162
  }
163
163
 
164
- const launchMode = options.launchMode || process.env.UFOO_LAUNCH_MODE || "";
164
+ const explicitLaunchMode = typeof options.launchMode === "string"
165
+ ? options.launchMode.trim()
166
+ : "";
167
+ const envLaunchMode = typeof process.env.UFOO_LAUNCH_MODE === "string"
168
+ ? process.env.UFOO_LAUNCH_MODE.trim()
169
+ : "";
170
+ const preservedLaunchMode = existingMeta?.launch_mode || "";
171
+ const inferredLaunchMode = process.env.TMUX_PANE ? "tmux" : "";
172
+ const launchMode = explicitLaunchMode || envLaunchMode || preservedLaunchMode || inferredLaunchMode;
165
173
  const overridePid = Number.isFinite(options.parentPid) && options.parentPid > 0
166
174
  ? options.parentPid
167
175
  : null;
@@ -184,6 +192,11 @@ class SubscriberManager {
184
192
  const preserved = existingMeta && typeof existingMeta === "object"
185
193
  ? { ...existingMeta }
186
194
  : {};
195
+ const explicitTmuxPane = typeof options.tmuxPane === "string" ? options.tmuxPane.trim() : "";
196
+ const envTmuxPane = typeof process.env.TMUX_PANE === "string" ? process.env.TMUX_PANE.trim() : "";
197
+ const preservedTmuxPane = typeof existingMeta?.tmux_pane === "string" ? existingMeta.tmux_pane.trim() : "";
198
+ const tmuxPane = explicitTmuxPane || envTmuxPane || preservedTmuxPane;
199
+
187
200
  this.busData.agents[subscriber] = {
188
201
  ...preserved,
189
202
  agent_type: agentType,
@@ -194,7 +207,7 @@ class SubscriberManager {
194
207
  pid: overridePid || getJoinedPid(),
195
208
  tty: finalTty,
196
209
  tty_shell_pid: ttyInfo?.shellPid || 0,
197
- tmux_pane: options.tmuxPane || process.env.TMUX_PANE || "",
210
+ tmux_pane: tmuxPane,
198
211
  launch_mode: launchMode,
199
212
  };
200
213
 
@@ -69,6 +69,7 @@ function createCommandExecutor(options = {}) {
69
69
  createCronTask = () => null,
70
70
  listCronTasks = () => [],
71
71
  stopCronTask = () => false,
72
+ requestCron = null,
72
73
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
73
74
  schedule = (fn, ms) => setTimeout(fn, ms),
74
75
  } = options;
@@ -419,9 +420,44 @@ function createCommandExecutor(options = {}) {
419
420
  .filter(Boolean);
420
421
  }
421
422
 
422
- async function handleCornCommand(args = []) {
423
+ function parseCronAtMs(raw = "") {
424
+ const text = String(raw || "").trim();
425
+ if (!text) return 0;
426
+
427
+ if (/^\d+$/.test(text)) {
428
+ const value = Number.parseInt(text, 10);
429
+ if (!Number.isFinite(value) || value <= 0) return 0;
430
+ return text.length <= 10 ? value * 1000 : value;
431
+ }
432
+
433
+ const normalized = text.replace(/\//g, "-");
434
+ const directMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2})(?::(\d{2}))?$/);
435
+ if (directMatch) {
436
+ const seconds = directMatch[3] || "00";
437
+ const parsed = Date.parse(`${directMatch[1]}T${directMatch[2]}:${seconds}`);
438
+ return Number.isFinite(parsed) ? parsed : 0;
439
+ }
440
+
441
+ const parsed = Date.parse(normalized);
442
+ return Number.isFinite(parsed) ? parsed : 0;
443
+ }
444
+
445
+ function formatCronAt(ms = 0) {
446
+ const ts = Number(ms) || 0;
447
+ if (ts <= 0) return "";
448
+ const d = new Date(ts);
449
+ const pad = (v) => String(v).padStart(2, "0");
450
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
451
+ }
452
+
453
+ async function handleCronCommand(args = []) {
423
454
  const action = String(args[0] || "").trim().toLowerCase();
424
455
  if (action === "list" || action === "ls") {
456
+ if (typeof requestCron === "function") {
457
+ requestCron({ operation: "list" });
458
+ schedule(requestStatus, 200);
459
+ return;
460
+ }
425
461
  const tasks = Array.isArray(listCronTasks()) ? listCronTasks() : [];
426
462
  if (tasks.length === 0) {
427
463
  logMessage("system", "{cyan-fg}Cron:{/cyan-fg} none");
@@ -437,7 +473,12 @@ function createCommandExecutor(options = {}) {
437
473
  if (action === "stop" || action === "rm" || action === "remove") {
438
474
  const target = String(args[1] || "").trim();
439
475
  if (!target) {
440
- logMessage("error", "{white-fg}✗{/white-fg} Usage: /corn stop <id|all>");
476
+ logMessage("error", "{white-fg}✗{/white-fg} Usage: /cron stop <id|all>");
477
+ return;
478
+ }
479
+ if (typeof requestCron === "function") {
480
+ requestCron({ operation: "stop", id: target });
481
+ schedule(requestStatus, 200);
441
482
  return;
442
483
  }
443
484
  if (target === "all") {
@@ -464,6 +505,16 @@ function createCommandExecutor(options = {}) {
464
505
  const intervalRaw = String(
465
506
  kv.every || kv.interval || kv.interval_ms || kv.ms || ""
466
507
  ).trim();
508
+ const atRaw = String(
509
+ kv.at ||
510
+ kv.once ||
511
+ kv.run_at ||
512
+ kv.runat ||
513
+ kv.datetime ||
514
+ kv.date_time ||
515
+ ((kv.date && kv.time) ? `${kv.date} ${kv.time}` : "") ||
516
+ ""
517
+ ).trim();
467
518
  const targetsRaw = String(
468
519
  kv.target || kv.targets || kv.agent || kv.agents || ""
469
520
  ).trim();
@@ -471,26 +522,66 @@ function createCommandExecutor(options = {}) {
471
522
  kv.prompt || kv.message || kv.msg || nonKvParts.join(" ") || ""
472
523
  ).trim();
473
524
 
474
- if (!intervalRaw || !targetsRaw || !prompt) {
525
+ if ((!intervalRaw && !atRaw) || !targetsRaw || !prompt) {
475
526
  logMessage(
476
527
  "error",
477
- "{white-fg}✗{/white-fg} Usage: /corn start every=<10s|5m> target=<agent1,agent2> prompt=\"...\""
528
+ "{white-fg}✗{/white-fg} Usage: /cron start every=<10s|5m> or at=\"YYYY-MM-DD HH:mm\" target=<agent1,agent2> prompt=\"...\""
478
529
  );
479
530
  return;
480
531
  }
481
532
 
482
- const intervalMs = parseIntervalMs(intervalRaw);
483
- if (!Number.isFinite(intervalMs) || intervalMs < 1000) {
533
+ if (intervalRaw && atRaw) {
534
+ logMessage("error", "{white-fg}✗{/white-fg} Use either every=... or at=..., not both");
535
+ return;
536
+ }
537
+
538
+ const intervalMs = intervalRaw ? parseIntervalMs(intervalRaw) : 0;
539
+ if (intervalRaw && (!Number.isFinite(intervalMs) || intervalMs < 1000)) {
484
540
  logMessage("error", "{white-fg}✗{/white-fg} Invalid interval (min 1s)");
485
541
  return;
486
542
  }
487
543
 
544
+ const atMs = atRaw ? parseCronAtMs(atRaw) : 0;
545
+ if (atRaw && (!Number.isFinite(atMs) || atMs <= 0)) {
546
+ logMessage("error", "{white-fg}✗{/white-fg} Invalid one-time schedule, use at=\"YYYY-MM-DD HH:mm\"");
547
+ return;
548
+ }
549
+ if (atMs > 0 && atMs <= Date.now()) {
550
+ logMessage("error", "{white-fg}✗{/white-fg} One-time schedule must be in the future");
551
+ return;
552
+ }
553
+
488
554
  const targets = parseCronTargets(targetsRaw);
489
555
  if (targets.length === 0) {
490
556
  logMessage("error", "{white-fg}✗{/white-fg} At least one target agent is required");
491
557
  return;
492
558
  }
493
559
 
560
+ if (typeof requestCron === "function") {
561
+ if (atMs > 0) {
562
+ requestCron({
563
+ operation: "start",
564
+ once_at_ms: atMs,
565
+ targets,
566
+ prompt,
567
+ });
568
+ } else {
569
+ requestCron({
570
+ operation: "start",
571
+ interval_ms: intervalMs,
572
+ targets,
573
+ prompt,
574
+ });
575
+ }
576
+ schedule(requestStatus, 200);
577
+ return;
578
+ }
579
+
580
+ if (atMs > 0) {
581
+ logMessage("error", "{white-fg}✗{/white-fg} One-time cron requires daemon-backed scheduler");
582
+ return;
583
+ }
584
+
494
585
  const task = createCronTask({
495
586
  intervalMs,
496
587
  targets,
@@ -503,7 +594,7 @@ function createCommandExecutor(options = {}) {
503
594
 
504
595
  logMessage(
505
596
  "system",
506
- `{white-fg}✓{/white-fg} Cron started ${task.id}: every ${formatIntervalMs(intervalMs)} -> ${targets.join(", ")}`
597
+ `{white-fg}✓{/white-fg} Cron started ${task.id}: ${atMs > 0 ? `at ${formatCronAt(atMs)}` : `every ${formatIntervalMs(intervalMs)}`} -> ${targets.join(", ")}`
507
598
  );
508
599
  }
509
600
 
@@ -554,6 +645,39 @@ function createCommandExecutor(options = {}) {
554
645
  });
555
646
  }
556
647
 
648
+ async function handleUfooCommand(args = []) {
649
+ // Handle /ufoo command (session marker from daemon)
650
+ // When daemon sends /ufoo <marker>, we should just check for pending messages
651
+ if (args.length > 0) {
652
+ // This is a probe marker, check for pending messages
653
+ const subscriberId = process.env.UFOO_SUBSCRIBER_ID;
654
+ if (subscriberId) {
655
+ try {
656
+ const bus = createBus(projectRoot);
657
+ bus.ensureBus();
658
+ const pendingMessages = bus.checkMessages(subscriberId);
659
+ if (pendingMessages && pendingMessages.length > 0) {
660
+ logMessage("system", `{cyan-fg}[bus]{/cyan-fg} ${pendingMessages.length} pending message(s)`);
661
+ }
662
+ } catch {
663
+ // Ignore errors when checking messages
664
+ }
665
+ }
666
+ // Don't log anything else for probe markers
667
+ return;
668
+ }
669
+
670
+ // Without arguments, show ufoo protocol documentation
671
+ logMessage("system", "{cyan-fg}ufoo Protocol{/cyan-fg}");
672
+ logMessage("system", "");
673
+ logMessage("system", "This project uses ufoo for agent coordination:");
674
+ logMessage("system", " • Context decisions: /ctx");
675
+ logMessage("system", " • Event bus: /bus");
676
+ logMessage("system", " • Initialize: /init");
677
+ logMessage("system", "");
678
+ logMessage("system", "For detailed documentation, see .ufoo/docs/");
679
+ }
680
+
557
681
  async function handleUcodeConfigCommand(args = []) {
558
682
  const first = String(args[0] || "").trim().toLowerCase();
559
683
  const hasInlineKv = args.some((item) => String(item || "").includes("="));
@@ -668,12 +792,15 @@ function createCommandExecutor(options = {}) {
668
792
  case "resume":
669
793
  await handleResumeCommand(args);
670
794
  return true;
671
- case "corn":
672
- await handleCornCommand(args);
795
+ case "cron":
796
+ await handleCronCommand(args);
673
797
  return true;
674
798
  case "settings":
675
799
  await handleSettingsCommand(args);
676
800
  return true;
801
+ case "ufoo":
802
+ await handleUfooCommand(args);
803
+ return true;
677
804
  default:
678
805
  logMessage("error", `{white-fg}✗{/white-fg} Unknown command: /${command}`);
679
806
  return true;
@@ -691,9 +818,10 @@ function createCommandExecutor(options = {}) {
691
818
  handleSkillsCommand,
692
819
  handleLaunchCommand,
693
820
  handleResumeCommand,
694
- handleCornCommand,
821
+ handleCronCommand,
695
822
  handleSettingsCommand,
696
823
  handleUcodeConfigCommand,
824
+ handleUfooCommand,
697
825
  };
698
826
  }
699
827
 
@@ -27,7 +27,7 @@ const COMMAND_TREE = {
27
27
  },
28
28
  },
29
29
  "/doctor": { desc: "Health check diagnostics" },
30
- "/corn": {
30
+ "/cron": {
31
31
  desc: "Cron scheduler operations",
32
32
  children: {
33
33
  start: { desc: "Create cron task" },
@@ -64,6 +64,7 @@ const COMMAND_TREE = {
64
64
  },
65
65
  },
66
66
  "/status": { desc: "Status display" },
67
+ "/ufoo": { desc: "ufoo protocol (session marker)" },
67
68
  };
68
69
 
69
70
  const COMMAND_ORDER = ["/launch", "/bus", "/ctx"];
@@ -110,6 +110,46 @@ function createDaemonMessageRouter(options = {}) {
110
110
  }
111
111
  }
112
112
 
113
+ if (payload.cron && typeof payload.cron === "object") {
114
+ const cron = payload.cron;
115
+ const operation = String(cron.operation || "").toLowerCase();
116
+ if (!cron.ok) {
117
+ logMessage("error", `{white-fg}✗{/white-fg} ${escapeBlessed(cron.error || "cron failed")}`);
118
+ } else if (operation === "list" || operation === "ls") {
119
+ const tasks = Array.isArray(cron.tasks) ? cron.tasks : [];
120
+ if (tasks.length === 0) {
121
+ logMessage("system", "{cyan-fg}Cron:{/cyan-fg} none");
122
+ } else {
123
+ logMessage("system", `{cyan-fg}Cron:{/cyan-fg} ${tasks.length} task(s)`);
124
+ tasks.forEach((task) => {
125
+ const summary = task && (task.summary || task.id) ? (task.summary || task.id) : "";
126
+ if (summary) {
127
+ logMessage("system", ` • ${escapeBlessed(summary)}`);
128
+ }
129
+ });
130
+ }
131
+ } else if (operation === "start" && cron.task) {
132
+ const task = cron.task;
133
+ if (task.mode === "once") {
134
+ logMessage(
135
+ "system",
136
+ `{white-fg}✓{/white-fg} Cron scheduled ${escapeBlessed(task.id)} at ${escapeBlessed(task.onceAt || String(task.onceAtMs || ""))}`
137
+ );
138
+ } else {
139
+ logMessage(
140
+ "system",
141
+ `{white-fg}✓{/white-fg} Cron started ${escapeBlessed(task.id)}: every ${escapeBlessed(task.interval || String(task.intervalMs || ""))}`
142
+ );
143
+ }
144
+ } else if (operation === "stop") {
145
+ if (cron.id === "all") {
146
+ logMessage("system", `{white-fg}✓{/white-fg} Stopped ${Number(cron.stopped) || 0} cron task(s)`);
147
+ } else if (cron.id) {
148
+ logMessage("system", `{white-fg}✓{/white-fg} Stopped cron task ${escapeBlessed(cron.id)}`);
149
+ }
150
+ }
151
+ }
152
+
113
153
  if (payload.dispatch && payload.dispatch.length > 0) {
114
154
  const targets = payload.dispatch.map((d) => d.target || d).join(", ");
115
155
  logMessage("dispatch", `{white-fg}→{/white-fg} Dispatched to: ${escapeBlessed(targets)}`);