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.
- package/OPTIONAL_SKILLS/ubus-poll/SKILL.md +76 -0
- package/README.md +22 -0
- package/README.zh-CN.md +20 -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 +2 -1
- package/scripts/postinstall-skills.js +81 -0
- package/scripts/postinstall.js +8 -15
- package/src/app/cli/busCoreCommands.js +112 -6
- package/src/app/cli/features/doctor.js +4 -4
- package/src/app/cli/features/skills.js +22 -3
- package/src/app/cli/run.js +50 -5
- package/src/coordination/bus/index.js +108 -0
- package/src/coordination/bus/message.js +4 -0
- package/src/coordination/bus/poll.js +190 -0
- package/src/coordination/bus/queue.js +44 -1
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "3.0.
|
|
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
|
+
};
|
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) {
|
|
@@ -40,17 +40,106 @@ function parseSendArgs(cmdArgs = []) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function resolvePollSubscriber(cmdArgs = [], env = process.env) {
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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, "
|
|
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) {
|
|
@@ -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
|
|
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;
|
package/src/app/cli/run.js
CHANGED
|
@@ -1102,11 +1102,12 @@ async function runCli(argv) {
|
|
|
1102
1102
|
skills
|
|
1103
1103
|
.command("list")
|
|
1104
1104
|
.description("List available skills")
|
|
1105
|
-
.
|
|
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
|
}
|