u-foo 3.0.14 → 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.
- package/README.md +5 -0
- package/README.zh-CN.md +4 -0
- package/SKILLS/ufoo/SKILL.md +56 -179
- package/SKILLS/ufoo/agents/openai.yaml +4 -0
- package/SKILLS/ufoo-bus/SKILL.md +92 -0
- package/SKILLS/ufoo-bus/agents/openai.yaml +4 -0
- package/SKILLS/ufoo-context/SKILL.md +101 -0
- package/SKILLS/ufoo-context/agents/openai.yaml +4 -0
- package/SKILLS/ufoo-online/SKILL.md +43 -112
- package/SKILLS/ufoo-online/agents/openai.yaml +4 -0
- package/package.json +1 -1
- package/scripts/postinstall-skills.js +81 -0
- package/scripts/postinstall.js +8 -15
- package/src/app/cli/features/doctor.js +4 -4
- package/src/coordination/bus/store.js +1 -1
- package/src/coordination/context/doctor.js +2 -2
- package/SKILLS/ubus/SKILL.md +0 -250
- package/SKILLS/uctx/SKILL.md +0 -156
- package/SKILLS/uinit/SKILL.md +0 -76
- package/SKILLS/ustatus/SKILL.md +0 -36
|
@@ -1,144 +1,75 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ufoo-online
|
|
3
|
-
description:
|
|
4
|
-
Connect
|
|
5
|
-
public
|
|
6
|
-
|
|
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
|
-
#
|
|
10
|
+
# ufoo online
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
Operate remote collaboration through a long-running relay connection.
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Connect an agent
|
|
14
15
|
|
|
15
|
-
|
|
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
|
-
|
|
22
|
+
Run one connection as a streaming background task:
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
|
-
|
|
25
|
-
ufoo online connect --nickname
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
##
|
|
32
|
+
## Send and receive messages
|
|
83
33
|
|
|
84
34
|
```bash
|
|
85
|
-
|
|
86
|
-
ufoo online
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
41
|
+
Channel inbox entries are retained for 7 days; room entries are retained for
|
|
42
|
+
30 days.
|
|
96
43
|
|
|
97
|
-
|
|
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
|
-
|
|
108
|
-
ufoo online channel
|
|
109
|
-
ufoo online
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
57
|
+
## Manage identities and tokens
|
|
122
58
|
|
|
123
59
|
```bash
|
|
124
|
-
ufoo online
|
|
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
|
-
|
|
63
|
+
Tokens persist under `~/.ufoo/online/tokens.json`. Do not print token values or
|
|
64
|
+
room passwords in reports.
|
|
132
65
|
|
|
133
|
-
|
|
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
|
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
|
|
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.
|
package/package.json
CHANGED
|
@@ -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
|
+
};
|
package/scripts/postinstall.js
CHANGED
|
@@ -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)
|
|
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) {
|
|
@@ -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, "
|
|
40
|
-
const busSkill = path.join(skillsDir, "
|
|
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(`-
|
|
53
|
-
if (fs.existsSync(busSkill)) console.log(`-
|
|
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) {
|
|
@@ -95,7 +95,7 @@ class BusStore {
|
|
|
95
95
|
ensure() {
|
|
96
96
|
if (!fs.existsSync(this.busDir) || !fs.existsSync(this.paths.agentDir)) {
|
|
97
97
|
throw new Error(
|
|
98
|
-
"Event bus not initialized. Please run: ufoo
|
|
98
|
+
"Event bus not initialized. Please run: ufoo init --targets bus"
|
|
99
99
|
);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -86,7 +86,7 @@ class ContextDoctor {
|
|
|
86
86
|
* Lint bundled context skill.
|
|
87
87
|
*/
|
|
88
88
|
lintProtocol() {
|
|
89
|
-
const repoSkill = path.join(this.projectRoot, "SKILLS", "
|
|
89
|
+
const repoSkill = path.join(this.projectRoot, "SKILLS", "ufoo-context", "SKILL.md");
|
|
90
90
|
|
|
91
91
|
if (!fs.existsSync(repoSkill)) {
|
|
92
92
|
console.log("No bundled context skill found (skipping protocol lint)");
|
|
@@ -94,7 +94,7 @@ class ContextDoctor {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
console.log(`Linting bundled context skill: ${repoSkill}`);
|
|
97
|
-
this.checkFile(repoSkill, "SKILLS/
|
|
97
|
+
this.checkFile(repoSkill, "SKILLS/ufoo-context/SKILL.md");
|
|
98
98
|
|
|
99
99
|
return !this.failed;
|
|
100
100
|
}
|
package/SKILLS/ubus/SKILL.md
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ubus
|
|
3
|
-
description: |
|
|
4
|
-
Check and handle pending event-bus messages when /ubus is explicitly invoked.
|
|
5
|
-
Use when: (1) asked to check messages, (2) view bus status, (3) use watch/listen/auto modes.
|
|
6
|
-
If not yet joined bus, will auto-join.
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# /ubus - Check Event Bus Messages
|
|
10
|
-
|
|
11
|
-
Check and handle pending messages on the event bus when `/ubus` is explicitly
|
|
12
|
-
invoked.
|
|
13
|
-
|
|
14
|
-
## Arguments
|
|
15
|
-
|
|
16
|
-
- `/ubus` - Pull pending messages and show status
|
|
17
|
-
- `/ubus watch` - Start background auto-notification (title badge + bell + notification center)
|
|
18
|
-
- `/ubus stop` - Stop background auto-notification
|
|
19
|
-
- `/ubus listen` - Foreground continuous listener, print new messages (suitable for side terminal)
|
|
20
|
-
- `/ubus auto` - Unattended auto-execute (auto-inject `/ubus` and press Enter)
|
|
21
|
-
|
|
22
|
-
## Execution Flow
|
|
23
|
-
|
|
24
|
-
### 1. Check if .ufoo/bus exists
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
if [[ ! -d ".ufoo/bus" ]]; then
|
|
28
|
-
echo "Event bus not initialized, please run /uinit and select bus module"
|
|
29
|
-
exit
|
|
30
|
-
fi
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### 2. Get or create subscriber ID
|
|
34
|
-
|
|
35
|
-
**IMPORTANT**: Always check for existing subscriber ID first to avoid creating duplicates.
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
# Reuse existing subscriber first (env -> whoami), join only if missing
|
|
39
|
-
SUBSCRIBER="${UFOO_SUBSCRIBER_ID:-$(ufoo bus whoami 2>/dev/null || true)}"
|
|
40
|
-
if [ -n "$SUBSCRIBER" ]; then
|
|
41
|
-
echo "Using existing subscriber ID: $SUBSCRIBER"
|
|
42
|
-
else
|
|
43
|
-
# Not launched via uclaude/ucodex, need to join manually
|
|
44
|
-
SUBSCRIBER=$(ufoo bus join | tail -n 1)
|
|
45
|
-
echo "Joined event bus: $SUBSCRIBER"
|
|
46
|
-
# Example output: codex:0e293156 (nickname: codex-1)
|
|
47
|
-
fi
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
**Why this matters**:
|
|
51
|
-
- `uclaude`/`ucodex` automatically set `UFOO_SUBSCRIBER_ID` during launch
|
|
52
|
-
- `ufoo bus whoami` can recover current ID even when env is missing
|
|
53
|
-
- Re-joining may create identity drift and message routing issues
|
|
54
|
-
- Always reuse existing ID when available
|
|
55
|
-
|
|
56
|
-
To join with a custom nickname:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
ufoo bus join [session-id] [agent-type] "your-nickname"
|
|
60
|
-
# Example: ufoo bus join abc123 claude-code "architect"
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 3. Handle arguments
|
|
64
|
-
|
|
65
|
-
If argument is `watch`, use **Bash tool's `run_in_background: true`** to start background notification:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
# Title badge + bell + notification center (no accessibility permission needed)
|
|
69
|
-
ufoo bus alert "$SUBSCRIBER" 2 --notify --daemon
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
If argument is `listen`, foreground blocking listener (no background task tool needed):
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
ufoo bus listen "$SUBSCRIBER" --from-beginning
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
If argument is `auto`, use unattended auto-execute:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
# Start daemon (background resident), auto-inject /ubus + Enter on new message
|
|
82
|
-
ufoo bus daemon --daemon
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Tips:
|
|
86
|
-
- Need to use `uclaude`/`ucodex` wrapper to start Claude Code/Codex (auto-records tty)
|
|
87
|
-
- Terminal.app needs Accessibility permission (for keyboard input injection)
|
|
88
|
-
|
|
89
|
-
If argument is `stop`, stop background notification:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
ufoo bus alert "$SUBSCRIBER" --stop
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 4. Check pending events
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
ufoo bus check "$SUBSCRIBER"
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
The system automatically prefixes each message with `[ufoo]<from:id(nickname)>` to identify the sender. You do not need to add this prefix yourself.
|
|
102
|
-
|
|
103
|
-
If pending events exist, output looks like:
|
|
104
|
-
|
|
105
|
-
```
|
|
106
|
-
[ufoo]<from:claude-code:abc123(architect)>
|
|
107
|
-
Type: message/targeted/message
|
|
108
|
-
Content: {"message":"review src/main.ts","injection_mode":"immediate"}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
- The sender ID and nickname are in the `[ufoo]<from:...>` line — use the ID to reply
|
|
112
|
-
- The actual task is in `Content.message`
|
|
113
|
-
|
|
114
|
-
### 5. IMPORTANT: Acknowledge messages after handling
|
|
115
|
-
|
|
116
|
-
After you have read and processed the messages, you MUST acknowledge them to prevent repeated notifications:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
ufoo bus ack "$SUBSCRIBER"
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
**This is critical** - if you don't ack, the runtime may retry delivery or keep
|
|
123
|
-
the event pending.
|
|
124
|
-
|
|
125
|
-
**Default behavior is ack-only, no reply.** If there's nothing to do (no actionable task, no question to answer, no follow-up the sender genuinely needs), just ack and stop. Silence is a valid response — see "Handling Received Messages" below for when a reply IS warranted.
|
|
126
|
-
|
|
127
|
-
### 6. Routing Override
|
|
128
|
-
|
|
129
|
-
If the message explicitly instructs you to report to a specific PM/DEV/TEST ID, **send the result to that ID instead of the publisher**.
|
|
130
|
-
|
|
131
|
-
### 5. Show bus status
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
ufoo bus status
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Output (now includes nicknames):
|
|
138
|
-
|
|
139
|
-
```
|
|
140
|
-
=== Event Bus Status ===
|
|
141
|
-
My identity: claude-code:xyz789
|
|
142
|
-
Online agents: 2
|
|
143
|
-
- claude-code:abc123 (architect)
|
|
144
|
-
- claude-code:xyz789 (dev-lead)
|
|
145
|
-
Recent events: 5
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Managing Nicknames
|
|
149
|
-
|
|
150
|
-
### View and Change Nicknames
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
# Change an agent's nickname
|
|
154
|
-
ufoo bus rename <subscriber-id> "new-nickname"
|
|
155
|
-
# Example: ufoo bus rename claude-code:47b1d525 "backend-dev"
|
|
156
|
-
|
|
157
|
-
# Nickname alias command
|
|
158
|
-
ufoo bus nick <subscriber-id> "new-nickname"
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
**Important Notes:**
|
|
162
|
-
- Nicknames must be globally unique
|
|
163
|
-
- Cannot change nickname during join (use `rename` command instead)
|
|
164
|
-
- Re-joining with same subscriber ID will reuse existing nickname
|
|
165
|
-
- Auto-generated nicknames: `codex-1`, `codex-2`, `claude-1`, `claude-2`, etc.
|
|
166
|
-
|
|
167
|
-
## Handling Received Messages
|
|
168
|
-
|
|
169
|
-
When receiving targeted messages, the default flow is **execute → ack → stop**.
|
|
170
|
-
Replies are the exception, not the default.
|
|
171
|
-
|
|
172
|
-
1. **Understand request** — Read message content.
|
|
173
|
-
2. **Execute task** — If the message delegates a task, do it.
|
|
174
|
-
3. **`ufoo bus ack "$SUBSCRIBER"`** — Always ack, even when not replying.
|
|
175
|
-
4. **Reply ONLY when substantive.** Send `ufoo bus send` to the sender only if at least one of the following is true:
|
|
176
|
-
- The sender asked a question → reply with the answer.
|
|
177
|
-
- The sender delegated a task → reply with the result / artifact / status.
|
|
178
|
-
- You discovered something the sender needs to proceed → reply with that fact.
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
# Use this only when the criteria above are met.
|
|
182
|
-
ufoo bus send "<sender-id>" "<substantive-reply>"
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### Anti-pattern: greet / ack loops
|
|
186
|
-
|
|
187
|
-
If the inbound message is itself just a greeting, an acknowledgment, or a
|
|
188
|
-
pleasantry, **do not reply**. Acking is enough. A bare-acknowledgment reply
|
|
189
|
-
will be auto-injected on the other side, triggering them to reply in kind,
|
|
190
|
-
and the two of you will ping-pong forever.
|
|
191
|
-
|
|
192
|
-
| Inbound | Reply? |
|
|
193
|
-
|---|---|
|
|
194
|
-
| `👋` / `hi` / `hello` / `你好` | ❌ ack only |
|
|
195
|
-
| `👍` / `ok` / `收到` / `thanks` / `noted` | ❌ ack only |
|
|
196
|
-
| `已完成 / done / finished` (without a result the sender asked for) | ❌ ack only |
|
|
197
|
-
| `请把 src/foo.ts 改成 ...` (task) | ✅ reply with result |
|
|
198
|
-
| `这个 bug 的根因是什么?` (question) | ✅ reply with answer |
|
|
199
|
-
| `我帮你找到了 X,需要你做 Y` (request) | ✅ reply with status |
|
|
200
|
-
|
|
201
|
-
When in doubt: ack and stop. If the sender genuinely needs something from you,
|
|
202
|
-
they will follow up with a concrete question or task.
|
|
203
|
-
|
|
204
|
-
## Sending Messages
|
|
205
|
-
|
|
206
|
-
After sending a message, do not run `/ubus`, poll, sleep, or wait for a reply.
|
|
207
|
-
Continue the current task. Any follow-up message will be automatically injected
|
|
208
|
-
into your prompt/session.
|
|
209
|
-
|
|
210
|
-
### Smart Routing (when you don't know the target ID)
|
|
211
|
-
|
|
212
|
-
If the user says "notify codex to do X" without specifying an ID, use smart routing:
|
|
213
|
-
|
|
214
|
-
```bash
|
|
215
|
-
# Step 1: Find candidates
|
|
216
|
-
ufoo bus resolve "$SUBSCRIBER" codex
|
|
217
|
-
|
|
218
|
-
# Output shows:
|
|
219
|
-
# - If only 1 codex: directly shows the ID
|
|
220
|
-
# - If multiple: shows each with nickname and message history
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
Based on the output:
|
|
224
|
-
- **Single match**: Use that ID directly
|
|
225
|
-
- **Multiple matches**: Analyze the message history to find the right target
|
|
226
|
-
- Look for context clues in previous conversations
|
|
227
|
-
- If still unclear, ask the user which one, or send to all of that type
|
|
228
|
-
|
|
229
|
-
### Direct Send
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
# Send to specific Agent by full ID
|
|
233
|
-
ufoo bus send "claude-code:abc123" "message content"
|
|
234
|
-
|
|
235
|
-
# Send to specific Agent by nickname (NEW!)
|
|
236
|
-
ufoo bus send "architect" "message content"
|
|
237
|
-
ufoo bus send "backend-dev" "message content"
|
|
238
|
-
|
|
239
|
-
# Send to all Agents of same type
|
|
240
|
-
ufoo bus send "codex" "message content"
|
|
241
|
-
|
|
242
|
-
# Broadcast to everyone
|
|
243
|
-
ufoo bus broadcast "message content"
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
**Target Resolution Priority:**
|
|
247
|
-
1. Exact subscriber ID (e.g., `claude-code:abc123`)
|
|
248
|
-
2. Nickname match (e.g., `architect` → resolves to subscriber ID)
|
|
249
|
-
3. Agent type (e.g., `codex` → all codex agents)
|
|
250
|
-
4. Wildcard (`*` → all agents)
|