jefrichat-mcp 0.49.3 → 0.49.5
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 +179 -49
- package/dist/index.js +179 -49
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -62435,31 +62435,128 @@ 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
|
-
|
|
62441
|
-
|
|
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
|
+
}
|
|
62460
|
+
var MINT_LINK_DEADLINE_MS = (() => {
|
|
62461
|
+
const n = Number(process.env.MCP_MINT_LINK_DEADLINE_MS);
|
|
62462
|
+
return Number.isInteger(n) && n > 0 ? n : 1e4;
|
|
62463
|
+
})();
|
|
62464
|
+
async function mintFileLink(hub, token, fileId) {
|
|
62465
|
+
const ac = new AbortController();
|
|
62466
|
+
const killer = setTimeout(() => ac.abort(), MINT_LINK_DEADLINE_MS);
|
|
62467
|
+
try {
|
|
62468
|
+
const lr = await fetch(`${hub}/api/files/${fileId}/link`, {
|
|
62469
|
+
method: "POST",
|
|
62470
|
+
headers: { authorization: `Bearer ${token}` },
|
|
62471
|
+
signal: ac.signal
|
|
62472
|
+
});
|
|
62473
|
+
if (!lr.ok) {
|
|
62474
|
+
try {
|
|
62475
|
+
await lr.body?.cancel();
|
|
62476
|
+
} catch {
|
|
62477
|
+
}
|
|
62478
|
+
return null;
|
|
62479
|
+
}
|
|
62480
|
+
const body = await lr.json();
|
|
62481
|
+
return body?.url ?? null;
|
|
62482
|
+
} catch {
|
|
62483
|
+
return null;
|
|
62484
|
+
} finally {
|
|
62485
|
+
ac.abort();
|
|
62486
|
+
clearTimeout(killer);
|
|
62487
|
+
}
|
|
62488
|
+
}
|
|
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).
|
|
62491
|
+
`;
|
|
62492
|
+
async function remoteUploadCommand(c, ctx, target, path3, caption, where, signal) {
|
|
62442
62493
|
const fname = crossBasename(path3);
|
|
62443
62494
|
const fmime = mimeOf(fname);
|
|
62444
62495
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
62445
62496
|
try {
|
|
62446
|
-
const
|
|
62447
|
-
|
|
62448
|
-
|
|
62449
|
-
|
|
62450
|
-
|
|
62451
|
-
|
|
62452
|
-
|
|
62453
|
-
|
|
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
|
+
{
|
|
62454
62544
|
return {
|
|
62455
|
-
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):
|
|
62456
62551
|
|
|
62457
62552
|
curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path3)}
|
|
62458
62553
|
|
|
62459
62554
|
Windows PowerShell instead:
|
|
62460
62555
|
Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path3)}
|
|
62461
62556
|
|
|
62462
|
-
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.`
|
|
62463
62560
|
};
|
|
62464
62561
|
}
|
|
62465
62562
|
} catch {
|
|
@@ -62469,7 +62566,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
|
|
|
62469
62566
|
const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
62470
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)}`;
|
|
62471
62568
|
return {
|
|
62472
|
-
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:
|
|
62473
62570
|
|
|
62474
62571
|
\u2705 open the Jefri Chat web app and DRAG "${fname}" into the chat with ${where}:
|
|
62475
62572
|
${dropLink}
|
|
@@ -62477,7 +62574,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
|
|
|
62477
62574
|
\u2014 or from a terminal (set JEFRI_TOKEN from the app's Connect dialog first):
|
|
62478
62575
|
${cmd}
|
|
62479
62576
|
|
|
62480
|
-
|
|
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.`
|
|
62481
62578
|
};
|
|
62482
62579
|
}
|
|
62483
62580
|
async function fetchHistory(c, convId) {
|
|
@@ -62503,7 +62600,22 @@ var MIME = {
|
|
|
62503
62600
|
".txt": "text/plain",
|
|
62504
62601
|
".md": "text/markdown",
|
|
62505
62602
|
".json": "application/json",
|
|
62506
|
-
".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"
|
|
62507
62619
|
};
|
|
62508
62620
|
var mimeOf = (name) => MIME[np3.extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
62509
62621
|
function fmtTime(iso) {
|
|
@@ -62574,9 +62686,13 @@ function resolveLocalFile(rawPath) {
|
|
|
62574
62686
|
return null;
|
|
62575
62687
|
}
|
|
62576
62688
|
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
62577
|
-
var
|
|
62578
|
-
|
|
62579
|
-
|
|
62689
|
+
var posIntEnv = (v, dflt) => {
|
|
62690
|
+
const n = Number(v);
|
|
62691
|
+
return Number.isInteger(n) && n > 0 ? n : dflt;
|
|
62692
|
+
};
|
|
62693
|
+
var MAX_REMOTE_URL_BYTES = posIntEnv(process.env.MCP_MAX_REMOTE_URL_BYTES, 25 * 1024 * 1024);
|
|
62694
|
+
var MAX_CONCURRENT_REMOTE_DOWNLOADS = posIntEnv(process.env.MCP_MAX_REMOTE_DL, 3);
|
|
62695
|
+
var REMOTE_DOWNLOAD_DEADLINE_MS = posIntEnv(process.env.MCP_REMOTE_DL_DEADLINE_MS, 3e4);
|
|
62580
62696
|
var activeRemoteDownloads = 0;
|
|
62581
62697
|
var MB = (n) => (n / 1024 / 1024).toFixed(1);
|
|
62582
62698
|
var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
|
|
@@ -63443,21 +63559,22 @@ ${fp}`);
|
|
|
63443
63559
|
"jefri_send_file",
|
|
63444
63560
|
{
|
|
63445
63561
|
title: "Send a file, PDF, or image",
|
|
63446
|
-
description: "Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document.
|
|
63562
|
+
description: "Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. PICK THE INPUT BY WHERE THE FILE LIVES: (1) on the USER'S machine \u2192 give `path` (e.g. ~/Downloads/report.pdf); even on the remote/cloud connector this answers ONE ready-to-run terminal command for the user (a single-use upload link) \u2014 never base64 a user-disk file. (2) reachable by public URL \u2192 pass `fileUrl`; the connector fetches the bytes itself (up to 25MB). (3) in YOUR OWN sandbox (you are a cloud-sandboxed assistant with no network egress, e.g. Claude apps/Cowork) and \u2264256KB \u2192 send it as `dataUrl` + `fileName`; over 256KB \u2192 call this tool with the file's sandbox `path`: you get a one-tap upload LINK \u2014 present the file to the user for download and have them open the link and pick it. Optional `caption` in every case.",
|
|
63447
63563
|
inputSchema: {
|
|
63448
63564
|
to: external_exports.string().describe("recipient username"),
|
|
63449
63565
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
63450
63566
|
caption: external_exports.string().optional().describe("optional text caption to send with it"),
|
|
63451
|
-
dataUrl: external_exports.string().optional().describe("
|
|
63567
|
+
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."),
|
|
63452
63568
|
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
|
|
63453
63569
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
63454
63570
|
}
|
|
63455
63571
|
},
|
|
63456
63572
|
async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
63457
63573
|
let name, mime, url;
|
|
63574
|
+
let inlineBytes = null;
|
|
63458
63575
|
if (path3) {
|
|
63459
63576
|
if (!ctx.local) {
|
|
63460
|
-
const r = await remoteUploadCommand(c, ctx, { to }, path3, caption, `@${safeHandle(to)}
|
|
63577
|
+
const r = await remoteUploadCommand(c, ctx, { to }, path3, caption, `@${safeHandle(to)}`, extra?.signal);
|
|
63461
63578
|
return "ok" in r ? ok3(r.ok) : fail(r.fail);
|
|
63462
63579
|
}
|
|
63463
63580
|
const abs = resolveLocalFile(path3);
|
|
@@ -63469,10 +63586,11 @@ ${fp}`);
|
|
|
63469
63586
|
mime = mimeOf(name);
|
|
63470
63587
|
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
63471
63588
|
} else if (dataUrl && fileName) {
|
|
63472
|
-
const
|
|
63473
|
-
if (
|
|
63589
|
+
const parsed = parseInlineDataUrl(dataUrl);
|
|
63590
|
+
if ("error" in parsed) return fail(parsed.error);
|
|
63474
63591
|
name = fileName;
|
|
63475
|
-
mime =
|
|
63592
|
+
mime = parsed.mime;
|
|
63593
|
+
inlineBytes = parsed.bytes;
|
|
63476
63594
|
url = dataUrl;
|
|
63477
63595
|
} else if (fileUrl) {
|
|
63478
63596
|
try {
|
|
@@ -63481,9 +63599,11 @@ ${fp}`);
|
|
|
63481
63599
|
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
63482
63600
|
}
|
|
63483
63601
|
} else {
|
|
63484
|
-
return fail(
|
|
63602
|
+
return fail(
|
|
63603
|
+
"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."
|
|
63604
|
+
);
|
|
63485
63605
|
}
|
|
63486
|
-
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
63606
|
+
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
63487
63607
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
63488
63608
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
63489
63609
|
if (isStateless(c)) {
|
|
@@ -63671,7 +63791,7 @@ ${fp}`);
|
|
|
63671
63791
|
"jefri_download_file",
|
|
63672
63792
|
{
|
|
63673
63793
|
title: "Read or save a file someone sent you",
|
|
63674
|
-
description: "Get a file another agent/user sent you. For a DM give the sender's username `from`; for a GROUP file give `groupId` (from jefri_groups / jefri_inbox). Add `fileName` if there are several. TEXT files (code, markdown, JSON, CSV, logs \u2014 up to 256KB) come back with their FULL CONTENT inline, PDFs come back as EXTRACTED TEXT, and IMAGES (png/jpeg/gif/webp up to 4MB) come back as the ACTUAL PICTURE you can look at \u2014 so you can read, view and discuss all three directly, no terminal needed. Only video, very large images and other binaries return a download link. On the local connector, files are saved straight to disk. Use this when the user says 'read the file sara sent', 'download that file', 'what does the doc say', etc.",
|
|
63794
|
+
description: "Get a file another agent/user sent you. For a DM give the sender's username `from`; for a GROUP file give `groupId` (from jefri_groups / jefri_inbox). Add `fileName` if there are several. TEXT files (code, markdown, JSON, CSV, logs \u2014 up to 256KB) come back with their FULL CONTENT inline, PDFs come back as EXTRACTED TEXT plus a signed link to the ORIGINAL file (use the link with jefri_send_file's fileUrl to forward the real bytes \u2014 never reconstruct a file from text), and IMAGES (png/jpeg/gif/webp up to 4MB) come back as the ACTUAL PICTURE you can look at \u2014 so you can read, view and discuss all three directly, no terminal needed. Only video, very large images and other binaries return a download link. On the local connector, files are saved straight to disk. Use this when the user says 'read the file sara sent', 'download that file', 'what does the doc say', etc.",
|
|
63675
63795
|
inputSchema: {
|
|
63676
63796
|
from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
|
|
63677
63797
|
groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
|
|
@@ -63762,12 +63882,24 @@ ${fp}`);
|
|
|
63762
63882
|
const r = await fetch(`${hub}/api/files/${match.id}/text`, { headers: { authorization: `Bearer ${c.token}` }, signal: ac.signal });
|
|
63763
63883
|
if (r.ok) {
|
|
63764
63884
|
const body = await r.json();
|
|
63765
|
-
if (body?.text)
|
|
63885
|
+
if (body?.text) {
|
|
63886
|
+
const pdfLink = await mintFileLink(hub, c.token, match.id);
|
|
63887
|
+
const webPdf = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
63766
63888
|
return ok3(
|
|
63767
|
-
`\u{1F4CE} ${fname2} from ${where} \u2014
|
|
63889
|
+
`\u{1F4CE} ${fname2} from ${where} \u2014 THE ORIGINAL FILE:
|
|
63890
|
+
` + (pdfLink ? `\u2022 Direct download (valid ~5 minutes): ${pdfLink}
|
|
63891
|
+
` : "") + (pdfLink ? forwardTip(pdfLink, fname2) : "") + `\u2022 Web app (renders in the chat): ${webPdf}
|
|
63892
|
+
` + // The closing line must only reference what EXISTS: with
|
|
63893
|
+
// no minted link, "via the link above" pointed at nothing.
|
|
63894
|
+
(pdfLink ? `Never reconstruct a file from extracted text \u2014 forward the original via the link above.
|
|
63895
|
+
|
|
63896
|
+
` : `Never reconstruct a file from extracted text \u2014 the link service didn't answer just now; call jefri_download_file again for a fresh link, or use the web app above.
|
|
63897
|
+
|
|
63898
|
+
`) + `\u2014 Extracted text (${body.chars ?? body.text.length} chars):
|
|
63768
63899
|
|
|
63769
63900
|
` + body.text
|
|
63770
63901
|
);
|
|
63902
|
+
}
|
|
63771
63903
|
}
|
|
63772
63904
|
} finally {
|
|
63773
63905
|
clearTimeout(killer);
|
|
@@ -63828,17 +63960,11 @@ ${fp}`);
|
|
|
63828
63960
|
const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${out}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hub}/api/files/${match.id}`)}`;
|
|
63829
63961
|
const web2 = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
63830
63962
|
let directLink = "";
|
|
63831
|
-
|
|
63832
|
-
const
|
|
63833
|
-
|
|
63834
|
-
|
|
63835
|
-
|
|
63836
|
-
if (lr.ok) {
|
|
63837
|
-
const body = await lr.json();
|
|
63838
|
-
if (body?.url) directLink = `\u2022 Direct download (click it \u2014 valid ~5 minutes): ${body.url}
|
|
63839
|
-
`;
|
|
63840
|
-
}
|
|
63841
|
-
} catch {
|
|
63963
|
+
{
|
|
63964
|
+
const linkUrl = await mintFileLink(hub, c.token, match.id);
|
|
63965
|
+
if (linkUrl)
|
|
63966
|
+
directLink = `\u2022 Direct download (click it \u2014 valid ~5 minutes): ${linkUrl}
|
|
63967
|
+
` + forwardTip(linkUrl, fname2);
|
|
63842
63968
|
}
|
|
63843
63969
|
return ok3(
|
|
63844
63970
|
`\u{1F4CE} ${match.fileName} (${fmime2}) \u2014 I can't show it inline${imageWhy ? `: ${imageWhy}` : " (not a viewable type)"}.
|
|
@@ -64214,21 +64340,22 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
64214
64340
|
"jefri_send_group_file",
|
|
64215
64341
|
{
|
|
64216
64342
|
title: "Send a file to a group",
|
|
64217
|
-
description: "Send a file/image to a GROUP by its group id.
|
|
64343
|
+
description: "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). Pick the input by where the file lives: user's machine \u2192 `path`; public URL \u2192 `fileUrl`; your OWN sandbox (cloud-sandboxed assistant, no egress) and \u2264256KB \u2192 `dataUrl` + `fileName`, over 256KB \u2192 call with the sandbox `path` for a one-tap upload LINK, present the file for download, and have the user open the link and pick it.",
|
|
64218
64344
|
inputSchema: {
|
|
64219
64345
|
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
64220
64346
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
64221
64347
|
caption: external_exports.string().optional().describe("optional text caption"),
|
|
64222
|
-
dataUrl: external_exports.string().optional().describe("
|
|
64348
|
+
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."),
|
|
64223
64349
|
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
|
|
64224
64350
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
64225
64351
|
}
|
|
64226
64352
|
},
|
|
64227
64353
|
async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
64228
64354
|
let name, mime, url;
|
|
64355
|
+
let inlineBytes = null;
|
|
64229
64356
|
if (path3) {
|
|
64230
64357
|
if (!ctx.local) {
|
|
64231
|
-
const r = await remoteUploadCommand(c, ctx, { groupId }, path3, caption, "the group");
|
|
64358
|
+
const r = await remoteUploadCommand(c, ctx, { groupId }, path3, caption, "the group", extra?.signal);
|
|
64232
64359
|
return "ok" in r ? ok3(r.ok) : fail(r.fail);
|
|
64233
64360
|
}
|
|
64234
64361
|
const abs = resolveLocalFile(path3);
|
|
@@ -64238,10 +64365,11 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
64238
64365
|
mime = mimeOf(name);
|
|
64239
64366
|
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
64240
64367
|
} else if (dataUrl && fileName) {
|
|
64241
|
-
const
|
|
64242
|
-
if (
|
|
64368
|
+
const parsed = parseInlineDataUrl(dataUrl);
|
|
64369
|
+
if ("error" in parsed) return fail(parsed.error);
|
|
64243
64370
|
name = fileName;
|
|
64244
|
-
mime =
|
|
64371
|
+
mime = parsed.mime;
|
|
64372
|
+
inlineBytes = parsed.bytes;
|
|
64245
64373
|
url = dataUrl;
|
|
64246
64374
|
} else if (fileUrl) {
|
|
64247
64375
|
try {
|
|
@@ -64250,9 +64378,11 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
64250
64378
|
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
64251
64379
|
}
|
|
64252
64380
|
} else {
|
|
64253
|
-
return fail(
|
|
64381
|
+
return fail(
|
|
64382
|
+
"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."
|
|
64383
|
+
);
|
|
64254
64384
|
}
|
|
64255
|
-
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
64385
|
+
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
64256
64386
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
64257
64387
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
64258
64388
|
if (isStateless(c)) {
|
package/dist/index.js
CHANGED
|
@@ -42155,31 +42155,128 @@ 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
|
-
|
|
42161
|
-
|
|
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
|
+
}
|
|
42180
|
+
var MINT_LINK_DEADLINE_MS = (() => {
|
|
42181
|
+
const n = Number(process.env.MCP_MINT_LINK_DEADLINE_MS);
|
|
42182
|
+
return Number.isInteger(n) && n > 0 ? n : 1e4;
|
|
42183
|
+
})();
|
|
42184
|
+
async function mintFileLink(hub, token, fileId) {
|
|
42185
|
+
const ac = new AbortController();
|
|
42186
|
+
const killer = setTimeout(() => ac.abort(), MINT_LINK_DEADLINE_MS);
|
|
42187
|
+
try {
|
|
42188
|
+
const lr = await fetch(`${hub}/api/files/${fileId}/link`, {
|
|
42189
|
+
method: "POST",
|
|
42190
|
+
headers: { authorization: `Bearer ${token}` },
|
|
42191
|
+
signal: ac.signal
|
|
42192
|
+
});
|
|
42193
|
+
if (!lr.ok) {
|
|
42194
|
+
try {
|
|
42195
|
+
await lr.body?.cancel();
|
|
42196
|
+
} catch {
|
|
42197
|
+
}
|
|
42198
|
+
return null;
|
|
42199
|
+
}
|
|
42200
|
+
const body = await lr.json();
|
|
42201
|
+
return body?.url ?? null;
|
|
42202
|
+
} catch {
|
|
42203
|
+
return null;
|
|
42204
|
+
} finally {
|
|
42205
|
+
ac.abort();
|
|
42206
|
+
clearTimeout(killer);
|
|
42207
|
+
}
|
|
42208
|
+
}
|
|
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).
|
|
42211
|
+
`;
|
|
42212
|
+
async function remoteUploadCommand(c2, ctx, target, path5, caption, where, signal) {
|
|
42162
42213
|
const fname = crossBasename(path5);
|
|
42163
42214
|
const fmime = mimeOf(fname);
|
|
42164
42215
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
42165
42216
|
try {
|
|
42166
|
-
const
|
|
42167
|
-
|
|
42168
|
-
|
|
42169
|
-
|
|
42170
|
-
|
|
42171
|
-
|
|
42172
|
-
|
|
42173
|
-
|
|
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
|
+
{
|
|
42174
42264
|
return {
|
|
42175
|
-
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):
|
|
42176
42271
|
|
|
42177
42272
|
curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path5)}
|
|
42178
42273
|
|
|
42179
42274
|
Windows PowerShell instead:
|
|
42180
42275
|
Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path5)}
|
|
42181
42276
|
|
|
42182
|
-
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.`
|
|
42183
42280
|
};
|
|
42184
42281
|
}
|
|
42185
42282
|
} catch {
|
|
@@ -42189,7 +42286,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
|
|
|
42189
42286
|
const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
42190
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)}`;
|
|
42191
42288
|
return {
|
|
42192
|
-
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:
|
|
42193
42290
|
|
|
42194
42291
|
\u2705 open the Jefri Chat web app and DRAG "${fname}" into the chat with ${where}:
|
|
42195
42292
|
${dropLink}
|
|
@@ -42197,7 +42294,7 @@ The link is single-use and expires in ${mins} min; the file name and caption are
|
|
|
42197
42294
|
\u2014 or from a terminal (set JEFRI_TOKEN from the app's Connect dialog first):
|
|
42198
42295
|
${cmd}
|
|
42199
42296
|
|
|
42200
|
-
|
|
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.`
|
|
42201
42298
|
};
|
|
42202
42299
|
}
|
|
42203
42300
|
async function fetchHistory2(c2, convId) {
|
|
@@ -42223,7 +42320,22 @@ var MIME = {
|
|
|
42223
42320
|
".txt": "text/plain",
|
|
42224
42321
|
".md": "text/markdown",
|
|
42225
42322
|
".json": "application/json",
|
|
42226
|
-
".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"
|
|
42227
42339
|
};
|
|
42228
42340
|
var mimeOf = (name) => MIME[np4.extname(name).toLowerCase()] ?? "application/octet-stream";
|
|
42229
42341
|
function fmtTime(iso) {
|
|
@@ -42294,9 +42406,13 @@ function resolveLocalFile(rawPath) {
|
|
|
42294
42406
|
return null;
|
|
42295
42407
|
}
|
|
42296
42408
|
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
42297
|
-
var
|
|
42298
|
-
|
|
42299
|
-
|
|
42409
|
+
var posIntEnv = (v, dflt) => {
|
|
42410
|
+
const n = Number(v);
|
|
42411
|
+
return Number.isInteger(n) && n > 0 ? n : dflt;
|
|
42412
|
+
};
|
|
42413
|
+
var MAX_REMOTE_URL_BYTES = posIntEnv(process.env.MCP_MAX_REMOTE_URL_BYTES, 25 * 1024 * 1024);
|
|
42414
|
+
var MAX_CONCURRENT_REMOTE_DOWNLOADS = posIntEnv(process.env.MCP_MAX_REMOTE_DL, 3);
|
|
42415
|
+
var REMOTE_DOWNLOAD_DEADLINE_MS = posIntEnv(process.env.MCP_REMOTE_DL_DEADLINE_MS, 3e4);
|
|
42300
42416
|
var activeRemoteDownloads = 0;
|
|
42301
42417
|
var MB = (n) => (n / 1024 / 1024).toFixed(1);
|
|
42302
42418
|
var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
|
|
@@ -43166,21 +43282,22 @@ ${fp}`);
|
|
|
43166
43282
|
"jefri_send_file",
|
|
43167
43283
|
{
|
|
43168
43284
|
title: "Send a file, PDF, or image",
|
|
43169
|
-
description: "Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document.
|
|
43285
|
+
description: "Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. PICK THE INPUT BY WHERE THE FILE LIVES: (1) on the USER'S machine \u2192 give `path` (e.g. ~/Downloads/report.pdf); even on the remote/cloud connector this answers ONE ready-to-run terminal command for the user (a single-use upload link) \u2014 never base64 a user-disk file. (2) reachable by public URL \u2192 pass `fileUrl`; the connector fetches the bytes itself (up to 25MB). (3) in YOUR OWN sandbox (you are a cloud-sandboxed assistant with no network egress, e.g. Claude apps/Cowork) and \u2264256KB \u2192 send it as `dataUrl` + `fileName`; over 256KB \u2192 call this tool with the file's sandbox `path`: you get a one-tap upload LINK \u2014 present the file to the user for download and have them open the link and pick it. Optional `caption` in every case.",
|
|
43170
43286
|
inputSchema: {
|
|
43171
43287
|
to: external_exports.string().describe("recipient username"),
|
|
43172
43288
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
43173
43289
|
caption: external_exports.string().optional().describe("optional text caption to send with it"),
|
|
43174
|
-
dataUrl: external_exports.string().optional().describe("
|
|
43290
|
+
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."),
|
|
43175
43291
|
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
|
|
43176
43292
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
43177
43293
|
}
|
|
43178
43294
|
},
|
|
43179
43295
|
async ({ to, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
|
|
43180
43296
|
let name, mime, url;
|
|
43297
|
+
let inlineBytes = null;
|
|
43181
43298
|
if (path5) {
|
|
43182
43299
|
if (!ctx.local) {
|
|
43183
|
-
const r = await remoteUploadCommand(c2, ctx, { to }, path5, caption, `@${safeHandle(to)}
|
|
43300
|
+
const r = await remoteUploadCommand(c2, ctx, { to }, path5, caption, `@${safeHandle(to)}`, extra?.signal);
|
|
43184
43301
|
return "ok" in r ? ok3(r.ok) : fail(r.fail);
|
|
43185
43302
|
}
|
|
43186
43303
|
const abs = resolveLocalFile(path5);
|
|
@@ -43192,10 +43309,11 @@ ${fp}`);
|
|
|
43192
43309
|
mime = mimeOf(name);
|
|
43193
43310
|
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
43194
43311
|
} else if (dataUrl && fileName) {
|
|
43195
|
-
const
|
|
43196
|
-
if (
|
|
43312
|
+
const parsed = parseInlineDataUrl(dataUrl);
|
|
43313
|
+
if ("error" in parsed) return fail(parsed.error);
|
|
43197
43314
|
name = fileName;
|
|
43198
|
-
mime =
|
|
43315
|
+
mime = parsed.mime;
|
|
43316
|
+
inlineBytes = parsed.bytes;
|
|
43199
43317
|
url = dataUrl;
|
|
43200
43318
|
} else if (fileUrl) {
|
|
43201
43319
|
try {
|
|
@@ -43204,9 +43322,11 @@ ${fp}`);
|
|
|
43204
43322
|
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
43205
43323
|
}
|
|
43206
43324
|
} else {
|
|
43207
|
-
return fail(
|
|
43325
|
+
return fail(
|
|
43326
|
+
"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."
|
|
43327
|
+
);
|
|
43208
43328
|
}
|
|
43209
|
-
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
43329
|
+
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
43210
43330
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
43211
43331
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
43212
43332
|
if (isStateless(c2)) {
|
|
@@ -43394,7 +43514,7 @@ ${fp}`);
|
|
|
43394
43514
|
"jefri_download_file",
|
|
43395
43515
|
{
|
|
43396
43516
|
title: "Read or save a file someone sent you",
|
|
43397
|
-
description: "Get a file another agent/user sent you. For a DM give the sender's username `from`; for a GROUP file give `groupId` (from jefri_groups / jefri_inbox). Add `fileName` if there are several. TEXT files (code, markdown, JSON, CSV, logs \u2014 up to 256KB) come back with their FULL CONTENT inline, PDFs come back as EXTRACTED TEXT, and IMAGES (png/jpeg/gif/webp up to 4MB) come back as the ACTUAL PICTURE you can look at \u2014 so you can read, view and discuss all three directly, no terminal needed. Only video, very large images and other binaries return a download link. On the local connector, files are saved straight to disk. Use this when the user says 'read the file sara sent', 'download that file', 'what does the doc say', etc.",
|
|
43517
|
+
description: "Get a file another agent/user sent you. For a DM give the sender's username `from`; for a GROUP file give `groupId` (from jefri_groups / jefri_inbox). Add `fileName` if there are several. TEXT files (code, markdown, JSON, CSV, logs \u2014 up to 256KB) come back with their FULL CONTENT inline, PDFs come back as EXTRACTED TEXT plus a signed link to the ORIGINAL file (use the link with jefri_send_file's fileUrl to forward the real bytes \u2014 never reconstruct a file from text), and IMAGES (png/jpeg/gif/webp up to 4MB) come back as the ACTUAL PICTURE you can look at \u2014 so you can read, view and discuss all three directly, no terminal needed. Only video, very large images and other binaries return a download link. On the local connector, files are saved straight to disk. Use this when the user says 'read the file sara sent', 'download that file', 'what does the doc say', etc.",
|
|
43398
43518
|
inputSchema: {
|
|
43399
43519
|
from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
|
|
43400
43520
|
groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
|
|
@@ -43485,12 +43605,24 @@ ${fp}`);
|
|
|
43485
43605
|
const r = await fetch(`${hub}/api/files/${match.id}/text`, { headers: { authorization: `Bearer ${c2.token}` }, signal: ac.signal });
|
|
43486
43606
|
if (r.ok) {
|
|
43487
43607
|
const body = await r.json();
|
|
43488
|
-
if (body?.text)
|
|
43608
|
+
if (body?.text) {
|
|
43609
|
+
const pdfLink = await mintFileLink(hub, c2.token, match.id);
|
|
43610
|
+
const webPdf = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
43489
43611
|
return ok3(
|
|
43490
|
-
`\u{1F4CE} ${fname2} from ${where} \u2014
|
|
43612
|
+
`\u{1F4CE} ${fname2} from ${where} \u2014 THE ORIGINAL FILE:
|
|
43613
|
+
` + (pdfLink ? `\u2022 Direct download (valid ~5 minutes): ${pdfLink}
|
|
43614
|
+
` : "") + (pdfLink ? forwardTip(pdfLink, fname2) : "") + `\u2022 Web app (renders in the chat): ${webPdf}
|
|
43615
|
+
` + // The closing line must only reference what EXISTS: with
|
|
43616
|
+
// no minted link, "via the link above" pointed at nothing.
|
|
43617
|
+
(pdfLink ? `Never reconstruct a file from extracted text \u2014 forward the original via the link above.
|
|
43618
|
+
|
|
43619
|
+
` : `Never reconstruct a file from extracted text \u2014 the link service didn't answer just now; call jefri_download_file again for a fresh link, or use the web app above.
|
|
43620
|
+
|
|
43621
|
+
`) + `\u2014 Extracted text (${body.chars ?? body.text.length} chars):
|
|
43491
43622
|
|
|
43492
43623
|
` + body.text
|
|
43493
43624
|
);
|
|
43625
|
+
}
|
|
43494
43626
|
}
|
|
43495
43627
|
} finally {
|
|
43496
43628
|
clearTimeout(killer);
|
|
@@ -43551,17 +43683,11 @@ ${fp}`);
|
|
|
43551
43683
|
const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${out}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hub}/api/files/${match.id}`)}`;
|
|
43552
43684
|
const web2 = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
43553
43685
|
let directLink = "";
|
|
43554
|
-
|
|
43555
|
-
const
|
|
43556
|
-
|
|
43557
|
-
|
|
43558
|
-
|
|
43559
|
-
if (lr.ok) {
|
|
43560
|
-
const body = await lr.json();
|
|
43561
|
-
if (body?.url) directLink = `\u2022 Direct download (click it \u2014 valid ~5 minutes): ${body.url}
|
|
43562
|
-
`;
|
|
43563
|
-
}
|
|
43564
|
-
} catch {
|
|
43686
|
+
{
|
|
43687
|
+
const linkUrl = await mintFileLink(hub, c2.token, match.id);
|
|
43688
|
+
if (linkUrl)
|
|
43689
|
+
directLink = `\u2022 Direct download (click it \u2014 valid ~5 minutes): ${linkUrl}
|
|
43690
|
+
` + forwardTip(linkUrl, fname2);
|
|
43565
43691
|
}
|
|
43566
43692
|
return ok3(
|
|
43567
43693
|
`\u{1F4CE} ${match.fileName} (${fmime2}) \u2014 I can't show it inline${imageWhy ? `: ${imageWhy}` : " (not a viewable type)"}.
|
|
@@ -43937,21 +44063,22 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
43937
44063
|
"jefri_send_group_file",
|
|
43938
44064
|
{
|
|
43939
44065
|
title: "Send a file to a group",
|
|
43940
|
-
description: "Send a file/image to a GROUP by its group id.
|
|
44066
|
+
description: "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). Pick the input by where the file lives: user's machine \u2192 `path`; public URL \u2192 `fileUrl`; your OWN sandbox (cloud-sandboxed assistant, no egress) and \u2264256KB \u2192 `dataUrl` + `fileName`, over 256KB \u2192 call with the sandbox `path` for a one-tap upload LINK, present the file for download, and have the user open the link and pick it.",
|
|
43941
44067
|
inputSchema: {
|
|
43942
44068
|
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
43943
44069
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
43944
44070
|
caption: external_exports.string().optional().describe("optional text caption"),
|
|
43945
|
-
dataUrl: external_exports.string().optional().describe("
|
|
44071
|
+
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."),
|
|
43946
44072
|
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
|
|
43947
44073
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
43948
44074
|
}
|
|
43949
44075
|
},
|
|
43950
44076
|
async ({ groupId, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
|
|
43951
44077
|
let name, mime, url;
|
|
44078
|
+
let inlineBytes = null;
|
|
43952
44079
|
if (path5) {
|
|
43953
44080
|
if (!ctx.local) {
|
|
43954
|
-
const r = await remoteUploadCommand(c2, ctx, { groupId }, path5, caption, "the group");
|
|
44081
|
+
const r = await remoteUploadCommand(c2, ctx, { groupId }, path5, caption, "the group", extra?.signal);
|
|
43955
44082
|
return "ok" in r ? ok3(r.ok) : fail(r.fail);
|
|
43956
44083
|
}
|
|
43957
44084
|
const abs = resolveLocalFile(path5);
|
|
@@ -43961,10 +44088,11 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
43961
44088
|
mime = mimeOf(name);
|
|
43962
44089
|
url = `data:${mime};base64,${buf.toString("base64")}`;
|
|
43963
44090
|
} else if (dataUrl && fileName) {
|
|
43964
|
-
const
|
|
43965
|
-
if (
|
|
44091
|
+
const parsed = parseInlineDataUrl(dataUrl);
|
|
44092
|
+
if ("error" in parsed) return fail(parsed.error);
|
|
43966
44093
|
name = fileName;
|
|
43967
|
-
mime =
|
|
44094
|
+
mime = parsed.mime;
|
|
44095
|
+
inlineBytes = parsed.bytes;
|
|
43968
44096
|
url = dataUrl;
|
|
43969
44097
|
} else if (fileUrl) {
|
|
43970
44098
|
try {
|
|
@@ -43973,9 +44101,11 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
43973
44101
|
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
43974
44102
|
}
|
|
43975
44103
|
} else {
|
|
43976
|
-
return fail(
|
|
44104
|
+
return fail(
|
|
44105
|
+
"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."
|
|
44106
|
+
);
|
|
43977
44107
|
}
|
|
43978
|
-
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
44108
|
+
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
43979
44109
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
43980
44110
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
43981
44111
|
if (isStateless(c2)) {
|
package/package.json
CHANGED