jefrichat-mcp 0.49.51 → 0.49.53
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 +80 -19
- package/dist/index.js +94 -34
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -67197,8 +67197,34 @@ function parseInlineDataUrl(dataUrl) {
|
|
|
67197
67197
|
}
|
|
67198
67198
|
var optionalFileString = external_exports.string().nullish().transform((value) => value ?? void 0);
|
|
67199
67199
|
var REMOTE_FILE_GUIDANCE = "REMOTE connector (ChatGPT, Claude, or another cloud assistant): choose exactly ONE file source. (1) File in YOUR OWN sandbox: check its decoded byte size first. If readable and \u2264256KB (262144 bytes), send directly with `dataUrl` + `fileName`, no user upload step. This includes PDFs, CSVs, code, and small images. Use the actual bytes as canonical base64 in data:<mime>;base64,<base64>; omit `path` and `fileUrl`. (2) Over 256KB, unreadable, or unable to fit the tool call: use jefri_upload_link with the destination and fileName (up to 500 MB per file). If in your sandbox, present the file for download so the user can select it. Never compress, convert, resize, split, or reconstruct a file just to fit the inline limit; never stream base64 in prose. (3) File only on the USER'S DEVICE: use jefri_upload_link; do not read/base64 their local file. (4) An existing public http(s) URL \u2192 `fileUrl` (\u226425MB). Never invent a URL or pass sandbox:/file: URLs. A remote `path` (including /mnt/data) CANNOT read the assistant's sandbox or user's disk; it only creates an upload link, for compatibility. Link creation is NOT file delivery. Omit unused arguments (null is also accepted). If the host blocks a tool call for safety, respect the block; do not retry other arguments to bypass it.";
|
|
67200
|
-
|
|
67201
|
-
|
|
67200
|
+
function hostFileUploadEnabled(ctx) {
|
|
67201
|
+
return ctx.local === false && ctx.credentialLookup?.kind === "profile" && capsForCtx(ctx).has(CAP.REMOTE_SANDBOX) && (ctx.credentialLookup.profile === "grok_bot" || ctx.credentialLookup.profile === "muse");
|
|
67202
|
+
}
|
|
67203
|
+
var HOST_FILE_GUIDANCE = "Grok Bot/Muse REMOTE connector: choose by where the original file bytes live and the tools actually available. If YOUR OWN runtime can read the file and has an allowed outbound HTTP upload tool, use jefri_upload_link with the destination and fileName, then upload the original binary body from YOUR runtime to its scoped POST URL (up to 500 MB per file). Do not ask the user to re-upload a file you can upload yourself. The connector cannot read your runtime or the user's disk; a remote path only prepares an upload grant, never sends bytes. If no permitted binary upload tool is available, readable files \u2264256KB (262144 bytes) can use dataUrl + fileName with canonical data:<mime>;base64,<base64>, omitting path and fileUrl. Otherwise present the file for download and give the user the manual upload link. Files only on the USER'S DEVICE also need that manual upload. An existing public http(s) file URL can use fileUrl (\u226425MB); never invent a URL, use sandbox:/file: URLs, or make a private file public for this. Choose exactly ONE source; omit unused arguments (null is also accepted). Never compress, convert, resize, split, or reconstruct files to fit limits; never print file bytes or large base64 in prose. Link creation is NOT file delivery: confirm the upload's HTTP/JSON receipt; after an uncertain result check the conversation before retrying. Respect host permissions and safety blocks; never switch routes to bypass a block.";
|
|
67204
|
+
function hostUploadUrl(raw, hub) {
|
|
67205
|
+
if (typeof raw !== "string") return null;
|
|
67206
|
+
try {
|
|
67207
|
+
const u = new URL(raw);
|
|
67208
|
+
const base = new URL(hub);
|
|
67209
|
+
return (u.protocol === "https:" || u.protocol === "http:") && u.origin === base.origin && !u.username && !u.password && !u.search && !u.hash && /^\/api\/files\/upload\/[a-f0-9]{48}$/.test(u.pathname) ? u.href : null;
|
|
67210
|
+
} catch {
|
|
67211
|
+
return null;
|
|
67212
|
+
}
|
|
67213
|
+
}
|
|
67214
|
+
function hostUploadInstructions(uploads, where, mins, manualUrl, command) {
|
|
67215
|
+
return `Upload prepared for ${where}. No file has been sent yet. Single-use links expire in ~${mins} min; destination, file name and caption are fixed.
|
|
67216
|
+
${JSON.stringify({ uploads })}
|
|
67217
|
+
|
|
67218
|
+
First check each file's actual size (up to 500 MB) and verify that YOUR OWN runtime can read it and has an allowed outbound HTTP upload tool. If so, upload it yourself now: POST each file's raw binary body to its matching uploadUrl, with its actual Content-Type (application/octet-stream if unknown). No multipart, no base64, no Authorization header, and no redirects. For multiple files, upload sequentially, once per URL; never POST to the batch page or swap file-to-URL assignments. Keep grant URLs private; never send your Jefri token. Use a bounded request with no blind automatic retries. ` + (command ? `If your runtime has an allowed shell AND this exact file, the command is:
|
|
67219
|
+
${command}
|
|
67220
|
+
|
|
67221
|
+
` : "") + `Confirm each delivery only after HTTP success AND JSON ok === true with the delivery id; a prepared link is not success. On a timeout, connection loss, or 5xx, delivery is uncertain: check jefri_history (with for DMs, groupId for groups) before retrying; do not mint a fresh link and resend blindly. A 409/already-used response is not a new delivery. If you cannot verify the outcome, report it as unconfirmed and ask before resending.
|
|
67222
|
+
|
|
67223
|
+
If no permitted binary upload tool exists: readable files \u2264256KB can use dataUrl + fileName via jefri_send_file (jefri_send_group_file for groups); larger/unreadable files need the manual link: ${manualUrl}
|
|
67224
|
+
For manual upload, present a runtime file for download first so the user can pick it. A file only on the user's device requires their upload. Never claim this connector can read their disk or your runtime. Never reconstruct, compress, split, or expose a private file publicly; never inline more than 256KB. Respect all host permissions; a safety denial means stop, not try a different route to bypass it.`;
|
|
67225
|
+
}
|
|
67226
|
+
var fileSendSchema = (local, hostUpload = false) => ({
|
|
67227
|
+
path: optionalFileString.describe(local ? "Local file path to read and send directly, e.g. ~/Downloads/report.pdf. Do not combine with dataUrl or fileUrl." : hostUpload ? "Prepares a scoped upload grant; NEVER reads this path remotely or sends the file. If your runtime can read this file and has an allowed HTTP upload tool, follow the returned binary POST instructions. Otherwise use the manual link." : "Compatibility only: creates an upload link, NEVER reads /mnt/data or any local/sandbox path. For a readable sandbox file \u2264256KB use dataUrl + fileName WITHOUT path; otherwise use jefri_upload_link."),
|
|
67202
67228
|
caption: optionalFileString.describe("optional text caption"),
|
|
67203
67229
|
dataUrl: optionalFileString.describe(local ? "Small content generated in-conversation, max 256KB decoded. Use canonical data:<mime>;base64,<base64> with fileName; omit path and fileUrl. For files on disk use path directly, never read them into base64 yourself." : "Actual content you hold or can read in YOUR OWN sandbox (ChatGPT, Claude apps/Cowork, or another assistant), max 256KB decoded (262144 bytes). Use canonical data:<mime>;base64,<base64> and fileName; omit path and fileUrl. Never base64 a file on the USER machine. Never reconstruct missing bytes or inline files over the cap."),
|
|
67204
67230
|
fileUrl: optionalFileString.describe("An existing public http(s) URL the connector can fetch (\u226425MB), not a sandbox path or invented URL. Do not combine with path or dataUrl."),
|
|
@@ -67211,8 +67237,8 @@ function fileSourceError(args) {
|
|
|
67211
67237
|
return "A nonblank `fileName` is required with `dataUrl`. Nothing was sent.";
|
|
67212
67238
|
return null;
|
|
67213
67239
|
}
|
|
67214
|
-
function missingFileSource(local) {
|
|
67215
|
-
return "Provide exactly one file source: `path`, `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. " + (local ? "On this LOCAL connector, prefer path to send directly from disk." : REMOTE_FILE_GUIDANCE);
|
|
67240
|
+
function missingFileSource(local, hostUpload = false) {
|
|
67241
|
+
return "Provide exactly one file source: `path`, `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. " + (local ? "On this LOCAL connector, prefer path to send directly from disk." : hostUpload ? HOST_FILE_GUIDANCE : REMOTE_FILE_GUIDANCE);
|
|
67216
67242
|
}
|
|
67217
67243
|
var MINT_LINK_DEADLINE_MS = (() => {
|
|
67218
67244
|
const n = Number(process.env.MCP_MINT_LINK_DEADLINE_MS);
|
|
@@ -67299,6 +67325,17 @@ async function remoteUploadCommand(c, ctx, target, path4, caption, where, signal
|
|
|
67299
67325
|
clearTimeout(gKiller);
|
|
67300
67326
|
if (signal) signal.removeEventListener("abort", onHostAbort);
|
|
67301
67327
|
}
|
|
67328
|
+
if (hostFileUploadEnabled(ctx)) {
|
|
67329
|
+
const scoped = hostUploadUrl(uploadUrl, hub);
|
|
67330
|
+
if (!scoped) return { fail: "The upload-link service returned an unexpected upload URL. No file has been sent; do not upload to it." };
|
|
67331
|
+
return { ok: hostUploadInstructions(
|
|
67332
|
+
[{ fileName: fname, uploadUrl: scoped }],
|
|
67333
|
+
where,
|
|
67334
|
+
mins,
|
|
67335
|
+
scoped,
|
|
67336
|
+
`curl --silent --show-error --fail-with-body --connect-timeout 15 --max-time 300 -X POST ${shq(scoped)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path4)}`
|
|
67337
|
+
) };
|
|
67338
|
+
}
|
|
67302
67339
|
{
|
|
67303
67340
|
return {
|
|
67304
67341
|
ok: `This cloud connector can't read that path. No file has been sent.
|
|
@@ -67321,6 +67358,9 @@ Never base64 anything over 256KB through dataUrl \u2014 that is minutes of model
|
|
|
67321
67358
|
}
|
|
67322
67359
|
} catch {
|
|
67323
67360
|
}
|
|
67361
|
+
if (hostFileUploadEnabled(ctx)) return {
|
|
67362
|
+
fail: "The upload-link service did not provide a usable grant. No file has been sent. Do not upload or expose your token. Retry jefri_upload_link when the service recovers; if the host denied permission, stop instead. " + HOST_FILE_GUIDANCE
|
|
67363
|
+
};
|
|
67324
67364
|
const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
67325
67365
|
const dropLink = target.to ? `${web}/?to=${encodeURIComponent(target.to)}` : `${web}/?group=${encodeURIComponent(target.groupId)}`;
|
|
67326
67366
|
const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
@@ -68272,6 +68312,8 @@ function ackStandbyDelivered(id4) {
|
|
|
68272
68312
|
}
|
|
68273
68313
|
}
|
|
68274
68314
|
function registerAcpTools(server2, ctx) {
|
|
68315
|
+
const hostFileUpload = hostFileUploadEnabled(ctx);
|
|
68316
|
+
const remoteFileGuidance = hostFileUpload ? HOST_FILE_GUIDANCE : REMOTE_FILE_GUIDANCE;
|
|
68275
68317
|
const desktopHeadlessOnly = isHeadlessOnlyDesktop(ctx.credentialLookup?.kind === "profile" ? ctx.credentialLookup.profile : ctx.profile);
|
|
68276
68318
|
const autonomyProfile = ctx.credentialLookup?.kind === "profile" ? ctx.credentialLookup.profile : ctx.profile;
|
|
68277
68319
|
const backgroundOnly = isBackgroundOnlyAutonomyProfile(autonomyProfile);
|
|
@@ -68709,12 +68751,12 @@ ${fp}`);
|
|
|
68709
68751
|
"jefri_upload_link",
|
|
68710
68752
|
{
|
|
68711
68753
|
title: "Get an upload link (any file up to 500 MB, full quality)",
|
|
68712
|
-
description: "Get a ONE-TAP UPLOAD LINK to send files or images \u2014 up to 5 PER LINK via `fileNames`, up to 500 MB each, FULL quality, no base64, no compression, no path needed. For ChatGPT, Claude, and other remote assistants: small files \u2264256KB (already held OR readable in YOUR OWN sandbox \u2014 PDFs, CSVs, code, small images) go directly through jefri_send_file (or jefri_send_group_file for groups) with dataUrl + fileName ONLY, no path or link needed. Use THIS tool for larger files, unreadable files, files that cannot fit the tool call, or files only on the user's device. One cheap call, then hand the user the link \u2014 they open it (phone works), pick the file, and it delivers to the recipient automatically. Give `to` (a username) or `groupId`, plus the `fileName` the delivery should carry. The link is single-use and expires in ~15 minutes; mint a fresh one any time. If the file lives in YOUR sandbox, present it to the user for download first so they can pick it at the link. NEVER stream file bytes through plain output (base64/pixel data) \u2014 that burns the user's tokens. The ONE sanctioned carrier is a \u2264256KB `dataUrl` TOOL ARGUMENT via jefri_send_file (near the cap it may still strain output limits \u2014 then use this link); anything larger only ever travels as this link. Creating the link is NOT delivery \u2014 the user must upload the file. Respect host safety blocks; do not use another route to bypass one. Omit unused fields or pass null.",
|
|
68754
|
+
description: hostFileUpload ? "Prepare scoped upload URLs, up to 5 files via fileNames (500 MB each). Give to OR groupId, plus fileName OR fileNames. Each URL is single-use, expires in ~15 minutes, and fixes the destination, name and caption. " + HOST_FILE_GUIDANCE : "Get a ONE-TAP UPLOAD LINK to send files or images \u2014 up to 5 PER LINK via `fileNames`, up to 500 MB each, FULL quality, no base64, no compression, no path needed. For ChatGPT, Claude, and other remote assistants: small files \u2264256KB (already held OR readable in YOUR OWN sandbox \u2014 PDFs, CSVs, code, small images) go directly through jefri_send_file (or jefri_send_group_file for groups) with dataUrl + fileName ONLY, no path or link needed. Use THIS tool for larger files, unreadable files, files that cannot fit the tool call, or files only on the user's device. One cheap call, then hand the user the link \u2014 they open it (phone works), pick the file, and it delivers to the recipient automatically. Give `to` (a username) or `groupId`, plus the `fileName` the delivery should carry. The link is single-use and expires in ~15 minutes; mint a fresh one any time. If the file lives in YOUR sandbox, present it to the user for download first so they can pick it at the link. NEVER stream file bytes through plain output (base64/pixel data) \u2014 that burns the user's tokens. The ONE sanctioned carrier is a \u2264256KB `dataUrl` TOOL ARGUMENT via jefri_send_file (near the cap it may still strain output limits \u2014 then use this link); anything larger only ever travels as this link. Creating the link is NOT delivery \u2014 the user must upload the file. Respect host safety blocks; do not use another route to bypass one. Omit unused fields or pass null.",
|
|
68713
68755
|
inputSchema: {
|
|
68714
68756
|
to: optionalFileString.describe("recipient username (or use groupId)"),
|
|
68715
68757
|
groupId: optionalFileString.describe("group id (from jefri_groups) instead of `to`"),
|
|
68716
68758
|
fileName: optionalFileString.describe("the name the delivered file should carry, e.g. photo.png (single file)"),
|
|
68717
|
-
fileNames: external_exports.array(external_exports.string()).nullish().transform((value) => value ?? void 0).describe('for a MULTI-file link (2\u20135): the delivery names in order, e.g. ["foto-1.jpg","foto-2.jpg"] \u2014 one link, the user picks them all'),
|
|
68759
|
+
fileNames: external_exports.array(external_exports.string()).nullish().transform((value) => value ?? void 0).describe(hostFileUpload ? "For 2\u20135 files: delivery names in order. Returns one binary POST URL per file plus a manual batch page." : 'for a MULTI-file link (2\u20135): the delivery names in order, e.g. ["foto-1.jpg","foto-2.jpg"] \u2014 one link, the user picks them all'),
|
|
68718
68760
|
caption: optionalFileString.describe("optional text caption delivered with the file")
|
|
68719
68761
|
}
|
|
68720
68762
|
},
|
|
@@ -68771,6 +68813,23 @@ ${fp}`);
|
|
|
68771
68813
|
const mins = Math.round(body.expiresInMs / 6e4);
|
|
68772
68814
|
const where = to ? `@${safeHandle(to)}` : "the group";
|
|
68773
68815
|
const delivers = names.length ? `${names.length} files: ${names.map((n) => `\u{1F4CE} ${n}`).join(", ")}` : `\u{1F4CE} ${fileName}`;
|
|
68816
|
+
if (hostFileUpload) {
|
|
68817
|
+
const expectedNames = (names.length ? names : [fileName]).map((name) => (name.split(/[\\/]/).pop() ?? "file").slice(0, 300) || "file");
|
|
68818
|
+
const rawUploads = names.length ? body.uploads : [{ fileName: expectedNames[0], uploadUrl: body.uploadUrl }];
|
|
68819
|
+
if (!Array.isArray(rawUploads) || rawUploads.length !== expectedNames.length)
|
|
68820
|
+
return fail("The upload-link service returned incomplete file upload data. No file has been sent; do not upload to it.");
|
|
68821
|
+
const uploads = [];
|
|
68822
|
+
for (const [i, upload] of rawUploads.entries()) {
|
|
68823
|
+
const scoped = hostUploadUrl(upload?.uploadUrl, hub);
|
|
68824
|
+
if (!scoped || upload?.fileName !== expectedNames[i] || uploads.some((u) => u.uploadUrl === scoped))
|
|
68825
|
+
return fail("The upload-link service returned unexpected file upload data. No file has been sent; do not upload to it.");
|
|
68826
|
+
uploads.push({ fileName: expectedNames[i], uploadUrl: scoped });
|
|
68827
|
+
}
|
|
68828
|
+
const expectedManual = names.length ? `${hub}/api/files/upload-batch/${uploads.map((u) => u.uploadUrl.split("/").pop()).join(".")}` : uploads[0].uploadUrl;
|
|
68829
|
+
if (parsedUrl !== expectedManual)
|
|
68830
|
+
return fail("The upload-link service returned an unexpected manual upload URL. No file has been sent; do not upload to it.");
|
|
68831
|
+
return ok3(hostUploadInstructions(uploads, where, mins, expectedManual));
|
|
68832
|
+
}
|
|
68774
68833
|
return ok3(
|
|
68775
68834
|
`\u{1F517} One-tap upload link for ${where} (delivers ${delivers}; valid ~${mins} min, single-use):
|
|
68776
68835
|
${parsedUrl}
|
|
@@ -68883,10 +68942,10 @@ ${lines.join("\n")}`
|
|
|
68883
68942
|
"jefri_send_file",
|
|
68884
68943
|
{
|
|
68885
68944
|
title: "Send a file, PDF, or image",
|
|
68886
|
-
description: ctx.local ? "Send a file to a Jefri Chat user/agent \u2014 PDF, image, video, any document. On this LOCAL connector, `path` reads the file off disk and sends it DIRECTLY (up to 500 MB) \u2014 always prefer `path`; never read a file into base64 yourself. `dataUrl` + `fileName` is only for small (\u2264256KB) content you generated in-conversation, and `fileUrl` fetches a public URL. Optional `caption` always." : "Send a file to a Jefri Chat user/agent. " +
|
|
68945
|
+
description: ctx.local ? "Send a file to a Jefri Chat user/agent \u2014 PDF, image, video, any document. On this LOCAL connector, `path` reads the file off disk and sends it DIRECTLY (up to 500 MB) \u2014 always prefer `path`; never read a file into base64 yourself. `dataUrl` + `fileName` is only for small (\u2264256KB) content you generated in-conversation, and `fileUrl` fetches a public URL. Optional `caption` always." : "Send a file to a Jefri Chat user/agent. " + remoteFileGuidance,
|
|
68887
68946
|
inputSchema: {
|
|
68888
68947
|
to: external_exports.string().describe("recipient username"),
|
|
68889
|
-
...fileSendSchema(!!ctx.local)
|
|
68948
|
+
...fileSendSchema(!!ctx.local, hostFileUpload)
|
|
68890
68949
|
}
|
|
68891
68950
|
},
|
|
68892
68951
|
async ({ to, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
@@ -68921,7 +68980,7 @@ ${lines.join("\n")}`
|
|
|
68921
68980
|
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
68922
68981
|
}
|
|
68923
68982
|
} else {
|
|
68924
|
-
return fail(missingFileSource(!!ctx.local));
|
|
68983
|
+
return fail(missingFileSource(!!ctx.local, hostFileUpload));
|
|
68925
68984
|
}
|
|
68926
68985
|
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
68927
68986
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
@@ -69745,10 +69804,10 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
69745
69804
|
"jefri_send_group_file",
|
|
69746
69805
|
{
|
|
69747
69806
|
title: "Send a file to a group",
|
|
69748
|
-
description: ctx.local ? "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). On this LOCAL connector, `path` sends the file DIRECTLY off disk (up to 500 MB) \u2014 always prefer `path`; `dataUrl` only for small generated content; `fileUrl` for a public URL." : "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). " +
|
|
69807
|
+
description: ctx.local ? "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). On this LOCAL connector, `path` sends the file DIRECTLY off disk (up to 500 MB) \u2014 always prefer `path`; `dataUrl` only for small generated content; `fileUrl` for a public URL." : "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). " + remoteFileGuidance,
|
|
69749
69808
|
inputSchema: {
|
|
69750
69809
|
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
69751
|
-
...fileSendSchema(!!ctx.local)
|
|
69810
|
+
...fileSendSchema(!!ctx.local, hostFileUpload)
|
|
69752
69811
|
}
|
|
69753
69812
|
},
|
|
69754
69813
|
async ({ groupId, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
@@ -69781,7 +69840,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
69781
69840
|
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
69782
69841
|
}
|
|
69783
69842
|
} else {
|
|
69784
|
-
return fail(missingFileSource(!!ctx.local));
|
|
69843
|
+
return fail(missingFileSource(!!ctx.local, hostFileUpload));
|
|
69785
69844
|
}
|
|
69786
69845
|
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
69787
69846
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
@@ -71561,15 +71620,16 @@ function registerGrokMessageTool(server2, options) {
|
|
|
71561
71620
|
if (options.lookup.kind !== "profile" || options.lookup.profile !== "grok_bot") return;
|
|
71562
71621
|
server2.registerTool("jefri_grok_read_message", {
|
|
71563
71622
|
title: "Read the exact message behind a Grok webhook",
|
|
71564
|
-
description: "Read one plain incoming DM by the webhook's from and hubMessageId (or captionMessageId). Works after inbox acknowledgement. Read-only; never marks anything seen. Verify jefri_whoami matches the event's agent and server first. For a file, use jefri_download_file with its exact messageId to retrieve bytes.",
|
|
71623
|
+
description: "Read one plain incoming DM or group message by the webhook's from and hubMessageId (or captionMessageId). Pass groupId only when present in the event; reply to that same group with jefri_send_group, never a DM fallback. Works after inbox acknowledgement. Read-only; never marks anything seen. Verify jefri_whoami matches the event's agent and server first. For a file, use jefri_download_file with its exact messageId and groupId when present to retrieve bytes.",
|
|
71565
71624
|
inputSchema: {
|
|
71566
71625
|
from: external_exports.string().min(1).max(120).describe("Exact sender username from the webhook"),
|
|
71567
|
-
messageId: external_exports.string().min(1).max(200).describe("Exact hubMessageId or captionMessageId from the webhook")
|
|
71626
|
+
messageId: external_exports.string().min(1).max(200).describe("Exact hubMessageId or captionMessageId from the webhook"),
|
|
71627
|
+
groupId: external_exports.string().min(1).max(200).optional().describe("Exact groupId from a group webhook; omit for DMs")
|
|
71568
71628
|
},
|
|
71569
71629
|
annotations: { readOnlyHint: true }
|
|
71570
|
-
}, async ({ from, messageId }, extra) => {
|
|
71630
|
+
}, async ({ from, messageId, groupId }, extra) => {
|
|
71571
71631
|
const fail2 = (text5) => ({ isError: true, content: [{ type: "text", text: text5 }] });
|
|
71572
|
-
if (!isRoutingKey(from) || !/^[A-Za-z0-9_-]{1,200}$/.test(messageId)) return fail2("Invalid sender or
|
|
71632
|
+
if (!isRoutingKey(from) || !/^[A-Za-z0-9_-]{1,200}$/.test(messageId) || groupId !== void 0 && !/^[A-Za-z0-9_-]{1,200}$/.test(groupId)) return fail2("Invalid sender, message or group id.");
|
|
71573
71633
|
const me2 = options.client.identity?.username;
|
|
71574
71634
|
if (!me2 || !options.client.token) return fail2("Jefri identity is not verified. Reconnect this Grok connector.");
|
|
71575
71635
|
const controller = new AbortController();
|
|
@@ -71579,7 +71639,7 @@ function registerGrokMessageTool(server2, options) {
|
|
|
71579
71639
|
const timeout = setTimeout(onAbort, 1e4);
|
|
71580
71640
|
try {
|
|
71581
71641
|
const url = new URL(options.hub.replace(/\/$/, "") + "/api/history");
|
|
71582
|
-
url.searchParams.set("conversationId", dmConversationId(me2, from));
|
|
71642
|
+
url.searchParams.set("conversationId", groupId ? groupConversationId(groupId) : dmConversationId(me2, from));
|
|
71583
71643
|
url.searchParams.set("around", messageId);
|
|
71584
71644
|
url.searchParams.set("peek", "1");
|
|
71585
71645
|
const response = await (options.fetch ?? fetch)(url, {
|
|
@@ -71590,13 +71650,14 @@ function registerGrokMessageTool(server2, options) {
|
|
|
71590
71650
|
if (!response.ok || !response.body) return fail2("The exact message is unavailable or access was denied. Do not substitute another message.");
|
|
71591
71651
|
const data = JSON.parse((await readCapped(response.body, 2 * 1024 * 1024)).toString("utf8"));
|
|
71592
71652
|
const row = Array.isArray(data.messages) ? data.messages.find((m) => m.id === messageId) : null;
|
|
71593
|
-
if (!row || row.senderUsername !== from || row.
|
|
71594
|
-
return fail2("No matching plain incoming
|
|
71653
|
+
if (!row || row.senderUsername !== from || row.debateId || row.encryptionMode !== "audited" || (groupId ? row.groupId !== groupId || row.receiver != null : row.receiver !== me2 || !!row.groupId) || !["text", "file"].includes(row.kind) || typeof row.content !== "string")
|
|
71654
|
+
return fail2("No matching plain incoming message was found. Do not substitute another message.");
|
|
71595
71655
|
const message = {
|
|
71596
71656
|
id: row.id,
|
|
71597
71657
|
from: row.senderUsername,
|
|
71598
71658
|
agent: me2,
|
|
71599
71659
|
kind: row.kind,
|
|
71660
|
+
...groupId ? { groupId } : {},
|
|
71600
71661
|
text: row.kind === "text" ? row.content : null,
|
|
71601
71662
|
fileId: row.kind === "file" ? row.id : null,
|
|
71602
71663
|
fileName: row.kind === "file" ? row.fileName : null,
|
package/dist/index.js
CHANGED
|
@@ -75,7 +75,7 @@ from gateway.platforms.event import MessageEvent, MessageType, ProcessingOutcome
|
|
|
75
75
|
from gateway.platforms._shared import extra_or_secret, get_scoped_secret
|
|
76
76
|
from gateway.status import acquire_scoped_lock, release_scoped_lock
|
|
77
77
|
|
|
78
|
-
from .native_state import NativeState, read_config
|
|
78
|
+
from .native_state import NativeState, read_config
|
|
79
79
|
from .sdk_client import JefriClient
|
|
80
80
|
from .recovery import HistoryRecovery
|
|
81
81
|
from .delivery import deliver, delivery_code, reply_chunks
|
|
@@ -132,6 +132,7 @@ class JefriAdapter(BasePlatformAdapter):
|
|
|
132
132
|
self.waiter = None
|
|
133
133
|
self.current = None
|
|
134
134
|
self.last_error = None
|
|
135
|
+
self.delivery_error = None
|
|
135
136
|
self.stopping = False
|
|
136
137
|
self.send_index = 0
|
|
137
138
|
self.sent_keys = {}
|
|
@@ -191,6 +192,8 @@ class JefriAdapter(BasePlatformAdapter):
|
|
|
191
192
|
self.observed_revision = read_config(self.connection_file).get("controlRevision")
|
|
192
193
|
self.recovery = HistoryRecovery()
|
|
193
194
|
self.recovery_error = self.connection_error = self.last_error = None
|
|
195
|
+
if self.state.recovered_uncertain:
|
|
196
|
+
self.last_error = "Interrupted native tasks need attention and will not be replayed; the owner's ON/OFF setting is unchanged"
|
|
194
197
|
self.next_recovery = self.next_connection_check = self.retry_work_after = 0
|
|
195
198
|
try:
|
|
196
199
|
self.version = json.loads((Path(self.connection_file).parent / "runtime/package.json").read_text())["version"]
|
|
@@ -510,9 +513,8 @@ class JefriAdapter(BasePlatformAdapter):
|
|
|
510
513
|
return
|
|
511
514
|
self.last_error = detail
|
|
512
515
|
await self._notice(message, "This task needs attention: execution or reply delivery could not be confirmed. I will not repeat its actions automatically.", "attention")
|
|
513
|
-
|
|
514
|
-
if
|
|
515
|
-
private_write(self.connection_file, {**current, "enabled": False})
|
|
516
|
+
# This ID is now fenced against execution. Keep the owner's intent and
|
|
517
|
+
# the rest of the queue intact, including if a notice races an OFF/ON.
|
|
516
518
|
|
|
517
519
|
async def _work(self):
|
|
518
520
|
while not self.stopping:
|
|
@@ -536,6 +538,7 @@ class JefriAdapter(BasePlatformAdapter):
|
|
|
536
538
|
self.started_at = time.monotonic()
|
|
537
539
|
self.next_notice = self.started_at + self.notice_delay
|
|
538
540
|
self.send_index = 0
|
|
541
|
+
self.delivery_error = None
|
|
539
542
|
self.sent_keys = {}
|
|
540
543
|
self.send_receipts = {}
|
|
541
544
|
self.edits = {}
|
|
@@ -563,7 +566,7 @@ class JefriAdapter(BasePlatformAdapter):
|
|
|
563
566
|
self.last_error = None
|
|
564
567
|
self.last_processed = int(time.time() * 1000)
|
|
565
568
|
else:
|
|
566
|
-
await self._uncertain(item, m, "
|
|
569
|
+
await self._uncertain(item, m, self.delivery_error or "Native completion unconfirmed; inspect native session and Jefri history. This task will not be replayed; other eligible messages can continue")
|
|
567
570
|
self._warning("delivery_uncertain")
|
|
568
571
|
except Exception as error:
|
|
569
572
|
if not dispatched:
|
|
@@ -571,7 +574,7 @@ class JefriAdapter(BasePlatformAdapter):
|
|
|
571
574
|
self.retry_work_after = time.monotonic() + 30
|
|
572
575
|
self._warning("preflight_retry")
|
|
573
576
|
continue
|
|
574
|
-
await self._uncertain(item, m, "Native execution
|
|
577
|
+
await self._uncertain(item, m, "Native execution unconfirmed (" + delivery_code(error) + "); inspect session before retrying. This task will not be replayed; other eligible messages can continue")
|
|
575
578
|
self._warning("execution_uncertain")
|
|
576
579
|
finally:
|
|
577
580
|
self._retire_questions()
|
|
@@ -588,6 +591,7 @@ class JefriAdapter(BasePlatformAdapter):
|
|
|
588
591
|
self.waiter.set_result(outcome)
|
|
589
592
|
|
|
590
593
|
async def send(self, chat_id, content, reply_to=None, metadata=None):
|
|
594
|
+
turn_id = None
|
|
591
595
|
try:
|
|
592
596
|
if isinstance(metadata, dict) and metadata.get("expect_edits"):
|
|
593
597
|
# Some Hermes proxy/older paths still construct a stream for
|
|
@@ -618,6 +622,8 @@ class JefriAdapter(BasePlatformAdapter):
|
|
|
618
622
|
return SendResult(success=True, message_id=self.send_receipts[chunk_key])
|
|
619
623
|
except Exception as error:
|
|
620
624
|
self._warning("send_unconfirmed")
|
|
625
|
+
if turn_id and self.current and self.current["id"] == turn_id and native_turn.get() == turn_id:
|
|
626
|
+
self.delivery_error = "Native reply delivery unconfirmed (" + delivery_code(error) + "); inspect Jefri history. This task will not be replayed; other eligible messages can continue"
|
|
621
627
|
# Suppress Hermes's differently formatted fallback after an unknown
|
|
622
628
|
# result: changing the payload/key could duplicate an accepted send.
|
|
623
629
|
return SendResult(success=False, error="Jefri send timed out or is unconfirmed (" + delivery_code(error) + "); inspect history before retrying")
|
|
@@ -757,10 +763,9 @@ class NativeState:
|
|
|
757
763
|
self.db.execute("CREATE TABLE IF NOT EXISTS turns (id TEXT PRIMARY KEY, chat TEXT NOT NULL, message TEXT, state TEXT NOT NULL, at INTEGER NOT NULL, reply TEXT, sent INTEGER NOT NULL DEFAULT 0, detail TEXT)")
|
|
758
764
|
self.db.execute("CREATE TABLE IF NOT EXISTS sibling_admissions (id TEXT PRIMARY KEY, sender TEXT NOT NULL, at INTEGER NOT NULL)")
|
|
759
765
|
self.db.execute("CREATE TABLE IF NOT EXISTS group_rearm (id TEXT PRIMARY KEY, cutoff TEXT NOT NULL)")
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
self.cancel_queued()
|
|
766
|
+
self.recovered_uncertain = self.db.execute("UPDATE turns SET state='uncertain',detail='Gateway stopped mid-turn; inspect native session and Jefri history before retrying' WHERE state IN ('running','sending')").rowcount
|
|
767
|
+
# Fence interrupted IDs, not the whole responder. A restart must not
|
|
768
|
+
# change the owner's saved choice or discard undispatched queued work.
|
|
764
769
|
|
|
765
770
|
@staticmethod
|
|
766
771
|
def _db(file):
|
|
@@ -59492,6 +59497,31 @@ function parseInlineDataUrl(dataUrl) {
|
|
|
59492
59497
|
};
|
|
59493
59498
|
return { mime: m[1], bytes: bytes2 };
|
|
59494
59499
|
}
|
|
59500
|
+
function hostFileUploadEnabled(ctx) {
|
|
59501
|
+
return ctx.local === false && ctx.credentialLookup?.kind === "profile" && capsForCtx(ctx).has(CAP.REMOTE_SANDBOX) && (ctx.credentialLookup.profile === "grok_bot" || ctx.credentialLookup.profile === "muse");
|
|
59502
|
+
}
|
|
59503
|
+
function hostUploadUrl(raw, hub) {
|
|
59504
|
+
if (typeof raw !== "string") return null;
|
|
59505
|
+
try {
|
|
59506
|
+
const u = new URL(raw);
|
|
59507
|
+
const base = new URL(hub);
|
|
59508
|
+
return (u.protocol === "https:" || u.protocol === "http:") && u.origin === base.origin && !u.username && !u.password && !u.search && !u.hash && /^\/api\/files\/upload\/[a-f0-9]{48}$/.test(u.pathname) ? u.href : null;
|
|
59509
|
+
} catch {
|
|
59510
|
+
return null;
|
|
59511
|
+
}
|
|
59512
|
+
}
|
|
59513
|
+
function hostUploadInstructions(uploads, where, mins, manualUrl, command) {
|
|
59514
|
+
return `Upload prepared for ${where}. No file has been sent yet. Single-use links expire in ~${mins} min; destination, file name and caption are fixed.
|
|
59515
|
+
${JSON.stringify({ uploads })}
|
|
59516
|
+
|
|
59517
|
+
First check each file's actual size (up to 500 MB) and verify that YOUR OWN runtime can read it and has an allowed outbound HTTP upload tool. If so, upload it yourself now: POST each file's raw binary body to its matching uploadUrl, with its actual Content-Type (application/octet-stream if unknown). No multipart, no base64, no Authorization header, and no redirects. For multiple files, upload sequentially, once per URL; never POST to the batch page or swap file-to-URL assignments. Keep grant URLs private; never send your Jefri token. Use a bounded request with no blind automatic retries. ` + (command ? `If your runtime has an allowed shell AND this exact file, the command is:
|
|
59518
|
+
${command}
|
|
59519
|
+
|
|
59520
|
+
` : "") + `Confirm each delivery only after HTTP success AND JSON ok === true with the delivery id; a prepared link is not success. On a timeout, connection loss, or 5xx, delivery is uncertain: check jefri_history (with for DMs, groupId for groups) before retrying; do not mint a fresh link and resend blindly. A 409/already-used response is not a new delivery. If you cannot verify the outcome, report it as unconfirmed and ask before resending.
|
|
59521
|
+
|
|
59522
|
+
If no permitted binary upload tool exists: readable files \u2264256KB can use dataUrl + fileName via jefri_send_file (jefri_send_group_file for groups); larger/unreadable files need the manual link: ${manualUrl}
|
|
59523
|
+
For manual upload, present a runtime file for download first so the user can pick it. A file only on the user's device requires their upload. Never claim this connector can read their disk or your runtime. Never reconstruct, compress, split, or expose a private file publicly; never inline more than 256KB. Respect all host permissions; a safety denial means stop, not try a different route to bypass it.`;
|
|
59524
|
+
}
|
|
59495
59525
|
function fileSourceError(args) {
|
|
59496
59526
|
if ([args.path, args.dataUrl, args.fileUrl].filter(Boolean).length > 1)
|
|
59497
59527
|
return "Choose exactly ONE file source: `path`, `dataUrl` + `fileName`, or `fileUrl`. Nothing was sent. For a readable sandbox file \u2264256KB, use `dataUrl` + `fileName` WITHOUT `path` or `fileUrl`.";
|
|
@@ -59499,8 +59529,8 @@ function fileSourceError(args) {
|
|
|
59499
59529
|
return "A nonblank `fileName` is required with `dataUrl`. Nothing was sent.";
|
|
59500
59530
|
return null;
|
|
59501
59531
|
}
|
|
59502
|
-
function missingFileSource(local) {
|
|
59503
|
-
return "Provide exactly one file source: `path`, `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. " + (local ? "On this LOCAL connector, prefer path to send directly from disk." : REMOTE_FILE_GUIDANCE);
|
|
59532
|
+
function missingFileSource(local, hostUpload = false) {
|
|
59533
|
+
return "Provide exactly one file source: `path`, `dataUrl` + `fileName` (\u2264256KB), or a public `fileUrl`. " + (local ? "On this LOCAL connector, prefer path to send directly from disk." : hostUpload ? HOST_FILE_GUIDANCE : REMOTE_FILE_GUIDANCE);
|
|
59504
59534
|
}
|
|
59505
59535
|
async function mintFileLink(hub, token, fileId) {
|
|
59506
59536
|
const ac = new AbortController();
|
|
@@ -59580,6 +59610,17 @@ async function remoteUploadCommand(c2, ctx, target, path39, caption, where, sign
|
|
|
59580
59610
|
clearTimeout(gKiller);
|
|
59581
59611
|
if (signal) signal.removeEventListener("abort", onHostAbort);
|
|
59582
59612
|
}
|
|
59613
|
+
if (hostFileUploadEnabled(ctx)) {
|
|
59614
|
+
const scoped = hostUploadUrl(uploadUrl, hub);
|
|
59615
|
+
if (!scoped) return { fail: "The upload-link service returned an unexpected upload URL. No file has been sent; do not upload to it." };
|
|
59616
|
+
return { ok: hostUploadInstructions(
|
|
59617
|
+
[{ fileName: fname, uploadUrl: scoped }],
|
|
59618
|
+
where,
|
|
59619
|
+
mins,
|
|
59620
|
+
scoped,
|
|
59621
|
+
`curl --silent --show-error --fail-with-body --connect-timeout 15 --max-time 300 -X POST ${shq(scoped)} -H ${shq(`Content-Type: ${fmime}`)} --data-binary @${shPathArg(path39)}`
|
|
59622
|
+
) };
|
|
59623
|
+
}
|
|
59583
59624
|
{
|
|
59584
59625
|
return {
|
|
59585
59626
|
ok: `This cloud connector can't read that path. No file has been sent.
|
|
@@ -59602,6 +59643,9 @@ Never base64 anything over 256KB through dataUrl \u2014 that is minutes of model
|
|
|
59602
59643
|
}
|
|
59603
59644
|
} catch {
|
|
59604
59645
|
}
|
|
59646
|
+
if (hostFileUploadEnabled(ctx)) return {
|
|
59647
|
+
fail: "The upload-link service did not provide a usable grant. No file has been sent. Do not upload or expose your token. Retry jefri_upload_link when the service recovers; if the host denied permission, stop instead. " + HOST_FILE_GUIDANCE
|
|
59648
|
+
};
|
|
59605
59649
|
const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
59606
59650
|
const dropLink = target.to ? `${web}/?to=${encodeURIComponent(target.to)}` : `${web}/?group=${encodeURIComponent(target.groupId)}`;
|
|
59607
59651
|
const q = (target.to ? `to=${encodeURIComponent(target.to)}` : `groupId=${encodeURIComponent(target.groupId)}`) + `&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
@@ -60349,6 +60393,8 @@ function ackStandbyDelivered(id5) {
|
|
|
60349
60393
|
}
|
|
60350
60394
|
}
|
|
60351
60395
|
function registerAcpTools(server2, ctx) {
|
|
60396
|
+
const hostFileUpload = hostFileUploadEnabled(ctx);
|
|
60397
|
+
const remoteFileGuidance = hostFileUpload ? HOST_FILE_GUIDANCE : REMOTE_FILE_GUIDANCE;
|
|
60352
60398
|
const desktopHeadlessOnly = isHeadlessOnlyDesktop(ctx.credentialLookup?.kind === "profile" ? ctx.credentialLookup.profile : ctx.profile);
|
|
60353
60399
|
const autonomyProfile = ctx.credentialLookup?.kind === "profile" ? ctx.credentialLookup.profile : ctx.profile;
|
|
60354
60400
|
const backgroundOnly = isBackgroundOnlyAutonomyProfile(autonomyProfile);
|
|
@@ -60786,12 +60832,12 @@ ${fp}`);
|
|
|
60786
60832
|
"jefri_upload_link",
|
|
60787
60833
|
{
|
|
60788
60834
|
title: "Get an upload link (any file up to 500 MB, full quality)",
|
|
60789
|
-
description: "Get a ONE-TAP UPLOAD LINK to send files or images \u2014 up to 5 PER LINK via `fileNames`, up to 500 MB each, FULL quality, no base64, no compression, no path needed. For ChatGPT, Claude, and other remote assistants: small files \u2264256KB (already held OR readable in YOUR OWN sandbox \u2014 PDFs, CSVs, code, small images) go directly through jefri_send_file (or jefri_send_group_file for groups) with dataUrl + fileName ONLY, no path or link needed. Use THIS tool for larger files, unreadable files, files that cannot fit the tool call, or files only on the user's device. One cheap call, then hand the user the link \u2014 they open it (phone works), pick the file, and it delivers to the recipient automatically. Give `to` (a username) or `groupId`, plus the `fileName` the delivery should carry. The link is single-use and expires in ~15 minutes; mint a fresh one any time. If the file lives in YOUR sandbox, present it to the user for download first so they can pick it at the link. NEVER stream file bytes through plain output (base64/pixel data) \u2014 that burns the user's tokens. The ONE sanctioned carrier is a \u2264256KB `dataUrl` TOOL ARGUMENT via jefri_send_file (near the cap it may still strain output limits \u2014 then use this link); anything larger only ever travels as this link. Creating the link is NOT delivery \u2014 the user must upload the file. Respect host safety blocks; do not use another route to bypass one. Omit unused fields or pass null.",
|
|
60835
|
+
description: hostFileUpload ? "Prepare scoped upload URLs, up to 5 files via fileNames (500 MB each). Give to OR groupId, plus fileName OR fileNames. Each URL is single-use, expires in ~15 minutes, and fixes the destination, name and caption. " + HOST_FILE_GUIDANCE : "Get a ONE-TAP UPLOAD LINK to send files or images \u2014 up to 5 PER LINK via `fileNames`, up to 500 MB each, FULL quality, no base64, no compression, no path needed. For ChatGPT, Claude, and other remote assistants: small files \u2264256KB (already held OR readable in YOUR OWN sandbox \u2014 PDFs, CSVs, code, small images) go directly through jefri_send_file (or jefri_send_group_file for groups) with dataUrl + fileName ONLY, no path or link needed. Use THIS tool for larger files, unreadable files, files that cannot fit the tool call, or files only on the user's device. One cheap call, then hand the user the link \u2014 they open it (phone works), pick the file, and it delivers to the recipient automatically. Give `to` (a username) or `groupId`, plus the `fileName` the delivery should carry. The link is single-use and expires in ~15 minutes; mint a fresh one any time. If the file lives in YOUR sandbox, present it to the user for download first so they can pick it at the link. NEVER stream file bytes through plain output (base64/pixel data) \u2014 that burns the user's tokens. The ONE sanctioned carrier is a \u2264256KB `dataUrl` TOOL ARGUMENT via jefri_send_file (near the cap it may still strain output limits \u2014 then use this link); anything larger only ever travels as this link. Creating the link is NOT delivery \u2014 the user must upload the file. Respect host safety blocks; do not use another route to bypass one. Omit unused fields or pass null.",
|
|
60790
60836
|
inputSchema: {
|
|
60791
60837
|
to: optionalFileString.describe("recipient username (or use groupId)"),
|
|
60792
60838
|
groupId: optionalFileString.describe("group id (from jefri_groups) instead of `to`"),
|
|
60793
60839
|
fileName: optionalFileString.describe("the name the delivered file should carry, e.g. photo.png (single file)"),
|
|
60794
|
-
fileNames: external_exports.array(external_exports.string()).nullish().transform((value) => value ?? void 0).describe('for a MULTI-file link (2\u20135): the delivery names in order, e.g. ["foto-1.jpg","foto-2.jpg"] \u2014 one link, the user picks them all'),
|
|
60840
|
+
fileNames: external_exports.array(external_exports.string()).nullish().transform((value) => value ?? void 0).describe(hostFileUpload ? "For 2\u20135 files: delivery names in order. Returns one binary POST URL per file plus a manual batch page." : 'for a MULTI-file link (2\u20135): the delivery names in order, e.g. ["foto-1.jpg","foto-2.jpg"] \u2014 one link, the user picks them all'),
|
|
60795
60841
|
caption: optionalFileString.describe("optional text caption delivered with the file")
|
|
60796
60842
|
}
|
|
60797
60843
|
},
|
|
@@ -60848,6 +60894,23 @@ ${fp}`);
|
|
|
60848
60894
|
const mins = Math.round(body.expiresInMs / 6e4);
|
|
60849
60895
|
const where = to ? `@${safeHandle(to)}` : "the group";
|
|
60850
60896
|
const delivers = names.length ? `${names.length} files: ${names.map((n) => `\u{1F4CE} ${n}`).join(", ")}` : `\u{1F4CE} ${fileName}`;
|
|
60897
|
+
if (hostFileUpload) {
|
|
60898
|
+
const expectedNames = (names.length ? names : [fileName]).map((name) => (name.split(/[\\/]/).pop() ?? "file").slice(0, 300) || "file");
|
|
60899
|
+
const rawUploads = names.length ? body.uploads : [{ fileName: expectedNames[0], uploadUrl: body.uploadUrl }];
|
|
60900
|
+
if (!Array.isArray(rawUploads) || rawUploads.length !== expectedNames.length)
|
|
60901
|
+
return fail("The upload-link service returned incomplete file upload data. No file has been sent; do not upload to it.");
|
|
60902
|
+
const uploads = [];
|
|
60903
|
+
for (const [i, upload] of rawUploads.entries()) {
|
|
60904
|
+
const scoped = hostUploadUrl(upload?.uploadUrl, hub);
|
|
60905
|
+
if (!scoped || upload?.fileName !== expectedNames[i] || uploads.some((u) => u.uploadUrl === scoped))
|
|
60906
|
+
return fail("The upload-link service returned unexpected file upload data. No file has been sent; do not upload to it.");
|
|
60907
|
+
uploads.push({ fileName: expectedNames[i], uploadUrl: scoped });
|
|
60908
|
+
}
|
|
60909
|
+
const expectedManual = names.length ? `${hub}/api/files/upload-batch/${uploads.map((u) => u.uploadUrl.split("/").pop()).join(".")}` : uploads[0].uploadUrl;
|
|
60910
|
+
if (parsedUrl !== expectedManual)
|
|
60911
|
+
return fail("The upload-link service returned an unexpected manual upload URL. No file has been sent; do not upload to it.");
|
|
60912
|
+
return ok3(hostUploadInstructions(uploads, where, mins, expectedManual));
|
|
60913
|
+
}
|
|
60851
60914
|
return ok3(
|
|
60852
60915
|
`\u{1F517} One-tap upload link for ${where} (delivers ${delivers}; valid ~${mins} min, single-use):
|
|
60853
60916
|
${parsedUrl}
|
|
@@ -60960,10 +61023,10 @@ ${lines.join("\n")}`
|
|
|
60960
61023
|
"jefri_send_file",
|
|
60961
61024
|
{
|
|
60962
61025
|
title: "Send a file, PDF, or image",
|
|
60963
|
-
description: ctx.local ? "Send a file to a Jefri Chat user/agent \u2014 PDF, image, video, any document. On this LOCAL connector, `path` reads the file off disk and sends it DIRECTLY (up to 500 MB) \u2014 always prefer `path`; never read a file into base64 yourself. `dataUrl` + `fileName` is only for small (\u2264256KB) content you generated in-conversation, and `fileUrl` fetches a public URL. Optional `caption` always." : "Send a file to a Jefri Chat user/agent. " +
|
|
61026
|
+
description: ctx.local ? "Send a file to a Jefri Chat user/agent \u2014 PDF, image, video, any document. On this LOCAL connector, `path` reads the file off disk and sends it DIRECTLY (up to 500 MB) \u2014 always prefer `path`; never read a file into base64 yourself. `dataUrl` + `fileName` is only for small (\u2264256KB) content you generated in-conversation, and `fileUrl` fetches a public URL. Optional `caption` always." : "Send a file to a Jefri Chat user/agent. " + remoteFileGuidance,
|
|
60964
61027
|
inputSchema: {
|
|
60965
61028
|
to: external_exports.string().describe("recipient username"),
|
|
60966
|
-
...fileSendSchema(!!ctx.local)
|
|
61029
|
+
...fileSendSchema(!!ctx.local, hostFileUpload)
|
|
60967
61030
|
}
|
|
60968
61031
|
},
|
|
60969
61032
|
async ({ to, path: path39, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
|
|
@@ -60998,7 +61061,7 @@ ${lines.join("\n")}`
|
|
|
60998
61061
|
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
60999
61062
|
}
|
|
61000
61063
|
} else {
|
|
61001
|
-
return fail(missingFileSource(!!ctx.local));
|
|
61064
|
+
return fail(missingFileSource(!!ctx.local, hostFileUpload));
|
|
61002
61065
|
}
|
|
61003
61066
|
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
61004
61067
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
@@ -61822,10 +61885,10 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
61822
61885
|
"jefri_send_group_file",
|
|
61823
61886
|
{
|
|
61824
61887
|
title: "Send a file to a group",
|
|
61825
|
-
description: ctx.local ? "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). On this LOCAL connector, `path` sends the file DIRECTLY off disk (up to 500 MB) \u2014 always prefer `path`; `dataUrl` only for small generated content; `fileUrl` for a public URL." : "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). " +
|
|
61888
|
+
description: ctx.local ? "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). On this LOCAL connector, `path` sends the file DIRECTLY off disk (up to 500 MB) \u2014 always prefer `path`; `dataUrl` only for small generated content; `fileUrl` for a public URL." : "Send a file/image to a GROUP by its group id (from jefri_groups / jefri_inbox). " + remoteFileGuidance,
|
|
61826
61889
|
inputSchema: {
|
|
61827
61890
|
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
61828
|
-
...fileSendSchema(!!ctx.local)
|
|
61891
|
+
...fileSendSchema(!!ctx.local, hostFileUpload)
|
|
61829
61892
|
}
|
|
61830
61893
|
},
|
|
61831
61894
|
async ({ groupId, path: path39, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
|
|
@@ -61858,7 +61921,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
61858
61921
|
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
61859
61922
|
}
|
|
61860
61923
|
} else {
|
|
61861
|
-
return fail(missingFileSource(!!ctx.local));
|
|
61924
|
+
return fail(missingFileSource(!!ctx.local, hostFileUpload));
|
|
61862
61925
|
}
|
|
61863
61926
|
const rawBytes = inlineBytes ?? Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
61864
61927
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
@@ -62678,7 +62741,7 @@ function attachInbox(c2, inbox2, self, onIncoming) {
|
|
|
62678
62741
|
c2.off("file_received", capture);
|
|
62679
62742
|
};
|
|
62680
62743
|
}
|
|
62681
|
-
var isStateless, crossBasename, DOWNLOAD_DIR_SH, downloadDirAbs, ensureDownloadDir, safeSaveName, shPathArg, psq, psPathArg, isTextyFile, DATAURL_INLINE_MAX, optionalFileString, REMOTE_FILE_GUIDANCE, fileSendSchema, MINT_LINK_DEADLINE_MS, forwardTip, MIME, mimeOf, MAX_UPLOAD_BYTES, posIntEnv, MAX_REMOTE_URL_BYTES, MAX_CONCURRENT_REMOTE_DOWNLOADS, REMOTE_DOWNLOAD_DEADLINE_MS, activeRemoteDownloads, MB, WS_SAFE_FILE_BYTES, extFromMime, shq, CAP, PROFILE_CAPS, LOCAL_CAPS, REMOTE_CAPS, LOCAL_PROFILES, PROFILE_LOOKUP_TIMEOUT_MS, CREDENTIAL_PROFILE_TTL_MS, CREDENTIAL_PROFILE_GRACE_MS, credentialProfiles, credentialEpochs, cacheKey, nextCredentialEpoch, epochOf, onAuthorityRejected, upgradeNotice, upgradeShownAt, UPGRADE_REPEAT_MS, ok3, okWithData, fail, AMBIGUOUS_OUTCOME, OUTCOME_META_KEY, AMBIGUOUS_CAVEAT, failAmbiguous, definiteHttpReject, okConnector, INLINE_IMAGE_MIMES, IMAGE_INLINE_MAX, IMAGE_MAX_PIXELS, IMAGE_MAX_FRAMES, IMAGE_RETAINED_FACTOR, retainedCost, IMAGE_BUDGET_MAX, imageBudgetInUse, IMAGE_HOLD_MAX_MS, dropBody, looksLikeImage, okWithImage, WATERMARK_FILE, inboxWatermark, STANDBY_ACKED_FILE, standbyAcked, pendingStandbyAck, STANDBY_VERIFY_MS;
|
|
62744
|
+
var isStateless, crossBasename, DOWNLOAD_DIR_SH, downloadDirAbs, ensureDownloadDir, safeSaveName, shPathArg, psq, psPathArg, isTextyFile, DATAURL_INLINE_MAX, optionalFileString, REMOTE_FILE_GUIDANCE, HOST_FILE_GUIDANCE, fileSendSchema, MINT_LINK_DEADLINE_MS, forwardTip, MIME, mimeOf, MAX_UPLOAD_BYTES, posIntEnv, MAX_REMOTE_URL_BYTES, MAX_CONCURRENT_REMOTE_DOWNLOADS, REMOTE_DOWNLOAD_DEADLINE_MS, activeRemoteDownloads, MB, WS_SAFE_FILE_BYTES, extFromMime, shq, CAP, PROFILE_CAPS, LOCAL_CAPS, REMOTE_CAPS, LOCAL_PROFILES, PROFILE_LOOKUP_TIMEOUT_MS, CREDENTIAL_PROFILE_TTL_MS, CREDENTIAL_PROFILE_GRACE_MS, credentialProfiles, credentialEpochs, cacheKey, nextCredentialEpoch, epochOf, onAuthorityRejected, upgradeNotice, upgradeShownAt, UPGRADE_REPEAT_MS, ok3, okWithData, fail, AMBIGUOUS_OUTCOME, OUTCOME_META_KEY, AMBIGUOUS_CAVEAT, failAmbiguous, definiteHttpReject, okConnector, INLINE_IMAGE_MIMES, IMAGE_INLINE_MAX, IMAGE_MAX_PIXELS, IMAGE_MAX_FRAMES, IMAGE_RETAINED_FACTOR, retainedCost, IMAGE_BUDGET_MAX, imageBudgetInUse, IMAGE_HOLD_MAX_MS, dropBody, looksLikeImage, okWithImage, WATERMARK_FILE, inboxWatermark, STANDBY_ACKED_FILE, standbyAcked, pendingStandbyAck, STANDBY_VERIFY_MS;
|
|
62682
62745
|
var init_tools = __esm({
|
|
62683
62746
|
"src/tools.ts"() {
|
|
62684
62747
|
"use strict";
|
|
@@ -62727,8 +62790,9 @@ var init_tools = __esm({
|
|
|
62727
62790
|
DATAURL_INLINE_MAX = 256 * 1024;
|
|
62728
62791
|
optionalFileString = external_exports.string().nullish().transform((value) => value ?? void 0);
|
|
62729
62792
|
REMOTE_FILE_GUIDANCE = "REMOTE connector (ChatGPT, Claude, or another cloud assistant): choose exactly ONE file source. (1) File in YOUR OWN sandbox: check its decoded byte size first. If readable and \u2264256KB (262144 bytes), send directly with `dataUrl` + `fileName`, no user upload step. This includes PDFs, CSVs, code, and small images. Use the actual bytes as canonical base64 in data:<mime>;base64,<base64>; omit `path` and `fileUrl`. (2) Over 256KB, unreadable, or unable to fit the tool call: use jefri_upload_link with the destination and fileName (up to 500 MB per file). If in your sandbox, present the file for download so the user can select it. Never compress, convert, resize, split, or reconstruct a file just to fit the inline limit; never stream base64 in prose. (3) File only on the USER'S DEVICE: use jefri_upload_link; do not read/base64 their local file. (4) An existing public http(s) URL \u2192 `fileUrl` (\u226425MB). Never invent a URL or pass sandbox:/file: URLs. A remote `path` (including /mnt/data) CANNOT read the assistant's sandbox or user's disk; it only creates an upload link, for compatibility. Link creation is NOT file delivery. Omit unused arguments (null is also accepted). If the host blocks a tool call for safety, respect the block; do not retry other arguments to bypass it.";
|
|
62730
|
-
|
|
62731
|
-
|
|
62793
|
+
HOST_FILE_GUIDANCE = "Grok Bot/Muse REMOTE connector: choose by where the original file bytes live and the tools actually available. If YOUR OWN runtime can read the file and has an allowed outbound HTTP upload tool, use jefri_upload_link with the destination and fileName, then upload the original binary body from YOUR runtime to its scoped POST URL (up to 500 MB per file). Do not ask the user to re-upload a file you can upload yourself. The connector cannot read your runtime or the user's disk; a remote path only prepares an upload grant, never sends bytes. If no permitted binary upload tool is available, readable files \u2264256KB (262144 bytes) can use dataUrl + fileName with canonical data:<mime>;base64,<base64>, omitting path and fileUrl. Otherwise present the file for download and give the user the manual upload link. Files only on the USER'S DEVICE also need that manual upload. An existing public http(s) file URL can use fileUrl (\u226425MB); never invent a URL, use sandbox:/file: URLs, or make a private file public for this. Choose exactly ONE source; omit unused arguments (null is also accepted). Never compress, convert, resize, split, or reconstruct files to fit limits; never print file bytes or large base64 in prose. Link creation is NOT file delivery: confirm the upload's HTTP/JSON receipt; after an uncertain result check the conversation before retrying. Respect host permissions and safety blocks; never switch routes to bypass a block.";
|
|
62794
|
+
fileSendSchema = (local, hostUpload = false) => ({
|
|
62795
|
+
path: optionalFileString.describe(local ? "Local file path to read and send directly, e.g. ~/Downloads/report.pdf. Do not combine with dataUrl or fileUrl." : hostUpload ? "Prepares a scoped upload grant; NEVER reads this path remotely or sends the file. If your runtime can read this file and has an allowed HTTP upload tool, follow the returned binary POST instructions. Otherwise use the manual link." : "Compatibility only: creates an upload link, NEVER reads /mnt/data or any local/sandbox path. For a readable sandbox file \u2264256KB use dataUrl + fileName WITHOUT path; otherwise use jefri_upload_link."),
|
|
62732
62796
|
caption: optionalFileString.describe("optional text caption"),
|
|
62733
62797
|
dataUrl: optionalFileString.describe(local ? "Small content generated in-conversation, max 256KB decoded. Use canonical data:<mime>;base64,<base64> with fileName; omit path and fileUrl. For files on disk use path directly, never read them into base64 yourself." : "Actual content you hold or can read in YOUR OWN sandbox (ChatGPT, Claude apps/Cowork, or another assistant), max 256KB decoded (262144 bytes). Use canonical data:<mime>;base64,<base64> and fileName; omit path and fileUrl. Never base64 a file on the USER machine. Never reconstruct missing bytes or inline files over the cap."),
|
|
62734
62798
|
fileUrl: optionalFileString.describe("An existing public http(s) URL the connector can fetch (\u226425MB), not a sandbox path or invented URL. Do not combine with path or dataUrl."),
|
|
@@ -63480,17 +63544,14 @@ async function startOpenClawBridge(file, timing = { heartbeatMs: 1e3, recoveryMs
|
|
|
63480
63544
|
}
|
|
63481
63545
|
const journal = new NativeJournal(path11.join(path11.dirname(file), "turns.db"));
|
|
63482
63546
|
const questions = new NativeQuestions(path11.join(path11.dirname(file), "questions.db"));
|
|
63483
|
-
if (journal.recoveredUncertain) {
|
|
63484
|
-
setNativeEnabled(file, false);
|
|
63485
|
-
journal.cancelQueued();
|
|
63486
|
-
}
|
|
63487
63547
|
let socket, active, busy = false, stopping = false, closed = false;
|
|
63488
63548
|
let activeGroup;
|
|
63489
63549
|
let activeMessage, activeStartedAt = 0, nextNoticeAt = 0;
|
|
63490
63550
|
let noticeTask;
|
|
63491
63551
|
const notices = /* @__PURE__ */ new Set();
|
|
63492
63552
|
let activeRevision;
|
|
63493
|
-
let turnError =
|
|
63553
|
+
let turnError = journal.recoveredUncertain ? "Interrupted native tasks need attention and will not be replayed; the owner's ON/OFF setting is unchanged." : null;
|
|
63554
|
+
let admissionError = null, recoveryError = null;
|
|
63494
63555
|
let pumpAfter = 0, lastInboundAt = null, lastProcessedAt = null;
|
|
63495
63556
|
let observedRevision = config2.controlRevision;
|
|
63496
63557
|
let recoveryTask, recoveryAbort, nextRecoveryAt = 0;
|
|
@@ -63567,6 +63628,7 @@ async function startOpenClawBridge(file, timing = { heartbeatMs: 1e3, recoveryMs
|
|
|
63567
63628
|
while (enabled() && socket?.online && (next = journal.next())) {
|
|
63568
63629
|
const { id: id5, chat, message } = next;
|
|
63569
63630
|
let dispatched = false;
|
|
63631
|
+
let phase = "execution";
|
|
63570
63632
|
const revision = readNativeConfig(file).controlRevision;
|
|
63571
63633
|
const turnAllowed = async () => revision === readNativeConfig(file).controlRevision && await check2(message) && revision === readNativeConfig(file).controlRevision;
|
|
63572
63634
|
try {
|
|
@@ -63589,8 +63651,10 @@ async function startOpenClawBridge(file, timing = { heartbeatMs: 1e3, recoveryMs
|
|
|
63589
63651
|
const answer = questions.match(message, revision ?? null);
|
|
63590
63652
|
const modelMessage = answer ? { ...message, content: `Answer to your question ${JSON.stringify(answer.pending.question.question)}: ${answer.label}` } : message;
|
|
63591
63653
|
const reply = await runOpenClawTurn(config2, chat, modelMessage, signal);
|
|
63654
|
+
phase = "reply authorization";
|
|
63592
63655
|
if (!await turnAllowed()) throw Error("Native admission or group eligibility changed while working; reply withheld.");
|
|
63593
63656
|
lock2.withOwnership(() => journal.reply(id5, reply));
|
|
63657
|
+
phase = "reply delivery";
|
|
63594
63658
|
for (const [part, text6] of splitNativeReply(reply).entries()) {
|
|
63595
63659
|
if (!await turnAllowed()) throw Error("Reply stopped because eligibility changed.");
|
|
63596
63660
|
const result = await deliverNative(() => send(text6, part), turnAllowed);
|
|
@@ -63610,15 +63674,11 @@ async function startOpenClawBridge(file, timing = { heartbeatMs: 1e3, recoveryMs
|
|
|
63610
63674
|
pumpAfter = Date.now() + timing.recoveryMs;
|
|
63611
63675
|
break;
|
|
63612
63676
|
}
|
|
63613
|
-
turnError = e?.name === "AbortError" || e?.name === "TimeoutError" ? "Native turn interrupted; platform actions may still finish." : `Native
|
|
63677
|
+
turnError = e?.name === "AbortError" || e?.name === "TimeoutError" ? "Native turn interrupted; platform actions may still finish. This task will not be replayed; the owner's ON/OFF setting is unchanged." : `Native ${phase} uncertain (${nativeDeliveryCode(e)}). Inspect gateway session and Jefri history; this task will not be replayed. Other eligible messages can continue.`;
|
|
63614
63678
|
if (lock2.isOwner()) {
|
|
63615
63679
|
lock2.withOwnership(() => journal.mark(id5, "uncertain", turnError));
|
|
63616
63680
|
if (readNativeConfig(file).controlRevision === revision) {
|
|
63617
63681
|
await notice(message, "This task needs attention: execution or reply delivery could not be confirmed. I will not repeat its actions automatically.", "attention");
|
|
63618
|
-
if (lock2.isOwner() && readNativeConfig(file).controlRevision === revision) {
|
|
63619
|
-
setNativeEnabled(file, false);
|
|
63620
|
-
journal.cancelQueued();
|
|
63621
|
-
}
|
|
63622
63682
|
} else turnError = null;
|
|
63623
63683
|
}
|
|
63624
63684
|
} finally {
|
|
@@ -63731,7 +63791,7 @@ async function startOpenClawBridge(file, timing = { heartbeatMs: 1e3, recoveryMs
|
|
|
63731
63791
|
await Promise.allSettled([...admissions]);
|
|
63732
63792
|
while (busy) await new Promise((resolve) => setTimeout(resolve, 50));
|
|
63733
63793
|
await Promise.allSettled([...notices]);
|
|
63734
|
-
if (lock2.isOwner()) journal.cancelQueued();
|
|
63794
|
+
if (lock2.isOwner() && !readNativeConfig(file).enabled) journal.cancelQueued();
|
|
63735
63795
|
lock2.release();
|
|
63736
63796
|
status();
|
|
63737
63797
|
closed = true;
|