jefrichat-mcp 0.48.12 → 0.48.14

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 +158 -62
  2. package/dist/index.js +158 -62
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -62113,6 +62113,70 @@ function declKey(extra, parts) {
62113
62113
  const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
62114
62114
  return "d-" + createHash2("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
62115
62115
  }
62116
+ var crossBasename = (p) => String(p).split(/[\\/]/).pop() || "file";
62117
+ var DOWNLOAD_DIR_SH = "~/Downloads/jefri";
62118
+ var downloadDirAbs = () => process.env.JEFRI_DOWNLOAD_DIR || np3.join(os4.homedir(), "Downloads", "jefri");
62119
+ var ensureDownloadDir = () => {
62120
+ const dir = downloadDirAbs();
62121
+ fs4.mkdirSync(dir, { recursive: true });
62122
+ const st = fs4.lstatSync(dir);
62123
+ if (st.isSymbolicLink() || !st.isDirectory())
62124
+ throw new Error(`refusing to save: ${dir} is a symlink (or not a directory) \u2014 a planted link could redirect downloads anywhere. Remove it and retry.`);
62125
+ return dir;
62126
+ };
62127
+ var safeSaveName = (p) => {
62128
+ const base = String(p ?? "").split(/[\\/]/).pop() ?? "";
62129
+ const clean = base.replace(/[\x00-\x1f\x7f]/g, "").trim();
62130
+ return !clean || clean === "." || clean === ".." ? "" : clean;
62131
+ };
62132
+ var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
62133
+ var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
62134
+ var psPathArg = (p) => p.startsWith("~/") || p.startsWith("~\\") ? `($HOME + ${psq(p.slice(1))})` : psq(p);
62135
+ 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);
62136
+ var DATAURL_INLINE_MAX = 256 * 1024;
62137
+ 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;
62138
+ async function remoteUploadCommand(c, ctx, target, path3, caption, where) {
62139
+ const fname = crossBasename(path3);
62140
+ const fmime = mimeOf(fname);
62141
+ const hub = ctx.serverUrl.replace(/\/$/, "");
62142
+ try {
62143
+ const g = await fetch(`${hub}/api/files/grant`, {
62144
+ method: "POST",
62145
+ headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
62146
+ body: JSON.stringify({ ...target.to ? { to: target.to } : { groupId: target.groupId }, fileName: fname, ...caption ? { caption } : {} })
62147
+ });
62148
+ if (g.ok) {
62149
+ const { uploadUrl, expiresInMs } = await g.json();
62150
+ const mins = Math.round((expiresInMs ?? 9e5) / 6e4);
62151
+ return {
62152
+ 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}):
62153
+
62154
+ curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path3)}
62155
+
62156
+ Windows PowerShell instead:
62157
+ Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path3)}
62158
+
62159
+ 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.`
62160
+ };
62161
+ }
62162
+ } catch {
62163
+ }
62164
+ const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
62165
+ const dropLink = target.to ? `${web}/?to=${encodeURIComponent(target.to)}` : `${web}/?group=${encodeURIComponent(target.groupId)}`;
62166
+ const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
62167
+ 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)}`;
62168
+ return {
62169
+ fail: `This is the cloud connector, so it can't read "${fname}" off your computer, and the upload-link service didn't answer. Options:
62170
+
62171
+ \u2705 open the Jefri Chat web app and DRAG "${fname}" into the chat with ${where}:
62172
+ ${dropLink}
62173
+
62174
+ \u2014 or from a terminal (set JEFRI_TOKEN from the app's Connect dialog first):
62175
+ ${cmd}
62176
+
62177
+ Do NOT base64 the file through dataUrl \u2014 that streams megabytes through the model and fails over ~3MB.`
62178
+ };
62179
+ }
62116
62180
  async function fetchHistory(c, convId) {
62117
62181
  if (isStateless(c)) return c.history(convId);
62118
62182
  const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
@@ -62729,12 +62793,12 @@ ${fp}`);
62729
62793
  "jefri_send_file",
62730
62794
  {
62731
62795
  title: "Send a file, PDF, or image",
62732
- description: 'Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. Give a local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption`.\n\nHOW IT WORKS: this reads the file where the connector runs. If you are connected via the LOCAL (stdio) connector, a `path` is read straight off this machine. If you are connected via the REMOTE/cloud connector (URL), this process runs in the cloud and CANNOT see your local disk \u2014 so for a local file, run the helper in your terminal instead (it reads + uploads the file locally):\n pnpm send-file --to <username> --path <absolute file path> [--caption "..."]\n(run from the acp repo; token + hub are auto-detected from your Claude config). Tiny inline content can be sent directly via `dataUrl` + `fileName`. If you only have a public URL for the file (e.g. an image you generated), pass it as `fileUrl` and the connector fetches the bytes and sends the real file.',
62796
+ 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.",
62733
62797
  inputSchema: {
62734
62798
  to: external_exports.string().describe("recipient username"),
62735
62799
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
62736
62800
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
62737
- dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
62801
+ 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."),
62738
62802
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
62739
62803
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
62740
62804
  }
@@ -62743,31 +62807,8 @@ ${fp}`);
62743
62807
  let name, mime, url;
62744
62808
  if (path3) {
62745
62809
  if (!ctx.local) {
62746
- const fname = np3.basename(path3);
62747
- const fmime = mimeOf(fname);
62748
- const hub = ctx.serverUrl.replace(/\/$/, "");
62749
- const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
62750
- const dropLink = `${web}/?to=${encodeURIComponent(to)}`;
62751
- const q = `to=${encodeURIComponent(to)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
62752
- const uploadUrl = `${hub}/api/files?${q}`;
62753
- const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
62754
- const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer $env:JEFRI_TOKEN" } -ContentType "${fmime}" -InFile "${path3}"`;
62755
- return fail(
62756
- `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
62757
-
62758
- \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
62759
- ${dropLink}
62760
-
62761
- (If instead this is an image you GENERATED and have a public URL for, just call this tool again with \`fileUrl\` instead of \`path\` \u2014 the image sends with no drag.)
62762
-
62763
- \u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
62764
- \u2022 macOS / Linux:
62765
- ${cmd}
62766
- \u2022 Windows (PowerShell):
62767
- ${psCmd}
62768
-
62769
- (These read your token from the JEFRI_TOKEN env var \u2014 set it first from the app's Connect dialog: export JEFRI_TOKEN=\u2026 (macOS/Linux) or $env:JEFRI_TOKEN="\u2026" (PowerShell). The token isn't printed here on purpose.)`
62770
- );
62810
+ const r = await remoteUploadCommand(c, ctx, { to }, path3, caption, `@${to}`);
62811
+ return "ok" in r ? ok3(r.ok) : fail(r.fail);
62771
62812
  }
62772
62813
  const abs = resolveLocalFile(path3);
62773
62814
  if (!abs) {
@@ -62778,6 +62819,8 @@ ${psCmd}
62778
62819
  mime = mimeOf(name);
62779
62820
  url = `data:${mime};base64,${buf.toString("base64")}`;
62780
62821
  } else if (dataUrl && fileName) {
62822
+ const tooBig = dataUrlTooBig(dataUrl);
62823
+ if (tooBig) return fail(tooBig);
62781
62824
  name = fileName;
62782
62825
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
62783
62826
  url = dataUrl;
@@ -62977,13 +63020,13 @@ ${psCmd}
62977
63020
  server2.registerTool(
62978
63021
  "jefri_download_file",
62979
63022
  {
62980
- title: "Download / save a file someone sent you",
62981
- description: "Save a file (PDF, image, video, any document) that another agent/user sent you, onto this machine. For a DM give the sender's username `from`; for a file sent in a GROUP give `groupId` instead (from jefri_groups / jefri_inbox). Add `fileName` if there are several. Returns a ready-to-run command that downloads the file locally \u2014 run it in the terminal. Use this when the user says things like 'download that file', 'save the pdf antonio sent', etc.",
63023
+ title: "Read or save a file someone sent you",
63024
+ 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, and PDFs come back as EXTRACTED TEXT \u2014 so you can read and discuss both directly, no terminal needed. Only images, video and other binaries return a save command (or view them in the web app). 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.",
62982
63025
  inputSchema: {
62983
63026
  from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
62984
63027
  groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
62985
63028
  fileName: external_exports.string().optional().describe("which file, if there are several"),
62986
- savePath: external_exports.string().optional().describe("where to save it (default: the file's own name in the current folder)")
63029
+ savePath: external_exports.string().optional().describe("the file NAME to save as (a bare name \u2014 every download lands in ~/Downloads/jefri/, never elsewhere); a taken name gets a -1/-2 suffix")
62987
63030
  }
62988
63031
  },
62989
63032
  async ({ from, groupId, fileName, savePath }) => withClient(async (c) => {
@@ -62996,26 +63039,92 @@ ${psCmd}
62996
63039
  const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
62997
63040
  if (!match) return fail(`No file named "${fileName}" in ${where}.`);
62998
63041
  const hub = ctx.serverUrl.replace(/\/$/, "");
62999
- const out = savePath || match.fileName || "downloaded.file";
63042
+ const safeName2 = safeSaveName(match.fileName) || "downloaded.file";
63043
+ const out = safeSaveName(savePath) || safeName2;
63000
63044
  if (ctx.local) {
63001
- const outPath = out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
63002
63045
  try {
63046
+ const dlDir = ensureDownloadDir();
63047
+ const outPath = np3.join(dlDir, out);
63003
63048
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
63004
63049
  if (!r.ok) throw new Error(`download failed (${r.status})`);
63005
63050
  const buf = Buffer.from(await r.arrayBuffer());
63006
- fs4.writeFileSync(outPath, buf);
63007
- return ok3(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
63051
+ let finalPath = outPath;
63052
+ const ext = np3.extname(finalPath);
63053
+ const stem = finalPath.slice(0, finalPath.length - ext.length);
63054
+ let written = false;
63055
+ for (let i = 0; i < 100 && !written; i++) {
63056
+ finalPath = i === 0 ? outPath : `${stem}-${i}${ext}`;
63057
+ try {
63058
+ fs4.writeFileSync(finalPath, buf, { flag: "wx" });
63059
+ written = true;
63060
+ } catch (e) {
63061
+ if (e?.code !== "EEXIST") throw e;
63062
+ }
63063
+ }
63064
+ if (!written) return fail(`Couldn't save: 100 files named like ${outPath} already exist here.`);
63065
+ return ok3(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${finalPath} (${MB(buf.length)} MB).`);
63008
63066
  } catch (e) {
63009
63067
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
63010
63068
  }
63011
63069
  }
63012
- const cmd = `curl -s -o ${shq(out)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hub}/api/files/${match.id}`)}`;
63070
+ const fname2 = safeName2;
63071
+ const fmime2 = match.fileMime || mimeOf(fname2);
63072
+ const sizeKnownTooBig = typeof match.fileSize === "number" && match.fileSize > DATAURL_INLINE_MAX;
63073
+ if (isTextyFile(fmime2, fname2) && !sizeKnownTooBig) {
63074
+ try {
63075
+ const ac = new AbortController();
63076
+ const killer = setTimeout(() => ac.abort(), 15e3);
63077
+ try {
63078
+ const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` }, signal: ac.signal });
63079
+ if (r.ok && r.body) {
63080
+ const declared = Number(r.headers.get("content-length") ?? 0);
63081
+ if (!declared || declared <= DATAURL_INLINE_MAX) {
63082
+ const bytes = await readCapped(r.body, DATAURL_INLINE_MAX, Date.now() + 15e3);
63083
+ return ok3(
63084
+ `\u{1F4CE} ${fname2} from ${where} (${(bytes.length / 1024).toFixed(1)} KB) \u2014 full content:
63085
+
63086
+ ` + bytes.toString("utf8")
63087
+ );
63088
+ }
63089
+ }
63090
+ } finally {
63091
+ clearTimeout(killer);
63092
+ }
63093
+ } catch {
63094
+ }
63095
+ }
63096
+ if (/pdf$/i.test(fmime2) || /\.pdf$/i.test(fname2)) {
63097
+ const pdfTooBig = typeof match.fileSize === "number" && match.fileSize > 15 * 1024 * 1024;
63098
+ if (!pdfTooBig) {
63099
+ try {
63100
+ const ac = new AbortController();
63101
+ const killer = setTimeout(() => ac.abort(), 3e4);
63102
+ try {
63103
+ const r = await fetch(`${hub}/api/files/${match.id}/text`, { headers: { authorization: `Bearer ${c.token}` }, signal: ac.signal });
63104
+ if (r.ok) {
63105
+ const body = await r.json();
63106
+ if (body?.text)
63107
+ return ok3(
63108
+ `\u{1F4CE} ${fname2} from ${where} \u2014 extracted text (${body.chars ?? body.text.length} chars):
63109
+
63110
+ ` + body.text
63111
+ );
63112
+ }
63113
+ } finally {
63114
+ clearTimeout(killer);
63115
+ }
63116
+ } catch {
63117
+ }
63118
+ }
63119
+ }
63120
+ 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}`)}`;
63121
+ const web2 = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
63013
63122
  return ok3(
63014
- `To save \u{1F4CE} ${match.fileName} from ${where}, run this in the terminal:
63123
+ `\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
63015
63124
 
63016
- ${cmd}
63017
-
63018
- It downloads the file to your machine (current folder unless you set a path). First set your token: export JEFRI_TOKEN=\u2026 (from the app's Connect dialog) \u2014 it isn't printed here on purpose.`
63125
+ \u2022 Easiest: open the chat in the web app, the file renders there: ${web2}
63126
+ \u2022 Terminal: ${cmd}
63127
+ (set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`
63019
63128
  );
63020
63129
  })
63021
63130
  );
@@ -63070,21 +63179,21 @@ It downloads the file to your machine (current folder unless you set a path). Fi
63070
63179
  inputSchema: {
63071
63180
  departmentId: external_exports.string(),
63072
63181
  fileId: external_exports.string(),
63073
- savePath: external_exports.string().optional().describe("where to save (default: the file's name in the current folder)")
63182
+ savePath: external_exports.string().optional().describe("the file NAME to save as (a bare name \u2014 the command saves into ~/Downloads/jefri/)")
63074
63183
  }
63075
63184
  },
63076
63185
  async ({ departmentId, fileId, savePath }) => withClient(async (c) => {
63077
- let name = savePath || "department-file";
63186
+ let name = safeSaveName(savePath) || "department-file";
63078
63187
  try {
63079
63188
  const r = await fetch(`${hubBase()}/api/departments/${departmentId}/drive`, { headers: { authorization: `Bearer ${c.token}` } });
63080
63189
  if (r.ok) {
63081
63190
  const { files } = await r.json();
63082
63191
  const f = files.find((x) => x.id === fileId);
63083
- if (f && !savePath) name = f.name;
63192
+ if (f && !safeSaveName(savePath)) name = safeSaveName(f.name) || name;
63084
63193
  }
63085
63194
  } catch {
63086
63195
  }
63087
- const cmd = `curl -s -o ${shq(name)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/departments/${departmentId}/files/${fileId}`)}`;
63196
+ const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${name}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/departments/${departmentId}/files/${fileId}`)}`;
63088
63197
  return ok3(`To save this department file, run in the terminal:
63089
63198
 
63090
63199
  ${cmd}
@@ -63174,7 +63283,7 @@ ${h.text}`).join("\n\n---\n\n")
63174
63283
  if (d.text) return ok3(`Contents of "${d.name}":
63175
63284
 
63176
63285
  ${d.text}`);
63177
- const cmd = `curl -s -o ${shq(d.name)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c.identity.username)}/docs/${encodeURIComponent(docId)}`)}`;
63286
+ const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${safeSaveName(d.name) || "document"}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c.identity.username)}/docs/${encodeURIComponent(docId)}`)}`;
63178
63287
  return ok3(`"${d.name}" is a ${d.mime} file. To download it and work with it locally, run in the terminal:
63179
63288
 
63180
63289
  ${cmd}
@@ -63365,7 +63474,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63365
63474
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
63366
63475
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
63367
63476
  caption: external_exports.string().optional().describe("optional text caption"),
63368
- dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
63477
+ 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."),
63369
63478
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
63370
63479
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
63371
63480
  }
@@ -63374,23 +63483,8 @@ Or just upload ${fname} from the agent's page in the web app.`
63374
63483
  let name, mime, url;
63375
63484
  if (path3) {
63376
63485
  if (!ctx.local) {
63377
- const hub = ctx.serverUrl.replace(/\/$/, "");
63378
- const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
63379
- const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
63380
- const fname = np3.basename(path3);
63381
- const fmime = mimeOf(fname);
63382
- const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
63383
- const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
63384
- return fail(
63385
- `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
63386
-
63387
- \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
63388
- ${dropLink}
63389
-
63390
- (If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
63391
-
63392
- \u2014 Advanced (terminal): ${cmd}`
63393
- );
63486
+ const r = await remoteUploadCommand(c, ctx, { groupId }, path3, caption, "the group");
63487
+ return "ok" in r ? ok3(r.ok) : fail(r.fail);
63394
63488
  }
63395
63489
  const abs = resolveLocalFile(path3);
63396
63490
  if (!abs) return fail(`I can't find a file at ${path3} on this machine.`);
@@ -63399,6 +63493,8 @@ Or just upload ${fname} from the agent's page in the web app.`
63399
63493
  mime = mimeOf(name);
63400
63494
  url = `data:${mime};base64,${buf.toString("base64")}`;
63401
63495
  } else if (dataUrl && fileName) {
63496
+ const tooBig = dataUrlTooBig(dataUrl);
63497
+ if (tooBig) return fail(tooBig);
63402
63498
  name = fileName;
63403
63499
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
63404
63500
  url = dataUrl;
package/dist/index.js CHANGED
@@ -40573,6 +40573,70 @@ function declKey(extra, parts) {
40573
40573
  const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
40574
40574
  return "d-" + createHash("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
40575
40575
  }
40576
+ var crossBasename = (p) => String(p).split(/[\\/]/).pop() || "file";
40577
+ var DOWNLOAD_DIR_SH = "~/Downloads/jefri";
40578
+ var downloadDirAbs = () => process.env.JEFRI_DOWNLOAD_DIR || np4.join(os4.homedir(), "Downloads", "jefri");
40579
+ var ensureDownloadDir = () => {
40580
+ const dir = downloadDirAbs();
40581
+ fs5.mkdirSync(dir, { recursive: true });
40582
+ const st = fs5.lstatSync(dir);
40583
+ if (st.isSymbolicLink() || !st.isDirectory())
40584
+ throw new Error(`refusing to save: ${dir} is a symlink (or not a directory) \u2014 a planted link could redirect downloads anywhere. Remove it and retry.`);
40585
+ return dir;
40586
+ };
40587
+ var safeSaveName = (p) => {
40588
+ const base = String(p ?? "").split(/[\\/]/).pop() ?? "";
40589
+ const clean2 = base.replace(/[\x00-\x1f\x7f]/g, "").trim();
40590
+ return !clean2 || clean2 === "." || clean2 === ".." ? "" : clean2;
40591
+ };
40592
+ var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
40593
+ var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
40594
+ var psPathArg = (p) => p.startsWith("~/") || p.startsWith("~\\") ? `($HOME + ${psq(p.slice(1))})` : psq(p);
40595
+ 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);
40596
+ var DATAURL_INLINE_MAX = 256 * 1024;
40597
+ 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;
40598
+ async function remoteUploadCommand(c2, ctx, target, path4, caption, where) {
40599
+ const fname = crossBasename(path4);
40600
+ const fmime = mimeOf(fname);
40601
+ const hub = ctx.serverUrl.replace(/\/$/, "");
40602
+ try {
40603
+ const g = await fetch(`${hub}/api/files/grant`, {
40604
+ method: "POST",
40605
+ headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
40606
+ body: JSON.stringify({ ...target.to ? { to: target.to } : { groupId: target.groupId }, fileName: fname, ...caption ? { caption } : {} })
40607
+ });
40608
+ if (g.ok) {
40609
+ const { uploadUrl, expiresInMs } = await g.json();
40610
+ const mins = Math.round((expiresInMs ?? 9e5) / 6e4);
40611
+ return {
40612
+ 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}):
40613
+
40614
+ curl -sS -X POST ${shq(uploadUrl)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path4)}
40615
+
40616
+ Windows PowerShell instead:
40617
+ Invoke-RestMethod -Uri ${psq(uploadUrl)} -Method Post -ContentType ${psq(fmime)} -InFile ${psPathArg(path4)}
40618
+
40619
+ 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.`
40620
+ };
40621
+ }
40622
+ } catch {
40623
+ }
40624
+ const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
40625
+ const dropLink = target.to ? `${web}/?to=${encodeURIComponent(target.to)}` : `${web}/?group=${encodeURIComponent(target.groupId)}`;
40626
+ const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
40627
+ const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path4)}`;
40628
+ return {
40629
+ fail: `This is the cloud connector, so it can't read "${fname}" off your computer, and the upload-link service didn't answer. Options:
40630
+
40631
+ \u2705 open the Jefri Chat web app and DRAG "${fname}" into the chat with ${where}:
40632
+ ${dropLink}
40633
+
40634
+ \u2014 or from a terminal (set JEFRI_TOKEN from the app's Connect dialog first):
40635
+ ${cmd}
40636
+
40637
+ Do NOT base64 the file through dataUrl \u2014 that streams megabytes through the model and fails over ~3MB.`
40638
+ };
40639
+ }
40576
40640
  async function fetchHistory2(c2, convId) {
40577
40641
  if (isStateless(c2)) return c2.history(convId);
40578
40642
  const p = waitFor(c2, "history", (e) => e.conversationId === convId, 4e3);
@@ -41192,12 +41256,12 @@ ${fp}`);
41192
41256
  "jefri_send_file",
41193
41257
  {
41194
41258
  title: "Send a file, PDF, or image",
41195
- description: 'Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. Give a local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption`.\n\nHOW IT WORKS: this reads the file where the connector runs. If you are connected via the LOCAL (stdio) connector, a `path` is read straight off this machine. If you are connected via the REMOTE/cloud connector (URL), this process runs in the cloud and CANNOT see your local disk \u2014 so for a local file, run the helper in your terminal instead (it reads + uploads the file locally):\n pnpm send-file --to <username> --path <absolute file path> [--caption "..."]\n(run from the acp repo; token + hub are auto-detected from your Claude config). Tiny inline content can be sent directly via `dataUrl` + `fileName`. If you only have a public URL for the file (e.g. an image you generated), pass it as `fileUrl` and the connector fetches the bytes and sends the real file.',
41259
+ 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.",
41196
41260
  inputSchema: {
41197
41261
  to: external_exports.string().describe("recipient username"),
41198
41262
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
41199
41263
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
41200
- dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
41264
+ 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."),
41201
41265
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
41202
41266
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41203
41267
  }
@@ -41206,31 +41270,8 @@ ${fp}`);
41206
41270
  let name, mime, url;
41207
41271
  if (path4) {
41208
41272
  if (!ctx.local) {
41209
- const fname = np4.basename(path4);
41210
- const fmime = mimeOf(fname);
41211
- const hub = ctx.serverUrl.replace(/\/$/, "");
41212
- const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
41213
- const dropLink = `${web}/?to=${encodeURIComponent(to)}`;
41214
- const q = `to=${encodeURIComponent(to)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
41215
- const uploadUrl = `${hub}/api/files?${q}`;
41216
- const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${fmime}" --data-binary @${shq(path4)}`;
41217
- const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer $env:JEFRI_TOKEN" } -ContentType "${fmime}" -InFile "${path4}"`;
41218
- return fail(
41219
- `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
41220
-
41221
- \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
41222
- ${dropLink}
41223
-
41224
- (If instead this is an image you GENERATED and have a public URL for, just call this tool again with \`fileUrl\` instead of \`path\` \u2014 the image sends with no drag.)
41225
-
41226
- \u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
41227
- \u2022 macOS / Linux:
41228
- ${cmd}
41229
- \u2022 Windows (PowerShell):
41230
- ${psCmd}
41231
-
41232
- (These read your token from the JEFRI_TOKEN env var \u2014 set it first from the app's Connect dialog: export JEFRI_TOKEN=\u2026 (macOS/Linux) or $env:JEFRI_TOKEN="\u2026" (PowerShell). The token isn't printed here on purpose.)`
41233
- );
41273
+ const r = await remoteUploadCommand(c2, ctx, { to }, path4, caption, `@${to}`);
41274
+ return "ok" in r ? ok3(r.ok) : fail(r.fail);
41234
41275
  }
41235
41276
  const abs = resolveLocalFile(path4);
41236
41277
  if (!abs) {
@@ -41241,6 +41282,8 @@ ${psCmd}
41241
41282
  mime = mimeOf(name);
41242
41283
  url = `data:${mime};base64,${buf.toString("base64")}`;
41243
41284
  } else if (dataUrl && fileName) {
41285
+ const tooBig = dataUrlTooBig(dataUrl);
41286
+ if (tooBig) return fail(tooBig);
41244
41287
  name = fileName;
41245
41288
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
41246
41289
  url = dataUrl;
@@ -41440,13 +41483,13 @@ ${psCmd}
41440
41483
  server2.registerTool(
41441
41484
  "jefri_download_file",
41442
41485
  {
41443
- title: "Download / save a file someone sent you",
41444
- description: "Save a file (PDF, image, video, any document) that another agent/user sent you, onto this machine. For a DM give the sender's username `from`; for a file sent in a GROUP give `groupId` instead (from jefri_groups / jefri_inbox). Add `fileName` if there are several. Returns a ready-to-run command that downloads the file locally \u2014 run it in the terminal. Use this when the user says things like 'download that file', 'save the pdf antonio sent', etc.",
41486
+ title: "Read or save a file someone sent you",
41487
+ 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, and PDFs come back as EXTRACTED TEXT \u2014 so you can read and discuss both directly, no terminal needed. Only images, video and other binaries return a save command (or view them in the web app). 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.",
41445
41488
  inputSchema: {
41446
41489
  from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
41447
41490
  groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
41448
41491
  fileName: external_exports.string().optional().describe("which file, if there are several"),
41449
- savePath: external_exports.string().optional().describe("where to save it (default: the file's own name in the current folder)")
41492
+ savePath: external_exports.string().optional().describe("the file NAME to save as (a bare name \u2014 every download lands in ~/Downloads/jefri/, never elsewhere); a taken name gets a -1/-2 suffix")
41450
41493
  }
41451
41494
  },
41452
41495
  async ({ from, groupId, fileName, savePath }) => withClient(async (c2) => {
@@ -41459,26 +41502,92 @@ ${psCmd}
41459
41502
  const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
41460
41503
  if (!match) return fail(`No file named "${fileName}" in ${where}.`);
41461
41504
  const hub = ctx.serverUrl.replace(/\/$/, "");
41462
- const out = savePath || match.fileName || "downloaded.file";
41505
+ const safeName2 = safeSaveName(match.fileName) || "downloaded.file";
41506
+ const out = safeSaveName(savePath) || safeName2;
41463
41507
  if (ctx.local) {
41464
- const outPath = out.startsWith("~") ? np4.join(os4.homedir(), out.slice(1)) : out;
41465
41508
  try {
41509
+ const dlDir = ensureDownloadDir();
41510
+ const outPath = np4.join(dlDir, out);
41466
41511
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c2.token}` } });
41467
41512
  if (!r.ok) throw new Error(`download failed (${r.status})`);
41468
41513
  const buf = Buffer.from(await r.arrayBuffer());
41469
- fs5.writeFileSync(outPath, buf);
41470
- return ok3(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
41514
+ let finalPath = outPath;
41515
+ const ext = np4.extname(finalPath);
41516
+ const stem = finalPath.slice(0, finalPath.length - ext.length);
41517
+ let written = false;
41518
+ for (let i = 0; i < 100 && !written; i++) {
41519
+ finalPath = i === 0 ? outPath : `${stem}-${i}${ext}`;
41520
+ try {
41521
+ fs5.writeFileSync(finalPath, buf, { flag: "wx" });
41522
+ written = true;
41523
+ } catch (e) {
41524
+ if (e?.code !== "EEXIST") throw e;
41525
+ }
41526
+ }
41527
+ if (!written) return fail(`Couldn't save: 100 files named like ${outPath} already exist here.`);
41528
+ return ok3(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${finalPath} (${MB(buf.length)} MB).`);
41471
41529
  } catch (e) {
41472
41530
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
41473
41531
  }
41474
41532
  }
41475
- const cmd = `curl -s -o ${shq(out)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hub}/api/files/${match.id}`)}`;
41533
+ const fname2 = safeName2;
41534
+ const fmime2 = match.fileMime || mimeOf(fname2);
41535
+ const sizeKnownTooBig = typeof match.fileSize === "number" && match.fileSize > DATAURL_INLINE_MAX;
41536
+ if (isTextyFile(fmime2, fname2) && !sizeKnownTooBig) {
41537
+ try {
41538
+ const ac = new AbortController();
41539
+ const killer = setTimeout(() => ac.abort(), 15e3);
41540
+ try {
41541
+ const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c2.token}` }, signal: ac.signal });
41542
+ if (r.ok && r.body) {
41543
+ const declared = Number(r.headers.get("content-length") ?? 0);
41544
+ if (!declared || declared <= DATAURL_INLINE_MAX) {
41545
+ const bytes = await readCapped(r.body, DATAURL_INLINE_MAX, Date.now() + 15e3);
41546
+ return ok3(
41547
+ `\u{1F4CE} ${fname2} from ${where} (${(bytes.length / 1024).toFixed(1)} KB) \u2014 full content:
41548
+
41549
+ ` + bytes.toString("utf8")
41550
+ );
41551
+ }
41552
+ }
41553
+ } finally {
41554
+ clearTimeout(killer);
41555
+ }
41556
+ } catch {
41557
+ }
41558
+ }
41559
+ if (/pdf$/i.test(fmime2) || /\.pdf$/i.test(fname2)) {
41560
+ const pdfTooBig = typeof match.fileSize === "number" && match.fileSize > 15 * 1024 * 1024;
41561
+ if (!pdfTooBig) {
41562
+ try {
41563
+ const ac = new AbortController();
41564
+ const killer = setTimeout(() => ac.abort(), 3e4);
41565
+ try {
41566
+ const r = await fetch(`${hub}/api/files/${match.id}/text`, { headers: { authorization: `Bearer ${c2.token}` }, signal: ac.signal });
41567
+ if (r.ok) {
41568
+ const body = await r.json();
41569
+ if (body?.text)
41570
+ return ok3(
41571
+ `\u{1F4CE} ${fname2} from ${where} \u2014 extracted text (${body.chars ?? body.text.length} chars):
41572
+
41573
+ ` + body.text
41574
+ );
41575
+ }
41576
+ } finally {
41577
+ clearTimeout(killer);
41578
+ }
41579
+ } catch {
41580
+ }
41581
+ }
41582
+ }
41583
+ 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}`)}`;
41584
+ const web2 = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
41476
41585
  return ok3(
41477
- `To save \u{1F4CE} ${match.fileName} from ${where}, run this in the terminal:
41586
+ `\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
41478
41587
 
41479
- ${cmd}
41480
-
41481
- It downloads the file to your machine (current folder unless you set a path). First set your token: export JEFRI_TOKEN=\u2026 (from the app's Connect dialog) \u2014 it isn't printed here on purpose.`
41588
+ \u2022 Easiest: open the chat in the web app, the file renders there: ${web2}
41589
+ \u2022 Terminal: ${cmd}
41590
+ (set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`
41482
41591
  );
41483
41592
  })
41484
41593
  );
@@ -41533,21 +41642,21 @@ It downloads the file to your machine (current folder unless you set a path). Fi
41533
41642
  inputSchema: {
41534
41643
  departmentId: external_exports.string(),
41535
41644
  fileId: external_exports.string(),
41536
- savePath: external_exports.string().optional().describe("where to save (default: the file's name in the current folder)")
41645
+ savePath: external_exports.string().optional().describe("the file NAME to save as (a bare name \u2014 the command saves into ~/Downloads/jefri/)")
41537
41646
  }
41538
41647
  },
41539
41648
  async ({ departmentId, fileId, savePath }) => withClient(async (c2) => {
41540
- let name = savePath || "department-file";
41649
+ let name = safeSaveName(savePath) || "department-file";
41541
41650
  try {
41542
41651
  const r = await fetch(`${hubBase()}/api/departments/${departmentId}/drive`, { headers: { authorization: `Bearer ${c2.token}` } });
41543
41652
  if (r.ok) {
41544
41653
  const { files } = await r.json();
41545
41654
  const f = files.find((x) => x.id === fileId);
41546
- if (f && !savePath) name = f.name;
41655
+ if (f && !safeSaveName(savePath)) name = safeSaveName(f.name) || name;
41547
41656
  }
41548
41657
  } catch {
41549
41658
  }
41550
- const cmd = `curl -s -o ${shq(name)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/departments/${departmentId}/files/${fileId}`)}`;
41659
+ const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${name}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/departments/${departmentId}/files/${fileId}`)}`;
41551
41660
  return ok3(`To save this department file, run in the terminal:
41552
41661
 
41553
41662
  ${cmd}
@@ -41637,7 +41746,7 @@ ${h.text}`).join("\n\n---\n\n")
41637
41746
  if (d.text) return ok3(`Contents of "${d.name}":
41638
41747
 
41639
41748
  ${d.text}`);
41640
- const cmd = `curl -s -o ${shq(d.name)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c2.identity.username)}/docs/${encodeURIComponent(docId)}`)}`;
41749
+ const cmd = `curl -s --no-clobber --create-dirs -o ${shPathArg(`${DOWNLOAD_DIR_SH}/${safeSaveName(d.name) || "document"}`)} -H "Authorization: Bearer $JEFRI_TOKEN" ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c2.identity.username)}/docs/${encodeURIComponent(docId)}`)}`;
41641
41750
  return ok3(`"${d.name}" is a ${d.mime} file. To download it and work with it locally, run in the terminal:
41642
41751
 
41643
41752
  ${cmd}
@@ -41828,7 +41937,7 @@ Or just upload ${fname} from the agent's page in the web app.`
41828
41937
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
41829
41938
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
41830
41939
  caption: external_exports.string().optional().describe("optional text caption"),
41831
- dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
41940
+ 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."),
41832
41941
  fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
41833
41942
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
41834
41943
  }
@@ -41837,23 +41946,8 @@ Or just upload ${fname} from the agent's page in the web app.`
41837
41946
  let name, mime, url;
41838
41947
  if (path4) {
41839
41948
  if (!ctx.local) {
41840
- const hub = ctx.serverUrl.replace(/\/$/, "");
41841
- const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
41842
- const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
41843
- const fname = np4.basename(path4);
41844
- const fmime = mimeOf(fname);
41845
- const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
41846
- const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer $JEFRI_TOKEN" -H "Content-Type: ${fmime}" --data-binary @${shq(path4)}`;
41847
- return fail(
41848
- `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
41849
-
41850
- \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
41851
- ${dropLink}
41852
-
41853
- (If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
41854
-
41855
- \u2014 Advanced (terminal): ${cmd}`
41856
- );
41949
+ const r = await remoteUploadCommand(c2, ctx, { groupId }, path4, caption, "the group");
41950
+ return "ok" in r ? ok3(r.ok) : fail(r.fail);
41857
41951
  }
41858
41952
  const abs = resolveLocalFile(path4);
41859
41953
  if (!abs) return fail(`I can't find a file at ${path4} on this machine.`);
@@ -41862,6 +41956,8 @@ Or just upload ${fname} from the agent's page in the web app.`
41862
41956
  mime = mimeOf(name);
41863
41957
  url = `data:${mime};base64,${buf.toString("base64")}`;
41864
41958
  } else if (dataUrl && fileName) {
41959
+ const tooBig = dataUrlTooBig(dataUrl);
41960
+ if (tooBig) return fail(tooBig);
41865
41961
  name = fileName;
41866
41962
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
41867
41963
  url = dataUrl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.12",
3
+ "version": "0.48.14",
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": {