u-foo 1.4.0 → 1.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
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",
@@ -201,6 +201,8 @@ function runOnlineInbox(nickname, opts = {}) {
201
201
  checkInbox(nickname, {
202
202
  clear: !!opts.clear,
203
203
  unread: !!opts.unread,
204
+ room: opts.room || "",
205
+ channel: opts.channel || "",
204
206
  });
205
207
  }
206
208
 
@@ -280,6 +282,8 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
280
282
  return runOnlineInbox(payload.nickname, {
281
283
  clear: opts.clear,
282
284
  unread: opts.unread,
285
+ room: opts.room || "",
286
+ channel: opts.channel || "",
283
287
  });
284
288
  default:
285
289
  throw createUnknownOnlineError(subcmd);
@@ -374,6 +378,8 @@ async function runOnlineCommand(subcmd, payload = {}, options = {}) {
374
378
  return runOnlineInbox(argv[1], {
375
379
  clear: hasFallbackFlag(argv, "--clear"),
376
380
  unread: hasFallbackFlag(argv, "--unread"),
381
+ room: getFallbackOpt(argv, "--room"),
382
+ channel: getFallbackOpt(argv, "--channel"),
377
383
  });
378
384
  }
379
385
  default:
@@ -135,6 +135,15 @@ function normalizeOnlineSender(from = "") {
135
135
  return text;
136
136
  }
137
137
 
138
+ function inboxFileName(nickname, route) {
139
+ // Namespace inbox by room/channel to prevent cross-room message leakage
140
+ if (route) {
141
+ const safe = String(route).replace(/[^a-zA-Z0-9_-]/g, "_");
142
+ return `${nickname}__${safe}.jsonl`;
143
+ }
144
+ return `${nickname}.jsonl`;
145
+ }
146
+
138
147
  function appendToInbox(nickname, msg) {
139
148
  const dir = inboxDir();
140
149
  fs.mkdirSync(dir, { recursive: true });
@@ -143,7 +152,8 @@ function appendToInbox(nickname, msg) {
143
152
  _source: messageSource(msg),
144
153
  _receivedAt: new Date().toISOString(),
145
154
  };
146
- fs.appendFileSync(path.join(dir, `${nickname}.jsonl`), JSON.stringify(entry) + "\n");
155
+ const route = msg.room || msg.channel || "";
156
+ fs.appendFileSync(path.join(dir, inboxFileName(nickname, route)), JSON.stringify(entry) + "\n");
147
157
  }
148
158
 
149
159
  // --- Helpers ---
@@ -14,6 +14,28 @@ function inboxFilePath(nickname) {
14
14
  return path.join(inboxDir(), `${nickname}.jsonl`);
15
15
  }
16
16
 
17
+ /**
18
+ * Find all inbox files for a given nickname (including room/channel-namespaced ones).
19
+ * Returns array of { file, route } objects.
20
+ */
21
+ function findInboxFiles(nickname) {
22
+ const dir = inboxDir();
23
+ if (!fs.existsSync(dir)) return [];
24
+ const prefix = `${nickname}__`;
25
+ const exact = `${nickname}.jsonl`;
26
+ const results = [];
27
+ for (const name of fs.readdirSync(dir)) {
28
+ if (!name.endsWith(".jsonl")) continue;
29
+ if (name === exact) {
30
+ results.push({ file: path.join(dir, name), route: "" });
31
+ } else if (name.startsWith(prefix)) {
32
+ const route = name.slice(prefix.length, -".jsonl".length);
33
+ results.push({ file: path.join(dir, name), route });
34
+ }
35
+ }
36
+ return results;
37
+ }
38
+
17
39
  function readMarkerPath(nickname) {
18
40
  return path.join(inboxDir(), `${nickname}.read`);
19
41
  }
@@ -80,6 +102,8 @@ function cleanupInbox(file) {
80
102
  function checkInbox(nickname, options = {}) {
81
103
  const clear = options.clear || false;
82
104
  const unreadOnly = options.unread || false;
105
+ const filterRoom = options.room || "";
106
+ const filterChannel = options.channel || "";
83
107
 
84
108
  if (!nickname) {
85
109
  console.error("nickname is required");
@@ -87,23 +111,24 @@ function checkInbox(nickname, options = {}) {
87
111
  return;
88
112
  }
89
113
 
90
- const file = inboxFilePath(nickname);
91
114
  const markerFile = readMarkerPath(nickname);
92
115
 
116
+ // Gather all inbox files for this nickname
117
+ const inboxFiles = findInboxFiles(nickname);
118
+
93
119
  if (clear) {
94
- if (fs.existsSync(file)) {
95
- fs.unlinkSync(file);
96
- console.log(`Inbox cleared for ${nickname}.`);
97
- } else {
98
- console.log(`No inbox file for ${nickname}.`);
120
+ let cleared = 0;
121
+ for (const { file } of inboxFiles) {
122
+ if (fs.existsSync(file)) { fs.unlinkSync(file); cleared++; }
99
123
  }
124
+ console.log(cleared > 0 ? `Inbox cleared for ${nickname} (${cleared} file(s)).` : `No inbox file for ${nickname}.`);
100
125
  return;
101
126
  }
102
127
 
103
- cleanupInbox(file);
104
-
105
128
  let messages = [];
106
- if (fs.existsSync(file)) {
129
+ for (const { file } of inboxFiles) {
130
+ cleanupInbox(file);
131
+ if (!fs.existsSync(file)) continue;
107
132
  const lines = fs.readFileSync(file, "utf-8").split("\n").filter(Boolean);
108
133
  for (const line of lines) {
109
134
  try {
@@ -114,6 +139,13 @@ function checkInbox(nickname, options = {}) {
114
139
  }
115
140
  }
116
141
 
142
+ // Filter by room/channel if specified
143
+ if (filterRoom) {
144
+ messages = messages.filter((m) => m.room === filterRoom);
145
+ } else if (filterChannel) {
146
+ messages = messages.filter((m) => m.channel === filterChannel);
147
+ }
148
+
117
149
  function displayWidth(str) {
118
150
  let w = 0;
119
151
  for (const ch of str) {
@@ -185,6 +185,14 @@ class OnlineServer extends EventEmitter {
185
185
  return;
186
186
  }
187
187
 
188
+ // DELETE /ufoo/online/rooms/:id
189
+ const roomDeleteMatch = pathname.match(/^\/ufoo\/online\/rooms\/([^/]+)$/);
190
+ if (roomDeleteMatch && req.method === "DELETE") {
191
+ if (!this.authenticateHttp(req, res)) return;
192
+ this.handleRoomDelete(req, res, decodeURIComponent(roomDeleteMatch[1]));
193
+ return;
194
+ }
195
+
188
196
  if (pathname.startsWith("/ufoo/online/rooms")) {
189
197
  // Step 4: HTTP auth
190
198
  if (!this.authenticateHttp(req, res)) return;
@@ -617,6 +625,25 @@ class OnlineServer extends EventEmitter {
617
625
  this.sendJson(res, 405, { ok: false, error: "Method not allowed" });
618
626
  }
619
627
 
628
+ handleRoomDelete(req, res, roomId) {
629
+ const room = this.rooms.get(roomId);
630
+ if (!room) {
631
+ this.sendJson(res, 404, { ok: false, error: "Room not found" });
632
+ return;
633
+ }
634
+ // Disconnect all members and observers
635
+ const kick = (client) => {
636
+ client.rooms.delete(roomId);
637
+ this.sendError(client.ws, "Room deleted", true, "ROOM_DELETED");
638
+ };
639
+ room.members.forEach(kick);
640
+ if (room.observers) room.observers.forEach(kick);
641
+ this.rooms.delete(roomId);
642
+ this.roomPasswords.delete(roomId);
643
+ this.roomMessageHistory.delete(roomId);
644
+ this.sendJson(res, 200, { ok: true, deleted: roomId });
645
+ }
646
+
620
647
  handleChannelsRequest(req, res) {
621
648
  if (req.method === "GET") {
622
649
  this.sendJson(res, 200, { ok: true, channels: this.listChannels() });