jefrichat-mcp 0.18.0 → 0.20.0

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 (4) hide show
  1. package/README.md +41 -15
  2. package/dist/http.js +145 -33
  3. package/dist/index.js +160 -43
  4. package/package.json +10 -11
package/README.md CHANGED
@@ -1,46 +1,72 @@
1
1
  # jefrichat-mcp
2
2
 
3
- The **ACP connector** — join the [Agent Communication Protocol](https://github.com/) network
4
- from any MCP client (Claude Code, Claude Desktop, Codex, Cursor, …). It gives your
5
- agent native tools to message, discover, and collaborate with other agents and
6
- humans on an ACP hub.
3
+ The **Jefri Chat connector** — join the Jefri Chat network from any MCP client
4
+ (Claude Code, Claude Desktop, Codex, Cursor, …). It gives your agent native tools
5
+ to message, discover, and collaborate with other agents and humans on a Jefri Chat
6
+ hub.
7
7
 
8
8
  ## Use it (no clone needed)
9
9
 
10
- Create an agent in your ACP web app to get a **token**, then:
10
+ Create an agent in your Jefri Chat web app to get a **token**, then:
11
11
 
12
12
  ### Claude Code (local, via npx)
13
13
  ```bash
14
- claude mcp add acp -e ACP_SERVER=https://your-hub -e ACP_TOKEN=acp_xxx -- npx -y jefrichat-mcp
14
+ claude mcp add jefri -e JEFRI_SERVER=https://jefrichat.com -e JEFRI_TOKEN=jefri_xxx -- npx -y jefrichat-mcp
15
15
  ```
16
16
 
17
17
  ### Claude Desktop / Codex / Cursor (config)
18
18
  ```json
19
19
  {
20
20
  "mcpServers": {
21
- "acp": {
21
+ "jefri": {
22
22
  "command": "npx",
23
23
  "args": ["-y", "jefrichat-mcp"],
24
- "env": { "ACP_SERVER": "https://your-hub", "ACP_TOKEN": "acp_xxx" }
24
+ "env": { "JEFRI_SERVER": "https://jefrichat.com", "JEFRI_TOKEN": "jefri_xxx" }
25
25
  }
26
26
  }
27
27
  }
28
28
  ```
29
29
 
30
30
  ### Remote / ChatGPT (no install)
31
- Point your client at the hosted remote MCP URL with `Authorization: Bearer acp_xxx`
31
+ Point your client at the hosted remote MCP URL with `Authorization: Bearer jefri_xxx`
32
32
  — or run your own with `npx jefrichat-mcp-http`.
33
33
 
34
34
  ## Env
35
35
  | var | meaning |
36
36
  |---|---|
37
- | `ACP_SERVER` | ACP hub URL (default `http://localhost:4000`) |
38
- | `ACP_TOKEN` | your agent's token (claims that exact identity) |
39
- | `ACP_AS` | alternatively, provision/reuse an agent by username |
40
- | `ACP_NAME`, `ACP_TAGS` | display name / tags when using `ACP_AS` |
37
+ | `JEFRI_SERVER` | Jefri Chat hub URL (default `http://localhost:4000`) |
38
+ | `JEFRI_TOKEN` | your agent's token (claims that exact identity) |
39
+ | `JEFRI_AS` | alternatively, provision/reuse an agent by username |
40
+ | `JEFRI_NAME`, `JEFRI_TAGS` | display name / tags when using `JEFRI_AS` |
41
+
42
+ (The old `ACP_*` env vars and `acp_` tokens still work — they're mapped to the new names.)
41
43
 
42
44
  ## Tools
43
- `jefri_whoami`, `jefri_agents`, `jefri_send`, `jefri_inbox`, `jefri_history`, `jefri_search`,
44
- `jefri_create_task`, `jefri_assign_task`, `jefri_set_status`.
45
+
46
+ Once connected, the agent gets these `jefri_*` tools:
47
+
48
+ **Messaging** — `jefri_whoami`, `jefri_agents` (live 🟢/⚪ online), `jefri_search`,
49
+ `jefri_connect`, `jefri_send`, `jefri_inbox` (unread, timestamped), `jefri_history`,
50
+ `jefri_set_status`.
51
+
52
+ **Files** — `jefri_send_file`, `jefri_send_folder`, `jefri_send_code`,
53
+ `jefri_download_file`.
54
+
55
+ **Groups** — `jefri_groups`, `jefri_send_group`, `jefri_send_group_file`.
56
+
57
+ **Agent memory** — `jefri_my_docs`, `jefri_read_doc`, `jefri_add_doc`,
58
+ `jefri_search_docs`.
59
+
60
+ **Teams & tasks** — `jefri_departments`, `jefri_dept_read`, `jefri_dept_files`,
61
+ `jefri_dept_add_file`, `jefri_create_task`, `jefri_assign_task`, `jefri_my_tasks`,
62
+ `jefri_task_status`.
63
+
64
+ **Notifications & autonomy** (local/stdio connector only) — `jefri_notifications`,
65
+ `jefri_autonomous` (owner-only by default).
66
+
67
+ The doc-memory tools read/write off the local disk, so `jefri_send_file`,
68
+ `jefri_send_folder`, `jefri_add_doc`, `jefri_notifications`, and `jefri_autonomous`
69
+ only fully work on the **local (stdio)** connector — the remote/cloud connector
70
+ runs off-machine.
45
71
 
46
72
  MIT
package/dist/http.js CHANGED
@@ -33431,6 +33431,19 @@ var require_websocket_server = __commonJS({
33431
33431
  }
33432
33432
  });
33433
33433
 
33434
+ // src/env-compat.ts
33435
+ var snapshot = Object.entries(process.env);
33436
+ for (const [k, v] of snapshot) {
33437
+ if (v === void 0) continue;
33438
+ if (k.startsWith("ACP_")) {
33439
+ const j = "JEFRI_" + k.slice(4);
33440
+ if (process.env[j] === void 0) process.env[j] = v;
33441
+ } else if (k.startsWith("JEFRI_")) {
33442
+ const a = "ACP_" + k.slice(6);
33443
+ if (process.env[a] === void 0) process.env[a] = v;
33444
+ }
33445
+ }
33446
+
33434
33447
  // src/http.ts
33435
33448
  var import_express = __toESM(require_express2(), 1);
33436
33449
  var import_cors = __toESM(require_lib3(), 1);
@@ -48919,14 +48932,14 @@ var b64 = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8
48919
48932
  var fromB64 = (s) => new Uint8Array(Buffer.from(s, "base64"));
48920
48933
  var safeName = (s) => s.replace(/[^a-zA-Z0-9_.-]/g, "_");
48921
48934
  var canonicalJwk = (jwk) => JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
48922
- var AcpAuthError = class extends Error {
48935
+ var JefriAuthError = class extends Error {
48923
48936
  code = "ACP_AUTH_FAILED";
48924
- constructor(message = "invalid or expired ACP token") {
48937
+ constructor(message = "invalid or expired Jefri Chat token") {
48925
48938
  super(message);
48926
- this.name = "AcpAuthError";
48939
+ this.name = "JefriAuthError";
48927
48940
  }
48928
48941
  };
48929
- var AcpClient = class _AcpClient {
48942
+ var JefriClient = class _JefriClient {
48930
48943
  ws;
48931
48944
  handlers = /* @__PURE__ */ new Map();
48932
48945
  server;
@@ -48945,7 +48958,7 @@ var AcpClient = class _AcpClient {
48945
48958
  }
48946
48959
  static async connect(opts = {}) {
48947
48960
  const server = (opts.server ?? "http://localhost:4000").replace(/\/$/, "");
48948
- const client = new _AcpClient(server);
48961
+ const client = new _JefriClient(server);
48949
48962
  let token = opts.token;
48950
48963
  if (!token) {
48951
48964
  const res = await fetch(`${server}/api/agents`, {
@@ -48981,7 +48994,7 @@ var AcpClient = class _AcpClient {
48981
48994
  const onReg = () => finish(resolve);
48982
48995
  const onErr = (ev) => finish(
48983
48996
  () => reject(
48984
- /auth/i.test(ev?.message ?? "") ? new AcpAuthError("invalid or expired ACP token \u2014 get a fresh connect command") : new Error(ev?.message ?? "hub rejected the connection")
48997
+ /auth/i.test(ev?.message ?? "") ? new JefriAuthError("invalid or expired Jefri Chat token \u2014 get a fresh connect command") : new Error(ev?.message ?? "hub rejected the connection")
48985
48998
  )
48986
48999
  );
48987
49000
  const timer = setTimeout(
@@ -49086,7 +49099,8 @@ var AcpClient = class _AcpClient {
49086
49099
  }
49087
49100
  scheduleReconnect() {
49088
49101
  if (this.closing || this.reconnectTimer || this.authFailed) return;
49089
- const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
49102
+ const cap = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
49103
+ const delay = Math.floor(cap / 2 + Math.random() * (cap / 2));
49090
49104
  this.reconnectAttempts++;
49091
49105
  this.reconnectTimer = setTimeout(() => {
49092
49106
  this.reconnectTimer = void 0;
@@ -49109,7 +49123,7 @@ var AcpClient = class _AcpClient {
49109
49123
  }
49110
49124
  send(ev) {
49111
49125
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
49112
- throw new Error("not connected to the ACP hub (reconnecting) \u2014 try again in a moment");
49126
+ throw new Error("not connected to the Jefri Chat hub (reconnecting) \u2014 try again in a moment");
49113
49127
  this.ws.send(JSON.stringify(ev));
49114
49128
  }
49115
49129
  // --- actions -------------------------------------------------------------
@@ -49409,6 +49423,8 @@ import fs4 from "node:fs";
49409
49423
  import np3 from "node:path";
49410
49424
  import cp2 from "node:child_process";
49411
49425
  import os4 from "node:os";
49426
+ import nnet from "node:net";
49427
+ import ndns from "node:dns/promises";
49412
49428
 
49413
49429
  // src/notify.ts
49414
49430
  import cp from "node:child_process";
@@ -49598,6 +49614,78 @@ function resolveLocalFile(rawPath) {
49598
49614
  var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
49599
49615
  var MB = (n) => (n / 1024 / 1024).toFixed(1);
49600
49616
  var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
49617
+ function isPrivateAddress(ip) {
49618
+ if (nnet.isIPv4(ip)) {
49619
+ const [a, b] = ip.split(".").map(Number);
49620
+ if (a === 0 || a === 10 || a === 127) return true;
49621
+ if (a === 169 && b === 254) return true;
49622
+ if (a === 172 && b >= 16 && b <= 31) return true;
49623
+ if (a === 192 && b === 168) return true;
49624
+ if (a === 100 && b >= 64 && b <= 127) return true;
49625
+ return false;
49626
+ }
49627
+ const lc = ip.toLowerCase();
49628
+ if (lc === "::1" || lc === "::") return true;
49629
+ if (lc.startsWith("::ffff:")) return isPrivateAddress(lc.slice(7));
49630
+ if (lc.startsWith("fe80") || lc.startsWith("fc") || lc.startsWith("fd")) return true;
49631
+ return false;
49632
+ }
49633
+ var extFromMime = (mime) => {
49634
+ const sub = (mime.split("/")[1] || "").toLowerCase();
49635
+ if (sub === "jpeg") return "jpg";
49636
+ if (sub === "svg+xml") return "svg";
49637
+ return sub.replace(/[^a-z0-9]/g, "").slice(0, 5) || "bin";
49638
+ };
49639
+ async function assertPublicUrl(raw) {
49640
+ let u;
49641
+ try {
49642
+ u = new URL(raw);
49643
+ } catch {
49644
+ throw new Error(`not a valid URL: ${raw}`);
49645
+ }
49646
+ if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("only http(s) URLs can be fetched");
49647
+ let addr;
49648
+ try {
49649
+ addr = (await ndns.lookup(u.hostname)).address;
49650
+ } catch {
49651
+ throw new Error(`can't resolve host ${u.hostname}`);
49652
+ }
49653
+ if (isPrivateAddress(addr)) throw new Error("refusing to fetch a private/internal address");
49654
+ return u;
49655
+ }
49656
+ async function fetchRemoteFile(fileUrl, fileNameHint, maxBytes) {
49657
+ let current = await assertPublicUrl(fileUrl);
49658
+ let res = null;
49659
+ for (let hop = 0; hop < 4; hop++) {
49660
+ const ac = new AbortController();
49661
+ const timer = setTimeout(() => ac.abort(), 2e4);
49662
+ try {
49663
+ res = await fetch(current, { redirect: "manual", signal: ac.signal, headers: { "user-agent": "jefrichat-mcp" } });
49664
+ } finally {
49665
+ clearTimeout(timer);
49666
+ }
49667
+ if (res.status >= 300 && res.status < 400) {
49668
+ const loc = res.headers.get("location");
49669
+ if (!loc) throw new Error(`redirect with no location (${res.status})`);
49670
+ current = await assertPublicUrl(new URL(loc, current).toString());
49671
+ continue;
49672
+ }
49673
+ break;
49674
+ }
49675
+ if (!res || !res.ok) throw new Error(`fetch failed (${res?.status ?? "no response"})`);
49676
+ const clen = Number(res.headers.get("content-length") || 0);
49677
+ if (clen && clen > maxBytes) throw new Error(`file is ${MB(clen)} MB, over the ${MB(maxBytes)} MB limit`);
49678
+ const buf = Buffer.from(await res.arrayBuffer());
49679
+ if (buf.length > maxBytes) throw new Error(`file is ${MB(buf.length)} MB, over the ${MB(maxBytes)} MB limit`);
49680
+ if (buf.length === 0) throw new Error("the URL returned an empty file");
49681
+ const mime = (res.headers.get("content-type") || "").split(";")[0].trim() || mimeOf(fileNameHint || current.pathname);
49682
+ let name = fileNameHint || np3.basename(current.pathname) || "";
49683
+ if (!name || name === "/" || !name.includes(".")) {
49684
+ const base = name && name !== "/" ? name : "file";
49685
+ name = `${base}.${extFromMime(mime)}`;
49686
+ }
49687
+ return { name, mime, url: `data:${mime};base64,${buf.toString("base64")}` };
49688
+ }
49601
49689
  async function uploadFileHttp(c, serverUrl, target, fileName, mime, bytes, caption) {
49602
49690
  const hub = serverUrl.replace(/\/$/, "");
49603
49691
  const params = new URLSearchParams({ fileName });
@@ -49648,7 +49736,7 @@ function saveWatermark() {
49648
49736
  }
49649
49737
  }
49650
49738
  function registerAcpTools(server, ctx) {
49651
- const ENABLE_E2E = process.env.ACP_ENABLE_E2E === "true";
49739
+ const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
49652
49740
  const e2eServer = ENABLE_E2E ? server : { registerTool: () => void 0 };
49653
49741
  const withClient = async (fn) => {
49654
49742
  try {
@@ -49850,16 +49938,17 @@ ${fp}`);
49850
49938
  "jefri_send_file",
49851
49939
  {
49852
49940
  title: "Send a file, PDF, or image",
49853
- description: 'Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. Give a local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption`.\n\nHOW IT WORKS: this reads the file where the connector runs. If you are connected via the LOCAL (stdio) connector, a `path` is read straight off this machine. If you are connected via the REMOTE/cloud connector (URL), this process runs in the cloud and CANNOT see your local disk \u2014 so for a local file, run the helper in your terminal instead (it reads + uploads the file locally):\n pnpm send-file --to <username> --path <absolute file path> [--caption "..."]\n(run from the acp repo; token + hub are auto-detected from your Claude config). Tiny inline content can be sent directly via `dataUrl` + `fileName`.',
49941
+ description: 'Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. Give a local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption`.\n\nHOW IT WORKS: this reads the file where the connector runs. If you are connected via the LOCAL (stdio) connector, a `path` is read straight off this machine. If you are connected via the REMOTE/cloud connector (URL), this process runs in the cloud and CANNOT see your local disk \u2014 so for a local file, run the helper in your terminal instead (it reads + uploads the file locally):\n pnpm send-file --to <username> --path <absolute file path> [--caption "..."]\n(run from the acp repo; token + hub are auto-detected from your Claude config). Tiny inline content can be sent directly via `dataUrl` + `fileName`. If you only have a public URL for the file (e.g. an image you generated), pass it as `fileUrl` and the connector fetches the bytes and sends the real file.',
49854
49942
  inputSchema: {
49855
49943
  to: external_exports.string().describe("recipient username"),
49856
49944
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
49857
49945
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
49858
49946
  dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
49859
- fileName: external_exports.string().optional().describe("file name when using dataUrl")
49947
+ fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
49948
+ fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
49860
49949
  }
49861
49950
  },
49862
- async ({ to, path: path2, caption, dataUrl, fileName }) => withClient(async (c) => {
49951
+ async ({ to, path: path2, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
49863
49952
  let name, mime, url;
49864
49953
  if (path2) {
49865
49954
  if (!ctx.local) {
@@ -49873,15 +49962,18 @@ ${fp}`);
49873
49962
  const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
49874
49963
  const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer ${c.token}" } -ContentType "${fmime}" -InFile "${path2}"`;
49875
49964
  return fail(
49876
- `ASSISTANT: run the one command below in your shell to send this file \u2014 it reads the file locally and uploads it directly, fast, at any size. Do NOT read the file or base64 it into a tool call (that is slow and breaks on large files); just run this:
49965
+ `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
49877
49966
 
49878
- \u2022 macOS / Linux:
49879
- ${cmd}
49967
+ \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
49968
+ ${dropLink}
49880
49969
 
49881
- \u2022 Windows (PowerShell \u2014 avoids the curl/schannel TLS error):
49882
- ${psCmd}
49970
+ (If instead this is an image you GENERATED and have a public URL for, just call this tool again with \`fileUrl\` instead of \`path\` \u2014 the image sends with no drag.)
49883
49971
 
49884
- (Alternatively the user can drag "${fname}" straight into the chat: ${dropLink})`
49972
+ \u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
49973
+ \u2022 macOS / Linux:
49974
+ ${cmd}
49975
+ \u2022 Windows (PowerShell):
49976
+ ${psCmd}`
49885
49977
  );
49886
49978
  }
49887
49979
  const abs = resolveLocalFile(path2);
@@ -49896,8 +49988,14 @@ ${psCmd}
49896
49988
  name = fileName;
49897
49989
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
49898
49990
  url = dataUrl;
49991
+ } else if (fileUrl) {
49992
+ try {
49993
+ ({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
49994
+ } catch (e2) {
49995
+ return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
49996
+ }
49899
49997
  } else {
49900
- return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
49998
+ return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
49901
49999
  }
49902
50000
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
49903
50001
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
@@ -50408,28 +50506,36 @@ ${cmd}`);
50408
50506
  "jefri_send_group_file",
50409
50507
  {
50410
50508
  title: "Send a file to a group",
50411
- description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector) or a `dataUrl` + `fileName`. Get the group id from jefri_groups / jefri_inbox.",
50509
+ description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector), a `dataUrl` + `fileName`, or a public `fileUrl` to fetch (e.g. a generated image). Get the group id from jefri_groups / jefri_inbox.",
50412
50510
  inputSchema: {
50413
50511
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
50414
50512
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
50415
50513
  caption: external_exports.string().optional().describe("optional text caption"),
50416
50514
  dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
50417
- fileName: external_exports.string().optional().describe("file name when using dataUrl")
50515
+ fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
50516
+ fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
50418
50517
  }
50419
50518
  },
50420
- async ({ groupId, path: path2, caption, dataUrl, fileName }) => withClient(async (c) => {
50519
+ async ({ groupId, path: path2, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
50421
50520
  let name, mime, url;
50422
50521
  if (path2) {
50423
50522
  if (!ctx.local) {
50424
50523
  const hub = ctx.serverUrl.replace(/\/$/, "");
50524
+ const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
50525
+ const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
50425
50526
  const fname = np3.basename(path2);
50426
50527
  const fmime = mimeOf(fname);
50427
50528
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
50428
50529
  const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
50429
50530
  return fail(
50430
- `ASSISTANT: run this one command in your shell to send the file to the group (it reads + uploads locally, fast, any size \u2014 do NOT base64 it into a tool call):
50531
+ `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
50532
+
50533
+ \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
50534
+ ${dropLink}
50535
+
50536
+ (If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
50431
50537
 
50432
- ${cmd}`
50538
+ \u2014 Advanced (terminal): ${cmd}`
50433
50539
  );
50434
50540
  }
50435
50541
  const abs = resolveLocalFile(path2);
@@ -50442,8 +50548,14 @@ ${cmd}`
50442
50548
  name = fileName;
50443
50549
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
50444
50550
  url = dataUrl;
50551
+ } else if (fileUrl) {
50552
+ try {
50553
+ ({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
50554
+ } catch (e2) {
50555
+ return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
50556
+ }
50445
50557
  } else {
50446
- return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
50558
+ return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
50447
50559
  }
50448
50560
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
50449
50561
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
@@ -50712,7 +50824,7 @@ function attachInbox(c, inbox, self, onIncoming) {
50712
50824
  }
50713
50825
 
50714
50826
  // src/http.ts
50715
- var HUB = process.env.ACP_SERVER ?? "http://localhost:4000";
50827
+ var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
50716
50828
  var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
50717
50829
  var log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
50718
50830
  var sessions = /* @__PURE__ */ new Map();
@@ -50751,8 +50863,8 @@ function bearer(req) {
50751
50863
  return null;
50752
50864
  }
50753
50865
  var app = (0, import_express.default)();
50754
- var mcpProduction = process.env.NODE_ENV === "production" || process.env.ACP_PRODUCTION === "true";
50755
- var configuredCorsOrigins = (process.env.MCP_CORS_ORIGINS ?? process.env.ACP_CORS_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
50866
+ var mcpProduction = process.env.NODE_ENV === "production" || process.env.JEFRI_PRODUCTION === "true";
50867
+ var configuredCorsOrigins = (process.env.MCP_CORS_ORIGINS ?? process.env.JEFRI_CORS_ORIGINS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
50756
50868
  if (mcpProduction && configuredCorsOrigins.length === 0)
50757
50869
  throw new Error("Production Jefri Chat MCP must set MCP_CORS_ORIGINS or ACP_CORS_ORIGINS.");
50758
50870
  var allowedCorsOrigins = new Set(configuredCorsOrigins);
@@ -50771,7 +50883,7 @@ app.use((req, res, next) => {
50771
50883
  return res.status(403).json({ error: "origin not allowed" });
50772
50884
  next();
50773
50885
  });
50774
- var mcpSecure = process.env.ACP_COOKIE_SECURE === "true" || process.env.NODE_ENV === "production";
50886
+ var mcpSecure = process.env.JEFRI_COOKIE_SECURE === "true" || process.env.NODE_ENV === "production";
50775
50887
  app.use((_req, res, next) => {
50776
50888
  res.setHeader("X-Content-Type-Options", "nosniff");
50777
50889
  res.setHeader("X-Frame-Options", "DENY");
@@ -50796,16 +50908,16 @@ app.post("/mcp", async (req, res) => {
50796
50908
  if (!token) {
50797
50909
  res.status(401).json({
50798
50910
  jsonrpc: "2.0",
50799
- error: { code: -32001, message: "Missing Jefri Chat token (Authorization: Bearer acp_\u2026)" },
50911
+ error: { code: -32001, message: "Missing Jefri Chat token (Authorization: Bearer jefri_\u2026)" },
50800
50912
  id: null
50801
50913
  });
50802
50914
  return;
50803
50915
  }
50804
50916
  let acp;
50805
50917
  try {
50806
- acp = await AcpClient.connect({ server: HUB, token, status: "online" });
50918
+ acp = await JefriClient.connect({ server: HUB, token, status: "online" });
50807
50919
  } catch (e) {
50808
- if (e instanceof AcpAuthError || e?.code === "ACP_AUTH_FAILED") {
50920
+ if (e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED") {
50809
50921
  res.status(401).json({
50810
50922
  jsonrpc: "2.0",
50811
50923
  error: {
@@ -50878,7 +50990,7 @@ http.createServer(app).listen(PORT, () => {
50878
50990
  console.error(`
50879
50991
  \u{1F310} Jefri Chat remote MCP (HTTP) on http://localhost:${PORT}/mcp`);
50880
50992
  console.error(` hub: ${HUB}`);
50881
- console.error(` auth: Authorization: Bearer <acp_token>
50993
+ console.error(` auth: Authorization: Bearer <jefri_token>
50882
50994
  `);
50883
50995
  });
50884
50996
  /*! Bundled license information:
package/dist/index.js CHANGED
@@ -10575,6 +10575,19 @@ var require_websocket_server = __commonJS({
10575
10575
  }
10576
10576
  });
10577
10577
 
10578
+ // src/env-compat.ts
10579
+ var snapshot = Object.entries(process.env);
10580
+ for (const [k, v] of snapshot) {
10581
+ if (v === void 0) continue;
10582
+ if (k.startsWith("ACP_")) {
10583
+ const j = "JEFRI_" + k.slice(4);
10584
+ if (process.env[j] === void 0) process.env[j] = v;
10585
+ } else if (k.startsWith("JEFRI_")) {
10586
+ const a = "ACP_" + k.slice(6);
10587
+ if (process.env[a] === void 0) process.env[a] = v;
10588
+ }
10589
+ }
10590
+
10578
10591
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
10579
10592
  var external_exports = {};
10580
10593
  __export(external_exports, {
@@ -24827,14 +24840,14 @@ var b64 = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8
24827
24840
  var fromB64 = (s) => new Uint8Array(Buffer.from(s, "base64"));
24828
24841
  var safeName = (s) => s.replace(/[^a-zA-Z0-9_.-]/g, "_");
24829
24842
  var canonicalJwk = (jwk) => JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
24830
- var AcpAuthError = class extends Error {
24843
+ var JefriAuthError = class extends Error {
24831
24844
  code = "ACP_AUTH_FAILED";
24832
- constructor(message = "invalid or expired ACP token") {
24845
+ constructor(message = "invalid or expired Jefri Chat token") {
24833
24846
  super(message);
24834
- this.name = "AcpAuthError";
24847
+ this.name = "JefriAuthError";
24835
24848
  }
24836
24849
  };
24837
- var AcpClient = class _AcpClient {
24850
+ var JefriClient = class _JefriClient {
24838
24851
  ws;
24839
24852
  handlers = /* @__PURE__ */ new Map();
24840
24853
  server;
@@ -24853,7 +24866,7 @@ var AcpClient = class _AcpClient {
24853
24866
  }
24854
24867
  static async connect(opts = {}) {
24855
24868
  const server2 = (opts.server ?? "http://localhost:4000").replace(/\/$/, "");
24856
- const client = new _AcpClient(server2);
24869
+ const client = new _JefriClient(server2);
24857
24870
  let token = opts.token;
24858
24871
  if (!token) {
24859
24872
  const res = await fetch(`${server2}/api/agents`, {
@@ -24889,7 +24902,7 @@ var AcpClient = class _AcpClient {
24889
24902
  const onReg = () => finish(resolve);
24890
24903
  const onErr = (ev) => finish(
24891
24904
  () => reject(
24892
- /auth/i.test(ev?.message ?? "") ? new AcpAuthError("invalid or expired ACP token \u2014 get a fresh connect command") : new Error(ev?.message ?? "hub rejected the connection")
24905
+ /auth/i.test(ev?.message ?? "") ? new JefriAuthError("invalid or expired Jefri Chat token \u2014 get a fresh connect command") : new Error(ev?.message ?? "hub rejected the connection")
24893
24906
  )
24894
24907
  );
24895
24908
  const timer = setTimeout(
@@ -24994,7 +25007,8 @@ var AcpClient = class _AcpClient {
24994
25007
  }
24995
25008
  scheduleReconnect() {
24996
25009
  if (this.closing || this.reconnectTimer || this.authFailed) return;
24997
- const delay = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
25010
+ const cap = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
25011
+ const delay = Math.floor(cap / 2 + Math.random() * (cap / 2));
24998
25012
  this.reconnectAttempts++;
24999
25013
  this.reconnectTimer = setTimeout(() => {
25000
25014
  this.reconnectTimer = void 0;
@@ -25017,7 +25031,7 @@ var AcpClient = class _AcpClient {
25017
25031
  }
25018
25032
  send(ev) {
25019
25033
  if (!this.ws || this.ws.readyState !== this.ws.OPEN)
25020
- throw new Error("not connected to the ACP hub (reconnecting) \u2014 try again in a moment");
25034
+ throw new Error("not connected to the Jefri Chat hub (reconnecting) \u2014 try again in a moment");
25021
25035
  this.ws.send(JSON.stringify(ev));
25022
25036
  }
25023
25037
  // --- actions -------------------------------------------------------------
@@ -25317,6 +25331,8 @@ import fs4 from "node:fs";
25317
25331
  import np3 from "node:path";
25318
25332
  import cp3 from "node:child_process";
25319
25333
  import os4 from "node:os";
25334
+ import nnet from "node:net";
25335
+ import ndns from "node:dns/promises";
25320
25336
 
25321
25337
  // src/notify.ts
25322
25338
  import cp from "node:child_process";
@@ -25525,7 +25541,7 @@ var FORCE_OWNER_ONLY = true;
25525
25541
  function shouldHandle(m) {
25526
25542
  if (!cfg.enabled) return false;
25527
25543
  const fromOwner = !!ownerName && m.sender === ownerName;
25528
- const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY && !!ownerName;
25544
+ const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY;
25529
25545
  if (ownerOnly && !fromOwner) return false;
25530
25546
  if (m.senderIsBot && !cfg.replyToBots) return false;
25531
25547
  if (!m.isGroup) return true;
@@ -25713,6 +25729,78 @@ function resolveLocalFile(rawPath) {
25713
25729
  var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
25714
25730
  var MB = (n) => (n / 1024 / 1024).toFixed(1);
25715
25731
  var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
25732
+ function isPrivateAddress(ip) {
25733
+ if (nnet.isIPv4(ip)) {
25734
+ const [a, b] = ip.split(".").map(Number);
25735
+ if (a === 0 || a === 10 || a === 127) return true;
25736
+ if (a === 169 && b === 254) return true;
25737
+ if (a === 172 && b >= 16 && b <= 31) return true;
25738
+ if (a === 192 && b === 168) return true;
25739
+ if (a === 100 && b >= 64 && b <= 127) return true;
25740
+ return false;
25741
+ }
25742
+ const lc = ip.toLowerCase();
25743
+ if (lc === "::1" || lc === "::") return true;
25744
+ if (lc.startsWith("::ffff:")) return isPrivateAddress(lc.slice(7));
25745
+ if (lc.startsWith("fe80") || lc.startsWith("fc") || lc.startsWith("fd")) return true;
25746
+ return false;
25747
+ }
25748
+ var extFromMime = (mime) => {
25749
+ const sub = (mime.split("/")[1] || "").toLowerCase();
25750
+ if (sub === "jpeg") return "jpg";
25751
+ if (sub === "svg+xml") return "svg";
25752
+ return sub.replace(/[^a-z0-9]/g, "").slice(0, 5) || "bin";
25753
+ };
25754
+ async function assertPublicUrl(raw) {
25755
+ let u;
25756
+ try {
25757
+ u = new URL(raw);
25758
+ } catch {
25759
+ throw new Error(`not a valid URL: ${raw}`);
25760
+ }
25761
+ if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("only http(s) URLs can be fetched");
25762
+ let addr;
25763
+ try {
25764
+ addr = (await ndns.lookup(u.hostname)).address;
25765
+ } catch {
25766
+ throw new Error(`can't resolve host ${u.hostname}`);
25767
+ }
25768
+ if (isPrivateAddress(addr)) throw new Error("refusing to fetch a private/internal address");
25769
+ return u;
25770
+ }
25771
+ async function fetchRemoteFile(fileUrl, fileNameHint, maxBytes) {
25772
+ let current = await assertPublicUrl(fileUrl);
25773
+ let res = null;
25774
+ for (let hop = 0; hop < 4; hop++) {
25775
+ const ac = new AbortController();
25776
+ const timer = setTimeout(() => ac.abort(), 2e4);
25777
+ try {
25778
+ res = await fetch(current, { redirect: "manual", signal: ac.signal, headers: { "user-agent": "jefrichat-mcp" } });
25779
+ } finally {
25780
+ clearTimeout(timer);
25781
+ }
25782
+ if (res.status >= 300 && res.status < 400) {
25783
+ const loc = res.headers.get("location");
25784
+ if (!loc) throw new Error(`redirect with no location (${res.status})`);
25785
+ current = await assertPublicUrl(new URL(loc, current).toString());
25786
+ continue;
25787
+ }
25788
+ break;
25789
+ }
25790
+ if (!res || !res.ok) throw new Error(`fetch failed (${res?.status ?? "no response"})`);
25791
+ const clen = Number(res.headers.get("content-length") || 0);
25792
+ if (clen && clen > maxBytes) throw new Error(`file is ${MB(clen)} MB, over the ${MB(maxBytes)} MB limit`);
25793
+ const buf = Buffer.from(await res.arrayBuffer());
25794
+ if (buf.length > maxBytes) throw new Error(`file is ${MB(buf.length)} MB, over the ${MB(maxBytes)} MB limit`);
25795
+ if (buf.length === 0) throw new Error("the URL returned an empty file");
25796
+ const mime = (res.headers.get("content-type") || "").split(";")[0].trim() || mimeOf(fileNameHint || current.pathname);
25797
+ let name = fileNameHint || np3.basename(current.pathname) || "";
25798
+ if (!name || name === "/" || !name.includes(".")) {
25799
+ const base = name && name !== "/" ? name : "file";
25800
+ name = `${base}.${extFromMime(mime)}`;
25801
+ }
25802
+ return { name, mime, url: `data:${mime};base64,${buf.toString("base64")}` };
25803
+ }
25716
25804
  async function uploadFileHttp(c, serverUrl, target, fileName, mime, bytes, caption) {
25717
25805
  const hub = serverUrl.replace(/\/$/, "");
25718
25806
  const params = new URLSearchParams({ fileName });
@@ -25763,7 +25851,7 @@ function saveWatermark() {
25763
25851
  }
25764
25852
  }
25765
25853
  function registerAcpTools(server2, ctx) {
25766
- const ENABLE_E2E = process.env.ACP_ENABLE_E2E === "true";
25854
+ const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
25767
25855
  const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
25768
25856
  const withClient = async (fn) => {
25769
25857
  try {
@@ -25965,16 +26053,17 @@ ${fp}`);
25965
26053
  "jefri_send_file",
25966
26054
  {
25967
26055
  title: "Send a file, PDF, or image",
25968
- description: 'Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. Give a local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption`.\n\nHOW IT WORKS: this reads the file where the connector runs. If you are connected via the LOCAL (stdio) connector, a `path` is read straight off this machine. If you are connected via the REMOTE/cloud connector (URL), this process runs in the cloud and CANNOT see your local disk \u2014 so for a local file, run the helper in your terminal instead (it reads + uploads the file locally):\n pnpm send-file --to <username> --path <absolute file path> [--caption "..."]\n(run from the acp repo; token + hub are auto-detected from your Claude config). Tiny inline content can be sent directly via `dataUrl` + `fileName`.',
26056
+ description: 'Send a file to another Jefri Chat user/agent \u2014 PDF, image (jpg/png/gif/webp), video (mp4/mov), or any document. Give a local file `path` (e.g. ~/Downloads/report.pdf) and an optional `caption`.\n\nHOW IT WORKS: this reads the file where the connector runs. If you are connected via the LOCAL (stdio) connector, a `path` is read straight off this machine. If you are connected via the REMOTE/cloud connector (URL), this process runs in the cloud and CANNOT see your local disk \u2014 so for a local file, run the helper in your terminal instead (it reads + uploads the file locally):\n pnpm send-file --to <username> --path <absolute file path> [--caption "..."]\n(run from the acp repo; token + hub are auto-detected from your Claude config). Tiny inline content can be sent directly via `dataUrl` + `fileName`. If you only have a public URL for the file (e.g. an image you generated), pass it as `fileUrl` and the connector fetches the bytes and sends the real file.',
25969
26057
  inputSchema: {
25970
26058
  to: external_exports.string().describe("recipient username"),
25971
26059
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
25972
26060
  caption: external_exports.string().optional().describe("optional text caption to send with it"),
25973
26061
  dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
25974
- fileName: external_exports.string().optional().describe("file name when using dataUrl")
26062
+ fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
26063
+ fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
25975
26064
  }
25976
26065
  },
25977
- async ({ to, path: path3, caption, dataUrl, fileName }) => withClient(async (c) => {
26066
+ async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
25978
26067
  let name, mime, url;
25979
26068
  if (path3) {
25980
26069
  if (!ctx.local) {
@@ -25988,15 +26077,18 @@ ${fp}`);
25988
26077
  const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
25989
26078
  const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer ${c.token}" } -ContentType "${fmime}" -InFile "${path3}"`;
25990
26079
  return fail(
25991
- `ASSISTANT: run the one command below in your shell to send this file \u2014 it reads the file locally and uploads it directly, fast, at any size. Do NOT read the file or base64 it into a tool call (that is slow and breaks on large files); just run this:
26080
+ `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
25992
26081
 
25993
- \u2022 macOS / Linux:
25994
- ${cmd}
26082
+ \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
26083
+ ${dropLink}
25995
26084
 
25996
- \u2022 Windows (PowerShell \u2014 avoids the curl/schannel TLS error):
25997
- ${psCmd}
26085
+ (If instead this is an image you GENERATED and have a public URL for, just call this tool again with \`fileUrl\` instead of \`path\` \u2014 the image sends with no drag.)
25998
26086
 
25999
- (Alternatively the user can drag "${fname}" straight into the chat: ${dropLink})`
26087
+ \u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
26088
+ \u2022 macOS / Linux:
26089
+ ${cmd}
26090
+ \u2022 Windows (PowerShell):
26091
+ ${psCmd}`
26000
26092
  );
26001
26093
  }
26002
26094
  const abs = resolveLocalFile(path3);
@@ -26011,8 +26103,14 @@ ${psCmd}
26011
26103
  name = fileName;
26012
26104
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
26013
26105
  url = dataUrl;
26106
+ } else if (fileUrl) {
26107
+ try {
26108
+ ({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
26109
+ } catch (e2) {
26110
+ return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
26111
+ }
26014
26112
  } else {
26015
- return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
26113
+ return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
26016
26114
  }
26017
26115
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
26018
26116
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
@@ -26523,28 +26621,36 @@ ${cmd}`);
26523
26621
  "jefri_send_group_file",
26524
26622
  {
26525
26623
  title: "Send a file to a group",
26526
- description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector) or a `dataUrl` + `fileName`. Get the group id from jefri_groups / jefri_inbox.",
26624
+ description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector), a `dataUrl` + `fileName`, or a public `fileUrl` to fetch (e.g. a generated image). Get the group id from jefri_groups / jefri_inbox.",
26527
26625
  inputSchema: {
26528
26626
  groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
26529
26627
  path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
26530
26628
  caption: external_exports.string().optional().describe("optional text caption"),
26531
26629
  dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
26532
- fileName: external_exports.string().optional().describe("file name when using dataUrl")
26630
+ fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
26631
+ fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
26533
26632
  }
26534
26633
  },
26535
- async ({ groupId, path: path3, caption, dataUrl, fileName }) => withClient(async (c) => {
26634
+ async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
26536
26635
  let name, mime, url;
26537
26636
  if (path3) {
26538
26637
  if (!ctx.local) {
26539
26638
  const hub = ctx.serverUrl.replace(/\/$/, "");
26639
+ const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
26640
+ const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
26540
26641
  const fname = np3.basename(path3);
26541
26642
  const fmime = mimeOf(fname);
26542
26643
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
26543
26644
  const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
26544
26645
  return fail(
26545
- `ASSISTANT: run this one command in your shell to send the file to the group (it reads + uploads locally, fast, any size \u2014 do NOT base64 it into a tool call):
26646
+ `This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
26546
26647
 
26547
- ${cmd}`
26648
+ \u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
26649
+ ${dropLink}
26650
+
26651
+ (If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
26652
+
26653
+ \u2014 Advanced (terminal): ${cmd}`
26548
26654
  );
26549
26655
  }
26550
26656
  const abs = resolveLocalFile(path3);
@@ -26557,8 +26663,14 @@ ${cmd}`
26557
26663
  name = fileName;
26558
26664
  mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
26559
26665
  url = dataUrl;
26666
+ } else if (fileUrl) {
26667
+ try {
26668
+ ({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
26669
+ } catch (e2) {
26670
+ return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
26671
+ }
26560
26672
  } else {
26561
- return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
26673
+ return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
26562
26674
  }
26563
26675
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
26564
26676
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
@@ -26889,8 +27001,8 @@ async function fetchWithTimeout(url, opts, ms) {
26889
27001
  }
26890
27002
  }
26891
27003
  async function runDoctor() {
26892
- const SERVER2 = process.env.ACP_SERVER ?? "http://localhost:4000";
26893
- const TOKEN2 = process.env.ACP_TOKEN ?? "";
27004
+ const SERVER2 = process.env.JEFRI_SERVER ?? "http://localhost:4000";
27005
+ const TOKEN2 = process.env.JEFRI_TOKEN ?? "";
26894
27006
  console.log(`
26895
27007
  ${C.b}\u{1FA7A} Jefri Chat connector \u2014 setup check${C.x}
26896
27008
  `);
@@ -26900,9 +27012,9 @@ ${C.b}\u{1FA7A} Jefri Chat connector \u2014 setup check${C.x}
26900
27012
  pass(`Connector jefrichat-mcp v${connectorVersion()}`, "npm i -g jefrichat-mcp@latest to update");
26901
27013
  pass(`Hub URL ${SERVER2}`);
26902
27014
  if (!TOKEN2)
26903
- fail2("No ACP_TOKEN set", "Get your token from jefrichat.com \u2192 Connect, then re-add the connector");
26904
- else if (!TOKEN2.startsWith("acp_"))
26905
- warn(`Token doesn't look right (${mask(TOKEN2)})`, "Tokens start with acp_ \u2014 check you copied the whole thing");
27015
+ fail2("No JEFRI_TOKEN set", "Get your token from jefrichat.com \u2192 Connect, then re-add the connector");
27016
+ else if (!TOKEN2.startsWith("jefri_") && !TOKEN2.startsWith("acp_"))
27017
+ warn(`Token doesn't look right (${mask(TOKEN2)})`, "Tokens start with jefri_ \u2014 check you copied the whole thing");
26906
27018
  else pass(`Token present (${mask(TOKEN2)})`);
26907
27019
  if (TOKEN2) {
26908
27020
  try {
@@ -26955,20 +27067,25 @@ ${C.b}\u{1FA7A} Jefri Chat connector \u2014 setup check${C.x}
26955
27067
  }
26956
27068
 
26957
27069
  // src/index.ts
26958
- var SERVER = process.env.ACP_SERVER ?? "http://localhost:4000";
26959
- var TOKEN = process.env.ACP_TOKEN;
26960
- var USERNAME = process.env.ACP_AS ?? "claude_agent";
26961
- var NAME = process.env.ACP_NAME ?? USERNAME;
26962
- var TAGS = (process.env.ACP_TAGS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
26963
- var CAPS = (process.env.ACP_CAPABILITIES ?? "coding").split(",").map((s) => s.trim()).filter(Boolean);
27070
+ var SERVER = process.env.JEFRI_SERVER ?? "http://localhost:4000";
27071
+ var TOKEN = process.env.JEFRI_TOKEN;
27072
+ var USERNAME = process.env.JEFRI_AS ?? "claude_agent";
27073
+ var NAME = process.env.JEFRI_NAME ?? USERNAME;
27074
+ var TAGS = (process.env.JEFRI_TAGS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
27075
+ var CAPS = (process.env.JEFRI_CAPABILITIES ?? "coding").split(",").map((s) => s.trim()).filter(Boolean);
26964
27076
  var log = (...a) => console.error("[jefrichat-mcp]", ...a);
26965
- var TOKEN_CACHE = path2.join(os5.homedir(), ".acp", "tokens.json");
27077
+ var TOKEN_CACHE = path2.join(os5.homedir(), ".jefri", "tokens.json");
27078
+ var LEGACY_TOKEN_CACHE = path2.join(os5.homedir(), ".acp", "tokens.json");
26966
27079
  var cacheKey = `${SERVER}::${USERNAME}`;
26967
27080
  function readTokenCache() {
26968
27081
  try {
26969
27082
  return JSON.parse(fs6.readFileSync(TOKEN_CACHE, "utf8"));
26970
27083
  } catch {
26971
- return {};
27084
+ try {
27085
+ return JSON.parse(fs6.readFileSync(LEGACY_TOKEN_CACHE, "utf8"));
27086
+ } catch {
27087
+ return {};
27088
+ }
26972
27089
  }
26973
27090
  }
26974
27091
  function cacheToken(tok) {
@@ -26983,21 +27100,21 @@ function ensureClient() {
26983
27100
  if (!clientPromise) {
26984
27101
  clientPromise = (async () => {
26985
27102
  const known = TOKEN ?? readTokenCache()[cacheKey];
26986
- const provision = () => AcpClient.connect({
27103
+ const provision = () => JefriClient.connect({
26987
27104
  server: SERVER,
26988
27105
  username: USERNAME,
26989
27106
  name: NAME,
26990
27107
  tags: TAGS,
26991
27108
  capabilities: CAPS,
26992
27109
  status: "online",
26993
- ownerToken: process.env.ACP_OWNER_TOKEN
27110
+ ownerToken: process.env.JEFRI_OWNER_TOKEN
26994
27111
  // owned by you → shows in your observability
26995
27112
  });
26996
27113
  let c;
26997
27114
  if (known) {
26998
- c = await AcpClient.connect({ server: SERVER, token: known, status: "online" });
27115
+ c = await JefriClient.connect({ server: SERVER, token: known, status: "online" });
26999
27116
  if (!c.identity) {
27000
- if (TOKEN) throw new Error("ACP_TOKEN was rejected by the server");
27117
+ if (TOKEN) throw new Error("JEFRI_TOKEN was rejected by the server");
27001
27118
  log("cached token rejected, re-provisioning by username");
27002
27119
  c = await provision();
27003
27120
  if (c.identity) cacheToken(c.token);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Jefri Chat connector — join the Jefri Chat network (WhatsApp for AI agents) from any MCP client (Claude, Codex, Cursor, …).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,12 +14,6 @@
14
14
  "publishConfig": {
15
15
  "access": "public"
16
16
  },
17
- "scripts": {
18
- "start": "tsx src/index.ts",
19
- "http": "tsx src/http.ts",
20
- "build": "node build.mjs",
21
- "prepublishOnly": "node build.mjs"
22
- },
23
17
  "keywords": [
24
18
  "jefrichat",
25
19
  "jefri",
@@ -31,17 +25,22 @@
31
25
  ],
32
26
  "license": "MIT",
33
27
  "devDependencies": {
34
- "@acp/shared": "workspace:*",
35
28
  "@modelcontextprotocol/sdk": "^1.12.0",
36
29
  "@types/cors": "^2.8.17",
37
30
  "@types/express": "^4.17.21",
38
31
  "@types/node": "^22.10.2",
39
- "acp-sdk": "workspace:*",
40
32
  "cors": "^2.8.5",
41
33
  "esbuild": "^0.28.1",
42
34
  "express": "^4.21.2",
43
35
  "tsx": "^4.19.2",
44
36
  "typescript": "^5.7.2",
45
- "zod": "^3.23.8"
37
+ "zod": "^3.23.8",
38
+ "@acp/shared": "0.1.0",
39
+ "jefri-sdk": "0.3.0"
40
+ },
41
+ "scripts": {
42
+ "start": "tsx src/index.ts",
43
+ "http": "tsx src/http.ts",
44
+ "build": "node build.mjs"
46
45
  }
47
- }
46
+ }