u-foo 3.0.13 → 3.0.15

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.
@@ -1,144 +1,75 @@
1
1
  ---
2
2
  name: ufoo-online
3
- description: |
4
- Connect any agent to the ufoo-online WebSocket relay for public channel chat,
5
- public rooms, or private room collaboration. Use when users ask to join ufoo online,
6
- chat with other agents, or check inbox.
3
+ description: >-
4
+ Connect agents through the ufoo-online WebSocket relay, including public
5
+ channels, public or private rooms, inboxes, tokens, and relay administration.
6
+ Use when asked to join remote ufoo collaboration, send or receive online
7
+ messages, create rooms or channels, or operate a relay server.
7
8
  ---
8
9
 
9
- # /ufoo-online - Online Relay Client
10
+ # ufoo online
10
11
 
11
- Connect to the ufoo-online WebSocket relay, join channels/rooms, send messages, and check inbox.
12
+ Operate remote collaboration through a long-running relay connection.
12
13
 
13
- ## Quick Start
14
+ ## Connect an agent
14
15
 
15
- ### 1. Start a local relay server
16
+ Start a local relay when needed:
16
17
 
17
18
  ```bash
18
- ufoo online server --port 8787
19
+ ufoo online server --host 127.0.0.1 --port 8787
19
20
  ```
20
21
 
21
- ### 2. Connect (long-running, run in background)
22
+ Run one connection as a streaming background task:
22
23
 
23
24
  ```bash
24
- # Join a public channel
25
- ufoo online connect --nickname my-agent --join lobby --ping-ms 15000
26
-
27
- # Join a private room (enables bus/decisions/wake sync)
28
- ufoo online connect --nickname my-agent --room room_001 --room-password secret --ping-ms 15000
29
- ```
30
-
31
- Use `run_in_background: true` to keep the connection alive in agent sessions.
32
-
33
- ### 3. Send a message
34
-
35
- ```bash
36
- # Send to a channel
37
- ufoo online send --nickname my-agent --channel lobby --text "hello everyone"
38
-
39
- # Send to a room
40
- ufoo online send --nickname my-agent --room room_001 --text "hello team"
41
- ```
42
-
43
- Messages are queued to the local outbox (`~/.ufoo/online/outbox/<nickname>.jsonl`)
44
- and delivered by the running `connect` process. The connect process must be running
45
- for messages to be sent.
46
-
47
- ### 4. Check inbox
48
-
49
- ```bash
50
- # View all messages
51
- ufoo online inbox my-agent
52
-
53
- # View unread only
54
- ufoo online inbox my-agent --unread
55
-
56
- # Clear inbox
57
- ufoo online inbox my-agent --clear
58
- ```
59
-
60
- Inbox retention: channel messages 7 days, room messages 30 days.
61
-
62
- ## Full Connect Options
63
-
64
- ```bash
65
- ufoo online connect --nickname <name> [--url <wss://...>] [--subscriber <id>]
66
- [--token <tok>] [--token-hash <hash>] [--world <name>] [--ping-ms <ms>]
67
- [--join <channel>] [--room <room-id> --room-password <pwd>]
68
- [--interval <ms>] [--allow-insecure-ws]
69
- [--trust-remote] [--allow-from <subscriberId>]
25
+ ufoo online connect --nickname <name> --join <channel> --ping-ms 15000
26
+ ufoo online connect --nickname <name> --room <room-id> --room-password <password> --ping-ms 15000
70
27
  ```
71
28
 
72
- Features:
73
- - Auto-reconnect with exponential backoff (500ms -> 8s)
74
- - Auto-generates token if none exists; persists to `~/.ufoo/online/tokens.json`
75
- - Incoming messages saved to `~/.ufoo/online/inbox/<nickname>.jsonl`
76
- - Polls outbox for queued sends
77
- - Prints all messages to stdout as JSON; prints `CONNECTED` on handshake
78
- - Non-local `ws://` is blocked by default; use `wss://` or `--allow-insecure-ws`.
79
- - **Private room mode** (`--room`): bus/decisions/wake sync is gated; use
80
- `--trust-remote` or `--allow-from` to allow inbound sync.
29
+ Keep the connection running. Outbox messages are delivered by this process and
30
+ incoming messages are written to the local inbox.
81
31
 
82
- ## Server Management
32
+ ## Send and receive messages
83
33
 
84
34
  ```bash
85
- # Start relay (dev mode any token accepted)
86
- ufoo online server --port 8787
87
-
88
- # Start with token validation
89
- ufoo online server --port 8787 --token-file ~/.ufoo/online/tokens.json
90
-
91
- # Custom host/idle timeout
92
- ufoo online server --host 0.0.0.0 --port 8787 --idle-timeout 60000
35
+ ufoo online send --nickname <name> --channel <channel> --text "<message>"
36
+ ufoo online send --nickname <name> --room <room-id> --text "<message>"
37
+ ufoo online inbox <name> --unread
38
+ ufoo online inbox <name> --clear
93
39
  ```
94
40
 
95
- ## Token Management
41
+ Channel inbox entries are retained for 7 days; room entries are retained for
42
+ 30 days.
96
43
 
97
- ```bash
98
- ufoo online token <subscriber-id> --nickname <name> [--server <url>]
99
- ```
100
-
101
- Tokens are stored in `~/.ufoo/online/tokens.json`. The connect command
102
- auto-resolves tokens by subscriber ID or nickname lookup.
103
-
104
- ## Room & Channel Management
44
+ ## Manage channels and rooms
105
45
 
106
46
  ```bash
107
- # Channels (public broadcast, can join multiple)
108
- ufoo online channel list [--server <url>]
109
- ufoo online channel create --name <name> [--type world|public] [--server <url>]
110
-
111
- # Rooms (collaboration, can join one)
112
- ufoo online room list [--server <url>]
113
- ufoo online room create --type public|private [--name <room>] [--password <pwd>] [--server <url>]
47
+ ufoo online channel list --server <url>
48
+ ufoo online channel create --name <name> --type public --server <url>
49
+ ufoo online room list --server <url>
50
+ ufoo online room create --name <name> --type public --server <url>
51
+ ufoo online room create --name <name> --type private --password <password> --server <url>
114
52
  ```
115
53
 
116
- If the relay requires auth, pass `--auth-token <token>` (or `--token-file` +
117
- `--subscriber`/`--nickname`) to room/channel commands to send the Bearer token.
118
-
119
- ## Usage Scenarios
54
+ Pass `--auth-token`, or use `--token-file` with subscriber or nickname lookup,
55
+ when the relay requires authentication.
120
56
 
121
- ### 1. Public channel chat
57
+ ## Manage identities and tokens
122
58
 
123
59
  ```bash
124
- ufoo online server --port 8787 # Terminal 1
125
- ufoo online connect --nickname agent-a --join lobby # Terminal 2 (background)
126
- ufoo online connect --nickname agent-b --join lobby # Terminal 3 (background)
127
- ufoo online send --nickname agent-a --channel lobby --text "hi all"
128
- ufoo online inbox agent-b # See agent-a's message
60
+ ufoo online token <subscriber-id> --nickname <name> --server <url>
129
61
  ```
130
62
 
131
- ### 2. Private room collaboration
63
+ Tokens persist under `~/.ufoo/online/tokens.json`. Do not print token values or
64
+ room passwords in reports.
132
65
 
133
- ```bash
134
- ufoo online room create --type private --password secret --server https://online.ufoo.dev
135
- # → returns room_id
136
-
137
- ufoo online connect --nickname dev-1 --room room_001 --room-password secret
138
- ufoo online connect --nickname dev-2 --room room_001 --room-password secret
139
- ```
66
+ ## Apply transport safety
140
67
 
141
- In private room mode, agents automatically sync:
142
- - Bus messages (local bus <-> online relay, bidirectional)
143
- - Decisions (new .md files synced across team)
144
- - Wake events (remote agent can wake local agent via bus)
68
+ - Prefer `wss://` for non-local relays.
69
+ - Allow non-local `ws://` only with explicit user intent.
70
+ - Treat private-room bus, decision, and wake synchronization as untrusted by
71
+ default.
72
+ - Use `--trust-remote` only for a fully trusted room; otherwise restrict
73
+ inbound synchronization with `--allow-from <subscriber-id>`.
74
+ - Bind development relays to `127.0.0.1` unless the user explicitly requests
75
+ network exposure and accepts the security implications.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "ufoo online"
3
+ short_description: "Connect agents through ufoo online collaboration"
4
+ default_prompt: "Use $ufoo-online to connect this agent to an online collaboration room."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "3.0.13",
3
+ "version": "3.0.15",
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",
@@ -38,6 +38,7 @@
38
38
  "scripts/",
39
39
  "dist/tui/",
40
40
  "SKILLS/",
41
+ "OPTIONAL_SKILLS/",
41
42
  "LICENSE",
42
43
  "README.md"
43
44
  ],
@@ -0,0 +1,81 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const RETIRED_DEFAULT_SKILLS = Object.freeze(["ubus", "uctx", "uinit", "ustatus"]);
5
+ const LEGACY_COMMAND_NAMES = Object.freeze([
6
+ "ubus",
7
+ "uctx",
8
+ "ufoo",
9
+ "ufoo-bus",
10
+ "ufoo-context",
11
+ "ufoo-online",
12
+ "uinit",
13
+ "ustatus",
14
+ ]);
15
+
16
+ function removeManagedSymlink(linkPath, expectedTarget) {
17
+ try {
18
+ const stat = fs.lstatSync(linkPath);
19
+ if (!stat.isSymbolicLink()) return false;
20
+
21
+ const rawTarget = fs.readlinkSync(linkPath);
22
+ const resolvedTarget = path.resolve(path.dirname(linkPath), rawTarget);
23
+ if (resolvedTarget !== path.resolve(expectedTarget)) return false;
24
+
25
+ fs.rmSync(linkPath, { force: true });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ function removeLegacySkillAndCommandLinks({ pkgRoot, home, codexHome } = {}) {
33
+ const rawPackageRoot = String(pkgRoot || "").trim();
34
+ const rawUserHome = String(home || "").trim();
35
+ if (!rawPackageRoot || !rawUserHome) return [];
36
+
37
+ const packageRoot = path.resolve(rawPackageRoot);
38
+ const userHome = path.resolve(rawUserHome);
39
+ const codexRoots = new Set([
40
+ path.join(userHome, ".codex"),
41
+ path.resolve(String(codexHome || path.join(userHome, ".codex"))),
42
+ ]);
43
+ const removed = [];
44
+
45
+ for (const name of RETIRED_DEFAULT_SKILLS) {
46
+ const skillDir = path.join(packageRoot, "SKILLS", name);
47
+ const candidates = [
48
+ {
49
+ linkPath: path.join(userHome, ".claude", "skills", name),
50
+ expectedTarget: skillDir,
51
+ },
52
+ ...Array.from(codexRoots).map((root) => ({
53
+ linkPath: path.join(root, "skills", name),
54
+ expectedTarget: skillDir,
55
+ })),
56
+ ];
57
+
58
+ for (const candidate of candidates) {
59
+ if (removeManagedSymlink(candidate.linkPath, candidate.expectedTarget)) {
60
+ removed.push(candidate.linkPath);
61
+ }
62
+ }
63
+ }
64
+
65
+ for (const name of LEGACY_COMMAND_NAMES) {
66
+ const linkPath = path.join(userHome, ".claude", "commands", `${name}.md`);
67
+ const expectedTarget = path.join(packageRoot, "SKILLS", name, "SKILL.md");
68
+ if (removeManagedSymlink(linkPath, expectedTarget)) {
69
+ removed.push(linkPath);
70
+ }
71
+ }
72
+
73
+ return removed;
74
+ }
75
+
76
+ module.exports = {
77
+ RETIRED_DEFAULT_SKILLS,
78
+ LEGACY_COMMAND_NAMES,
79
+ removeManagedSymlink,
80
+ removeLegacySkillAndCommandLinks,
81
+ };
@@ -2,6 +2,7 @@
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
4
  const os = require("os");
5
+ const { removeLegacySkillAndCommandLinks } = require("./postinstall-skills");
5
6
 
6
7
  // Fix node-pty spawn-helper permissions on macOS (both arm64 and x64)
7
8
  const platforms = ["darwin-arm64", "darwin-x64"];
@@ -50,7 +51,7 @@ function collectSkillSources(pkgRoot) {
50
51
  if (entry.isDirectory()) {
51
52
  const skillMd = path.join(topSkills, entry.name, "SKILL.md");
52
53
  if (fs.existsSync(skillMd)) {
53
- sources.push({ name: entry.name, dir: path.join(topSkills, entry.name), md: skillMd });
54
+ sources.push({ name: entry.name, dir: path.join(topSkills, entry.name) });
54
55
  }
55
56
  }
56
57
  }
@@ -70,17 +71,6 @@ function forceSymlink(target, linkPath) {
70
71
  fs.symlinkSync(target, linkPath);
71
72
  }
72
73
 
73
- function installClaudeCommands(home, sources) {
74
- const commandsDir = path.join(home, ".claude", "commands");
75
- fs.mkdirSync(commandsDir, { recursive: true });
76
-
77
- for (const { name, md } of sources) {
78
- forceSymlink(md, path.join(commandsDir, `${name}.md`));
79
- }
80
-
81
- console.log(`[postinstall] Installed ${sources.length} ufoo command(s) to ${commandsDir}`);
82
- }
83
-
84
74
  function installSkillDirs(targetDir, sources, label) {
85
75
  fs.mkdirSync(targetDir, { recursive: true });
86
76
 
@@ -92,19 +82,22 @@ function installSkillDirs(targetDir, sources, label) {
92
82
  }
93
83
 
94
84
  // Install ufoo skills for Claude and Codex at npm install time.
95
- // - Claude slash commands: ~/.claude/commands/<name>.md -> SKILL.md
96
85
  // - Claude skills: ~/.claude/skills/<name> -> skill dir
97
86
  // - Codex skills: ${CODEX_HOME:-~/.codex}/skills/<name> -> skill dir
98
87
  try {
99
88
  const pkgRoot = path.resolve(__dirname, "..");
100
89
  const home = os.homedir();
101
90
  const sources = collectSkillSources(pkgRoot);
91
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
92
+ const retiredLinks = removeLegacySkillAndCommandLinks({ pkgRoot, home, codexHome });
93
+
94
+ if (retiredLinks.length > 0) {
95
+ console.log(`[postinstall] Removed ${retiredLinks.length} legacy ufoo link(s)`);
96
+ }
102
97
 
103
98
  if (sources.length > 0) {
104
- installClaudeCommands(home, sources);
105
99
  installSkillDirs(path.join(home, ".claude", "skills"), sources, "~/.claude/skills");
106
100
 
107
- const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
108
101
  installSkillDirs(path.join(codexHome, "skills"), sources, `${codexHome}/skills`);
109
102
  }
110
103
  } catch (err) {
@@ -40,17 +40,106 @@ function parseSendArgs(cmdArgs = []) {
40
40
  }
41
41
 
42
42
  function resolvePollSubscriber(cmdArgs = [], env = process.env) {
43
- const positionals = cmdArgs.filter((arg) => typeof arg === "string" && !arg.startsWith("--"));
44
- const subscriber = String(positionals[0] || env.UFOO_SUBSCRIBER_ID || "").trim();
43
+ let subscriber = "";
44
+ let autoAck = false;
45
+ let follow = false;
46
+ let intervalSeconds = 2;
47
+ let intervalWasSet = false;
48
+
49
+ for (let index = 0; index < cmdArgs.length; index += 1) {
50
+ const arg = String(cmdArgs[index] || "");
51
+ if (arg === "--ack" || arg === "--auto-ack") {
52
+ autoAck = true;
53
+ continue;
54
+ }
55
+ if (arg === "--follow") {
56
+ follow = true;
57
+ continue;
58
+ }
59
+ if (arg === "--interval") {
60
+ const value = cmdArgs[index + 1];
61
+ if (value === undefined || String(value).startsWith("--")) {
62
+ throw new Error("poll --interval requires <seconds>");
63
+ }
64
+ intervalSeconds = Number(value);
65
+ intervalWasSet = true;
66
+ index += 1;
67
+ continue;
68
+ }
69
+ if (arg.startsWith("--interval=")) {
70
+ intervalSeconds = Number(arg.slice("--interval=".length));
71
+ intervalWasSet = true;
72
+ continue;
73
+ }
74
+ if (arg.startsWith("--")) {
75
+ throw new Error(`Unknown poll option: ${arg}`);
76
+ }
77
+ if (subscriber) {
78
+ throw new Error("poll accepts at most one [subscriber]");
79
+ }
80
+ subscriber = arg.trim();
81
+ }
82
+
83
+ subscriber = String(subscriber || env.UFOO_SUBSCRIBER_ID || "").trim();
45
84
  if (!subscriber) {
46
85
  throw new Error("poll requires [subscriber] or UFOO_SUBSCRIBER_ID");
47
86
  }
87
+ if (follow && autoAck) {
88
+ throw new Error("poll --follow cannot be combined with --ack");
89
+ }
90
+ if (intervalWasSet && !follow) {
91
+ throw new Error("poll --interval requires --follow");
92
+ }
93
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds < 0.25) {
94
+ throw new Error("poll --interval must be at least 0.25 seconds");
95
+ }
96
+
48
97
  return {
49
98
  subscriber,
50
- autoAck: cmdArgs.includes("--ack") || cmdArgs.includes("--auto-ack"),
99
+ autoAck,
100
+ follow,
101
+ intervalSeconds,
51
102
  };
52
103
  }
53
104
 
105
+ function resolveAckArgs(cmdArgs = []) {
106
+ let subscriber = "";
107
+ let throughSeq = null;
108
+
109
+ for (let index = 0; index < cmdArgs.length; index += 1) {
110
+ const arg = String(cmdArgs[index] || "");
111
+ if (arg === "--through") {
112
+ const value = cmdArgs[index + 1];
113
+ if (value === undefined || String(value).startsWith("--")) {
114
+ throw new Error("ack --through requires <seq>");
115
+ }
116
+ throughSeq = Number(value);
117
+ index += 1;
118
+ continue;
119
+ }
120
+ if (arg.startsWith("--through=")) {
121
+ throughSeq = Number(arg.slice("--through=".length));
122
+ continue;
123
+ }
124
+ if (arg.startsWith("--")) {
125
+ throw new Error(`Unknown ack option: ${arg}`);
126
+ }
127
+ if (subscriber) {
128
+ throw new Error("ack accepts exactly one <subscriber>");
129
+ }
130
+ subscriber = arg.trim();
131
+ }
132
+
133
+ if (!subscriber) {
134
+ throw new Error("ack requires <subscriber>");
135
+ }
136
+ if (throughSeq !== null && (!Number.isInteger(throughSeq) || throughSeq <= 0)) {
137
+ throw new Error("ack --through requires a positive integer sequence");
138
+ }
139
+
140
+ return { subscriber, throughSeq };
141
+ }
142
+
54
143
  async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
55
144
  switch (cmd) {
56
145
  case "init":
@@ -91,11 +180,24 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
91
180
  case "poll":
92
181
  {
93
182
  const parsed = resolvePollSubscriber(cmdArgs);
94
- await eventBus.check(parsed.subscriber, parsed.autoAck);
183
+ if (parsed.follow) {
184
+ await eventBus.poll(parsed.subscriber, {
185
+ intervalSeconds: parsed.intervalSeconds,
186
+ });
187
+ } else {
188
+ await eventBus.check(parsed.subscriber, parsed.autoAck);
189
+ }
95
190
  }
96
191
  return {};
97
192
  case "ack":
98
- await eventBus.ack(cmdArgs[0]);
193
+ {
194
+ const parsed = resolveAckArgs(cmdArgs);
195
+ if (parsed.throughSeq !== null) {
196
+ await eventBus.ackThrough(parsed.subscriber, parsed.throughSeq);
197
+ } else {
198
+ await eventBus.ack(parsed.subscriber);
199
+ }
200
+ }
99
201
  return {};
100
202
  case "consume":
101
203
  await eventBus.consume(cmdArgs[0], cmdArgs.includes("--from-beginning"));
@@ -117,4 +219,8 @@ async function runBusCoreCommand(eventBus, cmd, cmdArgs = []) {
117
219
  }
118
220
  }
119
221
 
120
- module.exports = { runBusCoreCommand, resolvePollSubscriber };
222
+ module.exports = {
223
+ resolveAckArgs,
224
+ resolvePollSubscriber,
225
+ runBusCoreCommand,
226
+ };
@@ -36,8 +36,8 @@ class RepoDoctor {
36
36
 
37
37
  run() {
38
38
  const skillsDir = path.join(this.repoRoot, "SKILLS");
39
- const contextSkill = path.join(skillsDir, "uctx", "SKILL.md");
40
- const busSkill = path.join(skillsDir, "ubus", "SKILL.md");
39
+ const contextSkill = path.join(skillsDir, "ufoo-context", "SKILL.md");
40
+ const busSkill = path.join(skillsDir, "ufoo-bus", "SKILL.md");
41
41
 
42
42
  if (!fs.existsSync(contextSkill)) this.fail(`missing ${contextSkill}`);
43
43
  if (!fs.existsSync(busSkill)) this.fail(`missing ${busSkill}`);
@@ -49,8 +49,8 @@ class RepoDoctor {
49
49
  console.log("=== ufoo doctor ===");
50
50
  console.log(`Monorepo: ${this.repoRoot}`);
51
51
  console.log("Skills:");
52
- if (fs.existsSync(contextSkill)) console.log(`- uctx: ${contextSkill}`);
53
- if (fs.existsSync(busSkill)) console.log(`- ubus: ${busSkill}`);
52
+ if (fs.existsSync(contextSkill)) console.log(`- ufoo-context: ${contextSkill}`);
53
+ if (fs.existsSync(busSkill)) console.log(`- ufoo-bus: ${busSkill}`);
54
54
  this.reportTui();
55
55
 
56
56
  if (this.failed) {
@@ -8,6 +8,7 @@ class SkillsManager {
8
8
  constructor(repoRoot) {
9
9
  this.repoRoot = repoRoot;
10
10
  this.skillRoots = this.findSkillRoots();
11
+ this.optionalSkillRoots = this.findOptionalSkillRoots();
11
12
  }
12
13
 
13
14
  /**
@@ -22,13 +23,31 @@ class SkillsManager {
22
23
  return roots;
23
24
  }
24
25
 
26
+ /**
27
+ * Optional skills are discoverable and explicitly installable, but are not
28
+ * included in default listings or `skills install all`.
29
+ */
30
+ findOptionalSkillRoots() {
31
+ const roots = [];
32
+ const optionalSkills = path.join(this.repoRoot, "OPTIONAL_SKILLS");
33
+ if (fs.existsSync(optionalSkills)) {
34
+ roots.push(optionalSkills);
35
+ }
36
+ return roots;
37
+ }
38
+
25
39
  /**
26
40
  * 列出所有技能
27
41
  */
28
- list() {
42
+ list(options = {}) {
29
43
  const skills = new Set();
44
+ const roots = options.optionalOnly
45
+ ? this.optionalSkillRoots
46
+ : (options.includeOptional
47
+ ? [...this.skillRoots, ...this.optionalSkillRoots]
48
+ : this.skillRoots);
30
49
 
31
- for (const root of this.skillRoots) {
50
+ for (const root of roots) {
32
51
  if (!fs.existsSync(root)) {
33
52
  continue;
34
53
  }
@@ -48,7 +67,7 @@ class SkillsManager {
48
67
  * 查找技能路径
49
68
  */
50
69
  findSkill(name) {
51
- for (const root of this.skillRoots) {
70
+ for (const root of [...this.skillRoots, ...this.optionalSkillRoots]) {
52
71
  const skillPath = path.join(root, name);
53
72
  if (fs.existsSync(skillPath)) {
54
73
  return skillPath;
@@ -1102,11 +1102,12 @@ async function runCli(argv) {
1102
1102
  skills
1103
1103
  .command("list")
1104
1104
  .description("List available skills")
1105
- .action(() => {
1105
+ .option("--optional", "List opt-in skills that are not installed by default")
1106
+ .action((opts) => {
1106
1107
  const SkillsManager = require("./features/skills");
1107
1108
  const repoRoot = getPackageRoot();
1108
1109
  const manager = new SkillsManager(repoRoot);
1109
- const skillsList = manager.list();
1110
+ const skillsList = manager.list({ optionalOnly: Boolean(opts.optional) });
1110
1111
  skillsList.forEach((skill) => console.log(skill));
1111
1112
  });
1112
1113
  skills
@@ -1526,6 +1527,49 @@ async function runCli(argv) {
1526
1527
  process.exitCode = 1;
1527
1528
  });
1528
1529
  });
1530
+ bus
1531
+ .command("poll")
1532
+ .description("Check once, or stream pending messages for an opt-in background task")
1533
+ .argument("[subscriber]", "Subscriber ID (defaults to UFOO_SUBSCRIBER_ID)")
1534
+ .option("--ack", "Acknowledge after a one-shot check")
1535
+ .option("--auto-ack", "Alias for --ack")
1536
+ .option("--follow", "Continuously emit newly observed pending events")
1537
+ .option("--interval <seconds>", "Follow interval in seconds")
1538
+ .action(async (subscriber, opts) => {
1539
+ const EventBus = require("../../coordination/bus");
1540
+ const eventBus = new EventBus(process.cwd());
1541
+ const args = [];
1542
+ if (subscriber) args.push(subscriber);
1543
+ if (opts.ack) args.push("--ack");
1544
+ if (opts.autoAck) args.push("--auto-ack");
1545
+ if (opts.follow) args.push("--follow");
1546
+ if (opts.interval !== undefined) args.push("--interval", String(opts.interval));
1547
+
1548
+ try {
1549
+ await runBusCoreCommand(eventBus, "poll", args);
1550
+ } catch (err) {
1551
+ console.error(err.message);
1552
+ process.exitCode = 1;
1553
+ }
1554
+ });
1555
+ bus
1556
+ .command("ack")
1557
+ .description("Acknowledge pending messages")
1558
+ .argument("<subscriber>", "Subscriber ID")
1559
+ .option("--through <seq>", "Acknowledge only through this displayed sequence")
1560
+ .action(async (subscriber, opts) => {
1561
+ const EventBus = require("../../coordination/bus");
1562
+ const eventBus = new EventBus(process.cwd());
1563
+ const args = [subscriber];
1564
+ if (opts.through !== undefined) args.push("--through", String(opts.through));
1565
+
1566
+ try {
1567
+ await runBusCoreCommand(eventBus, "ack", args);
1568
+ } catch (err) {
1569
+ console.error(err.message);
1570
+ process.exitCode = 1;
1571
+ }
1572
+ });
1529
1573
  bus
1530
1574
  .command("inject")
1531
1575
  .description("Inject /bus into a Terminal.app tab by subscriber ID")
@@ -1695,7 +1739,7 @@ async function runCli(argv) {
1695
1739
  console.log(" ufoo report <start|progress|done|error|list> [message] [--task <id>] [--agent <id>]");
1696
1740
  console.log(" ufoo ucode [doctor|prepare|build] [--skip-install]");
1697
1741
  console.log(" ufoo init [--targets <list>] [--project <dir>]");
1698
- console.log(" ufoo skills list");
1742
+ console.log(" ufoo skills list [--optional]");
1699
1743
  console.log(" ufoo skills install <name|all> [--target <dir> | --codex | --agents]");
1700
1744
  console.log(" ufoo group templates [list|ls] [--json]");
1701
1745
  console.log(" ufoo group template <list|show|validate|new> [target] [--from <builtin>] [--global] [--force] [--json]");
@@ -1715,7 +1759,8 @@ async function runCli(argv) {
1715
1759
  console.log(" ufoo online send --nickname <name> --text <msg> [--channel <ch>] [--room <id>]");
1716
1760
  console.log(" ufoo online inbox <nickname> [--clear] [--unread]");
1717
1761
  console.log(" ufoo bus wake <target> [--reason <reason>] [--no-shake]");
1718
- console.log(" ufoo bus poll [subscriber] [--ack]");
1762
+ console.log(" ufoo bus poll [subscriber] [--ack | --follow --interval <seconds>]");
1763
+ console.log(" ufoo bus ack <subscriber> [--through <seq>]");
1719
1764
  console.log(" ufoo bus <args...> (JS bus implementation)");
1720
1765
  console.log(" ufoo ctx <subcmd> ... (doctor|lint|decisions|sync)");
1721
1766
  console.log(" ufoo history <build|show|prompt> [limit]");
@@ -2056,7 +2101,7 @@ async function runCli(argv) {
2056
2101
  const sub = rest[0] || "";
2057
2102
 
2058
2103
  if (sub === "list") {
2059
- const skillsList = manager.list();
2104
+ const skillsList = manager.list({ optionalOnly: rest.includes("--optional") });
2060
2105
  skillsList.forEach((skill) => console.log(skill));
2061
2106
  return;
2062
2107
  }