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.
Files changed (3) hide show
  1. package/dist/http.js +80 -19
  2. package/dist/index.js +94 -34
  3. 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
- var fileSendSchema = (local) => ({
67201
- path: optionalFileString.describe(local ? "Local file path to read and send directly, e.g. ~/Downloads/report.pdf. Do not combine with dataUrl or fileUrl." : "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."),
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. " + REMOTE_FILE_GUIDANCE,
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). " + REMOTE_FILE_GUIDANCE,
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 message id.");
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.receiver !== me2 || row.groupId || row.encryptionMode !== "audited" || !["text", "file"].includes(row.kind) || typeof row.content !== "string")
71594
- return fail2("No matching plain incoming DM was found. Do not substitute another message.");
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, private_write
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
- current = read_config(self.connection_file)
514
- if self.state.owner() and current.get("controlRevision") == self.turn_revision:
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, "No confirmed final delivery; inspect native session and Jefri history")
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/delivery unconfirmed (" + delivery_code(error) + "); inspect session before retrying")
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
- recovered = 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
761
- if recovered:
762
- private_write(self.file, {**read_config(self.file), "enabled": False})
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. " + REMOTE_FILE_GUIDANCE,
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). " + REMOTE_FILE_GUIDANCE,
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
- fileSendSchema = (local) => ({
62731
- path: optionalFileString.describe(local ? "Local file path to read and send directly, e.g. ~/Downloads/report.pdf. Do not combine with dataUrl or fileUrl." : "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."),
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 = null, admissionError = null, recoveryError = null;
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 execution/delivery uncertain (${nativeDeliveryCode(e)}). Inspect gateway session and Jefri history; no automatic retry.`;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.49.51",
3
+ "version": "0.49.53",
4
4
  "engines": {
5
5
  "node": ">=22.13"
6
6
  },