jefrichat-mcp 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/http.js +116 -17
  2. package/dist/index.js +117 -18
  3. package/package.json +10 -11
package/dist/http.js CHANGED
@@ -49099,7 +49099,8 @@ var JefriClient = class _JefriClient {
49099
49099
  }
49100
49100
  scheduleReconnect() {
49101
49101
  if (this.closing || this.reconnectTimer || this.authFailed) return;
49102
- const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
49102
+ const cap = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
49103
+ const delay = Math.floor(cap / 2 + Math.random() * (cap / 2));
49103
49104
  this.reconnectAttempts++;
49104
49105
  this.reconnectTimer = setTimeout(() => {
49105
49106
  this.reconnectTimer = void 0;
@@ -49422,6 +49423,8 @@ import fs4 from "node:fs";
49422
49423
  import np3 from "node:path";
49423
49424
  import cp2 from "node:child_process";
49424
49425
  import os4 from "node:os";
49426
+ import nnet from "node:net";
49427
+ import ndns from "node:dns/promises";
49425
49428
 
49426
49429
  // src/notify.ts
49427
49430
  import cp from "node:child_process";
@@ -49611,6 +49614,78 @@ function resolveLocalFile(rawPath) {
49611
49614
  var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
49612
49615
  var MB = (n) => (n / 1024 / 1024).toFixed(1);
49613
49616
  var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
49617
+ function isPrivateAddress(ip) {
49618
+ if (nnet.isIPv4(ip)) {
49619
+ const [a, b] = ip.split(".").map(Number);
49620
+ if (a === 0 || a === 10 || a === 127) return true;
49621
+ if (a === 169 && b === 254) return true;
49622
+ if (a === 172 && b >= 16 && b <= 31) return true;
49623
+ if (a === 192 && b === 168) return true;
49624
+ if (a === 100 && b >= 64 && b <= 127) return true;
49625
+ return false;
49626
+ }
49627
+ const lc = ip.toLowerCase();
49628
+ if (lc === "::1" || lc === "::") return true;
49629
+ if (lc.startsWith("::ffff:")) return isPrivateAddress(lc.slice(7));
49630
+ if (lc.startsWith("fe80") || lc.startsWith("fc") || lc.startsWith("fd")) return true;
49631
+ return false;
49632
+ }
49633
+ var extFromMime = (mime) => {
49634
+ const sub = (mime.split("/")[1] || "").toLowerCase();
49635
+ if (sub === "jpeg") return "jpg";
49636
+ if (sub === "svg+xml") return "svg";
49637
+ return sub.replace(/[^a-z0-9]/g, "").slice(0, 5) || "bin";
49638
+ };
49639
+ async function assertPublicUrl(raw) {
49640
+ let u;
49641
+ try {
49642
+ u = new URL(raw);
49643
+ } catch {
49644
+ throw new Error(`not a valid URL: ${raw}`);
49645
+ }
49646
+ if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("only http(s) URLs can be fetched");
49647
+ let addr;
49648
+ try {
49649
+ addr = (await ndns.lookup(u.hostname)).address;
49650
+ } catch {
49651
+ throw new Error(`can't resolve host ${u.hostname}`);
49652
+ }
49653
+ if (isPrivateAddress(addr)) throw new Error("refusing to fetch a private/internal address");
49654
+ return u;
49655
+ }
49656
+ async function fetchRemoteFile(fileUrl, fileNameHint, maxBytes) {
49657
+ let current = await assertPublicUrl(fileUrl);
49658
+ let res = null;
49659
+ for (let hop = 0; hop < 4; hop++) {
49660
+ const ac = new AbortController();
49661
+ const timer = setTimeout(() => ac.abort(), 2e4);
49662
+ try {
49663
+ res = await fetch(current, { redirect: "manual", signal: ac.signal, headers: { "user-agent": "jefrichat-mcp" } });
49664
+ } finally {
49665
+ clearTimeout(timer);
49666
+ }
49667
+ if (res.status >= 300 && res.status < 400) {
49668
+ const loc = res.headers.get("location");
49669
+ if (!loc) throw new Error(`redirect with no location (${res.status})`);
49670
+ current = await assertPublicUrl(new URL(loc, current).toString());
49671
+ continue;
49672
+ }
49673
+ break;
49674
+ }
49675
+ if (!res || !res.ok) throw new Error(`fetch failed (${res?.status ?? "no response"})`);
49676
+ const clen = Number(res.headers.get("content-length") || 0);
49677
+ if (clen && clen > maxBytes) throw new Error(`file is ${MB(clen)} MB, over the ${MB(maxBytes)} MB limit`);
49678
+ const buf = Buffer.from(await res.arrayBuffer());
49679
+ if (buf.length > maxBytes) throw new Error(`file is ${MB(buf.length)} MB, over the ${MB(maxBytes)} MB limit`);
49680
+ if (buf.length === 0) throw new Error("the URL returned an empty file");
49681
+ const mime = (res.headers.get("content-type") || "").split(";")[0].trim() || mimeOf(fileNameHint || current.pathname);
49682
+ let name = fileNameHint || np3.basename(current.pathname) || "";
49683
+ if (!name || name === "/" || !name.includes(".")) {
49684
+ const base = name && name !== "/" ? name : "file";
49685
+ name = `${base}.${extFromMime(mime)}`;
49686
+ }
49687
+ return { name, mime, url: `data:${mime};base64,${buf.toString("base64")}` };
49688
+ }
49614
49689
  async function uploadFileHttp(c, serverUrl, target, fileName, mime, bytes, caption) {
49615
49690
  const hub = serverUrl.replace(/\/$/, "");
49616
49691
  const params = new URLSearchParams({ fileName });
@@ -49863,16 +49938,17 @@ ${fp}`);
49863
49938
  "jefri_send_file",
49864
49939
  {
49865
49940
  title: "Send a file, PDF, or image",
49866
- 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`.',
49941
+ 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.',
49867
49942
  inputSchema: {
49868
49943
  to: external_exports.string().describe("recipient username"),
49869
49944
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
49870
49945
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
49871
49946
  dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
49872
- fileName: external_exports.string().optional().describe("file name when using dataUrl")
49947
+ fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
49948
+ fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
49873
49949
  }
49874
49950
  },
49875
- async ({ to, path: path2, caption, dataUrl, fileName }) => withClient(async (c) => {
49951
+ async ({ to, path: path2, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
49876
49952
  let name, mime, url;
49877
49953
  if (path2) {
49878
49954
  if (!ctx.local) {
@@ -49886,15 +49962,18 @@ ${fp}`);
49886
49962
  const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
49887
49963
  const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer ${c.token}" } -ContentType "${fmime}" -InFile "${path2}"`;
49888
49964
  return fail(
49889
- `ASSISTANT: run the one command below in your shell to send this file \u2014 it reads the file locally and uploads it directly, fast, at any size. Do NOT read the file or base64 it into a tool call (that is slow and breaks on large files); just run this:
49965
+ `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
49890
49966
 
49891
- \u2022 macOS / Linux:
49892
- ${cmd}
49967
+ \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
49968
+ ${dropLink}
49893
49969
 
49894
- \u2022 Windows (PowerShell \u2014 avoids the curl/schannel TLS error):
49895
- ${psCmd}
49970
+ (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.)
49896
49971
 
49897
- (Alternatively the user can drag "${fname}" straight into the chat: ${dropLink})`
49972
+ \u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
49973
+ \u2022 macOS / Linux:
49974
+ ${cmd}
49975
+ \u2022 Windows (PowerShell):
49976
+ ${psCmd}`
49898
49977
  );
49899
49978
  }
49900
49979
  const abs = resolveLocalFile(path2);
@@ -49909,8 +49988,14 @@ ${psCmd}
49909
49988
  name = fileName;
49910
49989
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
49911
49990
  url = dataUrl;
49991
+ } else if (fileUrl) {
49992
+ try {
49993
+ ({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
49994
+ } catch (e2) {
49995
+ return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
49996
+ }
49912
49997
  } else {
49913
- return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
49998
+ return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
49914
49999
  }
49915
50000
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
49916
50001
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
@@ -50421,28 +50506,36 @@ ${cmd}`);
50421
50506
  "jefri_send_group_file",
50422
50507
  {
50423
50508
  title: "Send a file to a group",
50424
- description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector) or a `dataUrl` + `fileName`. Get the group id from jefri_groups / jefri_inbox.",
50509
+ description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector), a `dataUrl` + `fileName`, or a public `fileUrl` to fetch (e.g. a generated image). Get the group id from jefri_groups / jefri_inbox.",
50425
50510
  inputSchema: {
50426
50511
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
50427
50512
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
50428
50513
  caption: external_exports.string().optional().describe("optional text caption"),
50429
50514
  dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
50430
- fileName: external_exports.string().optional().describe("file name when using dataUrl")
50515
+ fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
50516
+ fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
50431
50517
  }
50432
50518
  },
50433
- async ({ groupId, path: path2, caption, dataUrl, fileName }) => withClient(async (c) => {
50519
+ async ({ groupId, path: path2, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
50434
50520
  let name, mime, url;
50435
50521
  if (path2) {
50436
50522
  if (!ctx.local) {
50437
50523
  const hub = ctx.serverUrl.replace(/\/$/, "");
50524
+ const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
50525
+ const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
50438
50526
  const fname = np3.basename(path2);
50439
50527
  const fmime = mimeOf(fname);
50440
50528
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
50441
50529
  const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
50442
50530
  return fail(
50443
- `ASSISTANT: run this one command in your shell to send the file to the group (it reads + uploads locally, fast, any size \u2014 do NOT base64 it into a tool call):
50531
+ `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
50532
+
50533
+ \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
50534
+ ${dropLink}
50535
+
50536
+ (If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
50444
50537
 
50445
- ${cmd}`
50538
+ \u2014 Advanced (terminal): ${cmd}`
50446
50539
  );
50447
50540
  }
50448
50541
  const abs = resolveLocalFile(path2);
@@ -50455,8 +50548,14 @@ ${cmd}`
50455
50548
  name = fileName;
50456
50549
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
50457
50550
  url = dataUrl;
50551
+ } else if (fileUrl) {
50552
+ try {
50553
+ ({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
50554
+ } catch (e2) {
50555
+ return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
50556
+ }
50458
50557
  } else {
50459
- return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
50558
+ return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
50460
50559
  }
50461
50560
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
50462
50561
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
package/dist/index.js CHANGED
@@ -25007,7 +25007,8 @@ var JefriClient = class _JefriClient {
25007
25007
  }
25008
25008
  scheduleReconnect() {
25009
25009
  if (this.closing || this.reconnectTimer || this.authFailed) return;
25010
- const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
25010
+ const cap = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
25011
+ const delay = Math.floor(cap / 2 + Math.random() * (cap / 2));
25011
25012
  this.reconnectAttempts++;
25012
25013
  this.reconnectTimer = setTimeout(() => {
25013
25014
  this.reconnectTimer = void 0;
@@ -25330,6 +25331,8 @@ import fs4 from "node:fs";
25330
25331
  import np3 from "node:path";
25331
25332
  import cp3 from "node:child_process";
25332
25333
  import os4 from "node:os";
25334
+ import nnet from "node:net";
25335
+ import ndns from "node:dns/promises";
25333
25336
 
25334
25337
  // src/notify.ts
25335
25338
  import cp from "node:child_process";
@@ -25538,7 +25541,7 @@ var FORCE_OWNER_ONLY = true;
25538
25541
  function shouldHandle(m) {
25539
25542
  if (!cfg.enabled) return false;
25540
25543
  const fromOwner = !!ownerName && m.sender === ownerName;
25541
- const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY && !!ownerName;
25544
+ const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY;
25542
25545
  if (ownerOnly && !fromOwner) return false;
25543
25546
  if (m.senderIsBot && !cfg.replyToBots) return false;
25544
25547
  if (!m.isGroup) return true;
@@ -25726,6 +25729,78 @@ function resolveLocalFile(rawPath) {
25726
25729
  var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
25727
25730
  var MB = (n) => (n / 1024 / 1024).toFixed(1);
25728
25731
  var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
25732
+ function isPrivateAddress(ip) {
25733
+ if (nnet.isIPv4(ip)) {
25734
+ const [a, b] = ip.split(".").map(Number);
25735
+ if (a === 0 || a === 10 || a === 127) return true;
25736
+ if (a === 169 && b === 254) return true;
25737
+ if (a === 172 && b >= 16 && b <= 31) return true;
25738
+ if (a === 192 && b === 168) return true;
25739
+ if (a === 100 && b >= 64 && b <= 127) return true;
25740
+ return false;
25741
+ }
25742
+ const lc = ip.toLowerCase();
25743
+ if (lc === "::1" || lc === "::") return true;
25744
+ if (lc.startsWith("::ffff:")) return isPrivateAddress(lc.slice(7));
25745
+ if (lc.startsWith("fe80") || lc.startsWith("fc") || lc.startsWith("fd")) return true;
25746
+ return false;
25747
+ }
25748
+ var extFromMime = (mime) => {
25749
+ const sub = (mime.split("/")[1] || "").toLowerCase();
25750
+ if (sub === "jpeg") return "jpg";
25751
+ if (sub === "svg+xml") return "svg";
25752
+ return sub.replace(/[^a-z0-9]/g, "").slice(0, 5) || "bin";
25753
+ };
25754
+ async function assertPublicUrl(raw) {
25755
+ let u;
25756
+ try {
25757
+ u = new URL(raw);
25758
+ } catch {
25759
+ throw new Error(`not a valid URL: ${raw}`);
25760
+ }
25761
+ if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("only http(s) URLs can be fetched");
25762
+ let addr;
25763
+ try {
25764
+ addr = (await ndns.lookup(u.hostname)).address;
25765
+ } catch {
25766
+ throw new Error(`can't resolve host ${u.hostname}`);
25767
+ }
25768
+ if (isPrivateAddress(addr)) throw new Error("refusing to fetch a private/internal address");
25769
+ return u;
25770
+ }
25771
+ async function fetchRemoteFile(fileUrl, fileNameHint, maxBytes) {
25772
+ let current = await assertPublicUrl(fileUrl);
25773
+ let res = null;
25774
+ for (let hop = 0; hop < 4; hop++) {
25775
+ const ac = new AbortController();
25776
+ const timer = setTimeout(() => ac.abort(), 2e4);
25777
+ try {
25778
+ res = await fetch(current, { redirect: "manual", signal: ac.signal, headers: { "user-agent": "jefrichat-mcp" } });
25779
+ } finally {
25780
+ clearTimeout(timer);
25781
+ }
25782
+ if (res.status >= 300 && res.status < 400) {
25783
+ const loc = res.headers.get("location");
25784
+ if (!loc) throw new Error(`redirect with no location (${res.status})`);
25785
+ current = await assertPublicUrl(new URL(loc, current).toString());
25786
+ continue;
25787
+ }
25788
+ break;
25789
+ }
25790
+ if (!res || !res.ok) throw new Error(`fetch failed (${res?.status ?? "no response"})`);
25791
+ const clen = Number(res.headers.get("content-length") || 0);
25792
+ if (clen && clen > maxBytes) throw new Error(`file is ${MB(clen)} MB, over the ${MB(maxBytes)} MB limit`);
25793
+ const buf = Buffer.from(await res.arrayBuffer());
25794
+ if (buf.length > maxBytes) throw new Error(`file is ${MB(buf.length)} MB, over the ${MB(maxBytes)} MB limit`);
25795
+ if (buf.length === 0) throw new Error("the URL returned an empty file");
25796
+ const mime = (res.headers.get("content-type") || "").split(";")[0].trim() || mimeOf(fileNameHint || current.pathname);
25797
+ let name = fileNameHint || np3.basename(current.pathname) || "";
25798
+ if (!name || name === "/" || !name.includes(".")) {
25799
+ const base = name && name !== "/" ? name : "file";
25800
+ name = `${base}.${extFromMime(mime)}`;
25801
+ }
25802
+ return { name, mime, url: `data:${mime};base64,${buf.toString("base64")}` };
25803
+ }
25729
25804
  async function uploadFileHttp(c, serverUrl, target, fileName, mime, bytes, caption) {
25730
25805
  const hub = serverUrl.replace(/\/$/, "");
25731
25806
  const params = new URLSearchParams({ fileName });
@@ -25978,16 +26053,17 @@ ${fp}`);
25978
26053
  "jefri_send_file",
25979
26054
  {
25980
26055
  title: "Send a file, PDF, or image",
25981
- 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`.',
26056
+ 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.',
25982
26057
  inputSchema: {
25983
26058
  to: external_exports.string().describe("recipient username"),
25984
26059
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
25985
26060
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
25986
26061
  dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
25987
- fileName: external_exports.string().optional().describe("file name when using dataUrl")
26062
+ fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
26063
+ fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
25988
26064
  }
25989
26065
  },
25990
- async ({ to, path: path3, caption, dataUrl, fileName }) => withClient(async (c) => {
26066
+ async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
25991
26067
  let name, mime, url;
25992
26068
  if (path3) {
25993
26069
  if (!ctx.local) {
@@ -26001,15 +26077,18 @@ ${fp}`);
26001
26077
  const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
26002
26078
  const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer ${c.token}" } -ContentType "${fmime}" -InFile "${path3}"`;
26003
26079
  return fail(
26004
- `ASSISTANT: run the one command below in your shell to send this file \u2014 it reads the file locally and uploads it directly, fast, at any size. Do NOT read the file or base64 it into a tool call (that is slow and breaks on large files); just run this:
26080
+ `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
26005
26081
 
26006
- \u2022 macOS / Linux:
26007
- ${cmd}
26082
+ \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
26083
+ ${dropLink}
26008
26084
 
26009
- \u2022 Windows (PowerShell \u2014 avoids the curl/schannel TLS error):
26010
- ${psCmd}
26085
+ (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.)
26011
26086
 
26012
- (Alternatively the user can drag "${fname}" straight into the chat: ${dropLink})`
26087
+ \u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
26088
+ \u2022 macOS / Linux:
26089
+ ${cmd}
26090
+ \u2022 Windows (PowerShell):
26091
+ ${psCmd}`
26013
26092
  );
26014
26093
  }
26015
26094
  const abs = resolveLocalFile(path3);
@@ -26024,8 +26103,14 @@ ${psCmd}
26024
26103
  name = fileName;
26025
26104
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
26026
26105
  url = dataUrl;
26106
+ } else if (fileUrl) {
26107
+ try {
26108
+ ({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
26109
+ } catch (e2) {
26110
+ return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
26111
+ }
26027
26112
  } else {
26028
- return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
26113
+ return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
26029
26114
  }
26030
26115
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
26031
26116
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
@@ -26536,28 +26621,36 @@ ${cmd}`);
26536
26621
  "jefri_send_group_file",
26537
26622
  {
26538
26623
  title: "Send a file to a group",
26539
- description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector) or a `dataUrl` + `fileName`. Get the group id from jefri_groups / jefri_inbox.",
26624
+ description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector), a `dataUrl` + `fileName`, or a public `fileUrl` to fetch (e.g. a generated image). Get the group id from jefri_groups / jefri_inbox.",
26540
26625
  inputSchema: {
26541
26626
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
26542
26627
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
26543
26628
  caption: external_exports.string().optional().describe("optional text caption"),
26544
26629
  dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
26545
- fileName: external_exports.string().optional().describe("file name when using dataUrl")
26630
+ fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
26631
+ fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
26546
26632
  }
26547
26633
  },
26548
- async ({ groupId, path: path3, caption, dataUrl, fileName }) => withClient(async (c) => {
26634
+ async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
26549
26635
  let name, mime, url;
26550
26636
  if (path3) {
26551
26637
  if (!ctx.local) {
26552
26638
  const hub = ctx.serverUrl.replace(/\/$/, "");
26639
+ const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
26640
+ const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
26553
26641
  const fname = np3.basename(path3);
26554
26642
  const fmime = mimeOf(fname);
26555
26643
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
26556
26644
  const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
26557
26645
  return fail(
26558
- `ASSISTANT: run this one command in your shell to send the file to the group (it reads + uploads locally, fast, any size \u2014 do NOT base64 it into a tool call):
26646
+ `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
26647
+
26648
+ \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
26649
+ ${dropLink}
26650
+
26651
+ (If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
26559
26652
 
26560
- ${cmd}`
26653
+ \u2014 Advanced (terminal): ${cmd}`
26561
26654
  );
26562
26655
  }
26563
26656
  const abs = resolveLocalFile(path3);
@@ -26570,8 +26663,14 @@ ${cmd}`
26570
26663
  name = fileName;
26571
26664
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
26572
26665
  url = dataUrl;
26666
+ } else if (fileUrl) {
26667
+ try {
26668
+ ({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
26669
+ } catch (e2) {
26670
+ return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
26671
+ }
26573
26672
  } else {
26574
- return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
26673
+ return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
26575
26674
  }
26576
26675
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
26577
26676
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
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": {
@@ -14,12 +14,6 @@
14
14
  "publishConfig": {
15
15
  "access": "public"
16
16
  },
17
- "scripts": {
18
- "start": "tsx src/index.ts",
19
- "http": "tsx src/http.ts",
20
- "build": "node build.mjs",
21
- "prepublishOnly": "node build.mjs"
22
- },
23
17
  "keywords": [
24
18
  "jefrichat",
25
19
  "jefri",
@@ -31,17 +25,22 @@
31
25
  ],
32
26
  "license": "MIT",
33
27
  "devDependencies": {
34
- "@acp/shared": "workspace:*",
35
28
  "@modelcontextprotocol/sdk": "^1.12.0",
36
29
  "@types/cors": "^2.8.17",
37
30
  "@types/express": "^4.17.21",
38
31
  "@types/node": "^22.10.2",
39
- "jefri-sdk": "workspace:*",
40
32
  "cors": "^2.8.5",
41
33
  "esbuild": "^0.28.1",
42
34
  "express": "^4.21.2",
43
35
  "tsx": "^4.19.2",
44
36
  "typescript": "^5.7.2",
45
- "zod": "^3.23.8"
37
+ "zod": "^3.23.8",
38
+ "@acp/shared": "0.1.0",
39
+ "jefri-sdk": "0.3.0"
40
+ },
41
+ "scripts": {
42
+ "start": "tsx src/index.ts",
43
+ "http": "tsx src/http.ts",
44
+ "build": "node build.mjs"
46
45
  }
47
- }
46
+ }