jefrichat-mcp 0.49.4 → 0.49.6
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 +198 -33
- package/dist/index.js +198 -33
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -62435,9 +62435,28 @@ var safeSaveName = (p) => {
|
|
|
62435
62435
|
var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
|
|
62436
62436
|
var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
|
|
62437
62437
|
var psPathArg = (p) => p.startsWith("~/") || p.startsWith("~\\") ? `($HOME + ${psq(p.slice(1))})` : psq(p);
|
|
62438
|
-
var isTextyFile = (mime, name) => /^text\/|json|xml|javascript|typescript|markdown|csv|ya?ml|x-sh|toml/i.test(mime) ||
|
|
62438
|
+
var isTextyFile = (mime, name) => /^text\/|json|xml|javascript|typescript|markdown|csv|ya?ml|x-sh|toml/i.test(mime) || // The extension list matters because stored MIMEs are often octet-stream
|
|
62439
|
+
// (the taller-norte.html case: an HTML file fell to the link fallback
|
|
62440
|
+
// because .html was missing here — code files must come back READABLE).
|
|
62441
|
+
/\.(md|txt|json|csv|ya?ml|log|ts|tsx|js|jsx|mjs|py|rb|go|rs|java|c|h|cpp|hpp|cs|sh|sql|toml|env|cfg|ini|html?|css|scss|less|xml|vue|svelte|php|swift|kt|dart|lua)$/i.test(name);
|
|
62439
62442
|
var DATAURL_INLINE_MAX = 256 * 1024;
|
|
62440
|
-
|
|
62443
|
+
function parseInlineDataUrl(dataUrl) {
|
|
62444
|
+
const malformed = `That dataUrl is malformed \u2014 expected data:<mime>;base64,<canonical base64>. Re-encode the bytes (standard base64, correct padding, no whitespace) and try again \u2014 or pick the input that matches where the file lives (path for the user's machine, fileUrl for a public URL).`;
|
|
62445
|
+
const m = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/.exec(dataUrl);
|
|
62446
|
+
if (!m) return { error: malformed };
|
|
62447
|
+
const payload = m[2];
|
|
62448
|
+
if (payload.length % 4 !== 0) return { error: malformed };
|
|
62449
|
+
const bytes = Buffer.from(payload, "base64");
|
|
62450
|
+
if (bytes.toString("base64") !== payload) return { error: malformed };
|
|
62451
|
+
if (bytes.length > DATAURL_INLINE_MAX)
|
|
62452
|
+
return {
|
|
62453
|
+
error: `That dataUrl is ${MB(bytes.length)} MB decoded \u2014 past the ${Math.round(DATAURL_INLINE_MAX / 1024)}KB inline limit. What to do instead:
|
|
62454
|
+
\u2022 file on the USER'S machine \u2192 call again with its \`path\` (you'll get a single-use upload command)
|
|
62455
|
+
\u2022 file reachable by URL \u2192 call again with \`fileUrl\`
|
|
62456
|
+
\u2022 file in YOUR OWN sandbox (no egress) \u2192 too big to inline: present the file to the user for download, and point them at the upload LINK this tool mints (call it with the file's path if you don't have a link yet) \u2014 they tap it, pick the downloaded file, and it delivers.`
|
|
62457
|
+
};
|
|
62458
|
+
return { mime: m[1], bytes };
|
|
62459
|
+
}
|
|
62441
62460
|
var MINT_LINK_DEADLINE_MS = (() => {
|
|
62442
62461
|
const n = Number(process.env.MCP_MINT_LINK_DEADLINE_MS);
|
|
62443
62462
|
return Number.isInteger(n) && n > 0 ? n : 1e4;
|
|
@@ -62467,30 +62486,77 @@ async function mintFileLink(hub, token, fileId) {
|
|
|
62467
62486
|
clearTimeout(killer);
|
|
62468
62487
|
}
|
|
62469
62488
|
}
|
|
62470
|
-
var forwardTip = (link2, fileName) => `\u2022
|
|
62489
|
+
var forwardTip = (link2, fileName) => `\u2022 The direct-download link is for the HUMAN to click \u2014 don't fetch it with your own web tool (hosts restrict fetches, and the link expires ~5 min; if it has expired, call jefri_download_file again for a fresh one).
|
|
62490
|
+
\u2022 To FORWARD this exact file: to a PERSON call jefri_send_file with to:"<username>", fileUrl:${JSON.stringify(link2)}, fileName:${JSON.stringify(fileName)}; to a GROUP call jefri_send_group_file with groupId:"<id>" and the same fileUrl + fileName. The connector fetches and delivers the ORIGINAL bytes (up to 25 MB).
|
|
62471
62491
|
`;
|
|
62472
|
-
async function remoteUploadCommand(c, ctx, target, path3, caption, where) {
|
|
62492
|
+
async function remoteUploadCommand(c, ctx, target, path3, caption, where, signal) {
|
|
62473
62493
|
const fname = crossBasename(path3);
|
|
62474
62494
|
const fmime = mimeOf(fname);
|
|
62475
62495
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
62476
62496
|
try {
|
|
62477
|
-
const
|
|
62478
|
-
|
|
62479
|
-
|
|
62480
|
-
|
|
62481
|
-
|
|
62482
|
-
|
|
62483
|
-
|
|
62484
|
-
|
|
62497
|
+
const gc = new AbortController();
|
|
62498
|
+
const onHostAbort = () => gc.abort();
|
|
62499
|
+
const gKiller = setTimeout(() => gc.abort(), MINT_LINK_DEADLINE_MS);
|
|
62500
|
+
if (signal) {
|
|
62501
|
+
if (signal.aborted) {
|
|
62502
|
+
clearTimeout(gKiller);
|
|
62503
|
+
throw new Error("cancelled");
|
|
62504
|
+
}
|
|
62505
|
+
signal.addEventListener("abort", onHostAbort, { once: true });
|
|
62506
|
+
}
|
|
62507
|
+
let mins;
|
|
62508
|
+
let uploadUrl;
|
|
62509
|
+
try {
|
|
62510
|
+
const g = await fetch(`${hub}/api/files/grant`, {
|
|
62511
|
+
method: "POST",
|
|
62512
|
+
headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
|
|
62513
|
+
body: JSON.stringify({ ...target.to ? { to: target.to } : { groupId: target.groupId }, fileName: fname, ...caption ? { caption } : {} }),
|
|
62514
|
+
signal: gc.signal
|
|
62515
|
+
});
|
|
62516
|
+
if (!g.ok) {
|
|
62517
|
+
try {
|
|
62518
|
+
await g.body?.cancel();
|
|
62519
|
+
} catch {
|
|
62520
|
+
}
|
|
62521
|
+
throw new Error(`grant service answered ${g.status}`);
|
|
62522
|
+
}
|
|
62523
|
+
const body = await g.json().catch(() => ({}));
|
|
62524
|
+
const parsedUrl = (() => {
|
|
62525
|
+
if (typeof body.uploadUrl !== "string") return null;
|
|
62526
|
+
try {
|
|
62527
|
+
const u = new URL(body.uploadUrl);
|
|
62528
|
+
return (u.protocol === "http:" || u.protocol === "https:") && u.host ? body.uploadUrl : null;
|
|
62529
|
+
} catch {
|
|
62530
|
+
return null;
|
|
62531
|
+
}
|
|
62532
|
+
})();
|
|
62533
|
+
if (!parsedUrl) throw new Error("malformed grant response: uploadUrl");
|
|
62534
|
+
if (typeof body.expiresInMs !== "number" || !Number.isFinite(body.expiresInMs) || body.expiresInMs <= 0)
|
|
62535
|
+
throw new Error("malformed grant response: expiresInMs");
|
|
62536
|
+
uploadUrl = parsedUrl;
|
|
62537
|
+
mins = Math.round(body.expiresInMs / 6e4);
|
|
62538
|
+
} finally {
|
|
62539
|
+
gc.abort();
|
|
62540
|
+
clearTimeout(gKiller);
|
|
62541
|
+
if (signal) signal.removeEventListener("abort", onHostAbort);
|
|
62542
|
+
}
|
|
62543
|
+
{
|
|
62485
62544
|
return {
|
|
62486
|
-
ok: `This cloud connector can't read
|
|
62545
|
+
ok: `This cloud connector can't read that path \u2014 two fast paths, pick one:
|
|
62546
|
+
|
|
62547
|
+
\u{1F517} EASIEST \u2014 no terminal: have the USER open this link in any browser (phone works) and pick the file; it delivers straight to ${where}:
|
|
62548
|
+
${uploadUrl}
|
|
62549
|
+
|
|
62550
|
+
\u{1F4BB} Or run this ONE command on a machine that HAS the file and has internet (the user's terminal \u2014 NOT a sandboxed assistant environment without egress):
|
|
62487
62551
|
|
|
62488
62552
|
curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path3)}
|
|
62489
62553
|
|
|
62490
62554
|
Windows PowerShell instead:
|
|
62491
62555
|
Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path3)}
|
|
62492
62556
|
|
|
62493
|
-
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,...}).
|
|
62557
|
+
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,...}).
|
|
62558
|
+
\u26A0\uFE0F If YOU are a sandboxed assistant with no network egress and the file lives in YOUR sandbox: this command cannot run anywhere that can see the file. Instead \u2014 if the file is 256KB or less, call this tool again with dataUrl (base64 it) + fileName; if larger, PRESENT the file to the user for download and tell them to open the LINK ABOVE and pick the downloaded file. Do NOT paste this curl into your own shell.
|
|
62559
|
+
Never base64 anything over 256KB through dataUrl \u2014 that is minutes of model output and it will be refused.`
|
|
62494
62560
|
};
|
|
62495
62561
|
}
|
|
62496
62562
|
} catch {
|
|
@@ -62500,7 +62566,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
|
|
|
62500
62566
|
const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
62501
62567
|
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)}`;
|
|
62502
62568
|
return {
|
|
62503
|
-
fail: `This is the cloud connector, so it can't read "${fname}"
|
|
62569
|
+
fail: `This is the cloud connector, so it can't read "${fname}" from that path, and the upload-link service didn't answer. Options:
|
|
62504
62570
|
|
|
62505
62571
|
\u2705 open the Jefri Chat web app and DRAG "${fname}" into the chat with ${where}:
|
|
62506
62572
|
${dropLink}
|
|
@@ -62508,7 +62574,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
|
|
|
62508
62574
|
\u2014 or from a terminal (set JEFRI_TOKEN from the app's Connect dialog first):
|
|
62509
62575
|
${cmd}
|
|
62510
62576
|
|
|
62511
|
-
|
|
62577
|
+
Sandboxed assistant holding the file yourself? 256KB or less: send it via dataUrl + fileName; larger: present the file to the user for download and retry this tool with the path \u2014 the minted upload link lets them pick the downloaded file. Never base64 anything over 256KB through dataUrl.`
|
|
62512
62578
|
};
|
|
62513
62579
|
}
|
|
62514
62580
|
async function fetchHistory(c, convId) {
|
|
@@ -62534,7 +62600,22 @@ var MIME = {
|
|
|
62534
62600
|
".txt": "text/plain",
|
|
62535
62601
|
".md": "text/markdown",
|
|
62536
62602
|
".json": "application/json",
|
|
62537
|
-
".zip": "application/zip"
|
|
62603
|
+
".zip": "application/zip",
|
|
62604
|
+
// Web/code types (the taller-norte.html case): without these an .html send
|
|
62605
|
+
// shipped as octet-stream, so the web app couldn't render it and receivers
|
|
62606
|
+
// saw "binary". Bytes always travelled fine — the LABEL was wrong.
|
|
62607
|
+
".html": "text/html",
|
|
62608
|
+
".htm": "text/html",
|
|
62609
|
+
".css": "text/css",
|
|
62610
|
+
".js": "text/javascript",
|
|
62611
|
+
".mjs": "text/javascript",
|
|
62612
|
+
".ts": "text/plain",
|
|
62613
|
+
".tsx": "text/plain",
|
|
62614
|
+
".csv": "text/csv",
|
|
62615
|
+
".xml": "application/xml",
|
|
62616
|
+
".yaml": "text/yaml",
|
|
62617
|
+
".yml": "text/yaml",
|
|
62618
|
+
".log": "text/plain"
|
|
62538
62619
|
};
|
|
62539
62620
|
var mimeOf = (name) => MIME[np3.extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
62540
62621
|
function fmtTime(iso) {
|
|
@@ -63474,25 +63555,106 @@ ${fp}`);
|
|
|
63474
63555
|
return ok3(`Sent private E2E group file.`);
|
|
63475
63556
|
})
|
|
63476
63557
|
);
|
|
63558
|
+
if (!ctx.local) server2.registerTool(
|
|
63559
|
+
"jefri_upload_link",
|
|
63560
|
+
{
|
|
63561
|
+
title: "Get an upload link (any file up to 100 MB, full quality)",
|
|
63562
|
+
description: "Get a ONE-TAP UPLOAD LINK to send files or images \u2014 up to 5 PER LINK via `fileNames`, up to 100 MB each, FULL quality, no base64, no compression, no path needed. ALWAYS PREFER THIS for images and for files in your sandbox or on the user's device \u2014 the ONE exception: files \u2264256KB (already held OR readable in your sandbox \u2014 PDFs, CSVs, code, small images) go straight through jefri_send_file's dataUrl automatically, no link needed. One cheap call, then hand the user the link \u2014 they open it (phone works), pick the file, and it delivers to the recipient automatically. Give `to` (a username) or `groupId`, plus the `fileName` the delivery should carry. The link is single-use and expires in ~15 minutes; mint a fresh one any time. If the file lives in YOUR sandbox, present it to the user for download first so they can pick it at the link. NEVER stream file bytes through plain output (base64/pixel data) \u2014 that burns the user's tokens. The ONE sanctioned carrier is a \u2264256KB `dataUrl` TOOL ARGUMENT via jefri_send_file (near the cap it may still strain output limits \u2014 then use this link); anything larger only ever travels as this link.",
|
|
63563
|
+
inputSchema: {
|
|
63564
|
+
to: external_exports.string().optional().describe("recipient username (or use groupId)"),
|
|
63565
|
+
groupId: external_exports.string().optional().describe("group id (from jefri_groups) instead of `to`"),
|
|
63566
|
+
fileName: external_exports.string().optional().describe("the name the delivered file should carry, e.g. photo.png (single file)"),
|
|
63567
|
+
fileNames: external_exports.array(external_exports.string()).optional().describe('for a MULTI-file link (2\u20135): the delivery names in order, e.g. ["foto-1.jpg","foto-2.jpg"] \u2014 one link, the user picks them all'),
|
|
63568
|
+
caption: external_exports.string().optional().describe("optional text caption delivered with the file")
|
|
63569
|
+
}
|
|
63570
|
+
},
|
|
63571
|
+
async (args, extra) => withClient(async (c) => {
|
|
63572
|
+
const to = typeof args.to === "string" && args.to.trim() ? args.to.trim() : void 0;
|
|
63573
|
+
const groupId = typeof args.groupId === "string" && args.groupId.trim() ? args.groupId.trim() : void 0;
|
|
63574
|
+
if (!to && !groupId) return fail("give `to` (a username) or `groupId`");
|
|
63575
|
+
if (to && groupId) return fail("give either `to` or `groupId`, not both");
|
|
63576
|
+
const names = Array.isArray(args.fileNames) ? args.fileNames.map((n) => String(n ?? "").trim()).filter(Boolean).slice(0, 5) : [];
|
|
63577
|
+
const fileName = String(args.fileName ?? "").trim();
|
|
63578
|
+
if (!fileName && names.length === 0) return fail("give `fileName` (one file) or `fileNames` (2\u20135 files) \u2014 deliveries carry these names");
|
|
63579
|
+
if (Array.isArray(args.fileNames) && args.fileNames.length > 5)
|
|
63580
|
+
return fail("a link carries at most 5 files \u2014 mint another link for more");
|
|
63581
|
+
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
63582
|
+
const gc = new AbortController();
|
|
63583
|
+
const onHostAbort = () => gc.abort();
|
|
63584
|
+
const gKiller = setTimeout(() => gc.abort(), MINT_LINK_DEADLINE_MS);
|
|
63585
|
+
const signal = extra?.signal;
|
|
63586
|
+
if (signal) {
|
|
63587
|
+
if (signal.aborted) {
|
|
63588
|
+
clearTimeout(gKiller);
|
|
63589
|
+
return fail("cancelled");
|
|
63590
|
+
}
|
|
63591
|
+
signal.addEventListener("abort", onHostAbort, { once: true });
|
|
63592
|
+
}
|
|
63593
|
+
try {
|
|
63594
|
+
const g = await fetch(`${hub}/api/files/grant`, {
|
|
63595
|
+
method: "POST",
|
|
63596
|
+
headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
|
|
63597
|
+
body: JSON.stringify({ ...to ? { to } : { groupId }, ...names.length ? { fileNames: names } : { fileName }, ...args.caption ? { caption: args.caption } : {} }),
|
|
63598
|
+
signal: gc.signal
|
|
63599
|
+
});
|
|
63600
|
+
if (!g.ok) {
|
|
63601
|
+
try {
|
|
63602
|
+
await g.body?.cancel();
|
|
63603
|
+
} catch {
|
|
63604
|
+
}
|
|
63605
|
+
return fail(`the upload-link service answered ${g.status} \u2014 try again shortly. Real fallbacks while it's down: a public URL \u2192 jefri_send_file with fileUrl; otherwise present/download the file and DRAG it into the Jefri Chat web app.`);
|
|
63606
|
+
}
|
|
63607
|
+
const body = await g.json().catch(() => ({}));
|
|
63608
|
+
const parsedUrl = (() => {
|
|
63609
|
+
const raw = names.length ? body.batchUrl : body.uploadUrl;
|
|
63610
|
+
if (typeof raw !== "string") return null;
|
|
63611
|
+
try {
|
|
63612
|
+
const u = new URL(raw);
|
|
63613
|
+
return (u.protocol === "http:" || u.protocol === "https:") && u.host ? raw : null;
|
|
63614
|
+
} catch {
|
|
63615
|
+
return null;
|
|
63616
|
+
}
|
|
63617
|
+
})();
|
|
63618
|
+
if (!parsedUrl || typeof body.expiresInMs !== "number" || !Number.isFinite(body.expiresInMs) || body.expiresInMs <= 0)
|
|
63619
|
+
return fail("the upload-link service answered malformed data \u2014 try again shortly. Real fallbacks: a public URL \u2192 jefri_send_file with fileUrl; otherwise present/download the file and DRAG it into the Jefri Chat web app.");
|
|
63620
|
+
const mins = Math.round(body.expiresInMs / 6e4);
|
|
63621
|
+
const where = to ? `@${safeHandle(to)}` : "the group";
|
|
63622
|
+
const delivers = names.length ? `${names.length} files: ${names.map((n) => `\u{1F4CE} ${n}`).join(", ")}` : `\u{1F4CE} ${fileName}`;
|
|
63623
|
+
return ok3(
|
|
63624
|
+
`\u{1F517} One-tap upload link for ${where} (delivers ${delivers}; valid ~${mins} min, single-use):
|
|
63625
|
+
${parsedUrl}
|
|
63626
|
+
|
|
63627
|
+
SAY THIS to the user (adapt their language): "Here's your upload link \u2014 open it and pick the ${names.length ? "files" : "file"}; ${names.length ? "they deliver" : "it delivers"} to ${where} automatically at full quality: ${parsedUrl}" If the file is in YOUR sandbox, present it for download first so they can pick the downloaded copy. Do NOT open the link yourself and do NOT prepare or stream any bytes \u2014 your part is done.`
|
|
63628
|
+
);
|
|
63629
|
+
} catch (e) {
|
|
63630
|
+
return fail(`could not mint the upload link: ${e?.message ?? e} \u2014 try again shortly. Real fallbacks: a public URL \u2192 jefri_send_file with fileUrl; otherwise present/download the file and DRAG it into the Jefri Chat web app.`);
|
|
63631
|
+
} finally {
|
|
63632
|
+
gc.abort();
|
|
63633
|
+
clearTimeout(gKiller);
|
|
63634
|
+
if (signal) signal.removeEventListener("abort", onHostAbort);
|
|
63635
|
+
}
|
|
63636
|
+
})
|
|
63637
|
+
);
|
|
63477
63638
|
server2.registerTool(
|
|
63478
63639
|
"jefri_send_file",
|
|
63479
63640
|
{
|
|
63480
63641
|
title: "Send a file, PDF, or image",
|
|
63481
|
-
description: "Send a file to
|
|
63642
|
+
description: ctx.local ? "Send a file to a Jefri Chat user/agent \u2014 PDF, image, video, any document. On this LOCAL connector, `path` reads the file off disk and sends it DIRECTLY (up to 100 MB) \u2014 always prefer `path`; never read a file into base64 yourself. `dataUrl` + `fileName` is only for small (\u2264256KB) content you generated in-conversation, and `fileUrl` fetches a public URL. Optional `caption` always." : "Send a file to a Jefri Chat user/agent. THE RULE: a file \u2264256KB (check its size first \u2014 PDFs, CSVs, code, small images alike), whether already held as base64 or sitting in your sandbox, \u2192 read it and send NOW via `dataUrl` + `fileName`, automatically, no user step. ANYTHING over 256KB, or that would need TRANSFORMING (compressing, converting, resizing) \u2192 STOP and call jefri_upload_link instead \u2014 never transform. For a file on the USER'S DEVICE, CALL THIS TOOL FIRST with the file's `path` \u2014 even a path this connector cannot read: it answers a one-tap upload LINK the user opens to deliver the file at full quality. NEVER pre-process first: no compressing, no splitting base64 into chunks, no reading/retyping base64 through a viewer \u2014 that wastes many minutes and usually fails; the LINK route needs none of it. Inputs by where the file lives: (1) user's machine \u2192 `path` (also yields a runnable upload command). (2) public URL \u2192 `fileUrl` (connector fetches, \u226425MB). (3) YOUR OWN sandbox (cloud-sandboxed assistant, no egress): up to 100 MB \u2192 `path` for the LINK (present the file for the user to download and pick at the link); \u2264256KB (held OR read from your sandbox) \u2192 `dataUrl` + `fileName`, automatically. Large images: the LINK, full quality \u2014 do not offer to compress. Optional `caption` always.",
|
|
63482
63643
|
inputSchema: {
|
|
63483
63644
|
to: external_exports.string().describe("recipient username"),
|
|
63484
63645
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
63485
63646
|
caption: external_exports.string().optional().describe("optional text caption to send with it"),
|
|
63486
|
-
dataUrl: external_exports.string().optional().describe("
|
|
63647
|
+
dataUrl: external_exports.string().optional().describe("content you HOLD, max 256KB decoded. Two right uses: content you generated in-conversation, or a small file in YOUR OWN sandbox when you are a cloud-sandboxed assistant with no network egress (Claude apps/Cowork) \u2014 there dataUrl is the CORRECT way to send it. Never base64 a file on the USER machine (pass `path` instead), and never anything over 256KB."),
|
|
63487
63648
|
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
|
|
63488
63649
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
63489
63650
|
}
|
|
63490
63651
|
},
|
|
63491
63652
|
async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
63492
63653
|
let name, mime, url;
|
|
63654
|
+
let inlineBytes = null;
|
|
63493
63655
|
if (path3) {
|
|
63494
63656
|
if (!ctx.local) {
|
|
63495
|
-
const r = await remoteUploadCommand(c, ctx, { to }, path3, caption, `@${safeHandle(to)}
|
|
63657
|
+
const r = await remoteUploadCommand(c, ctx, { to }, path3, caption, `@${safeHandle(to)}`, extra?.signal);
|
|
63496
63658
|
return "ok" in r ? ok3(r.ok) : fail(r.fail);
|
|
63497
63659
|
}
|
|
63498
63660
|
const abs = resolveLocalFile(path3);
|
|
@@ -63504,10 +63666,11 @@ ${fp}`);
|
|
|
63504
63666
|
mime = mimeOf(name);
|
|
63505
63667
|
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
63506
63668
|
} else if (dataUrl && fileName) {
|
|
63507
|
-
const
|
|
63508
|
-
if (
|
|
63669
|
+
const parsed = parseInlineDataUrl(dataUrl);
|
|
63670
|
+
if ("error" in parsed) return fail(parsed.error);
|
|
63509
63671
|
name = fileName;
|
|
63510
|
-
mime =
|
|
63672
|
+
mime = parsed.mime;
|
|
63673
|
+
inlineBytes = parsed.bytes;
|
|
63511
63674
|
url = dataUrl;
|
|
63512
63675
|
} else if (fileUrl) {
|
|
63513
63676
|
try {
|
|
@@ -63517,10 +63680,10 @@ ${fp}`);
|
|
|
63517
63680
|
}
|
|
63518
63681
|
} else {
|
|
63519
63682
|
return fail(
|
|
63520
|
-
"provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`.
|
|
63683
|
+
"provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. File in YOUR OWN sandbox (you are a cloud-sandboxed assistant)? \u2264256KB \u2192 base64 it into `dataUrl` + `fileName`; larger \u2192 call again with the file's sandbox `path` to mint a one-tap upload LINK, present the file for download, and have the user open the link and pick it. File attached in this chat that YOU can't read? Ask the user to save it to their computer, then call again with that path."
|
|
63521
63684
|
);
|
|
63522
63685
|
}
|
|
63523
|
-
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
63686
|
+
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
63524
63687
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
63525
63688
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
63526
63689
|
if (isStateless(c)) {
|
|
@@ -64257,21 +64420,22 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
64257
64420
|
"jefri_send_group_file",
|
|
64258
64421
|
{
|
|
64259
64422
|
title: "Send a file to a group",
|
|
64260
|
-
description: "Send a file/image to a GROUP by its group id.
|
|
64423
|
+
description: ctx.local ? "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). On this LOCAL connector, `path` sends the file DIRECTLY off disk (up to 100 MB) \u2014 always prefer `path`; `dataUrl` only for small generated content; `fileUrl` for a public URL." : "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). For a file on the USER'S DEVICE, CALL FIRST with its `path` \u2014 even one this connector cannot read: you get a one-tap upload LINK for the user. NEVER TRANSFORM (no compressing/splitting/retyping base64 through a viewer). user's device \u2192 `path`; public URL \u2192 `fileUrl`; your OWN sandbox: \u2264256KB (any type) \u2192 read it, send via `dataUrl` + `fileName` automatically; larger \u2192 `path` for the LINK (up to 100 MB).",
|
|
64261
64424
|
inputSchema: {
|
|
64262
64425
|
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
64263
64426
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
64264
64427
|
caption: external_exports.string().optional().describe("optional text caption"),
|
|
64265
|
-
dataUrl: external_exports.string().optional().describe("
|
|
64428
|
+
dataUrl: external_exports.string().optional().describe("content you HOLD, max 256KB decoded. Two right uses: content you generated in-conversation, or a small file in YOUR OWN sandbox when you are a cloud-sandboxed assistant with no network egress (Claude apps/Cowork) \u2014 there dataUrl is the CORRECT way to send it. Never base64 a file on the USER machine (pass `path` instead), and never anything over 256KB."),
|
|
64266
64429
|
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
|
|
64267
64430
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
64268
64431
|
}
|
|
64269
64432
|
},
|
|
64270
64433
|
async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
64271
64434
|
let name, mime, url;
|
|
64435
|
+
let inlineBytes = null;
|
|
64272
64436
|
if (path3) {
|
|
64273
64437
|
if (!ctx.local) {
|
|
64274
|
-
const r = await remoteUploadCommand(c, ctx, { groupId }, path3, caption, "the group");
|
|
64438
|
+
const r = await remoteUploadCommand(c, ctx, { groupId }, path3, caption, "the group", extra?.signal);
|
|
64275
64439
|
return "ok" in r ? ok3(r.ok) : fail(r.fail);
|
|
64276
64440
|
}
|
|
64277
64441
|
const abs = resolveLocalFile(path3);
|
|
@@ -64281,10 +64445,11 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
64281
64445
|
mime = mimeOf(name);
|
|
64282
64446
|
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
64283
64447
|
} else if (dataUrl && fileName) {
|
|
64284
|
-
const
|
|
64285
|
-
if (
|
|
64448
|
+
const parsed = parseInlineDataUrl(dataUrl);
|
|
64449
|
+
if ("error" in parsed) return fail(parsed.error);
|
|
64286
64450
|
name = fileName;
|
|
64287
|
-
mime =
|
|
64451
|
+
mime = parsed.mime;
|
|
64452
|
+
inlineBytes = parsed.bytes;
|
|
64288
64453
|
url = dataUrl;
|
|
64289
64454
|
} else if (fileUrl) {
|
|
64290
64455
|
try {
|
|
@@ -64294,10 +64459,10 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
64294
64459
|
}
|
|
64295
64460
|
} else {
|
|
64296
64461
|
return fail(
|
|
64297
|
-
"provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`.
|
|
64462
|
+
"provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. File in YOUR OWN sandbox (you are a cloud-sandboxed assistant)? \u2264256KB \u2192 base64 it into `dataUrl` + `fileName`; larger \u2192 call again with the file's sandbox `path` to mint a one-tap upload LINK, present the file for download, and have the user open the link and pick it. File attached in this chat that YOU can't read? Ask the user to save it to their computer, then call again with that path."
|
|
64298
64463
|
);
|
|
64299
64464
|
}
|
|
64300
|
-
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
64465
|
+
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
64301
64466
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
64302
64467
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
64303
64468
|
if (isStateless(c)) {
|
package/dist/index.js
CHANGED
|
@@ -42155,9 +42155,28 @@ var safeSaveName = (p) => {
|
|
|
42155
42155
|
var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
|
|
42156
42156
|
var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
|
|
42157
42157
|
var psPathArg = (p) => p.startsWith("~/") || p.startsWith("~\\") ? `($HOME + ${psq(p.slice(1))})` : psq(p);
|
|
42158
|
-
var isTextyFile = (mime, name) => /^text\/|json|xml|javascript|typescript|markdown|csv|ya?ml|x-sh|toml/i.test(mime) ||
|
|
42158
|
+
var isTextyFile = (mime, name) => /^text\/|json|xml|javascript|typescript|markdown|csv|ya?ml|x-sh|toml/i.test(mime) || // The extension list matters because stored MIMEs are often octet-stream
|
|
42159
|
+
// (the taller-norte.html case: an HTML file fell to the link fallback
|
|
42160
|
+
// because .html was missing here — code files must come back READABLE).
|
|
42161
|
+
/\.(md|txt|json|csv|ya?ml|log|ts|tsx|js|jsx|mjs|py|rb|go|rs|java|c|h|cpp|hpp|cs|sh|sql|toml|env|cfg|ini|html?|css|scss|less|xml|vue|svelte|php|swift|kt|dart|lua)$/i.test(name);
|
|
42159
42162
|
var DATAURL_INLINE_MAX = 256 * 1024;
|
|
42160
|
-
|
|
42163
|
+
function parseInlineDataUrl(dataUrl) {
|
|
42164
|
+
const malformed = `That dataUrl is malformed \u2014 expected data:<mime>;base64,<canonical base64>. Re-encode the bytes (standard base64, correct padding, no whitespace) and try again \u2014 or pick the input that matches where the file lives (path for the user's machine, fileUrl for a public URL).`;
|
|
42165
|
+
const m = /^data:([^;,]+);base64,([A-Za-z0-9+/]*={0,2})$/.exec(dataUrl);
|
|
42166
|
+
if (!m) return { error: malformed };
|
|
42167
|
+
const payload = m[2];
|
|
42168
|
+
if (payload.length % 4 !== 0) return { error: malformed };
|
|
42169
|
+
const bytes = Buffer.from(payload, "base64");
|
|
42170
|
+
if (bytes.toString("base64") !== payload) return { error: malformed };
|
|
42171
|
+
if (bytes.length > DATAURL_INLINE_MAX)
|
|
42172
|
+
return {
|
|
42173
|
+
error: `That dataUrl is ${MB(bytes.length)} MB decoded \u2014 past the ${Math.round(DATAURL_INLINE_MAX / 1024)}KB inline limit. What to do instead:
|
|
42174
|
+
\u2022 file on the USER'S machine \u2192 call again with its \`path\` (you'll get a single-use upload command)
|
|
42175
|
+
\u2022 file reachable by URL \u2192 call again with \`fileUrl\`
|
|
42176
|
+
\u2022 file in YOUR OWN sandbox (no egress) \u2192 too big to inline: present the file to the user for download, and point them at the upload LINK this tool mints (call it with the file's path if you don't have a link yet) \u2014 they tap it, pick the downloaded file, and it delivers.`
|
|
42177
|
+
};
|
|
42178
|
+
return { mime: m[1], bytes };
|
|
42179
|
+
}
|
|
42161
42180
|
var MINT_LINK_DEADLINE_MS = (() => {
|
|
42162
42181
|
const n = Number(process.env.MCP_MINT_LINK_DEADLINE_MS);
|
|
42163
42182
|
return Number.isInteger(n) && n > 0 ? n : 1e4;
|
|
@@ -42187,30 +42206,77 @@ async function mintFileLink(hub, token, fileId) {
|
|
|
42187
42206
|
clearTimeout(killer);
|
|
42188
42207
|
}
|
|
42189
42208
|
}
|
|
42190
|
-
var forwardTip = (link2, fileName) => `\u2022
|
|
42209
|
+
var forwardTip = (link2, fileName) => `\u2022 The direct-download link is for the HUMAN to click \u2014 don't fetch it with your own web tool (hosts restrict fetches, and the link expires ~5 min; if it has expired, call jefri_download_file again for a fresh one).
|
|
42210
|
+
\u2022 To FORWARD this exact file: to a PERSON call jefri_send_file with to:"<username>", fileUrl:${JSON.stringify(link2)}, fileName:${JSON.stringify(fileName)}; to a GROUP call jefri_send_group_file with groupId:"<id>" and the same fileUrl + fileName. The connector fetches and delivers the ORIGINAL bytes (up to 25 MB).
|
|
42191
42211
|
`;
|
|
42192
|
-
async function remoteUploadCommand(c2, ctx, target, path5, caption, where) {
|
|
42212
|
+
async function remoteUploadCommand(c2, ctx, target, path5, caption, where, signal) {
|
|
42193
42213
|
const fname = crossBasename(path5);
|
|
42194
42214
|
const fmime = mimeOf(fname);
|
|
42195
42215
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
42196
42216
|
try {
|
|
42197
|
-
const
|
|
42198
|
-
|
|
42199
|
-
|
|
42200
|
-
|
|
42201
|
-
|
|
42202
|
-
|
|
42203
|
-
|
|
42204
|
-
|
|
42217
|
+
const gc = new AbortController();
|
|
42218
|
+
const onHostAbort = () => gc.abort();
|
|
42219
|
+
const gKiller = setTimeout(() => gc.abort(), MINT_LINK_DEADLINE_MS);
|
|
42220
|
+
if (signal) {
|
|
42221
|
+
if (signal.aborted) {
|
|
42222
|
+
clearTimeout(gKiller);
|
|
42223
|
+
throw new Error("cancelled");
|
|
42224
|
+
}
|
|
42225
|
+
signal.addEventListener("abort", onHostAbort, { once: true });
|
|
42226
|
+
}
|
|
42227
|
+
let mins;
|
|
42228
|
+
let uploadUrl;
|
|
42229
|
+
try {
|
|
42230
|
+
const g = await fetch(`${hub}/api/files/grant`, {
|
|
42231
|
+
method: "POST",
|
|
42232
|
+
headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
|
|
42233
|
+
body: JSON.stringify({ ...target.to ? { to: target.to } : { groupId: target.groupId }, fileName: fname, ...caption ? { caption } : {} }),
|
|
42234
|
+
signal: gc.signal
|
|
42235
|
+
});
|
|
42236
|
+
if (!g.ok) {
|
|
42237
|
+
try {
|
|
42238
|
+
await g.body?.cancel();
|
|
42239
|
+
} catch {
|
|
42240
|
+
}
|
|
42241
|
+
throw new Error(`grant service answered ${g.status}`);
|
|
42242
|
+
}
|
|
42243
|
+
const body = await g.json().catch(() => ({}));
|
|
42244
|
+
const parsedUrl = (() => {
|
|
42245
|
+
if (typeof body.uploadUrl !== "string") return null;
|
|
42246
|
+
try {
|
|
42247
|
+
const u = new URL(body.uploadUrl);
|
|
42248
|
+
return (u.protocol === "http:" || u.protocol === "https:") && u.host ? body.uploadUrl : null;
|
|
42249
|
+
} catch {
|
|
42250
|
+
return null;
|
|
42251
|
+
}
|
|
42252
|
+
})();
|
|
42253
|
+
if (!parsedUrl) throw new Error("malformed grant response: uploadUrl");
|
|
42254
|
+
if (typeof body.expiresInMs !== "number" || !Number.isFinite(body.expiresInMs) || body.expiresInMs <= 0)
|
|
42255
|
+
throw new Error("malformed grant response: expiresInMs");
|
|
42256
|
+
uploadUrl = parsedUrl;
|
|
42257
|
+
mins = Math.round(body.expiresInMs / 6e4);
|
|
42258
|
+
} finally {
|
|
42259
|
+
gc.abort();
|
|
42260
|
+
clearTimeout(gKiller);
|
|
42261
|
+
if (signal) signal.removeEventListener("abort", onHostAbort);
|
|
42262
|
+
}
|
|
42263
|
+
{
|
|
42205
42264
|
return {
|
|
42206
|
-
ok: `This cloud connector can't read
|
|
42265
|
+
ok: `This cloud connector can't read that path \u2014 two fast paths, pick one:
|
|
42266
|
+
|
|
42267
|
+
\u{1F517} EASIEST \u2014 no terminal: have the USER open this link in any browser (phone works) and pick the file; it delivers straight to ${where}:
|
|
42268
|
+
${uploadUrl}
|
|
42269
|
+
|
|
42270
|
+
\u{1F4BB} Or run this ONE command on a machine that HAS the file and has internet (the user's terminal \u2014 NOT a sandboxed assistant environment without egress):
|
|
42207
42271
|
|
|
42208
42272
|
curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path5)}
|
|
42209
42273
|
|
|
42210
42274
|
Windows PowerShell instead:
|
|
42211
42275
|
Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path5)}
|
|
42212
42276
|
|
|
42213
|
-
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,...}).
|
|
42277
|
+
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,...}).
|
|
42278
|
+
\u26A0\uFE0F If YOU are a sandboxed assistant with no network egress and the file lives in YOUR sandbox: this command cannot run anywhere that can see the file. Instead \u2014 if the file is 256KB or less, call this tool again with dataUrl (base64 it) + fileName; if larger, PRESENT the file to the user for download and tell them to open the LINK ABOVE and pick the downloaded file. Do NOT paste this curl into your own shell.
|
|
42279
|
+
Never base64 anything over 256KB through dataUrl \u2014 that is minutes of model output and it will be refused.`
|
|
42214
42280
|
};
|
|
42215
42281
|
}
|
|
42216
42282
|
} catch {
|
|
@@ -42220,7 +42286,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
|
|
|
42220
42286
|
const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
42221
42287
|
const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path5)}`;
|
|
42222
42288
|
return {
|
|
42223
|
-
fail: `This is the cloud connector, so it can't read "${fname}"
|
|
42289
|
+
fail: `This is the cloud connector, so it can't read "${fname}" from that path, and the upload-link service didn't answer. Options:
|
|
42224
42290
|
|
|
42225
42291
|
\u2705 open the Jefri Chat web app and DRAG "${fname}" into the chat with ${where}:
|
|
42226
42292
|
${dropLink}
|
|
@@ -42228,7 +42294,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
|
|
|
42228
42294
|
\u2014 or from a terminal (set JEFRI_TOKEN from the app's Connect dialog first):
|
|
42229
42295
|
${cmd}
|
|
42230
42296
|
|
|
42231
|
-
|
|
42297
|
+
Sandboxed assistant holding the file yourself? 256KB or less: send it via dataUrl + fileName; larger: present the file to the user for download and retry this tool with the path \u2014 the minted upload link lets them pick the downloaded file. Never base64 anything over 256KB through dataUrl.`
|
|
42232
42298
|
};
|
|
42233
42299
|
}
|
|
42234
42300
|
async function fetchHistory2(c2, convId) {
|
|
@@ -42254,7 +42320,22 @@ var MIME = {
|
|
|
42254
42320
|
".txt": "text/plain",
|
|
42255
42321
|
".md": "text/markdown",
|
|
42256
42322
|
".json": "application/json",
|
|
42257
|
-
".zip": "application/zip"
|
|
42323
|
+
".zip": "application/zip",
|
|
42324
|
+
// Web/code types (the taller-norte.html case): without these an .html send
|
|
42325
|
+
// shipped as octet-stream, so the web app couldn't render it and receivers
|
|
42326
|
+
// saw "binary". Bytes always travelled fine — the LABEL was wrong.
|
|
42327
|
+
".html": "text/html",
|
|
42328
|
+
".htm": "text/html",
|
|
42329
|
+
".css": "text/css",
|
|
42330
|
+
".js": "text/javascript",
|
|
42331
|
+
".mjs": "text/javascript",
|
|
42332
|
+
".ts": "text/plain",
|
|
42333
|
+
".tsx": "text/plain",
|
|
42334
|
+
".csv": "text/csv",
|
|
42335
|
+
".xml": "application/xml",
|
|
42336
|
+
".yaml": "text/yaml",
|
|
42337
|
+
".yml": "text/yaml",
|
|
42338
|
+
".log": "text/plain"
|
|
42258
42339
|
};
|
|
42259
42340
|
var mimeOf = (name) => MIME[np4.extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
42260
42341
|
function fmtTime(iso) {
|
|
@@ -43197,25 +43278,106 @@ ${fp}`);
|
|
|
43197
43278
|
return ok3(`Sent private E2E group file.`);
|
|
43198
43279
|
})
|
|
43199
43280
|
);
|
|
43281
|
+
if (!ctx.local) server2.registerTool(
|
|
43282
|
+
"jefri_upload_link",
|
|
43283
|
+
{
|
|
43284
|
+
title: "Get an upload link (any file up to 100 MB, full quality)",
|
|
43285
|
+
description: "Get a ONE-TAP UPLOAD LINK to send files or images \u2014 up to 5 PER LINK via `fileNames`, up to 100 MB each, FULL quality, no base64, no compression, no path needed. ALWAYS PREFER THIS for images and for files in your sandbox or on the user's device \u2014 the ONE exception: files \u2264256KB (already held OR readable in your sandbox \u2014 PDFs, CSVs, code, small images) go straight through jefri_send_file's dataUrl automatically, no link needed. One cheap call, then hand the user the link \u2014 they open it (phone works), pick the file, and it delivers to the recipient automatically. Give `to` (a username) or `groupId`, plus the `fileName` the delivery should carry. The link is single-use and expires in ~15 minutes; mint a fresh one any time. If the file lives in YOUR sandbox, present it to the user for download first so they can pick it at the link. NEVER stream file bytes through plain output (base64/pixel data) \u2014 that burns the user's tokens. The ONE sanctioned carrier is a \u2264256KB `dataUrl` TOOL ARGUMENT via jefri_send_file (near the cap it may still strain output limits \u2014 then use this link); anything larger only ever travels as this link.",
|
|
43286
|
+
inputSchema: {
|
|
43287
|
+
to: external_exports.string().optional().describe("recipient username (or use groupId)"),
|
|
43288
|
+
groupId: external_exports.string().optional().describe("group id (from jefri_groups) instead of `to`"),
|
|
43289
|
+
fileName: external_exports.string().optional().describe("the name the delivered file should carry, e.g. photo.png (single file)"),
|
|
43290
|
+
fileNames: external_exports.array(external_exports.string()).optional().describe('for a MULTI-file link (2\u20135): the delivery names in order, e.g. ["foto-1.jpg","foto-2.jpg"] \u2014 one link, the user picks them all'),
|
|
43291
|
+
caption: external_exports.string().optional().describe("optional text caption delivered with the file")
|
|
43292
|
+
}
|
|
43293
|
+
},
|
|
43294
|
+
async (args, extra) => withClient(async (c2) => {
|
|
43295
|
+
const to = typeof args.to === "string" && args.to.trim() ? args.to.trim() : void 0;
|
|
43296
|
+
const groupId = typeof args.groupId === "string" && args.groupId.trim() ? args.groupId.trim() : void 0;
|
|
43297
|
+
if (!to && !groupId) return fail("give `to` (a username) or `groupId`");
|
|
43298
|
+
if (to && groupId) return fail("give either `to` or `groupId`, not both");
|
|
43299
|
+
const names = Array.isArray(args.fileNames) ? args.fileNames.map((n) => String(n ?? "").trim()).filter(Boolean).slice(0, 5) : [];
|
|
43300
|
+
const fileName = String(args.fileName ?? "").trim();
|
|
43301
|
+
if (!fileName && names.length === 0) return fail("give `fileName` (one file) or `fileNames` (2\u20135 files) \u2014 deliveries carry these names");
|
|
43302
|
+
if (Array.isArray(args.fileNames) && args.fileNames.length > 5)
|
|
43303
|
+
return fail("a link carries at most 5 files \u2014 mint another link for more");
|
|
43304
|
+
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
43305
|
+
const gc = new AbortController();
|
|
43306
|
+
const onHostAbort = () => gc.abort();
|
|
43307
|
+
const gKiller = setTimeout(() => gc.abort(), MINT_LINK_DEADLINE_MS);
|
|
43308
|
+
const signal = extra?.signal;
|
|
43309
|
+
if (signal) {
|
|
43310
|
+
if (signal.aborted) {
|
|
43311
|
+
clearTimeout(gKiller);
|
|
43312
|
+
return fail("cancelled");
|
|
43313
|
+
}
|
|
43314
|
+
signal.addEventListener("abort", onHostAbort, { once: true });
|
|
43315
|
+
}
|
|
43316
|
+
try {
|
|
43317
|
+
const g = await fetch(`${hub}/api/files/grant`, {
|
|
43318
|
+
method: "POST",
|
|
43319
|
+
headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
|
|
43320
|
+
body: JSON.stringify({ ...to ? { to } : { groupId }, ...names.length ? { fileNames: names } : { fileName }, ...args.caption ? { caption: args.caption } : {} }),
|
|
43321
|
+
signal: gc.signal
|
|
43322
|
+
});
|
|
43323
|
+
if (!g.ok) {
|
|
43324
|
+
try {
|
|
43325
|
+
await g.body?.cancel();
|
|
43326
|
+
} catch {
|
|
43327
|
+
}
|
|
43328
|
+
return fail(`the upload-link service answered ${g.status} \u2014 try again shortly. Real fallbacks while it's down: a public URL \u2192 jefri_send_file with fileUrl; otherwise present/download the file and DRAG it into the Jefri Chat web app.`);
|
|
43329
|
+
}
|
|
43330
|
+
const body = await g.json().catch(() => ({}));
|
|
43331
|
+
const parsedUrl = (() => {
|
|
43332
|
+
const raw = names.length ? body.batchUrl : body.uploadUrl;
|
|
43333
|
+
if (typeof raw !== "string") return null;
|
|
43334
|
+
try {
|
|
43335
|
+
const u = new URL(raw);
|
|
43336
|
+
return (u.protocol === "http:" || u.protocol === "https:") && u.host ? raw : null;
|
|
43337
|
+
} catch {
|
|
43338
|
+
return null;
|
|
43339
|
+
}
|
|
43340
|
+
})();
|
|
43341
|
+
if (!parsedUrl || typeof body.expiresInMs !== "number" || !Number.isFinite(body.expiresInMs) || body.expiresInMs <= 0)
|
|
43342
|
+
return fail("the upload-link service answered malformed data \u2014 try again shortly. Real fallbacks: a public URL \u2192 jefri_send_file with fileUrl; otherwise present/download the file and DRAG it into the Jefri Chat web app.");
|
|
43343
|
+
const mins = Math.round(body.expiresInMs / 6e4);
|
|
43344
|
+
const where = to ? `@${safeHandle(to)}` : "the group";
|
|
43345
|
+
const delivers = names.length ? `${names.length} files: ${names.map((n) => `\u{1F4CE} ${n}`).join(", ")}` : `\u{1F4CE} ${fileName}`;
|
|
43346
|
+
return ok3(
|
|
43347
|
+
`\u{1F517} One-tap upload link for ${where} (delivers ${delivers}; valid ~${mins} min, single-use):
|
|
43348
|
+
${parsedUrl}
|
|
43349
|
+
|
|
43350
|
+
SAY THIS to the user (adapt their language): "Here's your upload link \u2014 open it and pick the ${names.length ? "files" : "file"}; ${names.length ? "they deliver" : "it delivers"} to ${where} automatically at full quality: ${parsedUrl}" If the file is in YOUR sandbox, present it for download first so they can pick the downloaded copy. Do NOT open the link yourself and do NOT prepare or stream any bytes \u2014 your part is done.`
|
|
43351
|
+
);
|
|
43352
|
+
} catch (e) {
|
|
43353
|
+
return fail(`could not mint the upload link: ${e?.message ?? e} \u2014 try again shortly. Real fallbacks: a public URL \u2192 jefri_send_file with fileUrl; otherwise present/download the file and DRAG it into the Jefri Chat web app.`);
|
|
43354
|
+
} finally {
|
|
43355
|
+
gc.abort();
|
|
43356
|
+
clearTimeout(gKiller);
|
|
43357
|
+
if (signal) signal.removeEventListener("abort", onHostAbort);
|
|
43358
|
+
}
|
|
43359
|
+
})
|
|
43360
|
+
);
|
|
43200
43361
|
server2.registerTool(
|
|
43201
43362
|
"jefri_send_file",
|
|
43202
43363
|
{
|
|
43203
43364
|
title: "Send a file, PDF, or image",
|
|
43204
|
-
description: "Send a file to
|
|
43365
|
+
description: ctx.local ? "Send a file to a Jefri Chat user/agent \u2014 PDF, image, video, any document. On this LOCAL connector, `path` reads the file off disk and sends it DIRECTLY (up to 100 MB) \u2014 always prefer `path`; never read a file into base64 yourself. `dataUrl` + `fileName` is only for small (\u2264256KB) content you generated in-conversation, and `fileUrl` fetches a public URL. Optional `caption` always." : "Send a file to a Jefri Chat user/agent. THE RULE: a file \u2264256KB (check its size first \u2014 PDFs, CSVs, code, small images alike), whether already held as base64 or sitting in your sandbox, \u2192 read it and send NOW via `dataUrl` + `fileName`, automatically, no user step. ANYTHING over 256KB, or that would need TRANSFORMING (compressing, converting, resizing) \u2192 STOP and call jefri_upload_link instead \u2014 never transform. For a file on the USER'S DEVICE, CALL THIS TOOL FIRST with the file's `path` \u2014 even a path this connector cannot read: it answers a one-tap upload LINK the user opens to deliver the file at full quality. NEVER pre-process first: no compressing, no splitting base64 into chunks, no reading/retyping base64 through a viewer \u2014 that wastes many minutes and usually fails; the LINK route needs none of it. Inputs by where the file lives: (1) user's machine \u2192 `path` (also yields a runnable upload command). (2) public URL \u2192 `fileUrl` (connector fetches, \u226425MB). (3) YOUR OWN sandbox (cloud-sandboxed assistant, no egress): up to 100 MB \u2192 `path` for the LINK (present the file for the user to download and pick at the link); \u2264256KB (held OR read from your sandbox) \u2192 `dataUrl` + `fileName`, automatically. Large images: the LINK, full quality \u2014 do not offer to compress. Optional `caption` always.",
|
|
43205
43366
|
inputSchema: {
|
|
43206
43367
|
to: external_exports.string().describe("recipient username"),
|
|
43207
43368
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
43208
43369
|
caption: external_exports.string().optional().describe("optional text caption to send with it"),
|
|
43209
|
-
dataUrl: external_exports.string().optional().describe("
|
|
43370
|
+
dataUrl: external_exports.string().optional().describe("content you HOLD, max 256KB decoded. Two right uses: content you generated in-conversation, or a small file in YOUR OWN sandbox when you are a cloud-sandboxed assistant with no network egress (Claude apps/Cowork) \u2014 there dataUrl is the CORRECT way to send it. Never base64 a file on the USER machine (pass `path` instead), and never anything over 256KB."),
|
|
43210
43371
|
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
|
|
43211
43372
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
43212
43373
|
}
|
|
43213
43374
|
},
|
|
43214
43375
|
async ({ to, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
|
|
43215
43376
|
let name, mime, url;
|
|
43377
|
+
let inlineBytes = null;
|
|
43216
43378
|
if (path5) {
|
|
43217
43379
|
if (!ctx.local) {
|
|
43218
|
-
const r = await remoteUploadCommand(c2, ctx, { to }, path5, caption, `@${safeHandle(to)}
|
|
43380
|
+
const r = await remoteUploadCommand(c2, ctx, { to }, path5, caption, `@${safeHandle(to)}`, extra?.signal);
|
|
43219
43381
|
return "ok" in r ? ok3(r.ok) : fail(r.fail);
|
|
43220
43382
|
}
|
|
43221
43383
|
const abs = resolveLocalFile(path5);
|
|
@@ -43227,10 +43389,11 @@ ${fp}`);
|
|
|
43227
43389
|
mime = mimeOf(name);
|
|
43228
43390
|
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
43229
43391
|
} else if (dataUrl && fileName) {
|
|
43230
|
-
const
|
|
43231
|
-
if (
|
|
43392
|
+
const parsed = parseInlineDataUrl(dataUrl);
|
|
43393
|
+
if ("error" in parsed) return fail(parsed.error);
|
|
43232
43394
|
name = fileName;
|
|
43233
|
-
mime =
|
|
43395
|
+
mime = parsed.mime;
|
|
43396
|
+
inlineBytes = parsed.bytes;
|
|
43234
43397
|
url = dataUrl;
|
|
43235
43398
|
} else if (fileUrl) {
|
|
43236
43399
|
try {
|
|
@@ -43240,10 +43403,10 @@ ${fp}`);
|
|
|
43240
43403
|
}
|
|
43241
43404
|
} else {
|
|
43242
43405
|
return fail(
|
|
43243
|
-
"provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`.
|
|
43406
|
+
"provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. File in YOUR OWN sandbox (you are a cloud-sandboxed assistant)? \u2264256KB \u2192 base64 it into `dataUrl` + `fileName`; larger \u2192 call again with the file's sandbox `path` to mint a one-tap upload LINK, present the file for download, and have the user open the link and pick it. File attached in this chat that YOU can't read? Ask the user to save it to their computer, then call again with that path."
|
|
43244
43407
|
);
|
|
43245
43408
|
}
|
|
43246
|
-
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
43409
|
+
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
43247
43410
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
43248
43411
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
43249
43412
|
if (isStateless(c2)) {
|
|
@@ -43980,21 +44143,22 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
43980
44143
|
"jefri_send_group_file",
|
|
43981
44144
|
{
|
|
43982
44145
|
title: "Send a file to a group",
|
|
43983
|
-
description: "Send a file/image to a GROUP by its group id.
|
|
44146
|
+
description: ctx.local ? "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). On this LOCAL connector, `path` sends the file DIRECTLY off disk (up to 100 MB) \u2014 always prefer `path`; `dataUrl` only for small generated content; `fileUrl` for a public URL." : "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). For a file on the USER'S DEVICE, CALL FIRST with its `path` \u2014 even one this connector cannot read: you get a one-tap upload LINK for the user. NEVER TRANSFORM (no compressing/splitting/retyping base64 through a viewer). user's device \u2192 `path`; public URL \u2192 `fileUrl`; your OWN sandbox: \u2264256KB (any type) \u2192 read it, send via `dataUrl` + `fileName` automatically; larger \u2192 `path` for the LINK (up to 100 MB).",
|
|
43984
44147
|
inputSchema: {
|
|
43985
44148
|
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
43986
44149
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
43987
44150
|
caption: external_exports.string().optional().describe("optional text caption"),
|
|
43988
|
-
dataUrl: external_exports.string().optional().describe("
|
|
44151
|
+
dataUrl: external_exports.string().optional().describe("content you HOLD, max 256KB decoded. Two right uses: content you generated in-conversation, or a small file in YOUR OWN sandbox when you are a cloud-sandboxed assistant with no network egress (Claude apps/Cowork) \u2014 there dataUrl is the CORRECT way to send it. Never base64 a file on the USER machine (pass `path` instead), and never anything over 256KB."),
|
|
43989
44152
|
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
|
|
43990
44153
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
43991
44154
|
}
|
|
43992
44155
|
},
|
|
43993
44156
|
async ({ groupId, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
|
|
43994
44157
|
let name, mime, url;
|
|
44158
|
+
let inlineBytes = null;
|
|
43995
44159
|
if (path5) {
|
|
43996
44160
|
if (!ctx.local) {
|
|
43997
|
-
const r = await remoteUploadCommand(c2, ctx, { groupId }, path5, caption, "the group");
|
|
44161
|
+
const r = await remoteUploadCommand(c2, ctx, { groupId }, path5, caption, "the group", extra?.signal);
|
|
43998
44162
|
return "ok" in r ? ok3(r.ok) : fail(r.fail);
|
|
43999
44163
|
}
|
|
44000
44164
|
const abs = resolveLocalFile(path5);
|
|
@@ -44004,10 +44168,11 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
44004
44168
|
mime = mimeOf(name);
|
|
44005
44169
|
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
44006
44170
|
} else if (dataUrl && fileName) {
|
|
44007
|
-
const
|
|
44008
|
-
if (
|
|
44171
|
+
const parsed = parseInlineDataUrl(dataUrl);
|
|
44172
|
+
if ("error" in parsed) return fail(parsed.error);
|
|
44009
44173
|
name = fileName;
|
|
44010
|
-
mime =
|
|
44174
|
+
mime = parsed.mime;
|
|
44175
|
+
inlineBytes = parsed.bytes;
|
|
44011
44176
|
url = dataUrl;
|
|
44012
44177
|
} else if (fileUrl) {
|
|
44013
44178
|
try {
|
|
@@ -44017,10 +44182,10 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
44017
44182
|
}
|
|
44018
44183
|
} else {
|
|
44019
44184
|
return fail(
|
|
44020
|
-
"provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`.
|
|
44185
|
+
"provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. File in YOUR OWN sandbox (you are a cloud-sandboxed assistant)? \u2264256KB \u2192 base64 it into `dataUrl` + `fileName`; larger \u2192 call again with the file's sandbox `path` to mint a one-tap upload LINK, present the file for download, and have the user open the link and pick it. File attached in this chat that YOU can't read? Ask the user to save it to their computer, then call again with that path."
|
|
44021
44186
|
);
|
|
44022
44187
|
}
|
|
44023
|
-
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
44188
|
+
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
44024
44189
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
44025
44190
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
44026
44191
|
if (isStateless(c2)) {
|
package/package.json
CHANGED