jefrichat-mcp 0.48.13 → 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 +99 -17
  2. package/dist/index.js +99 -17
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -62114,9 +62114,25 @@ function declKey(extra, parts) {
62114
62114
  return "d-" + createHash2("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
62115
62115
  }
62116
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
+ };
62117
62132
  var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
62118
62133
  var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
62119
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);
62120
62136
  var DATAURL_INLINE_MAX = 256 * 1024;
62121
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;
62122
62138
  async function remoteUploadCommand(c, ctx, target, path3, caption, where) {
@@ -63004,13 +63020,13 @@ ${fp}`);
63004
63020
  server2.registerTool(
63005
63021
  "jefri_download_file",
63006
63022
  {
63007
- title: "Download / save a file someone sent you",
63008
- 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.",
63009
63025
  inputSchema: {
63010
63026
  from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
63011
63027
  groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
63012
63028
  fileName: external_exports.string().optional().describe("which file, if there are several"),
63013
- 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")
63014
63030
  }
63015
63031
  },
63016
63032
  async ({ from, groupId, fileName, savePath }) => withClient(async (c) => {
@@ -63023,26 +63039,92 @@ ${fp}`);
63023
63039
  const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
63024
63040
  if (!match) return fail(`No file named "${fileName}" in ${where}.`);
63025
63041
  const hub = ctx.serverUrl.replace(/\/$/, "");
63026
- const out = savePath || match.fileName || "downloaded.file";
63042
+ const safeName2 = safeSaveName(match.fileName) || "downloaded.file";
63043
+ const out = safeSaveName(savePath) || safeName2;
63027
63044
  if (ctx.local) {
63028
- const outPath = out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
63029
63045
  try {
63046
+ const dlDir = ensureDownloadDir();
63047
+ const outPath = np3.join(dlDir, out);
63030
63048
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
63031
63049
  if (!r.ok) throw new Error(`download failed (${r.status})`);
63032
63050
  const buf = Buffer.from(await r.arrayBuffer());
63033
- fs4.writeFileSync(outPath, buf);
63034
- 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).`);
63035
63066
  } catch (e) {
63036
63067
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
63037
63068
  }
63038
63069
  }
63039
- 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");
63040
63122
  return ok3(
63041
- `To save \u{1F4CE} ${match.fileName} from ${where}, run this in the terminal:
63042
-
63043
- ${cmd}
63123
+ `\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
63044
63124
 
63045
- 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.)`
63046
63128
  );
63047
63129
  })
63048
63130
  );
@@ -63097,21 +63179,21 @@ It downloads the file to your machine (current folder unless you set a path). Fi
63097
63179
  inputSchema: {
63098
63180
  departmentId: external_exports.string(),
63099
63181
  fileId: external_exports.string(),
63100
- 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/)")
63101
63183
  }
63102
63184
  },
63103
63185
  async ({ departmentId, fileId, savePath }) => withClient(async (c) => {
63104
- let name = savePath || "department-file";
63186
+ let name = safeSaveName(savePath) || "department-file";
63105
63187
  try {
63106
63188
  const r = await fetch(`${hubBase()}/api/departments/${departmentId}/drive`, { headers: { authorization: `Bearer ${c.token}` } });
63107
63189
  if (r.ok) {
63108
63190
  const { files } = await r.json();
63109
63191
  const f = files.find((x) => x.id === fileId);
63110
- if (f && !savePath) name = f.name;
63192
+ if (f && !safeSaveName(savePath)) name = safeSaveName(f.name) || name;
63111
63193
  }
63112
63194
  } catch {
63113
63195
  }
63114
- 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}`)}`;
63115
63197
  return ok3(`To save this department file, run in the terminal:
63116
63198
 
63117
63199
  ${cmd}
@@ -63201,7 +63283,7 @@ ${h.text}`).join("\n\n---\n\n")
63201
63283
  if (d.text) return ok3(`Contents of "${d.name}":
63202
63284
 
63203
63285
  ${d.text}`);
63204
- 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)}`)}`;
63205
63287
  return ok3(`"${d.name}" is a ${d.mime} file. To download it and work with it locally, run in the terminal:
63206
63288
 
63207
63289
  ${cmd}
package/dist/index.js CHANGED
@@ -40574,9 +40574,25 @@ function declKey(extra, parts) {
40574
40574
  return "d-" + createHash("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
40575
40575
  }
40576
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
+ };
40577
40592
  var shPathArg = (p) => p.startsWith("~/") ? `"$HOME"${shq(p.slice(1))}` : shq(p);
40578
40593
  var psq = (s) => `'${String(s).replace(/'/g, "''")}'`;
40579
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);
40580
40596
  var DATAURL_INLINE_MAX = 256 * 1024;
40581
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;
40582
40598
  async function remoteUploadCommand(c2, ctx, target, path4, caption, where) {
@@ -41467,13 +41483,13 @@ ${fp}`);
41467
41483
  server2.registerTool(
41468
41484
  "jefri_download_file",
41469
41485
  {
41470
- title: "Download / save a file someone sent you",
41471
- 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.",
41472
41488
  inputSchema: {
41473
41489
  from: external_exports.string().optional().describe("who sent you the file (their username) \u2014 for a DM"),
41474
41490
  groupId: external_exports.string().optional().describe("the group's id \u2014 for a file sent in a group"),
41475
41491
  fileName: external_exports.string().optional().describe("which file, if there are several"),
41476
- 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")
41477
41493
  }
41478
41494
  },
41479
41495
  async ({ from, groupId, fileName, savePath }) => withClient(async (c2) => {
@@ -41486,26 +41502,92 @@ ${fp}`);
41486
41502
  const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
41487
41503
  if (!match) return fail(`No file named "${fileName}" in ${where}.`);
41488
41504
  const hub = ctx.serverUrl.replace(/\/$/, "");
41489
- const out = savePath || match.fileName || "downloaded.file";
41505
+ const safeName2 = safeSaveName(match.fileName) || "downloaded.file";
41506
+ const out = safeSaveName(savePath) || safeName2;
41490
41507
  if (ctx.local) {
41491
- const outPath = out.startsWith("~") ? np4.join(os4.homedir(), out.slice(1)) : out;
41492
41508
  try {
41509
+ const dlDir = ensureDownloadDir();
41510
+ const outPath = np4.join(dlDir, out);
41493
41511
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c2.token}` } });
41494
41512
  if (!r.ok) throw new Error(`download failed (${r.status})`);
41495
41513
  const buf = Buffer.from(await r.arrayBuffer());
41496
- fs5.writeFileSync(outPath, buf);
41497
- 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).`);
41498
41529
  } catch (e) {
41499
41530
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
41500
41531
  }
41501
41532
  }
41502
- 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");
41503
41585
  return ok3(
41504
- `To save \u{1F4CE} ${match.fileName} from ${where}, run this in the terminal:
41505
-
41506
- ${cmd}
41586
+ `\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
41507
41587
 
41508
- 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.)`
41509
41591
  );
41510
41592
  })
41511
41593
  );
@@ -41560,21 +41642,21 @@ It downloads the file to your machine (current folder unless you set a path). Fi
41560
41642
  inputSchema: {
41561
41643
  departmentId: external_exports.string(),
41562
41644
  fileId: external_exports.string(),
41563
- 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/)")
41564
41646
  }
41565
41647
  },
41566
41648
  async ({ departmentId, fileId, savePath }) => withClient(async (c2) => {
41567
- let name = savePath || "department-file";
41649
+ let name = safeSaveName(savePath) || "department-file";
41568
41650
  try {
41569
41651
  const r = await fetch(`${hubBase()}/api/departments/${departmentId}/drive`, { headers: { authorization: `Bearer ${c2.token}` } });
41570
41652
  if (r.ok) {
41571
41653
  const { files } = await r.json();
41572
41654
  const f = files.find((x) => x.id === fileId);
41573
- if (f && !savePath) name = f.name;
41655
+ if (f && !safeSaveName(savePath)) name = safeSaveName(f.name) || name;
41574
41656
  }
41575
41657
  } catch {
41576
41658
  }
41577
- 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}`)}`;
41578
41660
  return ok3(`To save this department file, run in the terminal:
41579
41661
 
41580
41662
  ${cmd}
@@ -41664,7 +41746,7 @@ ${h.text}`).join("\n\n---\n\n")
41664
41746
  if (d.text) return ok3(`Contents of "${d.name}":
41665
41747
 
41666
41748
  ${d.text}`);
41667
- 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)}`)}`;
41668
41750
  return ok3(`"${d.name}" is a ${d.mime} file. To download it and work with it locally, run in the terminal:
41669
41751
 
41670
41752
  ${cmd}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.13",
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": {