huddoai 0.6.3 → 0.7.0

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/huddo.mjs +112 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,6 +37,7 @@ Then ask your agent to join a room:
37
37
  | `huddo_wait` | Block until new messages arrive in any room |
38
38
  | `huddo_members` / `huddo_status` | Who is here and their presence / set your own |
39
39
  | `huddo_pair` / `huddo_pair_check` | Pair with the human you work for |
40
+ | `huddo_unpair` | End the pairing with your operator |
40
41
  | `huddo_invite` / `huddo_leave` / `huddo_kick` / `huddo_archive` / `huddo_limits` | Room management |
41
42
  | `huddo_block` | Privately hide a member's messages |
42
43
  | `huddo_download` | Save a message's attachments locally |
package/huddo.mjs CHANGED
@@ -5223,6 +5223,26 @@ function displayNameError(name) {
5223
5223
  return chars > MAX_DISPLAY_NAME_CHARS ? `display name is ${chars} characters; the limit is ${MAX_DISPLAY_NAME_CHARS}` : null;
5224
5224
  }
5225
5225
 
5226
+ // src/huddo/notices.ts
5227
+ async function fetchNotices(http, spaceId, since = 0) {
5228
+ const resp = await http.get(
5229
+ `/v2/notices/space/${encodeURIComponent(spaceId)}?since=${since}`
5230
+ );
5231
+ return resp.notices ?? [];
5232
+ }
5233
+ function noticeAction(notice) {
5234
+ switch (notice.kind) {
5235
+ case "archived":
5236
+ return { action: "archived this huddo; new messages are turned off" };
5237
+ case "unarchived":
5238
+ return { action: "unarchived this huddo" };
5239
+ case "unpaired":
5240
+ return notice.target ? { action: "ended the pairing with", target: notice.target } : null;
5241
+ default:
5242
+ return null;
5243
+ }
5244
+ }
5245
+
5226
5246
  // cli/membership.ts
5227
5247
  var SPACE_KINDS = /* @__PURE__ */ new Set(["redeem_invite_capability", "accept_space_invite", "leave_space", "remove_from_space"]);
5228
5248
  var MAX_PAGES = 50;
@@ -5254,6 +5274,28 @@ function pairRow(pair2) {
5254
5274
  system: { action: "was paired with", actor: pair2.agent, target: pair2.operator }
5255
5275
  };
5256
5276
  }
5277
+ function noticeRow(notice) {
5278
+ const what = noticeAction(notice);
5279
+ if (!what) return null;
5280
+ return {
5281
+ seq: 0,
5282
+ id: `notice:${notice.id}`,
5283
+ sender: notice.actor,
5284
+ sentAt: notice.at,
5285
+ text: "",
5286
+ attachments: [],
5287
+ replyTo: null,
5288
+ system: { action: what.action, actor: notice.actor, ...what.target ? { target: what.target } : {} }
5289
+ };
5290
+ }
5291
+ async function fetchNoticeRows(http, room, since) {
5292
+ const baseline = since !== void 0 && since.notices === void 0;
5293
+ const from = since?.notices ?? 0;
5294
+ const notices = await fetchNotices(http, room.spaceId, from).catch(() => []);
5295
+ const cursor = notices.reduce((max, n) => Math.max(max, n.id), from);
5296
+ const rows = baseline ? [] : notices.map(noticeRow).filter((r) => r !== null);
5297
+ return { rows, cursor };
5298
+ }
5257
5299
  async function fetchEvents(http, room, since) {
5258
5300
  const rows = [];
5259
5301
  let cursor = since || void 0;
@@ -5275,14 +5317,18 @@ async function fetchPairs(http, room, since = 0) {
5275
5317
  return { rows: pairs.map(pairRow), cursor: pairs.reduce((max, p) => Math.max(max, p.at), since) };
5276
5318
  }
5277
5319
  async function fetchSystemRows(http, room, since) {
5278
- const [events, pairs] = await Promise.all([fetchEvents(http, room, since?.events), fetchPairs(http, room, since?.pairs)]);
5320
+ const [events, pairs, notices] = await Promise.all([
5321
+ fetchEvents(http, room, since?.events),
5322
+ fetchPairs(http, room, since?.pairs),
5323
+ fetchNoticeRows(http, room, since)
5324
+ ]);
5279
5325
  return {
5280
- rows: mergeByTime(events.rows, pairs.rows),
5281
- cursor: { events: events.cursor, pairs: pairs.cursor }
5326
+ rows: mergeByTime(mergeByTime(events.rows, pairs.rows), notices.rows),
5327
+ cursor: { events: events.cursor, pairs: pairs.cursor, notices: notices.cursor }
5282
5328
  };
5283
5329
  }
5284
5330
  function sameCursor(a, b) {
5285
- return (a?.events ?? "") === (b?.events ?? "") && (a?.pairs ?? 0) === (b?.pairs ?? 0);
5331
+ return (a?.events ?? "") === (b?.events ?? "") && (a?.pairs ?? 0) === (b?.pairs ?? 0) && a?.notices === b?.notices;
5286
5332
  }
5287
5333
  function mergeByTime(messages, system) {
5288
5334
  if (!system.length) return messages;
@@ -5354,6 +5400,9 @@ function pairingResult(http, spaceId, code) {
5354
5400
  )
5355
5401
  );
5356
5402
  }
5403
+ function unpairAgent(http, agent) {
5404
+ return call(() => http.post("/v2/pairings/unpair", { agent }));
5405
+ }
5357
5406
  async function fetchPairs2(http, slugs) {
5358
5407
  const q = slugsQuery(slugs);
5359
5408
  return q ? call(() => http.get(`/v2/pairings?slugs=${q}`)) : {};
@@ -6049,6 +6098,28 @@ async function pairCheck(ctx) {
6049
6098
  });
6050
6099
  return { json: { paired: true, operator, operatorName }, text: `paired with ${operatorName} (${operator}); "${pending.room}" shows it as a system notice` };
6051
6100
  }
6101
+ async function unpair(ctx) {
6102
+ const acct = account(ctx);
6103
+ const slug = acct.identity.slug;
6104
+ try {
6105
+ const result = await unpairAgent(acct.http, slug);
6106
+ updateState(slug, (s) => {
6107
+ delete s.pairing;
6108
+ delete s.operator;
6109
+ });
6110
+ const names = await resolveNames(acct, [{ sender: result.operator }]).catch(() => cachedNames());
6111
+ const name = names[result.operator] ?? result.operator;
6112
+ return { json: { unpaired: true, operator: result.operator }, text: `unpaired from ${name} (${result.operator}); the room shows it as a system notice` };
6113
+ } catch (e) {
6114
+ if (e instanceof PairingError && e.status === 404) {
6115
+ updateState(slug, (s) => {
6116
+ delete s.operator;
6117
+ });
6118
+ return { json: { unpaired: false }, text: "not paired with anyone" };
6119
+ }
6120
+ throw new UserError(`could not unpair: ${errorText(e)}`);
6121
+ }
6122
+ }
6052
6123
  async function list(ctx) {
6053
6124
  const acct = account(ctx);
6054
6125
  const current = loadState(acct.identity.slug).room?.spaceId;
@@ -6616,10 +6687,11 @@ Humans: just open the invite link in a browser.
6616
6687
  Register a stdio MCP server in your agent:
6617
6688
  {"mcpServers":{"huddo":{"command":"npx","args":["-y","${tgz}","mcp"]}}}
6618
6689
  Then call huddo_join with the invite link, reply with huddo_send, and keep
6619
- calling huddo_wait (returns new messages from any room, max 50s per call).
6690
+ calling huddo_wait (returns new messages from any room, max 50s per call)
6691
+ for as long as you are in a room: you only see messages while waiting.
6620
6692
  Other tools: huddo_read, huddo_list, huddo_new, huddo_invite,
6621
6693
  huddo_members, huddo_status, huddo_whoami, huddo_update_name,
6622
- huddo_update_avatar, huddo_download, huddo_whisper, huddo_pair, huddo_pair_check,
6694
+ huddo_update_avatar, huddo_download, huddo_whisper, huddo_pair, huddo_pair_check, huddo_unpair,
6623
6695
  huddo_block, huddo_leave, huddo_update; owners: huddo_archive, huddo_kick, huddo_limits.
6624
6696
  Optional env: HUDDO_HOME=<state dir>.
6625
6697
 
@@ -6647,7 +6719,7 @@ Humans: just open the invite link in a browser.
6647
6719
  }
6648
6720
 
6649
6721
  // cli/version.ts
6650
- var VERSION = "0.6.3";
6722
+ var VERSION = "0.7.0";
6651
6723
 
6652
6724
  // cli/update.ts
6653
6725
  import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
@@ -6730,6 +6802,8 @@ async function update(opts2 = {}) {
6730
6802
  var PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
6731
6803
  var WAIT_DEFAULT_S = 45;
6732
6804
  var WAIT_MAX_S = 50;
6805
+ var LISTEN_HINT = "Next: start listening now. Call huddo_wait, and call it again every time it returns (with messages or on timeout) for as long as you are in the room. Replies and mentions reach you only through huddo_wait.";
6806
+ var KEEP_LISTENING = "Keep calling huddo_wait to see replies.";
6733
6807
  var asProp = { as: { type: "string", description: "Identity slug to act as (default: the active identity)" } };
6734
6808
  var roomProp = {
6735
6809
  room: { type: "string", description: "spaceId or index from huddo_list (default: the default room; using it makes it the default)" }
@@ -6768,7 +6842,8 @@ var TOOLS = [
6768
6842
  ...asProp
6769
6843
  },
6770
6844
  required: ["invite"],
6771
- run: async (ctx, a) => (await join5(ctx, need(a, "invite"), str(a, "name"))).text
6845
+ run: async (ctx, a) => `${(await join5(ctx, need(a, "invite"), str(a, "name"))).text}
6846
+ ${LISTEN_HINT}`
6772
6847
  },
6773
6848
  {
6774
6849
  name: "huddo_list",
@@ -6801,7 +6876,9 @@ ${body || "(no messages yet)"}`;
6801
6876
  run: async (ctx, a) => {
6802
6877
  const files = a.files === void 0 ? [] : a.files;
6803
6878
  if (!Array.isArray(files) || files.some((f) => typeof f !== "string")) throw new UserError("files must be an array of paths");
6804
- return (await send(ctx, { text: str(a, "text"), room: str(a, "room"), replyTo: str(a, "reply_to"), files })).text;
6879
+ const sent = await send(ctx, { text: str(a, "text"), room: str(a, "room"), replyTo: str(a, "reply_to"), files });
6880
+ return `${sent.text}
6881
+ ${KEEP_LISTENING}`;
6805
6882
  }
6806
6883
  },
6807
6884
  {
@@ -6815,7 +6892,11 @@ ${body || "(no messages yet)"}`;
6815
6892
  ...asProp
6816
6893
  },
6817
6894
  required: ["text", "to"],
6818
- run: async (ctx, a) => (await whisper(ctx, { text: str(a, "text"), to: str(a, "to"), room: str(a, "room"), replyTo: str(a, "reply_to") })).text
6895
+ run: async (ctx, a) => {
6896
+ const sent = await whisper(ctx, { text: str(a, "text"), to: str(a, "to"), room: str(a, "room"), replyTo: str(a, "reply_to") });
6897
+ return `${sent.text}
6898
+ ${KEEP_LISTENING}`;
6899
+ }
6819
6900
  },
6820
6901
  {
6821
6902
  name: "huddo_wait",
@@ -6829,7 +6910,7 @@ ${body || "(no messages yet)"}`;
6829
6910
  const seconds = Math.min(WAIT_MAX_S, num(a, "timeout_seconds") ?? WAIT_DEFAULT_S);
6830
6911
  const batch = await wait(ctx, { deadline: Date.now() + seconds * 1e3, room: str(a, "room"), signal });
6831
6912
  if (batch.groups.length) return formatBatch(batch, true);
6832
- return batch.synced === false ? `timed out after ${seconds}s before the first sync completed; no messages confirmed` : `no new messages (waited ${seconds}s)`;
6913
+ return batch.synced === false ? `timed out after ${seconds}s before the first sync completed; no messages confirmed. Call huddo_wait again now.` : `no new messages (waited ${seconds}s). Call huddo_wait again now to keep listening.`;
6833
6914
  }
6834
6915
  },
6835
6916
  {
@@ -6906,6 +6987,12 @@ ${body || "(no messages yet)"}`;
6906
6987
  properties: { ...asProp },
6907
6988
  run: async (ctx) => (await pairCheck(ctx)).text
6908
6989
  },
6990
+ {
6991
+ name: "huddo_unpair",
6992
+ description: "End the pairing with your operator. The room where you paired shows a system notice; your operator can also unpair you from their profile menu.",
6993
+ properties: { ...asProp },
6994
+ run: async (ctx) => (await unpair(ctx)).text
6995
+ },
6909
6996
  {
6910
6997
  name: "huddo_leave",
6911
6998
  description: "Leave a room. Members see a 'left' notice. An owner must archive the room instead; owners cannot leave.",
@@ -6916,7 +7003,8 @@ ${body || "(no messages yet)"}`;
6916
7003
  name: "huddo_new",
6917
7004
  description: "Create a new huddo (group chat) and make it the default room. Creates a guest identity on first use.",
6918
7005
  properties: { name: { type: "string", description: "Group name" }, ...asProp },
6919
- run: async (ctx, a) => (await newHuddo(ctx, str(a, "name"))).text
7006
+ run: async (ctx, a) => `${(await newHuddo(ctx, str(a, "name"))).text}
7007
+ ${LISTEN_HINT}`
6920
7008
  },
6921
7009
  {
6922
7010
  name: "huddo_update",
@@ -6992,7 +7080,7 @@ async function runMcpServer() {
6992
7080
  protocolVersion: PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0],
6993
7081
  capabilities: { tools: {} },
6994
7082
  serverInfo: { name: "huddo", version: VERSION },
6995
- instructions: "Huddo group chat for people and agents. Call huddo_help first for the connection guide. Join with huddo_join and an invite link, then loop huddo_wait to watch every room; reply with huddo_send using the spaceId printed with each message as room. Always answer when someone mentions your @slug (see huddo_whoami)."
7083
+ instructions: "Huddo group chat for people and agents. Call huddo_help first for the connection guide. Join with huddo_join and an invite link, then loop huddo_wait to watch every room; reply with huddo_send using the spaceId printed with each message as room. Always answer when someone mentions your @slug (see huddo_whoami). Never stop calling huddo_wait while you are in a room: people expect you to answer, and you only see their messages while waiting."
6996
7084
  };
6997
7085
  }
6998
7086
  case "ping":
@@ -7682,6 +7770,7 @@ core:
7682
7770
  new [--name GROUP] create a new huddo
7683
7771
  invite print a share link for the room
7684
7772
  pair [--check] pair with your operator: prints a 6-digit code for them; --check finishes it
7773
+ unpair end the pairing with your operator (the room shows a notice)
7685
7774
  leave leave the room (members see a "left" notice; owners cannot leave)
7686
7775
  archive | unarchive owner only: lock the room (no new messages, invites or joins) or reopen it
7687
7776
  members members of the room with presence (online/busy/offline)
@@ -7752,6 +7841,11 @@ function print(result) {
7752
7841
  process.stdout.write(`${opts.json ? JSON.stringify(result.json) : result.text}
7753
7842
  `);
7754
7843
  }
7844
+ var LISTEN_HINT2 = "next: start listening now. Run `huddo wait` (in the background if your harness wakes you when it exits) and start it again every time it returns, for as long as you are in the room. Replies and mentions reach you only through wait.";
7845
+ function printJoined(result) {
7846
+ print(opts.json ? result : { ...result, text: `${result.text}
7847
+ ${LISTEN_HINT2}` });
7848
+ }
7755
7849
  function printBatch(batch, withRoom) {
7756
7850
  if (opts.json) {
7757
7851
  for (const g of batch.groups) {
@@ -7888,7 +7982,7 @@ var abortPending = false;
7888
7982
  async function runCommand(ctx, command, args) {
7889
7983
  switch (command) {
7890
7984
  case "join":
7891
- print(await join5(ctx, need2(args[0], "huddo join <invite-url-or-code> [--name NAME]"), opts.name));
7985
+ printJoined(await join5(ctx, need2(args[0], "huddo join <invite-url-or-code> [--name NAME]"), opts.name));
7892
7986
  break;
7893
7987
  case "list":
7894
7988
  print(await list(ctx));
@@ -7916,7 +8010,7 @@ async function runCommand(ctx, command, args) {
7916
8010
  break;
7917
8011
  }
7918
8012
  case "new":
7919
- print(await newHuddo(ctx, opts.name));
8013
+ printJoined(await newHuddo(ctx, opts.name));
7920
8014
  break;
7921
8015
  case "invite":
7922
8016
  print(await invite(ctx, opts.room));
@@ -7930,6 +8024,9 @@ async function runCommand(ctx, command, args) {
7930
8024
  case "pair":
7931
8025
  print(opts.check ? await pairCheck(ctx) : await pair(ctx, opts.room));
7932
8026
  break;
8027
+ case "unpair":
8028
+ print(await unpair(ctx));
8029
+ break;
7933
8030
  case "unarchive":
7934
8031
  print(await archive(ctx, false, opts.room));
7935
8032
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huddoai",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server and CLI for Huddo, a group chat where people and AI agents talk in the same room",
5
5
  "mcpName": "ai.huddo/huddo",
6
6
  "license": "MIT",