jefrichat-mcp 0.48.10 → 0.48.12

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/dist/http.js +290 -42
  2. package/dist/index.js +138 -24
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -32012,7 +32012,7 @@ var require_websocket = __commonJS({
32012
32012
  var http2 = __require("http");
32013
32013
  var net = __require("net");
32014
32014
  var tls = __require("tls");
32015
- var { randomBytes, createHash: createHash2 } = __require("crypto");
32015
+ var { randomBytes, createHash: createHash3 } = __require("crypto");
32016
32016
  var { Duplex, Readable: Readable2 } = __require("stream");
32017
32017
  var { URL: URL2 } = __require("url");
32018
32018
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -32680,7 +32680,7 @@ var require_websocket = __commonJS({
32680
32680
  abortHandshake(websocket, socket, "Invalid Upgrade header");
32681
32681
  return;
32682
32682
  }
32683
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
32683
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
32684
32684
  if (res.headers["sec-websocket-accept"] !== digest) {
32685
32685
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
32686
32686
  return;
@@ -33049,7 +33049,7 @@ var require_websocket_server = __commonJS({
33049
33049
  var EventEmitter = __require("events");
33050
33050
  var http2 = __require("http");
33051
33051
  var { Duplex } = __require("stream");
33052
- var { createHash: createHash2 } = __require("crypto");
33052
+ var { createHash: createHash3 } = __require("crypto");
33053
33053
  var extension3 = require_extension();
33054
33054
  var PerMessageDeflate2 = require_permessage_deflate();
33055
33055
  var subprotocol2 = require_subprotocol();
@@ -33356,7 +33356,7 @@ var require_websocket_server = __commonJS({
33356
33356
  );
33357
33357
  }
33358
33358
  if (this._state > RUNNING) return abortHandshake(socket, 503);
33359
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
33359
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
33360
33360
  const headers = [
33361
33361
  "HTTP/1.1 101 Switching Protocols",
33362
33362
  "Upgrade: websocket",
@@ -60785,8 +60785,12 @@ var JefriHttpClient = class {
60785
60785
  /** DECLARE a status — a user-level choice that hits every connection the
60786
60786
  * identity holds. The returned status is the SETTLED one, which may differ
60787
60787
  * from the request if another lease outvoted it. */
60788
- presence(status) {
60789
- return this.call("POST", "/api/presence", { status });
60788
+ /** `declId` (optional) makes the declaration retry-safe: a re-POST of the
60789
+ * same id replays — answering the CURRENT settled state — instead of
60790
+ * reapplying, so a stale retry can never overwrite a status another device
60791
+ * chose in between. Reusing an id with a DIFFERENT status is a 409. */
60792
+ presence(status, declId) {
60793
+ return this.call("POST", "/api/presence", declId ? { status, declId } : { status });
60790
60794
  }
60791
60795
  /** KEEPALIVE — "still here", declaring nothing. This is the call for a timer.
60792
60796
  * Heartbeating through presence("online") instead is a declaration on a
@@ -61519,8 +61523,10 @@ async function statelessPresenceBringUp(acp) {
61519
61523
  try {
61520
61524
  const hb = await acp.heartbeat();
61521
61525
  if (hb.state === "signedOff") await acp.presence("online");
61526
+ return true;
61522
61527
  } catch (e) {
61523
- log("stateless presence bring-up failed (will retry on sweep):", e?.message ?? e);
61528
+ log("stateless presence bring-up failed (the sweep retries it):", e?.message ?? e);
61529
+ return false;
61524
61530
  }
61525
61531
  }
61526
61532
  async function statelessSweepBeat(acp, pool, onAuthRevoked) {
@@ -61536,6 +61542,71 @@ async function statelessSweepBeat(acp, pool, onAuthRevoked) {
61536
61542
  }
61537
61543
  }
61538
61544
 
61545
+ // src/stateless-mcp.ts
61546
+ var rpcError = (res, status, code3, message) => res.status(status).json({ jsonrpc: "2.0", error: { code: code3, message }, id: null });
61547
+ function statelessMcpHandler(deps) {
61548
+ return async (req, res) => {
61549
+ if (deps.isDraining()) {
61550
+ res.setHeader("retry-after", "1");
61551
+ return rpcError(res, 503, -32e3, "server is shutting down \u2014 retry; another instance will answer");
61552
+ }
61553
+ const token = deps.bearer(req);
61554
+ if (!token)
61555
+ return rpcError(res, 401, -32001, "Missing Jefri Chat token \u2014 every request must include Authorization: Bearer <token> (or ?token=).");
61556
+ if (!deps.tryAcquireInflight(token))
61557
+ return rpcError(res, 429, -32003, "too many concurrent requests \u2014 slow down.");
61558
+ try {
61559
+ let acp;
61560
+ try {
61561
+ ({ acp } = await deps.acquire(token));
61562
+ } catch (e) {
61563
+ if (deps.classifyError(e) === "auth")
61564
+ return rpcError(
61565
+ res,
61566
+ 401,
61567
+ -32001,
61568
+ "Invalid or expired Jefri Chat token. Rotate it in the Jefri Chat web app (the agent's owner \u2192 \u{1F511} connect) and reconnect with the fresh command."
61569
+ );
61570
+ return rpcError(res, 502, -32002, `Hub unreachable: ${e?.message ?? e}`);
61571
+ }
61572
+ const mcp = new McpServer({ name: "jefrichat", version: "0.1.0" });
61573
+ const deferred = [];
61574
+ deps.registerTools(mcp, acp, { deferUntilDelivered: (fn) => deferred.push(fn) });
61575
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
61576
+ res.on("finish", () => {
61577
+ for (const fn of deferred) {
61578
+ try {
61579
+ void Promise.resolve(fn()).catch((e) => deps.log?.("deferred post-delivery work failed:", e?.message ?? e));
61580
+ } catch (e) {
61581
+ deps.log?.("deferred post-delivery work failed:", e?.message ?? e);
61582
+ }
61583
+ }
61584
+ deferred.length = 0;
61585
+ });
61586
+ res.on("close", () => {
61587
+ try {
61588
+ transport.close();
61589
+ } catch {
61590
+ }
61591
+ try {
61592
+ void mcp.close();
61593
+ } catch {
61594
+ }
61595
+ });
61596
+ await mcp.connect(transport);
61597
+ await transport.handleRequest(req, res, req.body);
61598
+ } catch (e) {
61599
+ deps.log?.("stateless request failed:", e?.message ?? e);
61600
+ if (!res.headersSent) rpcError(res, 500, -32603, "internal error handling the request");
61601
+ } finally {
61602
+ deps.releaseInflight(token);
61603
+ }
61604
+ };
61605
+ }
61606
+ function statelessMethodNotAllowed(_req, res) {
61607
+ res.status(405).json({ jsonrpc: "2.0", error: { code: -32e3, message: "stateless server: POST only (no SSE stream, no session to terminate)" }, id: null });
61608
+ }
61609
+
61539
61610
  // src/conn-pool.ts
61540
61611
  function attachRef(c, id) {
61541
61612
  if (c.closed) return false;
@@ -61574,7 +61645,8 @@ function createKeyedPool() {
61574
61645
  return v;
61575
61646
  },
61576
61647
  has: (key) => ready.has(key),
61577
- size: () => ready.size
61648
+ size: () => ready.size,
61649
+ keys: () => [...ready.keys()]
61578
61650
  };
61579
61651
  }
61580
61652
 
@@ -61602,6 +61674,7 @@ import cp3 from "node:child_process";
61602
61674
  import os4 from "node:os";
61603
61675
  import nnet from "node:net";
61604
61676
  import ndns from "node:dns/promises";
61677
+ import { createHash as createHash2 } from "node:crypto";
61605
61678
  import nhttp from "node:http";
61606
61679
  import nhttps from "node:https";
61607
61680
 
@@ -62024,6 +62097,22 @@ async function confirmWrite(c, fire) {
62024
62097
  const e = await err;
62025
62098
  return e ? { error: e.message } : {};
62026
62099
  }
62100
+ function retryKey(extra, parts) {
62101
+ const rid = extra?.requestId;
62102
+ if (rid === void 0 || rid === null) return void 0;
62103
+ const h = extra?.requestInfo?.headers;
62104
+ const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
62105
+ if (!ua) return void 0;
62106
+ const bucket = Math.floor(Date.now() / 75e3);
62107
+ return "mcp-" + createHash2("sha256").update(JSON.stringify([bucket, ua, rid, ...parts])).digest("hex").slice(0, 40);
62108
+ }
62109
+ function declKey(extra, parts) {
62110
+ const rid = extra?.requestId;
62111
+ if (rid === void 0 || rid === null) return void 0;
62112
+ const h = extra?.requestInfo?.headers;
62113
+ const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
62114
+ return "d-" + createHash2("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
62115
+ }
62027
62116
  async function fetchHistory(c, convId) {
62028
62117
  if (isStateless(c)) return c.history(convId);
62029
62118
  const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
@@ -62482,7 +62571,17 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62482
62571
  text: external_exports.string().describe("the message body")
62483
62572
  }
62484
62573
  },
62485
- async ({ to, text: text5 }) => withClient(async (c) => {
62574
+ async ({ to, text: text5 }, extra) => withClient(async (c) => {
62575
+ if (isStateless(c)) {
62576
+ try {
62577
+ const res = await c.message(to, text5, retryKey(extra, ["message", to, text5]));
62578
+ if (res?.deduplicated)
62579
+ return ok3(`Already delivered to @${to} \u2014 this request matched a recent identical send and was suppressed as a retry. If you meant it as a NEW message, send it again (it will not be suppressed twice).`);
62580
+ return ok3(`Sent to @${to}: "${text5}"`);
62581
+ } catch (e) {
62582
+ return fail(`Message rejected: ${e?.message ?? e}`);
62583
+ }
62584
+ }
62486
62585
  const r = await confirmWrite(c, () => c.message(to, text5));
62487
62586
  if (r.error) return fail(`Message rejected: ${r.error}`);
62488
62587
  return ok3(`Sent to @${to}: "${text5}"`);
@@ -62640,7 +62739,7 @@ ${fp}`);
62640
62739
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
62641
62740
  }
62642
62741
  },
62643
- async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
62742
+ async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
62644
62743
  let name, mime, url;
62645
62744
  if (path3) {
62646
62745
  if (!ctx.local) {
@@ -62694,7 +62793,23 @@ ${psCmd}
62694
62793
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
62695
62794
  if (rawBytes.length > MAX_UPLOAD_BYTES)
62696
62795
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62697
- if (isStateless(c) || rawBytes.length > WS_SAFE_FILE_BYTES) {
62796
+ if (isStateless(c)) {
62797
+ try {
62798
+ const res = await c.sendFile(
62799
+ { to, caption },
62800
+ name,
62801
+ mime,
62802
+ rawBytes,
62803
+ retryKey(extra, ["file", to, name, createHash2("sha256").update(rawBytes).digest("hex")])
62804
+ );
62805
+ if (res?.deduplicated)
62806
+ return ok3(`Already delivered \u2014 ${name} matched a recent identical upload and was suppressed as a retry (round 28 #2: suppression is SAID, never a false "Sent"). If you meant a new copy, send it again.`);
62807
+ } catch (e2) {
62808
+ return fail(`Rejected: ${e2?.message ?? e2}`);
62809
+ }
62810
+ return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
62811
+ }
62812
+ if (rawBytes.length > WS_SAFE_FILE_BYTES) {
62698
62813
  try {
62699
62814
  await uploadFileHttp(c, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
62700
62815
  } catch (e2) {
@@ -62722,13 +62837,29 @@ ${psCmd}
62722
62837
  caption: external_exports.string().optional()
62723
62838
  }
62724
62839
  },
62725
- async ({ to, fileName, content: content3, caption }) => withClient(async (c) => {
62840
+ async ({ to, fileName, content: content3, caption }, extra) => withClient(async (c) => {
62726
62841
  const known = mimeOf(fileName);
62727
62842
  const mime = known === "application/octet-stream" ? "text/plain" : known;
62728
62843
  const contentBytes = Buffer.from(content3, "utf8");
62729
62844
  if (contentBytes.length > MAX_UPLOAD_BYTES)
62730
62845
  return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62731
- if (isStateless(c) || contentBytes.length > WS_SAFE_FILE_BYTES) {
62846
+ if (isStateless(c)) {
62847
+ try {
62848
+ const res = await c.sendFile(
62849
+ { to, caption },
62850
+ fileName,
62851
+ mime,
62852
+ contentBytes,
62853
+ retryKey(extra, ["file", to, fileName, createHash2("sha256").update(contentBytes).digest("hex")])
62854
+ );
62855
+ if (res?.deduplicated)
62856
+ return ok3(`Already delivered \u2014 ${fileName} matched a recent identical upload and was suppressed as a retry. If you meant a new copy, send it again.`);
62857
+ } catch (e2) {
62858
+ return fail(`Rejected: ${e2?.message ?? e2}`);
62859
+ }
62860
+ return ok3(`Sent \u{1F4C4} ${fileName} (${MB(contentBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
62861
+ }
62862
+ if (contentBytes.length > WS_SAFE_FILE_BYTES) {
62732
62863
  try {
62733
62864
  await uploadFileHttp(c, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
62734
62865
  } catch (e2) {
@@ -63119,7 +63250,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63119
63250
  },
63120
63251
  async () => withClient(async (c) => {
63121
63252
  const me = c.identity.username;
63122
- const since = inboxWatermark.get(me);
63253
+ const since = ctx.stateless ? "stored" : inboxWatermark.get(me);
63123
63254
  const url = `${ctx.serverUrl.replace(/\/$/, "")}/api/inbox` + (since ? `?since=${encodeURIComponent(since)}` : "");
63124
63255
  let msgs = [];
63125
63256
  try {
@@ -63132,9 +63263,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63132
63263
  ctx.inbox.length = 0;
63133
63264
  }
63134
63265
  if (!msgs.length) return ok3("\u{1F4ED} No new messages.");
63135
- msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
63136
- inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
63137
- saveWatermark();
63266
+ msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : a.id < b.id ? -1 : 1);
63138
63267
  const groupNames = /* @__PURE__ */ new Map();
63139
63268
  if (msgs.some((m) => m.groupId)) {
63140
63269
  try {
@@ -63162,8 +63291,27 @@ Or just upload ${fname} from the agent's page in the web app.`
63162
63291
  const body = m.kind === "file" ? `\u{1F4CE} sent a file: ${m.fileName} \u2014 to save it call jefri_download_file(from: "${m.senderUsername}"${m.fileName ? `, fileName: "${m.fileName}"` : ""})` : m.content;
63163
63292
  return `${at}@${m.senderUsername}${where(m)}: ${body}`;
63164
63293
  }));
63165
- return ok3(`\u{1F4E8} ${lines.length} message(s):
63166
- ` + lines.join("\n"));
63294
+ const rendered = `\u{1F4E8} ${lines.length} message(s):
63295
+ ` + lines.join("\n");
63296
+ if (ctx.stateless) {
63297
+ const renderedIds = msgs.map((m) => m.id);
63298
+ const ack = async () => {
63299
+ try {
63300
+ await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
63301
+ method: "POST",
63302
+ headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
63303
+ body: JSON.stringify({ messageIds: renderedIds })
63304
+ });
63305
+ } catch {
63306
+ }
63307
+ };
63308
+ if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
63309
+ else await ack();
63310
+ } else {
63311
+ inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
63312
+ saveWatermark();
63313
+ }
63314
+ return ok3(rendered);
63167
63315
  })
63168
63316
  );
63169
63317
  server2.registerTool(
@@ -63192,7 +63340,17 @@ Or just upload ${fname} from the agent's page in the web app.`
63192
63340
  text: external_exports.string().describe("the message to post to the group")
63193
63341
  }
63194
63342
  },
63195
- async ({ groupId, text: text5 }) => withClient(async (c) => {
63343
+ async ({ groupId, text: text5 }, extra) => withClient(async (c) => {
63344
+ if (isStateless(c)) {
63345
+ try {
63346
+ const res = await c.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5]));
63347
+ if (res?.deduplicated)
63348
+ return ok3(`Already posted \u2014 this request matched a recent identical post and was suppressed as a retry. If you meant it as a NEW message, send it again.`);
63349
+ return ok3(`Posted to the group.`);
63350
+ } catch (e) {
63351
+ return fail(`Couldn't post to the group: ${e?.message ?? e}`);
63352
+ }
63353
+ }
63196
63354
  const r = await confirmWrite(c, () => c.groupMessage(groupId, text5));
63197
63355
  if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
63198
63356
  return ok3(`Posted to the group.`);
@@ -63212,7 +63370,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63212
63370
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
63213
63371
  }
63214
63372
  },
63215
- async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
63373
+ async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
63216
63374
  let name, mime, url;
63217
63375
  if (path3) {
63218
63376
  if (!ctx.local) {
@@ -63256,7 +63414,23 @@ Or just upload ${fname} from the agent's page in the web app.`
63256
63414
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
63257
63415
  if (rawBytes.length > MAX_UPLOAD_BYTES)
63258
63416
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
63259
- if (isStateless(c) || rawBytes.length > WS_SAFE_FILE_BYTES) {
63417
+ if (isStateless(c)) {
63418
+ try {
63419
+ const res = await c.sendFile(
63420
+ { groupId, caption },
63421
+ name,
63422
+ mime,
63423
+ rawBytes,
63424
+ retryKey(extra, ["file", groupId, name, createHash2("sha256").update(rawBytes).digest("hex")])
63425
+ );
63426
+ if (res?.deduplicated)
63427
+ return ok3(`Already delivered \u2014 ${name} matched a recent identical upload and was suppressed as a retry. If you meant a new copy, send it again.`);
63428
+ } catch (e2) {
63429
+ return fail(`Rejected: ${e2?.message ?? e2}`);
63430
+ }
63431
+ return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to the group.`);
63432
+ }
63433
+ if (rawBytes.length > WS_SAFE_FILE_BYTES) {
63260
63434
  try {
63261
63435
  await uploadFileHttp(c, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
63262
63436
  } catch (e2) {
@@ -63341,7 +63515,19 @@ Or just upload ${fname} from the agent's page in the web app.`
63341
63515
  status: external_exports.enum(["online", "away", "busy", "coding", "reviewing", "planning", "offline"])
63342
63516
  }
63343
63517
  },
63344
- async ({ status }) => withClient(async (c) => {
63518
+ async ({ status }, extra) => withClient(async (c) => {
63519
+ if (isStateless(c)) {
63520
+ const declId = declKey(extra, ["presence", status]);
63521
+ try {
63522
+ const res = await c.presence(status, declId);
63523
+ ctx.onPresenceDeclared?.(res.status);
63524
+ if (res.replayed && res.status !== status)
63525
+ return ok3(`That request was an old retry \u2014 your status is "${res.status}" (a newer declaration stands).`);
63526
+ return ok3(`Status set to ${res.status}.`);
63527
+ } catch (e) {
63528
+ return fail(`Couldn't set status: ${e?.message ?? e}`);
63529
+ }
63530
+ }
63345
63531
  const r = await confirmWrite(c, () => c.presence(status));
63346
63532
  if (r.error) return fail(`Couldn't set status: ${r.error}`);
63347
63533
  return ok3(`Status set to ${status}.`);
@@ -63541,6 +63727,7 @@ var sessions = /* @__PURE__ */ new Map();
63541
63727
  var HOST_FRESH_MS = Number(process.env.MCP_HOST_FRESH_MS ?? 10 * 60 * 1e3);
63542
63728
  var hubPool = createKeyedPool();
63543
63729
  var SESSION_TTL_MS = Number(process.env.MCP_SESSION_TTL_MS ?? 4 * 60 * 60 * 1e3);
63730
+ var STATELESS_POOL_IDLE_MS = Number(process.env.MCP_STATELESS_POOL_IDLE_MS ?? 10 * 60 * 1e3);
63544
63731
  var MAX_SESSIONS = Number(process.env.MCP_MAX_SESSIONS ?? 1500);
63545
63732
  var MAX_SESSIONS_PER_IDENTITY = Number(process.env.MCP_MAX_SESSIONS_PER_IDENTITY ?? 20);
63546
63733
  var INIT_WINDOW_MS = 6e4;
@@ -63601,8 +63788,8 @@ async function acquirePooledConn(token, poolKey) {
63601
63788
  if (STATELESS) {
63602
63789
  const acp2 = new JefriHttpClient({ server: HUB, token });
63603
63790
  await acp2.whoami();
63604
- await statelessPresenceBringUp(acp2);
63605
- return { acp: acp2, inbox: [], refs: /* @__PURE__ */ new Set(), closed: false };
63791
+ const broughtUp = await statelessPresenceBringUp(acp2);
63792
+ return { acp: acp2, inbox: [], refs: /* @__PURE__ */ new Set(), closed: false, lastSeen: Date.now(), needsBringUp: !broughtUp };
63606
63793
  }
63607
63794
  const acp = await JefriClient.connect({ server: HUB, token, status: "online" });
63608
63795
  const inbox = [];
@@ -63625,15 +63812,30 @@ setInterval(() => {
63625
63812
  for (const [sid, s] of sessions) {
63626
63813
  if (now - s.lastActive > SESSION_TTL_MS) closeSession(sid, "idle");
63627
63814
  }
63628
- const seenPools = /* @__PURE__ */ new Set();
63629
- for (const s of sessions.values()) {
63630
- if (seenPools.has(s.poolKey)) continue;
63631
- seenPools.add(s.poolKey);
63632
- const pooled = hubPool.get(s.poolKey);
63633
- if (!pooled) continue;
63634
- announcePoolHosts(s.poolKey, pooled);
63635
- if (STATELESS)
63636
- void statelessSweepBeat(pooled.acp, pooled, () => closePool(s.poolKey, "auth revoked"));
63815
+ if (STATELESS) {
63816
+ for (const key of hubPool.keys()) {
63817
+ const pooled = hubPool.get(key);
63818
+ if (!pooled) continue;
63819
+ if (now - (pooled.lastSeen ?? 0) > STATELESS_POOL_IDLE_MS) {
63820
+ hubPool.drop(key);
63821
+ continue;
63822
+ }
63823
+ if (pooled.needsBringUp) {
63824
+ void statelessPresenceBringUp(pooled.acp).then((ok4) => {
63825
+ if (ok4) pooled.needsBringUp = false;
63826
+ });
63827
+ } else {
63828
+ void statelessSweepBeat(pooled.acp, pooled, () => closePool(key, "auth revoked"));
63829
+ }
63830
+ }
63831
+ } else {
63832
+ const seenPools = /* @__PURE__ */ new Set();
63833
+ for (const s of sessions.values()) {
63834
+ if (seenPools.has(s.poolKey)) continue;
63835
+ seenPools.add(s.poolKey);
63836
+ const pooled = hubPool.get(s.poolKey);
63837
+ if (pooled) announcePoolHosts(s.poolKey, pooled);
63838
+ }
63637
63839
  }
63638
63840
  for (const [ip, e] of initHits) {
63639
63841
  if (now > e.resetAt) initHits.delete(ip);
@@ -63697,7 +63899,7 @@ var health = (_req, res) => res.status(draining ? 503 : 200).json({
63697
63899
  });
63698
63900
  app.get("/health", health);
63699
63901
  app.get("/mcp/health", health);
63700
- app.post("/mcp", async (req, res) => {
63902
+ var statefulPost = async (req, res) => {
63701
63903
  const sessionId = req.headers["mcp-session-id"];
63702
63904
  if (sessionId && sessions.has(sessionId)) {
63703
63905
  const s = sessions.get(sessionId);
@@ -63777,10 +63979,6 @@ app.post("/mcp", async (req, res) => {
63777
63979
  res.status(502).json({ jsonrpc: "2.0", error: { code: -32002, message: `Hub unreachable: ${e?.message ?? e}` }, id: null });
63778
63980
  return;
63779
63981
  }
63780
- if (STATELESS && pooled.hbSignedOff) {
63781
- pooled.hbSignedOff = false;
63782
- void statelessPresenceBringUp(pooled.acp);
63783
- }
63784
63982
  const acp = pooled.acp;
63785
63983
  if (!acp.identity) {
63786
63984
  closePool(poolKey, "no identity");
@@ -63864,7 +64062,7 @@ app.post("/mcp", async (req, res) => {
63864
64062
  error: { code: -32001, message: "Session expired \u2014 reinitialize (your token is still valid)" },
63865
64063
  id: null
63866
64064
  });
63867
- });
64065
+ };
63868
64066
  var bySession = async (req, res) => {
63869
64067
  const sessionId = req.headers["mcp-session-id"];
63870
64068
  const s = sessionId ? sessions.get(sessionId) : void 0;
@@ -63882,8 +64080,58 @@ var bySession = async (req, res) => {
63882
64080
  touch(sessionId);
63883
64081
  await s.transport.handleRequest(req, res);
63884
64082
  };
63885
- app.get("/mcp", bySession);
63886
- app.delete("/mcp", bySession);
64083
+ if (STATELESS) {
64084
+ app.post(
64085
+ "/mcp",
64086
+ statelessMcpHandler({
64087
+ bearer,
64088
+ acquire: async (token) => {
64089
+ const poolKey = sha256(token).toString("hex");
64090
+ const pooled = await acquirePooledConn(token, poolKey);
64091
+ pooled.lastSeen = Date.now();
64092
+ return pooled;
64093
+ },
64094
+ // registerTools below closes over the POOLED entry via acp — the presence
64095
+ // resume/stop signal needs the pool, so it is looked up by identity of
64096
+ // the shared client (one pool owns exactly one client instance).
64097
+ classifyError: (e) => e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED" || e instanceof JefriHttpError && (e.status === 401 || e.status === 403) ? "auth" : "hub",
64098
+ registerTools: (mcp, acp, hooks) => registerAcpTools(mcp, {
64099
+ ensureClient: async () => acp,
64100
+ inbox: [],
64101
+ serverUrl: HUB,
64102
+ local: false,
64103
+ stateless: true,
64104
+ deferUntilDelivered: hooks.deferUntilDelivered,
64105
+ // Round 26 #4a: a successful declaration RESUMES heartbeats (the
64106
+ // user came back — the hub cleared its sign-off marker, the pool
64107
+ // must clear its own), and a sign-off STOPS them proactively.
64108
+ onPresenceDeclared: (status) => {
64109
+ for (const key of hubPool.keys()) {
64110
+ const pooled = hubPool.get(key);
64111
+ if (pooled && pooled.acp === acp) {
64112
+ if (status === "offline") pooled.hbSignedOff = true;
64113
+ else {
64114
+ pooled.hbSignedOff = false;
64115
+ pooled.needsBringUp = false;
64116
+ }
64117
+ break;
64118
+ }
64119
+ }
64120
+ }
64121
+ }),
64122
+ isDraining: () => draining,
64123
+ tryAcquireInflight: (token) => tryAcquire(tokenInflight, sha256(token).toString("hex"), MAX_INFLIGHT_PER_TOKEN),
64124
+ releaseInflight: (token) => release(tokenInflight, sha256(token).toString("hex")),
64125
+ log: log2
64126
+ })
64127
+ );
64128
+ app.get("/mcp", statelessMethodNotAllowed);
64129
+ app.delete("/mcp", statelessMethodNotAllowed);
64130
+ } else {
64131
+ app.post("/mcp", statefulPost);
64132
+ app.get("/mcp", bySession);
64133
+ app.delete("/mcp", bySession);
64134
+ }
63887
64135
  var server = http.createServer(app).listen(PORT, () => {
63888
64136
  console.error(`
63889
64137
  \u{1F310} Jefri Chat remote MCP (HTTP) on http://localhost:${PORT}/mcp`);
package/dist/index.js CHANGED
@@ -9152,7 +9152,7 @@ var require_websocket = __commonJS({
9152
9152
  var http = __require("http");
9153
9153
  var net4 = __require("net");
9154
9154
  var tls = __require("tls");
9155
- var { randomBytes: randomBytes3, createHash: createHash3 } = __require("crypto");
9155
+ var { randomBytes: randomBytes3, createHash: createHash4 } = __require("crypto");
9156
9156
  var { Duplex, Readable } = __require("stream");
9157
9157
  var { URL: URL2 } = __require("url");
9158
9158
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -9820,7 +9820,7 @@ var require_websocket = __commonJS({
9820
9820
  abortHandshake(websocket, socket, "Invalid Upgrade header");
9821
9821
  return;
9822
9822
  }
9823
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
9823
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
9824
9824
  if (res.headers["sec-websocket-accept"] !== digest) {
9825
9825
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
9826
9826
  return;
@@ -10189,7 +10189,7 @@ var require_websocket_server = __commonJS({
10189
10189
  var EventEmitter = __require("events");
10190
10190
  var http = __require("http");
10191
10191
  var { Duplex } = __require("stream");
10192
- var { createHash: createHash3 } = __require("crypto");
10192
+ var { createHash: createHash4 } = __require("crypto");
10193
10193
  var extension3 = require_extension();
10194
10194
  var PerMessageDeflate2 = require_permessage_deflate();
10195
10195
  var subprotocol2 = require_subprotocol();
@@ -10496,7 +10496,7 @@ var require_websocket_server = __commonJS({
10496
10496
  );
10497
10497
  }
10498
10498
  if (this._state > RUNNING) return abortHandshake(socket, 503);
10499
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
10499
+ const digest = createHash4("sha1").update(key + GUID).digest("base64");
10500
10500
  const headers = [
10501
10501
  "HTTP/1.1 101 Switching Protocols",
10502
10502
  "Upgrade: websocket",
@@ -10891,7 +10891,7 @@ import fs6 from "node:fs";
10891
10891
  import net from "node:net";
10892
10892
  import os5 from "node:os";
10893
10893
  import np5 from "node:path";
10894
- import { createHash, randomBytes } from "node:crypto";
10894
+ import { createHash as createHash2, randomBytes } from "node:crypto";
10895
10895
  function parseInstanceName(name) {
10896
10896
  if (name.startsWith(".")) return null;
10897
10897
  const lastDot = name.lastIndexOf(".");
@@ -10922,7 +10922,7 @@ function safeUserParts(u) {
10922
10922
  const raw = u || "unknown";
10923
10923
  return {
10924
10924
  base: raw.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 40) || "agent",
10925
- hash: createHash("sha1").update(raw).digest("hex").slice(0, 8)
10925
+ hash: createHash2("sha1").update(raw).digest("hex").slice(0, 8)
10926
10926
  };
10927
10927
  }
10928
10928
  function safeUser2(u) {
@@ -10935,7 +10935,7 @@ function socketPathFor(username, home = os5.homedir(), platform = process.platfo
10935
10935
  const preferred = np5.join(home, ".jefri", "sock", `${safe}.sock`);
10936
10936
  if (Buffer.byteLength(preferred) <= 100) return preferred;
10937
10937
  const uid = typeof process.getuid === "function" ? String(process.getuid()) : process.env.USERNAME ?? "";
10938
- const scope = createHash("sha1").update(`${home}\0${uid}`).digest("hex").slice(0, 8);
10938
+ const scope = createHash2("sha1").update(`${home}\0${uid}`).digest("hex").slice(0, 8);
10939
10939
  const { base, hash } = safeUserParts(username);
10940
10940
  return np5.join(tmp, `jefri-${base.slice(0, 16)}-${hash}-${scope}.sock`);
10941
10941
  }
@@ -12118,7 +12118,7 @@ __export(run_exports, {
12118
12118
  tmuxRunArgv: () => tmuxRunArgv
12119
12119
  });
12120
12120
  import cp9 from "node:child_process";
12121
- import { createHash as createHash2 } from "node:crypto";
12121
+ import { createHash as createHash3 } from "node:crypto";
12122
12122
  import readline2 from "node:readline";
12123
12123
  function tmuxRunArgv(agentCmd, sessionName, cwd, envPairs = []) {
12124
12124
  const env = envPairs.flatMap((e) => ["-e", e]);
@@ -12127,7 +12127,7 @@ function tmuxRunArgv(agentCmd, sessionName, cwd, envPairs = []) {
12127
12127
  function sessionNameFor(agentCmd, cwd = process.cwd(), experimental = false) {
12128
12128
  const exe = (agentCmd[0] || "agent").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 32);
12129
12129
  const key = agentCmd.join("\0") + "\0" + cwd;
12130
- const h = createHash2("sha1").update(key + (experimental ? " x" : "")).digest("hex").slice(0, 8);
12130
+ const h = createHash3("sha1").update(key + (experimental ? " x" : "")).digest("hex").slice(0, 8);
12131
12131
  return `jefri-${exe}-${h}`;
12132
12132
  }
12133
12133
  function isShellExe(cmd) {
@@ -39391,6 +39391,7 @@ import cp3 from "node:child_process";
39391
39391
  import os4 from "node:os";
39392
39392
  import nnet from "node:net";
39393
39393
  import ndns from "node:dns/promises";
39394
+ import { createHash } from "node:crypto";
39394
39395
  import nhttp from "node:http";
39395
39396
  import nhttps from "node:https";
39396
39397
 
@@ -40556,6 +40557,22 @@ async function confirmWrite(c2, fire) {
40556
40557
  const e = await err;
40557
40558
  return e ? { error: e.message } : {};
40558
40559
  }
40560
+ function retryKey(extra, parts) {
40561
+ const rid = extra?.requestId;
40562
+ if (rid === void 0 || rid === null) return void 0;
40563
+ const h = extra?.requestInfo?.headers;
40564
+ const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
40565
+ if (!ua) return void 0;
40566
+ const bucket = Math.floor(Date.now() / 75e3);
40567
+ return "mcp-" + createHash("sha256").update(JSON.stringify([bucket, ua, rid, ...parts])).digest("hex").slice(0, 40);
40568
+ }
40569
+ function declKey(extra, parts) {
40570
+ const rid = extra?.requestId;
40571
+ if (rid === void 0 || rid === null) return void 0;
40572
+ const h = extra?.requestInfo?.headers;
40573
+ const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
40574
+ return "d-" + createHash("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
40575
+ }
40559
40576
  async function fetchHistory2(c2, convId) {
40560
40577
  if (isStateless(c2)) return c2.history(convId);
40561
40578
  const p = waitFor(c2, "history", (e) => e.conversationId === convId, 4e3);
@@ -41017,7 +41034,17 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
41017
41034
  text: external_exports.string().describe("the message body")
41018
41035
  }
41019
41036
  },
41020
- async ({ to, text: text5 }) => withClient(async (c2) => {
41037
+ async ({ to, text: text5 }, extra) => withClient(async (c2) => {
41038
+ if (isStateless(c2)) {
41039
+ try {
41040
+ const res = await c2.message(to, text5, retryKey(extra, ["message", to, text5]));
41041
+ if (res?.deduplicated)
41042
+ return ok3(`Already delivered to @${to} \u2014 this request matched a recent identical send and was suppressed as a retry. If you meant it as a NEW message, send it again (it will not be suppressed twice).`);
41043
+ return ok3(`Sent to @${to}: "${text5}"`);
41044
+ } catch (e) {
41045
+ return fail(`Message rejected: ${e?.message ?? e}`);
41046
+ }
41047
+ }
41021
41048
  const r = await confirmWrite(c2, () => c2.message(to, text5));
41022
41049
  if (r.error) return fail(`Message rejected: ${r.error}`);
41023
41050
  return ok3(`Sent to @${to}: "${text5}"`);
@@ -41175,7 +41202,7 @@ ${fp}`);
41175
41202
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41176
41203
  }
41177
41204
  },
41178
- async ({ to, path: path4, caption, dataUrl, fileUrl, fileName }) => withClient(async (c2) => {
41205
+ async ({ to, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
41179
41206
  let name, mime, url;
41180
41207
  if (path4) {
41181
41208
  if (!ctx.local) {
@@ -41229,7 +41256,23 @@ ${psCmd}
41229
41256
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
41230
41257
  if (rawBytes.length > MAX_UPLOAD_BYTES)
41231
41258
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
41232
- if (isStateless(c2) || rawBytes.length > WS_SAFE_FILE_BYTES) {
41259
+ if (isStateless(c2)) {
41260
+ try {
41261
+ const res = await c2.sendFile(
41262
+ { to, caption },
41263
+ name,
41264
+ mime,
41265
+ rawBytes,
41266
+ retryKey(extra, ["file", to, name, createHash("sha256").update(rawBytes).digest("hex")])
41267
+ );
41268
+ if (res?.deduplicated)
41269
+ return ok3(`Already delivered \u2014 ${name} matched a recent identical upload and was suppressed as a retry (round 28 #2: suppression is SAID, never a false "Sent"). If you meant a new copy, send it again.`);
41270
+ } catch (e2) {
41271
+ return fail(`Rejected: ${e2?.message ?? e2}`);
41272
+ }
41273
+ return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
41274
+ }
41275
+ if (rawBytes.length > WS_SAFE_FILE_BYTES) {
41233
41276
  try {
41234
41277
  await uploadFileHttp(c2, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
41235
41278
  } catch (e2) {
@@ -41257,13 +41300,29 @@ ${psCmd}
41257
41300
  caption: external_exports.string().optional()
41258
41301
  }
41259
41302
  },
41260
- async ({ to, fileName, content: content3, caption }) => withClient(async (c2) => {
41303
+ async ({ to, fileName, content: content3, caption }, extra) => withClient(async (c2) => {
41261
41304
  const known = mimeOf(fileName);
41262
41305
  const mime = known === "application/octet-stream" ? "text/plain" : known;
41263
41306
  const contentBytes = Buffer.from(content3, "utf8");
41264
41307
  if (contentBytes.length > MAX_UPLOAD_BYTES)
41265
41308
  return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
41266
- if (isStateless(c2) || contentBytes.length > WS_SAFE_FILE_BYTES) {
41309
+ if (isStateless(c2)) {
41310
+ try {
41311
+ const res = await c2.sendFile(
41312
+ { to, caption },
41313
+ fileName,
41314
+ mime,
41315
+ contentBytes,
41316
+ retryKey(extra, ["file", to, fileName, createHash("sha256").update(contentBytes).digest("hex")])
41317
+ );
41318
+ if (res?.deduplicated)
41319
+ return ok3(`Already delivered \u2014 ${fileName} matched a recent identical upload and was suppressed as a retry. If you meant a new copy, send it again.`);
41320
+ } catch (e2) {
41321
+ return fail(`Rejected: ${e2?.message ?? e2}`);
41322
+ }
41323
+ return ok3(`Sent \u{1F4C4} ${fileName} (${MB(contentBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
41324
+ }
41325
+ if (contentBytes.length > WS_SAFE_FILE_BYTES) {
41267
41326
  try {
41268
41327
  await uploadFileHttp(c2, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
41269
41328
  } catch (e2) {
@@ -41654,7 +41713,7 @@ Or just upload ${fname} from the agent's page in the web app.`
41654
41713
  },
41655
41714
  async () => withClient(async (c2) => {
41656
41715
  const me = c2.identity.username;
41657
- const since = inboxWatermark.get(me);
41716
+ const since = ctx.stateless ? "stored" : inboxWatermark.get(me);
41658
41717
  const url = `${ctx.serverUrl.replace(/\/$/, "")}/api/inbox` + (since ? `?since=${encodeURIComponent(since)}` : "");
41659
41718
  let msgs = [];
41660
41719
  try {
@@ -41667,9 +41726,7 @@ Or just upload ${fname} from the agent's page in the web app.`
41667
41726
  ctx.inbox.length = 0;
41668
41727
  }
41669
41728
  if (!msgs.length) return ok3("\u{1F4ED} No new messages.");
41670
- msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
41671
- inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
41672
- saveWatermark();
41729
+ msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : a.id < b.id ? -1 : 1);
41673
41730
  const groupNames = /* @__PURE__ */ new Map();
41674
41731
  if (msgs.some((m) => m.groupId)) {
41675
41732
  try {
@@ -41697,8 +41754,27 @@ Or just upload ${fname} from the agent's page in the web app.`
41697
41754
  const body = m.kind === "file" ? `\u{1F4CE} sent a file: ${m.fileName} \u2014 to save it call jefri_download_file(from: "${m.senderUsername}"${m.fileName ? `, fileName: "${m.fileName}"` : ""})` : m.content;
41698
41755
  return `${at}@${m.senderUsername}${where(m)}: ${body}`;
41699
41756
  }));
41700
- return ok3(`\u{1F4E8} ${lines.length} message(s):
41701
- ` + lines.join("\n"));
41757
+ const rendered = `\u{1F4E8} ${lines.length} message(s):
41758
+ ` + lines.join("\n");
41759
+ if (ctx.stateless) {
41760
+ const renderedIds = msgs.map((m) => m.id);
41761
+ const ack = async () => {
41762
+ try {
41763
+ await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
41764
+ method: "POST",
41765
+ headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
41766
+ body: JSON.stringify({ messageIds: renderedIds })
41767
+ });
41768
+ } catch {
41769
+ }
41770
+ };
41771
+ if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
41772
+ else await ack();
41773
+ } else {
41774
+ inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
41775
+ saveWatermark();
41776
+ }
41777
+ return ok3(rendered);
41702
41778
  })
41703
41779
  );
41704
41780
  server2.registerTool(
@@ -41727,7 +41803,17 @@ Or just upload ${fname} from the agent's page in the web app.`
41727
41803
  text: external_exports.string().describe("the message to post to the group")
41728
41804
  }
41729
41805
  },
41730
- async ({ groupId, text: text5 }) => withClient(async (c2) => {
41806
+ async ({ groupId, text: text5 }, extra) => withClient(async (c2) => {
41807
+ if (isStateless(c2)) {
41808
+ try {
41809
+ const res = await c2.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5]));
41810
+ if (res?.deduplicated)
41811
+ return ok3(`Already posted \u2014 this request matched a recent identical post and was suppressed as a retry. If you meant it as a NEW message, send it again.`);
41812
+ return ok3(`Posted to the group.`);
41813
+ } catch (e) {
41814
+ return fail(`Couldn't post to the group: ${e?.message ?? e}`);
41815
+ }
41816
+ }
41731
41817
  const r = await confirmWrite(c2, () => c2.groupMessage(groupId, text5));
41732
41818
  if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
41733
41819
  return ok3(`Posted to the group.`);
@@ -41747,7 +41833,7 @@ Or just upload ${fname} from the agent's page in the web app.`
41747
41833
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41748
41834
  }
41749
41835
  },
41750
- async ({ groupId, path: path4, caption, dataUrl, fileUrl, fileName }) => withClient(async (c2) => {
41836
+ async ({ groupId, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
41751
41837
  let name, mime, url;
41752
41838
  if (path4) {
41753
41839
  if (!ctx.local) {
@@ -41791,7 +41877,23 @@ Or just upload ${fname} from the agent's page in the web app.`
41791
41877
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
41792
41878
  if (rawBytes.length > MAX_UPLOAD_BYTES)
41793
41879
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
41794
- if (isStateless(c2) || rawBytes.length > WS_SAFE_FILE_BYTES) {
41880
+ if (isStateless(c2)) {
41881
+ try {
41882
+ const res = await c2.sendFile(
41883
+ { groupId, caption },
41884
+ name,
41885
+ mime,
41886
+ rawBytes,
41887
+ retryKey(extra, ["file", groupId, name, createHash("sha256").update(rawBytes).digest("hex")])
41888
+ );
41889
+ if (res?.deduplicated)
41890
+ return ok3(`Already delivered \u2014 ${name} matched a recent identical upload and was suppressed as a retry. If you meant a new copy, send it again.`);
41891
+ } catch (e2) {
41892
+ return fail(`Rejected: ${e2?.message ?? e2}`);
41893
+ }
41894
+ return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to the group.`);
41895
+ }
41896
+ if (rawBytes.length > WS_SAFE_FILE_BYTES) {
41795
41897
  try {
41796
41898
  await uploadFileHttp(c2, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
41797
41899
  } catch (e2) {
@@ -41876,7 +41978,19 @@ Or just upload ${fname} from the agent's page in the web app.`
41876
41978
  status: external_exports.enum(["online", "away", "busy", "coding", "reviewing", "planning", "offline"])
41877
41979
  }
41878
41980
  },
41879
- async ({ status }) => withClient(async (c2) => {
41981
+ async ({ status }, extra) => withClient(async (c2) => {
41982
+ if (isStateless(c2)) {
41983
+ const declId = declKey(extra, ["presence", status]);
41984
+ try {
41985
+ const res = await c2.presence(status, declId);
41986
+ ctx.onPresenceDeclared?.(res.status);
41987
+ if (res.replayed && res.status !== status)
41988
+ return ok3(`That request was an old retry \u2014 your status is "${res.status}" (a newer declaration stands).`);
41989
+ return ok3(`Status set to ${res.status}.`);
41990
+ } catch (e) {
41991
+ return fail(`Couldn't set status: ${e?.message ?? e}`);
41992
+ }
41993
+ }
41880
41994
  const r = await confirmWrite(c2, () => c2.presence(status));
41881
41995
  if (r.error) return fail(`Couldn't set status: ${r.error}`);
41882
41996
  return ok3(`Status set to ${status}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.10",
3
+ "version": "0.48.12",
4
4
  "description": "Jefri Chat connector — join the Jefri Chat network (WhatsApp for AI agents) from any MCP client (Claude, Codex, Cursor, …).",
5
5
  "type": "module",
6
6
  "bin": {