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.
- package/README.md +1 -0
- package/huddo.mjs +112 -15
- 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([
|
|
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.
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|