jefrichat-mcp 0.48.11 → 0.48.13

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 +134 -81
  2. package/dist/index.js +134 -81
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -62102,9 +62102,65 @@ function retryKey(extra, parts) {
62102
62102
  if (rid === void 0 || rid === null) return void 0;
62103
62103
  const h = extra?.requestInfo?.headers;
62104
62104
  const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
62105
- const bucket = Math.floor(Date.now() / 6e5);
62105
+ if (!ua) return void 0;
62106
+ const bucket = Math.floor(Date.now() / 75e3);
62106
62107
  return "mcp-" + createHash2("sha256").update(JSON.stringify([bucket, ua, rid, ...parts])).digest("hex").slice(0, 40);
62107
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
+ }
62116
+ var crossBasename = (p) => String(p).split(/[\\/]/).pop() || "file";
62117
+ var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
62118
+ var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
62119
+ var psPathArg = (p) => p.startsWith("~/") || p.startsWith("~\\") ? `($HOME + ${psq(p.slice(1))})` : psq(p);
62120
+ var DATAURL_INLINE_MAX = 256 * 1024;
62121
+ var dataUrlTooBig = (dataUrl) => dataUrl.length > DATAURL_INLINE_MAX * 1.4 ? `That dataUrl is ~${MB(dataUrl.length * 0.75)} MB decoded \u2014 far past the ${Math.round(DATAURL_INLINE_MAX / 1024)}KB inline limit. NEVER read a local file and base64 it through the model. Call this tool again with the file's PATH \u2014 you'll get a single-use upload command that sends it in seconds.` : null;
62122
+ async function remoteUploadCommand(c, ctx, target, path3, caption, where) {
62123
+ const fname = crossBasename(path3);
62124
+ const fmime = mimeOf(fname);
62125
+ const hub = ctx.serverUrl.replace(/\/$/, "");
62126
+ try {
62127
+ const g = await fetch(`${hub}/api/files/grant`, {
62128
+ method: "POST",
62129
+ headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
62130
+ body: JSON.stringify({ ...target.to ? { to: target.to } : { groupId: target.groupId }, fileName: fname, ...caption ? { caption } : {} })
62131
+ });
62132
+ if (g.ok) {
62133
+ const { uploadUrl, expiresInMs } = await g.json();
62134
+ const mins = Math.round((expiresInMs ?? 9e5) / 6e4);
62135
+ return {
62136
+ ok: `This cloud connector can't read your disk \u2014 but here is the fast path. Run this ONE command in the terminal (it uploads AND delivers the file to ${where}):
62137
+
62138
+ curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path3)}
62139
+
62140
+ Windows PowerShell instead:
62141
+ Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path3)}
62142
+
62143
+ The link is single-use and expires in ${mins} min; the file name and caption are already attached to it. Run it now, then confirm delivery from its JSON response ({"ok":true,...}). Do NOT read the file and base64 it through dataUrl \u2014 that is minutes of model output and fails over ~3MB.`
62144
+ };
62145
+ }
62146
+ } catch {
62147
+ }
62148
+ const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
62149
+ const dropLink = target.to ? `${web}/?to=${encodeURIComponent(target.to)}` : `${web}/?group=${encodeURIComponent(target.groupId)}`;
62150
+ const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
62151
+ const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path3)}`;
62152
+ return {
62153
+ fail: `This is the cloud connector, so it can't read "${fname}" off your computer, and the upload-link service didn't answer. Options:
62154
+
62155
+ \u2705 open the Jefri Chat web app and DRAG "${fname}" into the chat with ${where}:
62156
+ ${dropLink}
62157
+
62158
+ \u2014 or from a terminal (set JEFRI_TOKEN from the app's Connect dialog first):
62159
+ ${cmd}
62160
+
62161
+ Do NOT base64 the file through dataUrl \u2014 that streams megabytes through the model and fails over ~3MB.`
62162
+ };
62163
+ }
62108
62164
  async function fetchHistory(c, convId) {
62109
62165
  if (isStateless(c)) return c.history(convId);
62110
62166
  const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
@@ -62564,10 +62620,17 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62564
62620
  }
62565
62621
  },
62566
62622
  async ({ to, text: text5 }, extra) => withClient(async (c) => {
62567
- const r = await confirmWrite(
62568
- c,
62569
- () => isStateless(c) ? c.message(to, text5, retryKey(extra, ["message", to, text5])) : c.message(to, text5)
62570
- );
62623
+ if (isStateless(c)) {
62624
+ try {
62625
+ const res = await c.message(to, text5, retryKey(extra, ["message", to, text5]));
62626
+ if (res?.deduplicated)
62627
+ 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).`);
62628
+ return ok3(`Sent to @${to}: "${text5}"`);
62629
+ } catch (e) {
62630
+ return fail(`Message rejected: ${e?.message ?? e}`);
62631
+ }
62632
+ }
62633
+ const r = await confirmWrite(c, () => c.message(to, text5));
62571
62634
  if (r.error) return fail(`Message rejected: ${r.error}`);
62572
62635
  return ok3(`Sent to @${to}: "${text5}"`);
62573
62636
  })
@@ -62714,12 +62777,12 @@ ${fp}`);
62714
62777
  "jefri_send_file",
62715
62778
  {
62716
62779
  title: "Send a file, PDF, or image",
62717
- description: 'Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. Give a local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption`.\n\nHOW IT WORKS: this reads the file where the connector runs. If you are connected via the LOCAL (stdio) connector, a `path` is read straight off this machine. If you are connected via the REMOTE/cloud connector (URL), this process runs in the cloud and CANNOT see your local disk \u2014 so for a local file, run the helper in your terminal instead (it reads + uploads the file locally):\n pnpm send-file --to <username> --path <absolute file path> [--caption "..."]\n(run from the acp repo; token + hub are auto-detected from your Claude config). Tiny inline content can be sent directly via `dataUrl` + `fileName`. If you only have a public URL for the file (e.g. an image you generated), pass it as `fileUrl` and the connector fetches the bytes and sends the real file.',
62780
+ description: "Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. ALWAYS give the local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption` \u2014 even on the remote/cloud connector: there this tool answers with ONE ready-to-run terminal command (a single-use upload link) that sends the file in seconds. NEVER read the file yourself and base64 it into `dataUrl` \u2014 that streams megabytes through the model, takes minutes, and fails over ~3MB; `dataUrl` is ONLY for tiny content you already hold inline (small generated snippets). If you only have a public URL for the file, pass `fileUrl` and the connector fetches the bytes itself.",
62718
62781
  inputSchema: {
62719
62782
  to: external_exports.string().describe("recipient username"),
62720
62783
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
62721
62784
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
62722
- dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
62785
+ dataUrl: external_exports.string().optional().describe("ONLY for tiny inline content you already hold (max 256KB decoded). NEVER construct this by reading a local file \u2014 pass `path` instead."),
62723
62786
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
62724
62787
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
62725
62788
  }
@@ -62728,31 +62791,8 @@ ${fp}`);
62728
62791
  let name, mime, url;
62729
62792
  if (path3) {
62730
62793
  if (!ctx.local) {
62731
- const fname = np3.basename(path3);
62732
- const fmime = mimeOf(fname);
62733
- const hub = ctx.serverUrl.replace(/\/$/, "");
62734
- const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
62735
- const dropLink = `${web}/?to=${encodeURIComponent(to)}`;
62736
- const q = `to=${encodeURIComponent(to)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
62737
- const uploadUrl = `${hub}/api/files?${q}`;
62738
- const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
62739
- const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer $env:JEFRI_TOKEN" } -ContentType "${fmime}" -InFile "${path3}"`;
62740
- return fail(
62741
- `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
62742
-
62743
- \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
62744
- ${dropLink}
62745
-
62746
- (If instead this is an image you GENERATED and have a public URL for, just call this tool again with \`fileUrl\` instead of \`path\` \u2014 the image sends with no drag.)
62747
-
62748
- \u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
62749
- \u2022 macOS / Linux:
62750
- ${cmd}
62751
- \u2022 Windows (PowerShell):
62752
- ${psCmd}
62753
-
62754
- (These read your token from the JEFRI_TOKEN env var \u2014 set it first from the app's Connect dialog: export JEFRI_TOKEN=\u2026 (macOS/Linux) or $env:JEFRI_TOKEN="\u2026" (PowerShell). The token isn't printed here on purpose.)`
62755
- );
62794
+ const r = await remoteUploadCommand(c, ctx, { to }, path3, caption, `@${to}`);
62795
+ return "ok" in r ? ok3(r.ok) : fail(r.fail);
62756
62796
  }
62757
62797
  const abs = resolveLocalFile(path3);
62758
62798
  if (!abs) {
@@ -62763,6 +62803,8 @@ ${psCmd}
62763
62803
  mime = mimeOf(name);
62764
62804
  url = `data:${mime};base64,${buf.toString("base64")}`;
62765
62805
  } else if (dataUrl && fileName) {
62806
+ const tooBig = dataUrlTooBig(dataUrl);
62807
+ if (tooBig) return fail(tooBig);
62766
62808
  name = fileName;
62767
62809
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
62768
62810
  url = dataUrl;
@@ -62780,13 +62822,15 @@ ${psCmd}
62780
62822
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62781
62823
  if (isStateless(c)) {
62782
62824
  try {
62783
- await c.sendFile(
62825
+ const res = await c.sendFile(
62784
62826
  { to, caption },
62785
62827
  name,
62786
62828
  mime,
62787
62829
  rawBytes,
62788
62830
  retryKey(extra, ["file", to, name, createHash2("sha256").update(rawBytes).digest("hex")])
62789
62831
  );
62832
+ if (res?.deduplicated)
62833
+ 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.`);
62790
62834
  } catch (e2) {
62791
62835
  return fail(`Rejected: ${e2?.message ?? e2}`);
62792
62836
  }
@@ -62828,13 +62872,15 @@ ${psCmd}
62828
62872
  return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62829
62873
  if (isStateless(c)) {
62830
62874
  try {
62831
- await c.sendFile(
62875
+ const res = await c.sendFile(
62832
62876
  { to, caption },
62833
62877
  fileName,
62834
62878
  mime,
62835
62879
  contentBytes,
62836
62880
  retryKey(extra, ["file", to, fileName, createHash2("sha256").update(contentBytes).digest("hex")])
62837
62881
  );
62882
+ if (res?.deduplicated)
62883
+ 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.`);
62838
62884
  } catch (e2) {
62839
62885
  return fail(`Rejected: ${e2?.message ?? e2}`);
62840
62886
  }
@@ -63244,25 +63290,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63244
63290
  ctx.inbox.length = 0;
63245
63291
  }
63246
63292
  if (!msgs.length) return ok3("\u{1F4ED} No new messages.");
63247
- msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
63248
- if (ctx.stateless) {
63249
- const last = msgs[msgs.length - 1];
63250
- const ack = async () => {
63251
- try {
63252
- await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
63253
- method: "POST",
63254
- headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
63255
- body: JSON.stringify({ until: last.createdAt, untilId: last.id })
63256
- });
63257
- } catch {
63258
- }
63259
- };
63260
- if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
63261
- else await ack();
63262
- } else {
63263
- inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
63264
- saveWatermark();
63265
- }
63293
+ msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : a.id < b.id ? -1 : 1);
63266
63294
  const groupNames = /* @__PURE__ */ new Map();
63267
63295
  if (msgs.some((m) => m.groupId)) {
63268
63296
  try {
@@ -63290,8 +63318,27 @@ Or just upload ${fname} from the agent's page in the web app.`
63290
63318
  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;
63291
63319
  return `${at}@${m.senderUsername}${where(m)}: ${body}`;
63292
63320
  }));
63293
- return ok3(`\u{1F4E8} ${lines.length} message(s):
63294
- ` + lines.join("\n"));
63321
+ const rendered = `\u{1F4E8} ${lines.length} message(s):
63322
+ ` + lines.join("\n");
63323
+ if (ctx.stateless) {
63324
+ const renderedIds = msgs.map((m) => m.id);
63325
+ const ack = async () => {
63326
+ try {
63327
+ await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
63328
+ method: "POST",
63329
+ headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
63330
+ body: JSON.stringify({ messageIds: renderedIds })
63331
+ });
63332
+ } catch {
63333
+ }
63334
+ };
63335
+ if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
63336
+ else await ack();
63337
+ } else {
63338
+ inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
63339
+ saveWatermark();
63340
+ }
63341
+ return ok3(rendered);
63295
63342
  })
63296
63343
  );
63297
63344
  server2.registerTool(
@@ -63321,10 +63368,17 @@ Or just upload ${fname} from the agent's page in the web app.`
63321
63368
  }
63322
63369
  },
63323
63370
  async ({ groupId, text: text5 }, extra) => withClient(async (c) => {
63324
- const r = await confirmWrite(
63325
- c,
63326
- () => isStateless(c) ? c.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5])) : c.groupMessage(groupId, text5)
63327
- );
63371
+ if (isStateless(c)) {
63372
+ try {
63373
+ const res = await c.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5]));
63374
+ if (res?.deduplicated)
63375
+ 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.`);
63376
+ return ok3(`Posted to the group.`);
63377
+ } catch (e) {
63378
+ return fail(`Couldn't post to the group: ${e?.message ?? e}`);
63379
+ }
63380
+ }
63381
+ const r = await confirmWrite(c, () => c.groupMessage(groupId, text5));
63328
63382
  if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
63329
63383
  return ok3(`Posted to the group.`);
63330
63384
  })
@@ -63338,7 +63392,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63338
63392
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
63339
63393
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
63340
63394
  caption: external_exports.string().optional().describe("optional text caption"),
63341
- dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
63395
+ dataUrl: external_exports.string().optional().describe("ONLY for tiny inline content you already hold (max 256KB decoded). NEVER construct this by reading a local file \u2014 pass `path` instead."),
63342
63396
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
63343
63397
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
63344
63398
  }
@@ -63347,23 +63401,8 @@ Or just upload ${fname} from the agent's page in the web app.`
63347
63401
  let name, mime, url;
63348
63402
  if (path3) {
63349
63403
  if (!ctx.local) {
63350
- const hub = ctx.serverUrl.replace(/\/$/, "");
63351
- const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
63352
- const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
63353
- const fname = np3.basename(path3);
63354
- const fmime = mimeOf(fname);
63355
- const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
63356
- const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
63357
- return fail(
63358
- `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
63359
-
63360
- \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
63361
- ${dropLink}
63362
-
63363
- (If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
63364
-
63365
- \u2014 Advanced (terminal): ${cmd}`
63366
- );
63404
+ const r = await remoteUploadCommand(c, ctx, { groupId }, path3, caption, "the group");
63405
+ return "ok" in r ? ok3(r.ok) : fail(r.fail);
63367
63406
  }
63368
63407
  const abs = resolveLocalFile(path3);
63369
63408
  if (!abs) return fail(`I can't find a file at ${path3} on this machine.`);
@@ -63372,6 +63411,8 @@ Or just upload ${fname} from the agent's page in the web app.`
63372
63411
  mime = mimeOf(name);
63373
63412
  url = `data:${mime};base64,${buf.toString("base64")}`;
63374
63413
  } else if (dataUrl && fileName) {
63414
+ const tooBig = dataUrlTooBig(dataUrl);
63415
+ if (tooBig) return fail(tooBig);
63375
63416
  name = fileName;
63376
63417
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
63377
63418
  url = dataUrl;
@@ -63389,13 +63430,15 @@ Or just upload ${fname} from the agent's page in the web app.`
63389
63430
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
63390
63431
  if (isStateless(c)) {
63391
63432
  try {
63392
- await c.sendFile(
63433
+ const res = await c.sendFile(
63393
63434
  { groupId, caption },
63394
63435
  name,
63395
63436
  mime,
63396
63437
  rawBytes,
63397
63438
  retryKey(extra, ["file", groupId, name, createHash2("sha256").update(rawBytes).digest("hex")])
63398
63439
  );
63440
+ if (res?.deduplicated)
63441
+ 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.`);
63399
63442
  } catch (e2) {
63400
63443
  return fail(`Rejected: ${e2?.message ?? e2}`);
63401
63444
  }
@@ -63487,10 +63530,20 @@ Or just upload ${fname} from the agent's page in the web app.`
63487
63530
  }
63488
63531
  },
63489
63532
  async ({ status }, extra) => withClient(async (c) => {
63490
- const declId = isStateless(c) ? retryKey(extra, ["presence", status])?.replace(/^mcp-/, "d-") : void 0;
63491
- const r = await confirmWrite(c, () => isStateless(c) ? c.presence(status, declId) : c.presence(status));
63533
+ if (isStateless(c)) {
63534
+ const declId = declKey(extra, ["presence", status]);
63535
+ try {
63536
+ const res = await c.presence(status, declId);
63537
+ ctx.onPresenceDeclared?.(res.status);
63538
+ if (res.replayed && res.status !== status)
63539
+ return ok3(`That request was an old retry \u2014 your status is "${res.status}" (a newer declaration stands).`);
63540
+ return ok3(`Status set to ${res.status}.`);
63541
+ } catch (e) {
63542
+ return fail(`Couldn't set status: ${e?.message ?? e}`);
63543
+ }
63544
+ }
63545
+ const r = await confirmWrite(c, () => c.presence(status));
63492
63546
  if (r.error) return fail(`Couldn't set status: ${r.error}`);
63493
- ctx.onPresenceDeclared?.(status);
63494
63547
  return ok3(`Status set to ${status}.`);
63495
63548
  })
63496
63549
  );
package/dist/index.js CHANGED
@@ -40562,9 +40562,65 @@ function retryKey(extra, parts) {
40562
40562
  if (rid === void 0 || rid === null) return void 0;
40563
40563
  const h = extra?.requestInfo?.headers;
40564
40564
  const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
40565
- const bucket = Math.floor(Date.now() / 6e5);
40565
+ if (!ua) return void 0;
40566
+ const bucket = Math.floor(Date.now() / 75e3);
40566
40567
  return "mcp-" + createHash("sha256").update(JSON.stringify([bucket, ua, rid, ...parts])).digest("hex").slice(0, 40);
40567
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
+ }
40576
+ var crossBasename = (p) => String(p).split(/[\\/]/).pop() || "file";
40577
+ var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
40578
+ var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
40579
+ var psPathArg = (p) => p.startsWith("~/") || p.startsWith("~\\") ? `($HOME + ${psq(p.slice(1))})` : psq(p);
40580
+ var DATAURL_INLINE_MAX = 256 * 1024;
40581
+ var dataUrlTooBig = (dataUrl) => dataUrl.length > DATAURL_INLINE_MAX * 1.4 ? `That dataUrl is ~${MB(dataUrl.length * 0.75)} MB decoded \u2014 far past the ${Math.round(DATAURL_INLINE_MAX / 1024)}KB inline limit. NEVER read a local file and base64 it through the model. Call this tool again with the file's PATH \u2014 you'll get a single-use upload command that sends it in seconds.` : null;
40582
+ async function remoteUploadCommand(c2, ctx, target, path4, caption, where) {
40583
+ const fname = crossBasename(path4);
40584
+ const fmime = mimeOf(fname);
40585
+ const hub = ctx.serverUrl.replace(/\/$/, "");
40586
+ try {
40587
+ const g = await fetch(`${hub}/api/files/grant`, {
40588
+ method: "POST",
40589
+ headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
40590
+ body: JSON.stringify({ ...target.to ? { to: target.to } : { groupId: target.groupId }, fileName: fname, ...caption ? { caption } : {} })
40591
+ });
40592
+ if (g.ok) {
40593
+ const { uploadUrl, expiresInMs } = await g.json();
40594
+ const mins = Math.round((expiresInMs ?? 9e5) / 6e4);
40595
+ return {
40596
+ ok: `This cloud connector can't read your disk \u2014 but here is the fast path. Run this ONE command in the terminal (it uploads AND delivers the file to ${where}):
40597
+
40598
+ curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path4)}
40599
+
40600
+ Windows PowerShell instead:
40601
+ Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path4)}
40602
+
40603
+ The link is single-use and expires in ${mins} min; the file name and caption are already attached to it. Run it now, then confirm delivery from its JSON response ({"ok":true,...}). Do NOT read the file and base64 it through dataUrl \u2014 that is minutes of model output and fails over ~3MB.`
40604
+ };
40605
+ }
40606
+ } catch {
40607
+ }
40608
+ const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
40609
+ const dropLink = target.to ? `${web}/?to=${encodeURIComponent(target.to)}` : `${web}/?group=${encodeURIComponent(target.groupId)}`;
40610
+ const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
40611
+ const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path4)}`;
40612
+ return {
40613
+ fail: `This is the cloud connector, so it can't read "${fname}" off your computer, and the upload-link service didn't answer. Options:
40614
+
40615
+ \u2705 open the Jefri Chat web app and DRAG "${fname}" into the chat with ${where}:
40616
+ ${dropLink}
40617
+
40618
+ \u2014 or from a terminal (set JEFRI_TOKEN from the app's Connect dialog first):
40619
+ ${cmd}
40620
+
40621
+ Do NOT base64 the file through dataUrl \u2014 that streams megabytes through the model and fails over ~3MB.`
40622
+ };
40623
+ }
40568
40624
  async function fetchHistory2(c2, convId) {
40569
40625
  if (isStateless(c2)) return c2.history(convId);
40570
40626
  const p = waitFor(c2, "history", (e) => e.conversationId === convId, 4e3);
@@ -41027,10 +41083,17 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
41027
41083
  }
41028
41084
  },
41029
41085
  async ({ to, text: text5 }, extra) => withClient(async (c2) => {
41030
- const r = await confirmWrite(
41031
- c2,
41032
- () => isStateless(c2) ? c2.message(to, text5, retryKey(extra, ["message", to, text5])) : c2.message(to, text5)
41033
- );
41086
+ if (isStateless(c2)) {
41087
+ try {
41088
+ const res = await c2.message(to, text5, retryKey(extra, ["message", to, text5]));
41089
+ if (res?.deduplicated)
41090
+ 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).`);
41091
+ return ok3(`Sent to @${to}: "${text5}"`);
41092
+ } catch (e) {
41093
+ return fail(`Message rejected: ${e?.message ?? e}`);
41094
+ }
41095
+ }
41096
+ const r = await confirmWrite(c2, () => c2.message(to, text5));
41034
41097
  if (r.error) return fail(`Message rejected: ${r.error}`);
41035
41098
  return ok3(`Sent to @${to}: "${text5}"`);
41036
41099
  })
@@ -41177,12 +41240,12 @@ ${fp}`);
41177
41240
  "jefri_send_file",
41178
41241
  {
41179
41242
  title: "Send a file, PDF, or image",
41180
- description: 'Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. Give a local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption`.\n\nHOW IT WORKS: this reads the file where the connector runs. If you are connected via the LOCAL (stdio) connector, a `path` is read straight off this machine. If you are connected via the REMOTE/cloud connector (URL), this process runs in the cloud and CANNOT see your local disk \u2014 so for a local file, run the helper in your terminal instead (it reads + uploads the file locally):\n pnpm send-file --to <username> --path <absolute file path> [--caption "..."]\n(run from the acp repo; token + hub are auto-detected from your Claude config). Tiny inline content can be sent directly via `dataUrl` + `fileName`. If you only have a public URL for the file (e.g. an image you generated), pass it as `fileUrl` and the connector fetches the bytes and sends the real file.',
41243
+ description: "Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. ALWAYS give the local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption` \u2014 even on the remote/cloud connector: there this tool answers with ONE ready-to-run terminal command (a single-use upload link) that sends the file in seconds. NEVER read the file yourself and base64 it into `dataUrl` \u2014 that streams megabytes through the model, takes minutes, and fails over ~3MB; `dataUrl` is ONLY for tiny content you already hold inline (small generated snippets). If you only have a public URL for the file, pass `fileUrl` and the connector fetches the bytes itself.",
41181
41244
  inputSchema: {
41182
41245
  to: external_exports.string().describe("recipient username"),
41183
41246
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
41184
41247
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
41185
- dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
41248
+ dataUrl: external_exports.string().optional().describe("ONLY for tiny inline content you already hold (max 256KB decoded). NEVER construct this by reading a local file \u2014 pass `path` instead."),
41186
41249
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
41187
41250
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41188
41251
  }
@@ -41191,31 +41254,8 @@ ${fp}`);
41191
41254
  let name, mime, url;
41192
41255
  if (path4) {
41193
41256
  if (!ctx.local) {
41194
- const fname = np4.basename(path4);
41195
- const fmime = mimeOf(fname);
41196
- const hub = ctx.serverUrl.replace(/\/$/, "");
41197
- const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
41198
- const dropLink = `${web}/?to=${encodeURIComponent(to)}`;
41199
- const q = `to=${encodeURIComponent(to)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
41200
- const uploadUrl = `${hub}/api/files?${q}`;
41201
- const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${fmime}" --data-binary @${shq(path4)}`;
41202
- const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer $env:JEFRI_TOKEN" } -ContentType "${fmime}" -InFile "${path4}"`;
41203
- return fail(
41204
- `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
41205
-
41206
- \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
41207
- ${dropLink}
41208
-
41209
- (If instead this is an image you GENERATED and have a public URL for, just call this tool again with \`fileUrl\` instead of \`path\` \u2014 the image sends with no drag.)
41210
-
41211
- \u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
41212
- \u2022 macOS / Linux:
41213
- ${cmd}
41214
- \u2022 Windows (PowerShell):
41215
- ${psCmd}
41216
-
41217
- (These read your token from the JEFRI_TOKEN env var \u2014 set it first from the app's Connect dialog: export JEFRI_TOKEN=\u2026 (macOS/Linux) or $env:JEFRI_TOKEN="\u2026" (PowerShell). The token isn't printed here on purpose.)`
41218
- );
41257
+ const r = await remoteUploadCommand(c2, ctx, { to }, path4, caption, `@${to}`);
41258
+ return "ok" in r ? ok3(r.ok) : fail(r.fail);
41219
41259
  }
41220
41260
  const abs = resolveLocalFile(path4);
41221
41261
  if (!abs) {
@@ -41226,6 +41266,8 @@ ${psCmd}
41226
41266
  mime = mimeOf(name);
41227
41267
  url = `data:${mime};base64,${buf.toString("base64")}`;
41228
41268
  } else if (dataUrl && fileName) {
41269
+ const tooBig = dataUrlTooBig(dataUrl);
41270
+ if (tooBig) return fail(tooBig);
41229
41271
  name = fileName;
41230
41272
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
41231
41273
  url = dataUrl;
@@ -41243,13 +41285,15 @@ ${psCmd}
41243
41285
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
41244
41286
  if (isStateless(c2)) {
41245
41287
  try {
41246
- await c2.sendFile(
41288
+ const res = await c2.sendFile(
41247
41289
  { to, caption },
41248
41290
  name,
41249
41291
  mime,
41250
41292
  rawBytes,
41251
41293
  retryKey(extra, ["file", to, name, createHash("sha256").update(rawBytes).digest("hex")])
41252
41294
  );
41295
+ if (res?.deduplicated)
41296
+ 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.`);
41253
41297
  } catch (e2) {
41254
41298
  return fail(`Rejected: ${e2?.message ?? e2}`);
41255
41299
  }
@@ -41291,13 +41335,15 @@ ${psCmd}
41291
41335
  return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
41292
41336
  if (isStateless(c2)) {
41293
41337
  try {
41294
- await c2.sendFile(
41338
+ const res = await c2.sendFile(
41295
41339
  { to, caption },
41296
41340
  fileName,
41297
41341
  mime,
41298
41342
  contentBytes,
41299
41343
  retryKey(extra, ["file", to, fileName, createHash("sha256").update(contentBytes).digest("hex")])
41300
41344
  );
41345
+ if (res?.deduplicated)
41346
+ 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.`);
41301
41347
  } catch (e2) {
41302
41348
  return fail(`Rejected: ${e2?.message ?? e2}`);
41303
41349
  }
@@ -41707,25 +41753,7 @@ Or just upload ${fname} from the agent's page in the web app.`
41707
41753
  ctx.inbox.length = 0;
41708
41754
  }
41709
41755
  if (!msgs.length) return ok3("\u{1F4ED} No new messages.");
41710
- msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
41711
- if (ctx.stateless) {
41712
- const last = msgs[msgs.length - 1];
41713
- const ack = async () => {
41714
- try {
41715
- await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
41716
- method: "POST",
41717
- headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
41718
- body: JSON.stringify({ until: last.createdAt, untilId: last.id })
41719
- });
41720
- } catch {
41721
- }
41722
- };
41723
- if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
41724
- else await ack();
41725
- } else {
41726
- inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
41727
- saveWatermark();
41728
- }
41756
+ msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : a.id < b.id ? -1 : 1);
41729
41757
  const groupNames = /* @__PURE__ */ new Map();
41730
41758
  if (msgs.some((m) => m.groupId)) {
41731
41759
  try {
@@ -41753,8 +41781,27 @@ Or just upload ${fname} from the agent's page in the web app.`
41753
41781
  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;
41754
41782
  return `${at}@${m.senderUsername}${where(m)}: ${body}`;
41755
41783
  }));
41756
- return ok3(`\u{1F4E8} ${lines.length} message(s):
41757
- ` + lines.join("\n"));
41784
+ const rendered = `\u{1F4E8} ${lines.length} message(s):
41785
+ ` + lines.join("\n");
41786
+ if (ctx.stateless) {
41787
+ const renderedIds = msgs.map((m) => m.id);
41788
+ const ack = async () => {
41789
+ try {
41790
+ await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
41791
+ method: "POST",
41792
+ headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
41793
+ body: JSON.stringify({ messageIds: renderedIds })
41794
+ });
41795
+ } catch {
41796
+ }
41797
+ };
41798
+ if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
41799
+ else await ack();
41800
+ } else {
41801
+ inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
41802
+ saveWatermark();
41803
+ }
41804
+ return ok3(rendered);
41758
41805
  })
41759
41806
  );
41760
41807
  server2.registerTool(
@@ -41784,10 +41831,17 @@ Or just upload ${fname} from the agent's page in the web app.`
41784
41831
  }
41785
41832
  },
41786
41833
  async ({ groupId, text: text5 }, extra) => withClient(async (c2) => {
41787
- const r = await confirmWrite(
41788
- c2,
41789
- () => isStateless(c2) ? c2.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5])) : c2.groupMessage(groupId, text5)
41790
- );
41834
+ if (isStateless(c2)) {
41835
+ try {
41836
+ const res = await c2.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5]));
41837
+ if (res?.deduplicated)
41838
+ 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.`);
41839
+ return ok3(`Posted to the group.`);
41840
+ } catch (e) {
41841
+ return fail(`Couldn't post to the group: ${e?.message ?? e}`);
41842
+ }
41843
+ }
41844
+ const r = await confirmWrite(c2, () => c2.groupMessage(groupId, text5));
41791
41845
  if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
41792
41846
  return ok3(`Posted to the group.`);
41793
41847
  })
@@ -41801,7 +41855,7 @@ Or just upload ${fname} from the agent's page in the web app.`
41801
41855
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
41802
41856
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
41803
41857
  caption: external_exports.string().optional().describe("optional text caption"),
41804
- dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
41858
+ dataUrl: external_exports.string().optional().describe("ONLY for tiny inline content you already hold (max 256KB decoded). NEVER construct this by reading a local file \u2014 pass `path` instead."),
41805
41859
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
41806
41860
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41807
41861
  }
@@ -41810,23 +41864,8 @@ Or just upload ${fname} from the agent's page in the web app.`
41810
41864
  let name, mime, url;
41811
41865
  if (path4) {
41812
41866
  if (!ctx.local) {
41813
- const hub = ctx.serverUrl.replace(/\/$/, "");
41814
- const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
41815
- const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
41816
- const fname = np4.basename(path4);
41817
- const fmime = mimeOf(fname);
41818
- const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
41819
- const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${fmime}" --data-binary @${shq(path4)}`;
41820
- return fail(
41821
- `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
41822
-
41823
- \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
41824
- ${dropLink}
41825
-
41826
- (If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
41827
-
41828
- \u2014 Advanced (terminal): ${cmd}`
41829
- );
41867
+ const r = await remoteUploadCommand(c2, ctx, { groupId }, path4, caption, "the group");
41868
+ return "ok" in r ? ok3(r.ok) : fail(r.fail);
41830
41869
  }
41831
41870
  const abs = resolveLocalFile(path4);
41832
41871
  if (!abs) return fail(`I can't find a file at ${path4} on this machine.`);
@@ -41835,6 +41874,8 @@ Or just upload ${fname} from the agent's page in the web app.`
41835
41874
  mime = mimeOf(name);
41836
41875
  url = `data:${mime};base64,${buf.toString("base64")}`;
41837
41876
  } else if (dataUrl && fileName) {
41877
+ const tooBig = dataUrlTooBig(dataUrl);
41878
+ if (tooBig) return fail(tooBig);
41838
41879
  name = fileName;
41839
41880
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
41840
41881
  url = dataUrl;
@@ -41852,13 +41893,15 @@ Or just upload ${fname} from the agent's page in the web app.`
41852
41893
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
41853
41894
  if (isStateless(c2)) {
41854
41895
  try {
41855
- await c2.sendFile(
41896
+ const res = await c2.sendFile(
41856
41897
  { groupId, caption },
41857
41898
  name,
41858
41899
  mime,
41859
41900
  rawBytes,
41860
41901
  retryKey(extra, ["file", groupId, name, createHash("sha256").update(rawBytes).digest("hex")])
41861
41902
  );
41903
+ if (res?.deduplicated)
41904
+ 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.`);
41862
41905
  } catch (e2) {
41863
41906
  return fail(`Rejected: ${e2?.message ?? e2}`);
41864
41907
  }
@@ -41950,10 +41993,20 @@ Or just upload ${fname} from the agent's page in the web app.`
41950
41993
  }
41951
41994
  },
41952
41995
  async ({ status }, extra) => withClient(async (c2) => {
41953
- const declId = isStateless(c2) ? retryKey(extra, ["presence", status])?.replace(/^mcp-/, "d-") : void 0;
41954
- const r = await confirmWrite(c2, () => isStateless(c2) ? c2.presence(status, declId) : c2.presence(status));
41996
+ if (isStateless(c2)) {
41997
+ const declId = declKey(extra, ["presence", status]);
41998
+ try {
41999
+ const res = await c2.presence(status, declId);
42000
+ ctx.onPresenceDeclared?.(res.status);
42001
+ if (res.replayed && res.status !== status)
42002
+ return ok3(`That request was an old retry \u2014 your status is "${res.status}" (a newer declaration stands).`);
42003
+ return ok3(`Status set to ${res.status}.`);
42004
+ } catch (e) {
42005
+ return fail(`Couldn't set status: ${e?.message ?? e}`);
42006
+ }
42007
+ }
42008
+ const r = await confirmWrite(c2, () => c2.presence(status));
41955
42009
  if (r.error) return fail(`Couldn't set status: ${r.error}`);
41956
- ctx.onPresenceDeclared?.(status);
41957
42010
  return ok3(`Status set to ${status}.`);
41958
42011
  })
41959
42012
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.11",
3
+ "version": "0.48.13",
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": {