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.
- package/dist/http.js +116 -17
- package/dist/index.js +117 -18
- 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
|
|
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
|
-
|
|
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
|
-
`
|
|
49965
|
+
`This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
|
|
49890
49966
|
|
|
49891
|
-
\
|
|
49892
|
-
${
|
|
49967
|
+
\u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
|
|
49968
|
+
${dropLink}
|
|
49893
49969
|
|
|
49894
|
-
|
|
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
|
-
(
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
`
|
|
26080
|
+
`This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
|
|
26005
26081
|
|
|
26006
|
-
\
|
|
26007
|
-
${
|
|
26082
|
+
\u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
|
|
26083
|
+
${dropLink}
|
|
26008
26084
|
|
|
26009
|
-
|
|
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
|
-
(
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
-
`
|
|
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
|
|
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.
|
|
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
|
+
}
|