jefrichat-mcp 0.49.4 → 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.
Files changed (3) hide show
  1. package/dist/http.js +118 -33
  2. package/dist/index.js +118 -33
  3. 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) || /\.(md|txt|json|csv|ya?ml|log|ts|tsx|js|jsx|py|rb|go|rs|java|c|h|cpp|sh|sql|toml|env|cfg|ini)$/i.test(name);
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
- var dataUrlTooBig = (dataUrl) => dataUrl.length > DATAURL_INLINE_MAX * 1.4 ? `That dataUrl is ~${MB(dataUrl.length * 0.75)} MB decoded \u2014 far past the ${Math.round(DATAURL_INLINE_MAX / 1024)}KB inline limit. NEVER read a local file and base64 it through the model. Call this tool again with the file's PATH \u2014 you'll get a single-use upload command that sends it in seconds.` : null;
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 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).
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 g = await fetch(`${hub}/api/files/grant`, {
62478
- method: "POST",
62479
- headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
62480
- body: JSON.stringify({ ...target.to ? { to: target.to } : { groupId: target.groupId }, fileName: fname, ...caption ? { caption } : {} })
62481
- });
62482
- if (g.ok) {
62483
- const { uploadUrl, expiresInMs } = await g.json();
62484
- const mins = Math.round((expiresInMs ?? 9e5) / 6e4);
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 your disk \u2014 but here is the fast path. Run this ONE command in the terminal (it uploads AND delivers the file to ${where}):
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,...}). Do NOT read the file and base64 it through dataUrl \u2014 that is minutes of model output and fails over ~3MB.`
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}" off your computer, and the upload-link service didn't answer. Options:
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
- Do NOT base64 the file through dataUrl \u2014 that streams megabytes through the model and fails over ~3MB.`
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) {
@@ -63478,21 +63559,22 @@ ${fp}`);
63478
63559
  "jefri_send_file",
63479
63560
  {
63480
63561
  title: "Send a file, PDF, or image",
63481
- description: "Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. ALWAYS give the local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption` \u2014 even on the remote/cloud connector: there this tool answers with ONE ready-to-run terminal command (a single-use upload link) that sends the file in seconds. NEVER read the file yourself and base64 it into `dataUrl` \u2014 that streams megabytes through the model, takes minutes, and fails over ~3MB; `dataUrl` is ONLY for tiny content you already hold inline (small generated snippets). If you only have a public URL for the file, pass `fileUrl` and the connector fetches the bytes itself.",
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.",
63482
63563
  inputSchema: {
63483
63564
  to: external_exports.string().describe("recipient username"),
63484
63565
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
63485
63566
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
63486
- dataUrl: external_exports.string().optional().describe("ONLY for tiny inline content you already hold (max 256KB decoded). NEVER construct this by reading a local file \u2014 pass `path` instead."),
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."),
63487
63568
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
63488
63569
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
63489
63570
  }
63490
63571
  },
63491
63572
  async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
63492
63573
  let name, mime, url;
63574
+ let inlineBytes = null;
63493
63575
  if (path3) {
63494
63576
  if (!ctx.local) {
63495
- 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);
63496
63578
  return "ok" in r ? ok3(r.ok) : fail(r.fail);
63497
63579
  }
63498
63580
  const abs = resolveLocalFile(path3);
@@ -63504,10 +63586,11 @@ ${fp}`);
63504
63586
  mime = mimeOf(name);
63505
63587
  url = `data:${mime};base64,${buf.toString("base64")}`;
63506
63588
  } else if (dataUrl && fileName) {
63507
- const tooBig = dataUrlTooBig(dataUrl);
63508
- if (tooBig) return fail(tooBig);
63589
+ const parsed = parseInlineDataUrl(dataUrl);
63590
+ if ("error" in parsed) return fail(parsed.error);
63509
63591
  name = fileName;
63510
- mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
63592
+ mime = parsed.mime;
63593
+ inlineBytes = parsed.bytes;
63511
63594
  url = dataUrl;
63512
63595
  } else if (fileUrl) {
63513
63596
  try {
@@ -63517,10 +63600,10 @@ ${fp}`);
63517
63600
  }
63518
63601
  } else {
63519
63602
  return fail(
63520
- "provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. If the file only exists INSIDE this chat (attached or generated here), the connector can't see it \u2014 ask the user to save it to their computer first, then call again with that path."
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."
63521
63604
  );
63522
63605
  }
63523
- const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
63606
+ const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
63524
63607
  if (rawBytes.length > MAX_UPLOAD_BYTES)
63525
63608
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
63526
63609
  if (isStateless(c)) {
@@ -64257,21 +64340,22 @@ Or just upload ${fname} from the agent's page in the web app.`
64257
64340
  "jefri_send_group_file",
64258
64341
  {
64259
64342
  title: "Send a file to a group",
64260
- description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector), a `dataUrl` + `fileName`, or a public `fileUrl` to fetch (e.g. a generated image). Get the group id from jefri_groups / jefri_inbox.",
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.",
64261
64344
  inputSchema: {
64262
64345
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
64263
64346
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
64264
64347
  caption: external_exports.string().optional().describe("optional text caption"),
64265
- dataUrl: external_exports.string().optional().describe("ONLY for tiny inline content you already hold (max 256KB decoded). NEVER construct this by reading a local file \u2014 pass `path` instead."),
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."),
64266
64349
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
64267
64350
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
64268
64351
  }
64269
64352
  },
64270
64353
  async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
64271
64354
  let name, mime, url;
64355
+ let inlineBytes = null;
64272
64356
  if (path3) {
64273
64357
  if (!ctx.local) {
64274
- 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);
64275
64359
  return "ok" in r ? ok3(r.ok) : fail(r.fail);
64276
64360
  }
64277
64361
  const abs = resolveLocalFile(path3);
@@ -64281,10 +64365,11 @@ Or just upload ${fname} from the agent's page in the web app.`
64281
64365
  mime = mimeOf(name);
64282
64366
  url = `data:${mime};base64,${buf.toString("base64")}`;
64283
64367
  } else if (dataUrl && fileName) {
64284
- const tooBig = dataUrlTooBig(dataUrl);
64285
- if (tooBig) return fail(tooBig);
64368
+ const parsed = parseInlineDataUrl(dataUrl);
64369
+ if ("error" in parsed) return fail(parsed.error);
64286
64370
  name = fileName;
64287
- mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
64371
+ mime = parsed.mime;
64372
+ inlineBytes = parsed.bytes;
64288
64373
  url = dataUrl;
64289
64374
  } else if (fileUrl) {
64290
64375
  try {
@@ -64294,10 +64379,10 @@ Or just upload ${fname} from the agent's page in the web app.`
64294
64379
  }
64295
64380
  } else {
64296
64381
  return fail(
64297
- "provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. If the file only exists INSIDE this chat (attached or generated here), the connector can't see it \u2014 ask the user to save it to their computer first, then call again with that path."
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."
64298
64383
  );
64299
64384
  }
64300
- const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
64385
+ const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
64301
64386
  if (rawBytes.length > MAX_UPLOAD_BYTES)
64302
64387
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
64303
64388
  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) || /\.(md|txt|json|csv|ya?ml|log|ts|tsx|js|jsx|py|rb|go|rs|java|c|h|cpp|sh|sql|toml|env|cfg|ini)$/i.test(name);
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
- var dataUrlTooBig = (dataUrl) => dataUrl.length > DATAURL_INLINE_MAX * 1.4 ? `That dataUrl is ~${MB(dataUrl.length * 0.75)} MB decoded \u2014 far past the ${Math.round(DATAURL_INLINE_MAX / 1024)}KB inline limit. NEVER read a local file and base64 it through the model. Call this tool again with the file's PATH \u2014 you'll get a single-use upload command that sends it in seconds.` : null;
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 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).
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 g = await fetch(`${hub}/api/files/grant`, {
42198
- method: "POST",
42199
- headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
42200
- body: JSON.stringify({ ...target.to ? { to: target.to } : { groupId: target.groupId }, fileName: fname, ...caption ? { caption } : {} })
42201
- });
42202
- if (g.ok) {
42203
- const { uploadUrl, expiresInMs } = await g.json();
42204
- const mins = Math.round((expiresInMs ?? 9e5) / 6e4);
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 your disk \u2014 but here is the fast path. Run this ONE command in the terminal (it uploads AND delivers the file to ${where}):
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,...}). Do NOT read the file and base64 it through dataUrl \u2014 that is minutes of model output and fails over ~3MB.`
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}" off your computer, and the upload-link service didn't answer. Options:
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
- Do NOT base64 the file through dataUrl \u2014 that streams megabytes through the model and fails over ~3MB.`
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) {
@@ -43201,21 +43282,22 @@ ${fp}`);
43201
43282
  "jefri_send_file",
43202
43283
  {
43203
43284
  title: "Send a file, PDF, or image",
43204
- description: "Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. ALWAYS give the local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption` \u2014 even on the remote/cloud connector: there this tool answers with ONE ready-to-run terminal command (a single-use upload link) that sends the file in seconds. NEVER read the file yourself and base64 it into `dataUrl` \u2014 that streams megabytes through the model, takes minutes, and fails over ~3MB; `dataUrl` is ONLY for tiny content you already hold inline (small generated snippets). If you only have a public URL for the file, pass `fileUrl` and the connector fetches the bytes itself.",
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.",
43205
43286
  inputSchema: {
43206
43287
  to: external_exports.string().describe("recipient username"),
43207
43288
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
43208
43289
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
43209
- dataUrl: external_exports.string().optional().describe("ONLY for tiny inline content you already hold (max 256KB decoded). NEVER construct this by reading a local file \u2014 pass `path` instead."),
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."),
43210
43291
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
43211
43292
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
43212
43293
  }
43213
43294
  },
43214
43295
  async ({ to, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
43215
43296
  let name, mime, url;
43297
+ let inlineBytes = null;
43216
43298
  if (path5) {
43217
43299
  if (!ctx.local) {
43218
- 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);
43219
43301
  return "ok" in r ? ok3(r.ok) : fail(r.fail);
43220
43302
  }
43221
43303
  const abs = resolveLocalFile(path5);
@@ -43227,10 +43309,11 @@ ${fp}`);
43227
43309
  mime = mimeOf(name);
43228
43310
  url = `data:${mime};base64,${buf.toString("base64")}`;
43229
43311
  } else if (dataUrl && fileName) {
43230
- const tooBig = dataUrlTooBig(dataUrl);
43231
- if (tooBig) return fail(tooBig);
43312
+ const parsed = parseInlineDataUrl(dataUrl);
43313
+ if ("error" in parsed) return fail(parsed.error);
43232
43314
  name = fileName;
43233
- mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
43315
+ mime = parsed.mime;
43316
+ inlineBytes = parsed.bytes;
43234
43317
  url = dataUrl;
43235
43318
  } else if (fileUrl) {
43236
43319
  try {
@@ -43240,10 +43323,10 @@ ${fp}`);
43240
43323
  }
43241
43324
  } else {
43242
43325
  return fail(
43243
- "provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. If the file only exists INSIDE this chat (attached or generated here), the connector can't see it \u2014 ask the user to save it to their computer first, then call again with that path."
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."
43244
43327
  );
43245
43328
  }
43246
- const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
43329
+ const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
43247
43330
  if (rawBytes.length > MAX_UPLOAD_BYTES)
43248
43331
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
43249
43332
  if (isStateless(c2)) {
@@ -43980,21 +44063,22 @@ Or just upload ${fname} from the agent's page in the web app.`
43980
44063
  "jefri_send_group_file",
43981
44064
  {
43982
44065
  title: "Send a file to a group",
43983
- description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector), a `dataUrl` + `fileName`, or a public `fileUrl` to fetch (e.g. a generated image). Get the group id from jefri_groups / jefri_inbox.",
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.",
43984
44067
  inputSchema: {
43985
44068
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
43986
44069
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
43987
44070
  caption: external_exports.string().optional().describe("optional text caption"),
43988
- dataUrl: external_exports.string().optional().describe("ONLY for tiny inline content you already hold (max 256KB decoded). NEVER construct this by reading a local file \u2014 pass `path` instead."),
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."),
43989
44072
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
43990
44073
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
43991
44074
  }
43992
44075
  },
43993
44076
  async ({ groupId, path: path5, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
43994
44077
  let name, mime, url;
44078
+ let inlineBytes = null;
43995
44079
  if (path5) {
43996
44080
  if (!ctx.local) {
43997
- 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);
43998
44082
  return "ok" in r ? ok3(r.ok) : fail(r.fail);
43999
44083
  }
44000
44084
  const abs = resolveLocalFile(path5);
@@ -44004,10 +44088,11 @@ Or just upload ${fname} from the agent's page in the web app.`
44004
44088
  mime = mimeOf(name);
44005
44089
  url = `data:${mime};base64,${buf.toString("base64")}`;
44006
44090
  } else if (dataUrl && fileName) {
44007
- const tooBig = dataUrlTooBig(dataUrl);
44008
- if (tooBig) return fail(tooBig);
44091
+ const parsed = parseInlineDataUrl(dataUrl);
44092
+ if ("error" in parsed) return fail(parsed.error);
44009
44093
  name = fileName;
44010
- mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
44094
+ mime = parsed.mime;
44095
+ inlineBytes = parsed.bytes;
44011
44096
  url = dataUrl;
44012
44097
  } else if (fileUrl) {
44013
44098
  try {
@@ -44017,10 +44102,10 @@ Or just upload ${fname} from the agent's page in the web app.`
44017
44102
  }
44018
44103
  } else {
44019
44104
  return fail(
44020
- "provide a local file `path`, a `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. If the file only exists INSIDE this chat (attached or generated here), the connector can't see it \u2014 ask the user to save it to their computer first, then call again with that path."
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."
44021
44106
  );
44022
44107
  }
44023
- const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
44108
+ const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
44024
44109
  if (rawBytes.length > MAX_UPLOAD_BYTES)
44025
44110
  return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
44026
44111
  if (isStateless(c2)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.49.4",
3
+ "version": "0.49.5",
4
4
  "description": "Jefri Chat connector — join the Jefri Chat network (WhatsApp for AI agents) from any MCP client (Claude, Codex, Cursor, …).",
5
5
  "type": "module",
6
6
  "bin": {