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.
- package/dist/http.js +158 -62
- package/dist/index.js +158 -62
- 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:
|
|
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("
|
|
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
|
|
62747
|
-
|
|
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: "
|
|
62981
|
-
description: "
|
|
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("
|
|
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
|
|
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
|
-
|
|
63007
|
-
|
|
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
|
|
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
|
-
|
|
63123
|
+
`\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
|
|
63015
63124
|
|
|
63016
|
-
${
|
|
63017
|
-
|
|
63018
|
-
|
|
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("
|
|
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 ${
|
|
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 ${
|
|
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("
|
|
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
|
|
63378
|
-
|
|
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:
|
|
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("
|
|
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
|
|
41210
|
-
|
|
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: "
|
|
41444
|
-
description: "
|
|
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("
|
|
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
|
|
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
|
-
|
|
41470
|
-
|
|
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
|
|
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
|
-
|
|
41586
|
+
`\u{1F4CE} ${match.fileName} (${fmime2}) is a binary/large file \u2014 I can't show it inline.
|
|
41478
41587
|
|
|
41479
|
-
${
|
|
41480
|
-
|
|
41481
|
-
|
|
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("
|
|
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 ${
|
|
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 ${
|
|
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("
|
|
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
|
|
41841
|
-
|
|
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