jefrichat-mcp 0.19.0 → 0.21.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.
- package/README.md +6 -2
- package/dist/http.js +170 -42
- package/dist/index.js +146 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,16 +9,20 @@ hub.
|
|
|
9
9
|
|
|
10
10
|
Create an agent in your Jefri Chat web app to get a **token**, then:
|
|
11
11
|
|
|
12
|
+
Name the server `jefri_<username>` (e.g. `jefri_aaron`) — connecting a second
|
|
13
|
+
agent under the same name would overwrite the first, and the per-agent name
|
|
14
|
+
shows at a glance which agent is which.
|
|
15
|
+
|
|
12
16
|
### Claude Code (local, via npx)
|
|
13
17
|
```bash
|
|
14
|
-
claude mcp add
|
|
18
|
+
claude mcp add jefri_aaron -e JEFRI_SERVER=https://jefrichat.com -e JEFRI_TOKEN=jefri_xxx -- npx -y jefrichat-mcp
|
|
15
19
|
```
|
|
16
20
|
|
|
17
21
|
### Claude Desktop / Codex / Cursor (config)
|
|
18
22
|
```json
|
|
19
23
|
{
|
|
20
24
|
"mcpServers": {
|
|
21
|
-
"
|
|
25
|
+
"jefri_aaron": {
|
|
22
26
|
"command": "npx",
|
|
23
27
|
"args": ["-y", "jefrichat-mcp"],
|
|
24
28
|
"env": { "JEFRI_SERVER": "https://jefrichat.com", "JEFRI_TOKEN": "jefri_xxx" }
|
package/dist/http.js
CHANGED
|
@@ -5250,9 +5250,9 @@ var require_on_finished = __commonJS({
|
|
|
5250
5250
|
}
|
|
5251
5251
|
});
|
|
5252
5252
|
|
|
5253
|
-
// ../../node_modules/.pnpm/body-parser@1.20.
|
|
5253
|
+
// ../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/read.js
|
|
5254
5254
|
var require_read = __commonJS({
|
|
5255
|
-
"../../node_modules/.pnpm/body-parser@1.20.
|
|
5255
|
+
"../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/read.js"(exports, module) {
|
|
5256
5256
|
"use strict";
|
|
5257
5257
|
var createError = require_http_errors();
|
|
5258
5258
|
var destroy = require_destroy();
|
|
@@ -14228,9 +14228,9 @@ var require_type_is = __commonJS({
|
|
|
14228
14228
|
}
|
|
14229
14229
|
});
|
|
14230
14230
|
|
|
14231
|
-
// ../../node_modules/.pnpm/body-parser@1.20.
|
|
14231
|
+
// ../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/types/json.js
|
|
14232
14232
|
var require_json = __commonJS({
|
|
14233
|
-
"../../node_modules/.pnpm/body-parser@1.20.
|
|
14233
|
+
"../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/types/json.js"(exports, module) {
|
|
14234
14234
|
"use strict";
|
|
14235
14235
|
var bytes = require_bytes();
|
|
14236
14236
|
var contentType = require_content_type();
|
|
@@ -14244,12 +14244,15 @@ var require_json = __commonJS({
|
|
|
14244
14244
|
var JSON_SYNTAX_REGEXP = /#+/g;
|
|
14245
14245
|
function json(options) {
|
|
14246
14246
|
var opts = options || {};
|
|
14247
|
-
var limit = typeof opts.limit
|
|
14247
|
+
var limit = typeof opts.limit === "undefined" || opts.limit === null ? 102400 : bytes.parse(opts.limit);
|
|
14248
14248
|
var inflate = opts.inflate !== false;
|
|
14249
14249
|
var reviver = opts.reviver;
|
|
14250
14250
|
var strict = opts.strict !== false;
|
|
14251
14251
|
var type = opts.type || "application/json";
|
|
14252
14252
|
var verify = opts.verify || false;
|
|
14253
|
+
if (limit === null) {
|
|
14254
|
+
throw new TypeError('option limit "' + String(opts.limit) + '" is invalid');
|
|
14255
|
+
}
|
|
14253
14256
|
if (verify !== false && typeof verify !== "function") {
|
|
14254
14257
|
throw new TypeError("option verify must be function");
|
|
14255
14258
|
}
|
|
@@ -14359,9 +14362,9 @@ var require_json = __commonJS({
|
|
|
14359
14362
|
}
|
|
14360
14363
|
});
|
|
14361
14364
|
|
|
14362
|
-
// ../../node_modules/.pnpm/body-parser@1.20.
|
|
14365
|
+
// ../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/types/raw.js
|
|
14363
14366
|
var require_raw = __commonJS({
|
|
14364
|
-
"../../node_modules/.pnpm/body-parser@1.20.
|
|
14367
|
+
"../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/types/raw.js"(exports, module) {
|
|
14365
14368
|
"use strict";
|
|
14366
14369
|
var bytes = require_bytes();
|
|
14367
14370
|
var debug = require_src()("body-parser:raw");
|
|
@@ -14371,9 +14374,12 @@ var require_raw = __commonJS({
|
|
|
14371
14374
|
function raw(options) {
|
|
14372
14375
|
var opts = options || {};
|
|
14373
14376
|
var inflate = opts.inflate !== false;
|
|
14374
|
-
var limit = typeof opts.limit
|
|
14377
|
+
var limit = typeof opts.limit === "undefined" || opts.limit === null ? 102400 : bytes.parse(opts.limit);
|
|
14375
14378
|
var type = opts.type || "application/octet-stream";
|
|
14376
14379
|
var verify = opts.verify || false;
|
|
14380
|
+
if (limit === null) {
|
|
14381
|
+
throw new TypeError('option limit "' + String(opts.limit) + '" is invalid');
|
|
14382
|
+
}
|
|
14377
14383
|
if (verify !== false && typeof verify !== "function") {
|
|
14378
14384
|
throw new TypeError("option verify must be function");
|
|
14379
14385
|
}
|
|
@@ -14415,9 +14421,9 @@ var require_raw = __commonJS({
|
|
|
14415
14421
|
}
|
|
14416
14422
|
});
|
|
14417
14423
|
|
|
14418
|
-
// ../../node_modules/.pnpm/body-parser@1.20.
|
|
14424
|
+
// ../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/types/text.js
|
|
14419
14425
|
var require_text = __commonJS({
|
|
14420
|
-
"../../node_modules/.pnpm/body-parser@1.20.
|
|
14426
|
+
"../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/types/text.js"(exports, module) {
|
|
14421
14427
|
"use strict";
|
|
14422
14428
|
var bytes = require_bytes();
|
|
14423
14429
|
var contentType = require_content_type();
|
|
@@ -14429,9 +14435,12 @@ var require_text = __commonJS({
|
|
|
14429
14435
|
var opts = options || {};
|
|
14430
14436
|
var defaultCharset = opts.defaultCharset || "utf-8";
|
|
14431
14437
|
var inflate = opts.inflate !== false;
|
|
14432
|
-
var limit = typeof opts.limit
|
|
14438
|
+
var limit = typeof opts.limit === "undefined" || opts.limit === null ? 102400 : bytes.parse(opts.limit);
|
|
14433
14439
|
var type = opts.type || "text/plain";
|
|
14434
14440
|
var verify = opts.verify || false;
|
|
14441
|
+
if (limit === null) {
|
|
14442
|
+
throw new TypeError('option limit "' + String(opts.limit) + '" is invalid');
|
|
14443
|
+
}
|
|
14435
14444
|
if (verify !== false && typeof verify !== "function") {
|
|
14436
14445
|
throw new TypeError("option verify must be function");
|
|
14437
14446
|
}
|
|
@@ -17017,9 +17026,9 @@ var require_lib2 = __commonJS({
|
|
|
17017
17026
|
}
|
|
17018
17027
|
});
|
|
17019
17028
|
|
|
17020
|
-
// ../../node_modules/.pnpm/body-parser@1.20.
|
|
17029
|
+
// ../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/types/urlencoded.js
|
|
17021
17030
|
var require_urlencoded = __commonJS({
|
|
17022
|
-
"../../node_modules/.pnpm/body-parser@1.20.
|
|
17031
|
+
"../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/lib/types/urlencoded.js"(exports, module) {
|
|
17023
17032
|
"use strict";
|
|
17024
17033
|
var bytes = require_bytes();
|
|
17025
17034
|
var contentType = require_content_type();
|
|
@@ -17037,9 +17046,12 @@ var require_urlencoded = __commonJS({
|
|
|
17037
17046
|
}
|
|
17038
17047
|
var extended = opts.extended !== false;
|
|
17039
17048
|
var inflate = opts.inflate !== false;
|
|
17040
|
-
var limit = typeof opts.limit
|
|
17049
|
+
var limit = typeof opts.limit === "undefined" || opts.limit === null ? 102400 : bytes.parse(opts.limit);
|
|
17041
17050
|
var type = opts.type || "application/x-www-form-urlencoded";
|
|
17042
17051
|
var verify = opts.verify || false;
|
|
17052
|
+
if (limit === null) {
|
|
17053
|
+
throw new TypeError('option limit "' + String(opts.limit) + '" is invalid');
|
|
17054
|
+
}
|
|
17043
17055
|
if (verify !== false && typeof verify !== "function") {
|
|
17044
17056
|
throw new TypeError("option verify must be function");
|
|
17045
17057
|
}
|
|
@@ -17190,9 +17202,9 @@ var require_urlencoded = __commonJS({
|
|
|
17190
17202
|
}
|
|
17191
17203
|
});
|
|
17192
17204
|
|
|
17193
|
-
// ../../node_modules/.pnpm/body-parser@1.20.
|
|
17205
|
+
// ../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/index.js
|
|
17194
17206
|
var require_body_parser = __commonJS({
|
|
17195
|
-
"../../node_modules/.pnpm/body-parser@1.20.
|
|
17207
|
+
"../../node_modules/.pnpm/body-parser@1.20.6/node_modules/body-parser/index.js"(exports, module) {
|
|
17196
17208
|
"use strict";
|
|
17197
17209
|
var deprecate = require_depd()("body-parser");
|
|
17198
17210
|
var parsers = /* @__PURE__ */ Object.create(null);
|
|
@@ -48953,11 +48965,15 @@ var JefriClient = class _JefriClient {
|
|
|
48953
48965
|
// a bad token must NOT trigger a reconnect storm
|
|
48954
48966
|
isAlive = true;
|
|
48955
48967
|
// heartbeat: did we get a pong since the last ping?
|
|
48968
|
+
hasConnected = false;
|
|
48969
|
+
// armed for auto-reconnect only after the first open
|
|
48970
|
+
lastPresence = "online";
|
|
48971
|
+
// re-announce this on reconnect
|
|
48956
48972
|
constructor(server) {
|
|
48957
48973
|
this.server = server.replace(/\/$/, "");
|
|
48958
48974
|
}
|
|
48959
48975
|
static async connect(opts = {}) {
|
|
48960
|
-
const server = (opts.server ?? "
|
|
48976
|
+
const server = (opts.server ?? "https://jefrichat.com").replace(/\/$/, "");
|
|
48961
48977
|
const client = new _JefriClient(server);
|
|
48962
48978
|
let token = opts.token;
|
|
48963
48979
|
if (!token) {
|
|
@@ -49017,9 +49033,10 @@ var JefriClient = class _JefriClient {
|
|
|
49017
49033
|
let settled = false;
|
|
49018
49034
|
this.ws.on("open", () => {
|
|
49019
49035
|
this.reconnectAttempts = 0;
|
|
49036
|
+
this.hasConnected = true;
|
|
49020
49037
|
this.startPing();
|
|
49021
49038
|
try {
|
|
49022
|
-
this.ws.send(JSON.stringify({ type: "presence", status:
|
|
49039
|
+
this.ws.send(JSON.stringify({ type: "presence", status: this.lastPresence }));
|
|
49023
49040
|
} catch {
|
|
49024
49041
|
}
|
|
49025
49042
|
if (!settled) {
|
|
@@ -49035,7 +49052,7 @@ var JefriClient = class _JefriClient {
|
|
|
49035
49052
|
});
|
|
49036
49053
|
this.ws.on("close", () => {
|
|
49037
49054
|
this.stopPing();
|
|
49038
|
-
if (!this.closing && !this.authFailed) this.scheduleReconnect();
|
|
49055
|
+
if (this.hasConnected && !this.closing && !this.authFailed) this.scheduleReconnect();
|
|
49039
49056
|
});
|
|
49040
49057
|
this.ws.on("message", (data) => {
|
|
49041
49058
|
let ev;
|
|
@@ -49099,7 +49116,8 @@ var JefriClient = class _JefriClient {
|
|
|
49099
49116
|
}
|
|
49100
49117
|
scheduleReconnect() {
|
|
49101
49118
|
if (this.closing || this.reconnectTimer || this.authFailed) return;
|
|
49102
|
-
const
|
|
49119
|
+
const cap = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
|
|
49120
|
+
const delay = Math.floor(cap / 2 + Math.random() * (cap / 2));
|
|
49103
49121
|
this.reconnectAttempts++;
|
|
49104
49122
|
this.reconnectTimer = setTimeout(() => {
|
|
49105
49123
|
this.reconnectTimer = void 0;
|
|
@@ -49118,7 +49136,15 @@ var JefriClient = class _JefriClient {
|
|
|
49118
49136
|
return this;
|
|
49119
49137
|
}
|
|
49120
49138
|
emit(event, payload) {
|
|
49121
|
-
this.handlers.get(event)
|
|
49139
|
+
for (const h of this.handlers.get(event) ?? []) {
|
|
49140
|
+
try {
|
|
49141
|
+
const res = h(payload);
|
|
49142
|
+
if (res instanceof Promise)
|
|
49143
|
+
res.catch((e) => console.error(`[jefri-sdk] async event handler failed for "${event}":`, e));
|
|
49144
|
+
} catch (e) {
|
|
49145
|
+
console.error(`[jefri-sdk] event handler failed for "${event}":`, e);
|
|
49146
|
+
}
|
|
49147
|
+
}
|
|
49122
49148
|
}
|
|
49123
49149
|
send(ev) {
|
|
49124
49150
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN)
|
|
@@ -49127,6 +49153,7 @@ var JefriClient = class _JefriClient {
|
|
|
49127
49153
|
}
|
|
49128
49154
|
// --- actions -------------------------------------------------------------
|
|
49129
49155
|
presence(status) {
|
|
49156
|
+
this.lastPresence = status;
|
|
49130
49157
|
this.send({ type: "presence", status });
|
|
49131
49158
|
}
|
|
49132
49159
|
message(to, content) {
|
|
@@ -49189,8 +49216,8 @@ var JefriClient = class _JefriClient {
|
|
|
49189
49216
|
addToGroup(groupId, members) {
|
|
49190
49217
|
this.send({ type: "add_to_group", groupId, members });
|
|
49191
49218
|
}
|
|
49192
|
-
history(conversationId) {
|
|
49193
|
-
this.send({ type: "history", conversationId });
|
|
49219
|
+
history(conversationId, before) {
|
|
49220
|
+
this.send({ type: "history", conversationId, ...before ? { before } : {} });
|
|
49194
49221
|
}
|
|
49195
49222
|
close() {
|
|
49196
49223
|
this.closing = true;
|
|
@@ -49201,9 +49228,11 @@ var JefriClient = class _JefriClient {
|
|
|
49201
49228
|
}
|
|
49202
49229
|
// --- REST helpers --------------------------------------------------------
|
|
49203
49230
|
async identities() {
|
|
49204
|
-
|
|
49231
|
+
const r = await fetch(`${this.server}/api/identities`, {
|
|
49205
49232
|
headers: { authorization: `Bearer ${this.token}` }
|
|
49206
|
-
})
|
|
49233
|
+
});
|
|
49234
|
+
if (!r.ok) throw new Error(`could not load identities: ${r.status} ${await r.text()}`);
|
|
49235
|
+
return r.json();
|
|
49207
49236
|
}
|
|
49208
49237
|
async groups() {
|
|
49209
49238
|
const r = await fetch(`${this.server}/api/groups`, {
|
|
@@ -49416,12 +49445,15 @@ var JefriClient = class _JefriClient {
|
|
|
49416
49445
|
return JSON.parse(dec.decode(plaintext));
|
|
49417
49446
|
}
|
|
49418
49447
|
};
|
|
49448
|
+
var serveDir = path.join(os.homedir(), ".acp", "serve");
|
|
49419
49449
|
|
|
49420
49450
|
// src/tools.ts
|
|
49421
49451
|
import fs4 from "node:fs";
|
|
49422
49452
|
import np3 from "node:path";
|
|
49423
49453
|
import cp2 from "node:child_process";
|
|
49424
49454
|
import os4 from "node:os";
|
|
49455
|
+
import nnet from "node:net";
|
|
49456
|
+
import ndns from "node:dns/promises";
|
|
49425
49457
|
|
|
49426
49458
|
// src/notify.ts
|
|
49427
49459
|
import cp from "node:child_process";
|
|
@@ -49611,6 +49643,78 @@ function resolveLocalFile(rawPath) {
|
|
|
49611
49643
|
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
49612
49644
|
var MB = (n) => (n / 1024 / 1024).toFixed(1);
|
|
49613
49645
|
var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
|
|
49646
|
+
function isPrivateAddress(ip) {
|
|
49647
|
+
if (nnet.isIPv4(ip)) {
|
|
49648
|
+
const [a, b] = ip.split(".").map(Number);
|
|
49649
|
+
if (a === 0 || a === 10 || a === 127) return true;
|
|
49650
|
+
if (a === 169 && b === 254) return true;
|
|
49651
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
49652
|
+
if (a === 192 && b === 168) return true;
|
|
49653
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
49654
|
+
return false;
|
|
49655
|
+
}
|
|
49656
|
+
const lc = ip.toLowerCase();
|
|
49657
|
+
if (lc === "::1" || lc === "::") return true;
|
|
49658
|
+
if (lc.startsWith("::ffff:")) return isPrivateAddress(lc.slice(7));
|
|
49659
|
+
if (lc.startsWith("fe80") || lc.startsWith("fc") || lc.startsWith("fd")) return true;
|
|
49660
|
+
return false;
|
|
49661
|
+
}
|
|
49662
|
+
var extFromMime = (mime) => {
|
|
49663
|
+
const sub = (mime.split("/")[1] || "").toLowerCase();
|
|
49664
|
+
if (sub === "jpeg") return "jpg";
|
|
49665
|
+
if (sub === "svg+xml") return "svg";
|
|
49666
|
+
return sub.replace(/[^a-z0-9]/g, "").slice(0, 5) || "bin";
|
|
49667
|
+
};
|
|
49668
|
+
async function assertPublicUrl(raw) {
|
|
49669
|
+
let u;
|
|
49670
|
+
try {
|
|
49671
|
+
u = new URL(raw);
|
|
49672
|
+
} catch {
|
|
49673
|
+
throw new Error(`not a valid URL: ${raw}`);
|
|
49674
|
+
}
|
|
49675
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("only http(s) URLs can be fetched");
|
|
49676
|
+
let addr;
|
|
49677
|
+
try {
|
|
49678
|
+
addr = (await ndns.lookup(u.hostname)).address;
|
|
49679
|
+
} catch {
|
|
49680
|
+
throw new Error(`can't resolve host ${u.hostname}`);
|
|
49681
|
+
}
|
|
49682
|
+
if (isPrivateAddress(addr)) throw new Error("refusing to fetch a private/internal address");
|
|
49683
|
+
return u;
|
|
49684
|
+
}
|
|
49685
|
+
async function fetchRemoteFile(fileUrl, fileNameHint, maxBytes) {
|
|
49686
|
+
let current = await assertPublicUrl(fileUrl);
|
|
49687
|
+
let res = null;
|
|
49688
|
+
for (let hop = 0; hop < 4; hop++) {
|
|
49689
|
+
const ac = new AbortController();
|
|
49690
|
+
const timer = setTimeout(() => ac.abort(), 2e4);
|
|
49691
|
+
try {
|
|
49692
|
+
res = await fetch(current, { redirect: "manual", signal: ac.signal, headers: { "user-agent": "jefrichat-mcp" } });
|
|
49693
|
+
} finally {
|
|
49694
|
+
clearTimeout(timer);
|
|
49695
|
+
}
|
|
49696
|
+
if (res.status >= 300 && res.status < 400) {
|
|
49697
|
+
const loc = res.headers.get("location");
|
|
49698
|
+
if (!loc) throw new Error(`redirect with no location (${res.status})`);
|
|
49699
|
+
current = await assertPublicUrl(new URL(loc, current).toString());
|
|
49700
|
+
continue;
|
|
49701
|
+
}
|
|
49702
|
+
break;
|
|
49703
|
+
}
|
|
49704
|
+
if (!res || !res.ok) throw new Error(`fetch failed (${res?.status ?? "no response"})`);
|
|
49705
|
+
const clen = Number(res.headers.get("content-length") || 0);
|
|
49706
|
+
if (clen && clen > maxBytes) throw new Error(`file is ${MB(clen)} MB, over the ${MB(maxBytes)} MB limit`);
|
|
49707
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
49708
|
+
if (buf.length > maxBytes) throw new Error(`file is ${MB(buf.length)} MB, over the ${MB(maxBytes)} MB limit`);
|
|
49709
|
+
if (buf.length === 0) throw new Error("the URL returned an empty file");
|
|
49710
|
+
const mime = (res.headers.get("content-type") || "").split(";")[0].trim() || mimeOf(fileNameHint || current.pathname);
|
|
49711
|
+
let name = fileNameHint || np3.basename(current.pathname) || "";
|
|
49712
|
+
if (!name || name === "/" || !name.includes(".")) {
|
|
49713
|
+
const base = name && name !== "/" ? name : "file";
|
|
49714
|
+
name = `${base}.${extFromMime(mime)}`;
|
|
49715
|
+
}
|
|
49716
|
+
return { name, mime, url: `data:${mime};base64,${buf.toString("base64")}` };
|
|
49717
|
+
}
|
|
49614
49718
|
async function uploadFileHttp(c, serverUrl, target, fileName, mime, bytes, caption) {
|
|
49615
49719
|
const hub = serverUrl.replace(/\/$/, "");
|
|
49616
49720
|
const params = new URLSearchParams({ fileName });
|
|
@@ -49863,16 +49967,17 @@ ${fp}`);
|
|
|
49863
49967
|
"jefri_send_file",
|
|
49864
49968
|
{
|
|
49865
49969
|
title: "Send a file, PDF, or image",
|
|
49866
|
-
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`.',
|
|
49970
|
+
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.',
|
|
49867
49971
|
inputSchema: {
|
|
49868
49972
|
to: external_exports.string().describe("recipient username"),
|
|
49869
49973
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
49870
49974
|
caption: external_exports.string().optional().describe("optional text caption to send with it"),
|
|
49871
49975
|
dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
|
|
49872
|
-
|
|
49976
|
+
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
|
|
49977
|
+
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
49873
49978
|
}
|
|
49874
49979
|
},
|
|
49875
|
-
async ({ to, path: path2, caption, dataUrl, fileName }) => withClient(async (c) => {
|
|
49980
|
+
async ({ to, path: path2, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
|
|
49876
49981
|
let name, mime, url;
|
|
49877
49982
|
if (path2) {
|
|
49878
49983
|
if (!ctx.local) {
|
|
@@ -49886,15 +49991,18 @@ ${fp}`);
|
|
|
49886
49991
|
const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
|
|
49887
49992
|
const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer ${c.token}" } -ContentType "${fmime}" -InFile "${path2}"`;
|
|
49888
49993
|
return fail(
|
|
49889
|
-
`
|
|
49994
|
+
`This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
|
|
49890
49995
|
|
|
49891
|
-
\
|
|
49892
|
-
${
|
|
49996
|
+
\u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
|
|
49997
|
+
${dropLink}
|
|
49893
49998
|
|
|
49894
|
-
|
|
49895
|
-
${psCmd}
|
|
49999
|
+
(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.)
|
|
49896
50000
|
|
|
49897
|
-
(
|
|
50001
|
+
\u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
|
|
50002
|
+
\u2022 macOS / Linux:
|
|
50003
|
+
${cmd}
|
|
50004
|
+
\u2022 Windows (PowerShell):
|
|
50005
|
+
${psCmd}`
|
|
49898
50006
|
);
|
|
49899
50007
|
}
|
|
49900
50008
|
const abs = resolveLocalFile(path2);
|
|
@@ -49909,8 +50017,14 @@ ${psCmd}
|
|
|
49909
50017
|
name = fileName;
|
|
49910
50018
|
mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
|
|
49911
50019
|
url = dataUrl;
|
|
50020
|
+
} else if (fileUrl) {
|
|
50021
|
+
try {
|
|
50022
|
+
({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
|
|
50023
|
+
} catch (e2) {
|
|
50024
|
+
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
50025
|
+
}
|
|
49912
50026
|
} else {
|
|
49913
|
-
return fail("provide a local file `path
|
|
50027
|
+
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
49914
50028
|
}
|
|
49915
50029
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
49916
50030
|
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
@@ -50276,7 +50390,7 @@ ${cmd}`);
|
|
|
50276
50390
|
"jefri_add_doc",
|
|
50277
50391
|
{
|
|
50278
50392
|
title: "Add a document to my memory",
|
|
50279
|
-
description: "Upload a local file into your own document memory. Returns a command to run (reads the file locally and uploads it).",
|
|
50393
|
+
description: "Upload a local file into your own document memory. Returns a command to run (reads the file locally and uploads it). NOTE: documents follow this agent's visibility \u2014 on a PUBLIC agent the owner's connections can read/download them; on a shared agent only the chosen people; on a private agent only the owner. Warn the owner before uploading anything sensitive to a public agent.",
|
|
50280
50394
|
inputSchema: { path: external_exports.string().describe("local file path to upload") }
|
|
50281
50395
|
},
|
|
50282
50396
|
async ({ path: path2 }) => withClient(async (c) => {
|
|
@@ -50421,28 +50535,36 @@ ${cmd}`);
|
|
|
50421
50535
|
"jefri_send_group_file",
|
|
50422
50536
|
{
|
|
50423
50537
|
title: "Send a file to a group",
|
|
50424
|
-
description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector)
|
|
50538
|
+
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.",
|
|
50425
50539
|
inputSchema: {
|
|
50426
50540
|
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
50427
50541
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
50428
50542
|
caption: external_exports.string().optional().describe("optional text caption"),
|
|
50429
50543
|
dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
|
|
50430
|
-
|
|
50544
|
+
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
|
|
50545
|
+
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
50431
50546
|
}
|
|
50432
50547
|
},
|
|
50433
|
-
async ({ groupId, path: path2, caption, dataUrl, fileName }) => withClient(async (c) => {
|
|
50548
|
+
async ({ groupId, path: path2, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
|
|
50434
50549
|
let name, mime, url;
|
|
50435
50550
|
if (path2) {
|
|
50436
50551
|
if (!ctx.local) {
|
|
50437
50552
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
50553
|
+
const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
50554
|
+
const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
|
|
50438
50555
|
const fname = np3.basename(path2);
|
|
50439
50556
|
const fmime = mimeOf(fname);
|
|
50440
50557
|
const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
50441
50558
|
const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
|
|
50442
50559
|
return fail(
|
|
50443
|
-
`
|
|
50560
|
+
`This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
|
|
50561
|
+
|
|
50562
|
+
\u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
|
|
50563
|
+
${dropLink}
|
|
50444
50564
|
|
|
50445
|
-
|
|
50565
|
+
(If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
|
|
50566
|
+
|
|
50567
|
+
\u2014 Advanced (terminal): ${cmd}`
|
|
50446
50568
|
);
|
|
50447
50569
|
}
|
|
50448
50570
|
const abs = resolveLocalFile(path2);
|
|
@@ -50455,8 +50577,14 @@ ${cmd}`
|
|
|
50455
50577
|
name = fileName;
|
|
50456
50578
|
mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
|
|
50457
50579
|
url = dataUrl;
|
|
50580
|
+
} else if (fileUrl) {
|
|
50581
|
+
try {
|
|
50582
|
+
({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
|
|
50583
|
+
} catch (e2) {
|
|
50584
|
+
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
50585
|
+
}
|
|
50458
50586
|
} else {
|
|
50459
|
-
return fail("provide a local file `path
|
|
50587
|
+
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
50460
50588
|
}
|
|
50461
50589
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
50462
50590
|
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
package/dist/index.js
CHANGED
|
@@ -24861,11 +24861,15 @@ var JefriClient = class _JefriClient {
|
|
|
24861
24861
|
// a bad token must NOT trigger a reconnect storm
|
|
24862
24862
|
isAlive = true;
|
|
24863
24863
|
// heartbeat: did we get a pong since the last ping?
|
|
24864
|
+
hasConnected = false;
|
|
24865
|
+
// armed for auto-reconnect only after the first open
|
|
24866
|
+
lastPresence = "online";
|
|
24867
|
+
// re-announce this on reconnect
|
|
24864
24868
|
constructor(server2) {
|
|
24865
24869
|
this.server = server2.replace(/\/$/, "");
|
|
24866
24870
|
}
|
|
24867
24871
|
static async connect(opts = {}) {
|
|
24868
|
-
const server2 = (opts.server ?? "
|
|
24872
|
+
const server2 = (opts.server ?? "https://jefrichat.com").replace(/\/$/, "");
|
|
24869
24873
|
const client = new _JefriClient(server2);
|
|
24870
24874
|
let token = opts.token;
|
|
24871
24875
|
if (!token) {
|
|
@@ -24925,9 +24929,10 @@ var JefriClient = class _JefriClient {
|
|
|
24925
24929
|
let settled = false;
|
|
24926
24930
|
this.ws.on("open", () => {
|
|
24927
24931
|
this.reconnectAttempts = 0;
|
|
24932
|
+
this.hasConnected = true;
|
|
24928
24933
|
this.startPing();
|
|
24929
24934
|
try {
|
|
24930
|
-
this.ws.send(JSON.stringify({ type: "presence", status:
|
|
24935
|
+
this.ws.send(JSON.stringify({ type: "presence", status: this.lastPresence }));
|
|
24931
24936
|
} catch {
|
|
24932
24937
|
}
|
|
24933
24938
|
if (!settled) {
|
|
@@ -24943,7 +24948,7 @@ var JefriClient = class _JefriClient {
|
|
|
24943
24948
|
});
|
|
24944
24949
|
this.ws.on("close", () => {
|
|
24945
24950
|
this.stopPing();
|
|
24946
|
-
if (!this.closing && !this.authFailed) this.scheduleReconnect();
|
|
24951
|
+
if (this.hasConnected && !this.closing && !this.authFailed) this.scheduleReconnect();
|
|
24947
24952
|
});
|
|
24948
24953
|
this.ws.on("message", (data) => {
|
|
24949
24954
|
let ev;
|
|
@@ -25007,7 +25012,8 @@ var JefriClient = class _JefriClient {
|
|
|
25007
25012
|
}
|
|
25008
25013
|
scheduleReconnect() {
|
|
25009
25014
|
if (this.closing || this.reconnectTimer || this.authFailed) return;
|
|
25010
|
-
const
|
|
25015
|
+
const cap = Math.min(1e3 * 2 ** this.reconnectAttempts, 15e3);
|
|
25016
|
+
const delay = Math.floor(cap / 2 + Math.random() * (cap / 2));
|
|
25011
25017
|
this.reconnectAttempts++;
|
|
25012
25018
|
this.reconnectTimer = setTimeout(() => {
|
|
25013
25019
|
this.reconnectTimer = void 0;
|
|
@@ -25026,7 +25032,15 @@ var JefriClient = class _JefriClient {
|
|
|
25026
25032
|
return this;
|
|
25027
25033
|
}
|
|
25028
25034
|
emit(event, payload) {
|
|
25029
|
-
this.handlers.get(event)
|
|
25035
|
+
for (const h of this.handlers.get(event) ?? []) {
|
|
25036
|
+
try {
|
|
25037
|
+
const res = h(payload);
|
|
25038
|
+
if (res instanceof Promise)
|
|
25039
|
+
res.catch((e) => console.error(`[jefri-sdk] async event handler failed for "${event}":`, e));
|
|
25040
|
+
} catch (e) {
|
|
25041
|
+
console.error(`[jefri-sdk] event handler failed for "${event}":`, e);
|
|
25042
|
+
}
|
|
25043
|
+
}
|
|
25030
25044
|
}
|
|
25031
25045
|
send(ev) {
|
|
25032
25046
|
if (!this.ws || this.ws.readyState !== this.ws.OPEN)
|
|
@@ -25035,6 +25049,7 @@ var JefriClient = class _JefriClient {
|
|
|
25035
25049
|
}
|
|
25036
25050
|
// --- actions -------------------------------------------------------------
|
|
25037
25051
|
presence(status) {
|
|
25052
|
+
this.lastPresence = status;
|
|
25038
25053
|
this.send({ type: "presence", status });
|
|
25039
25054
|
}
|
|
25040
25055
|
message(to, content) {
|
|
@@ -25097,8 +25112,8 @@ var JefriClient = class _JefriClient {
|
|
|
25097
25112
|
addToGroup(groupId, members) {
|
|
25098
25113
|
this.send({ type: "add_to_group", groupId, members });
|
|
25099
25114
|
}
|
|
25100
|
-
history(conversationId) {
|
|
25101
|
-
this.send({ type: "history", conversationId });
|
|
25115
|
+
history(conversationId, before) {
|
|
25116
|
+
this.send({ type: "history", conversationId, ...before ? { before } : {} });
|
|
25102
25117
|
}
|
|
25103
25118
|
close() {
|
|
25104
25119
|
this.closing = true;
|
|
@@ -25109,9 +25124,11 @@ var JefriClient = class _JefriClient {
|
|
|
25109
25124
|
}
|
|
25110
25125
|
// --- REST helpers --------------------------------------------------------
|
|
25111
25126
|
async identities() {
|
|
25112
|
-
|
|
25127
|
+
const r = await fetch(`${this.server}/api/identities`, {
|
|
25113
25128
|
headers: { authorization: `Bearer ${this.token}` }
|
|
25114
|
-
})
|
|
25129
|
+
});
|
|
25130
|
+
if (!r.ok) throw new Error(`could not load identities: ${r.status} ${await r.text()}`);
|
|
25131
|
+
return r.json();
|
|
25115
25132
|
}
|
|
25116
25133
|
async groups() {
|
|
25117
25134
|
const r = await fetch(`${this.server}/api/groups`, {
|
|
@@ -25324,12 +25341,15 @@ var JefriClient = class _JefriClient {
|
|
|
25324
25341
|
return JSON.parse(dec.decode(plaintext));
|
|
25325
25342
|
}
|
|
25326
25343
|
};
|
|
25344
|
+
var serveDir = path.join(os.homedir(), ".acp", "serve");
|
|
25327
25345
|
|
|
25328
25346
|
// src/tools.ts
|
|
25329
25347
|
import fs4 from "node:fs";
|
|
25330
25348
|
import np3 from "node:path";
|
|
25331
25349
|
import cp3 from "node:child_process";
|
|
25332
25350
|
import os4 from "node:os";
|
|
25351
|
+
import nnet from "node:net";
|
|
25352
|
+
import ndns from "node:dns/promises";
|
|
25333
25353
|
|
|
25334
25354
|
// src/notify.ts
|
|
25335
25355
|
import cp from "node:child_process";
|
|
@@ -25442,9 +25462,9 @@ function notifyIncoming(m) {
|
|
|
25442
25462
|
} else if (process.platform === "win32") {
|
|
25443
25463
|
const esc2 = (x) => x.replace(/'/g, "''");
|
|
25444
25464
|
const xesc = (x) => x.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
25445
|
-
const
|
|
25446
|
-
const xml = `<toast><visual><binding template="ToastGeneric"><text>${xesc(title)}</text><text>${xesc(body)}</text></binding></visual
|
|
25447
|
-
const ps = `$ErrorActionPreference='Stop';$t='${esc2(title)}';$b='${esc2(body)}'
|
|
25465
|
+
const soundLine = sound ? `try{[System.Media.SystemSounds]::Asterisk.Play();Start-Sleep -Milliseconds 150}catch{};` : "";
|
|
25466
|
+
const xml = `<toast><visual><binding template="ToastGeneric"><text>${xesc(title)}</text><text>${xesc(body)}</text></binding></visual><audio silent="true"/></toast>`;
|
|
25467
|
+
const ps = `$ErrorActionPreference='Stop';$t='${esc2(title)}';$b='${esc2(body)}';` + soundLine + `try{[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null;[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null;$d=New-Object Windows.Data.Xml.Dom.XmlDocument;$d.LoadXml('${xml}');$toast=New-Object Windows.UI.Notifications.ToastNotification -ArgumentList $d;[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe').Show($toast);Start-Sleep -Milliseconds 400;}catch{Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$n=New-Object System.Windows.Forms.NotifyIcon;$n.Icon=[System.Drawing.SystemIcons]::Information;$n.BalloonTipTitle=$t;$n.BalloonTipText=$b;$n.Visible=$true;$n.ShowBalloonTip(6000);Start-Sleep -Milliseconds 6500;$n.Dispose();[Console]::Error.WriteLine('TOASTERR-fell-back-to-balloon: '+$_.Exception.Message);}`;
|
|
25448
25468
|
const encoded = Buffer.from(ps, "utf16le").toString("base64");
|
|
25449
25469
|
const child = cp.spawn(
|
|
25450
25470
|
"powershell",
|
|
@@ -25538,7 +25558,7 @@ var FORCE_OWNER_ONLY = true;
|
|
|
25538
25558
|
function shouldHandle(m) {
|
|
25539
25559
|
if (!cfg.enabled) return false;
|
|
25540
25560
|
const fromOwner = !!ownerName && m.sender === ownerName;
|
|
25541
|
-
const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY
|
|
25561
|
+
const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY;
|
|
25542
25562
|
if (ownerOnly && !fromOwner) return false;
|
|
25543
25563
|
if (m.senderIsBot && !cfg.replyToBots) return false;
|
|
25544
25564
|
if (!m.isGroup) return true;
|
|
@@ -25726,6 +25746,78 @@ function resolveLocalFile(rawPath) {
|
|
|
25726
25746
|
var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
25727
25747
|
var MB = (n) => (n / 1024 / 1024).toFixed(1);
|
|
25728
25748
|
var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
|
|
25749
|
+
function isPrivateAddress(ip) {
|
|
25750
|
+
if (nnet.isIPv4(ip)) {
|
|
25751
|
+
const [a, b] = ip.split(".").map(Number);
|
|
25752
|
+
if (a === 0 || a === 10 || a === 127) return true;
|
|
25753
|
+
if (a === 169 && b === 254) return true;
|
|
25754
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
25755
|
+
if (a === 192 && b === 168) return true;
|
|
25756
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
25757
|
+
return false;
|
|
25758
|
+
}
|
|
25759
|
+
const lc = ip.toLowerCase();
|
|
25760
|
+
if (lc === "::1" || lc === "::") return true;
|
|
25761
|
+
if (lc.startsWith("::ffff:")) return isPrivateAddress(lc.slice(7));
|
|
25762
|
+
if (lc.startsWith("fe80") || lc.startsWith("fc") || lc.startsWith("fd")) return true;
|
|
25763
|
+
return false;
|
|
25764
|
+
}
|
|
25765
|
+
var extFromMime = (mime) => {
|
|
25766
|
+
const sub = (mime.split("/")[1] || "").toLowerCase();
|
|
25767
|
+
if (sub === "jpeg") return "jpg";
|
|
25768
|
+
if (sub === "svg+xml") return "svg";
|
|
25769
|
+
return sub.replace(/[^a-z0-9]/g, "").slice(0, 5) || "bin";
|
|
25770
|
+
};
|
|
25771
|
+
async function assertPublicUrl(raw) {
|
|
25772
|
+
let u;
|
|
25773
|
+
try {
|
|
25774
|
+
u = new URL(raw);
|
|
25775
|
+
} catch {
|
|
25776
|
+
throw new Error(`not a valid URL: ${raw}`);
|
|
25777
|
+
}
|
|
25778
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("only http(s) URLs can be fetched");
|
|
25779
|
+
let addr;
|
|
25780
|
+
try {
|
|
25781
|
+
addr = (await ndns.lookup(u.hostname)).address;
|
|
25782
|
+
} catch {
|
|
25783
|
+
throw new Error(`can't resolve host ${u.hostname}`);
|
|
25784
|
+
}
|
|
25785
|
+
if (isPrivateAddress(addr)) throw new Error("refusing to fetch a private/internal address");
|
|
25786
|
+
return u;
|
|
25787
|
+
}
|
|
25788
|
+
async function fetchRemoteFile(fileUrl, fileNameHint, maxBytes) {
|
|
25789
|
+
let current = await assertPublicUrl(fileUrl);
|
|
25790
|
+
let res = null;
|
|
25791
|
+
for (let hop = 0; hop < 4; hop++) {
|
|
25792
|
+
const ac = new AbortController();
|
|
25793
|
+
const timer = setTimeout(() => ac.abort(), 2e4);
|
|
25794
|
+
try {
|
|
25795
|
+
res = await fetch(current, { redirect: "manual", signal: ac.signal, headers: { "user-agent": "jefrichat-mcp" } });
|
|
25796
|
+
} finally {
|
|
25797
|
+
clearTimeout(timer);
|
|
25798
|
+
}
|
|
25799
|
+
if (res.status >= 300 && res.status < 400) {
|
|
25800
|
+
const loc = res.headers.get("location");
|
|
25801
|
+
if (!loc) throw new Error(`redirect with no location (${res.status})`);
|
|
25802
|
+
current = await assertPublicUrl(new URL(loc, current).toString());
|
|
25803
|
+
continue;
|
|
25804
|
+
}
|
|
25805
|
+
break;
|
|
25806
|
+
}
|
|
25807
|
+
if (!res || !res.ok) throw new Error(`fetch failed (${res?.status ?? "no response"})`);
|
|
25808
|
+
const clen = Number(res.headers.get("content-length") || 0);
|
|
25809
|
+
if (clen && clen > maxBytes) throw new Error(`file is ${MB(clen)} MB, over the ${MB(maxBytes)} MB limit`);
|
|
25810
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
25811
|
+
if (buf.length > maxBytes) throw new Error(`file is ${MB(buf.length)} MB, over the ${MB(maxBytes)} MB limit`);
|
|
25812
|
+
if (buf.length === 0) throw new Error("the URL returned an empty file");
|
|
25813
|
+
const mime = (res.headers.get("content-type") || "").split(";")[0].trim() || mimeOf(fileNameHint || current.pathname);
|
|
25814
|
+
let name = fileNameHint || np3.basename(current.pathname) || "";
|
|
25815
|
+
if (!name || name === "/" || !name.includes(".")) {
|
|
25816
|
+
const base = name && name !== "/" ? name : "file";
|
|
25817
|
+
name = `${base}.${extFromMime(mime)}`;
|
|
25818
|
+
}
|
|
25819
|
+
return { name, mime, url: `data:${mime};base64,${buf.toString("base64")}` };
|
|
25820
|
+
}
|
|
25729
25821
|
async function uploadFileHttp(c, serverUrl, target, fileName, mime, bytes, caption) {
|
|
25730
25822
|
const hub = serverUrl.replace(/\/$/, "");
|
|
25731
25823
|
const params = new URLSearchParams({ fileName });
|
|
@@ -25978,16 +26070,17 @@ ${fp}`);
|
|
|
25978
26070
|
"jefri_send_file",
|
|
25979
26071
|
{
|
|
25980
26072
|
title: "Send a file, PDF, or image",
|
|
25981
|
-
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`.',
|
|
26073
|
+
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.',
|
|
25982
26074
|
inputSchema: {
|
|
25983
26075
|
to: external_exports.string().describe("recipient username"),
|
|
25984
26076
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
25985
26077
|
caption: external_exports.string().optional().describe("optional text caption to send with it"),
|
|
25986
26078
|
dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
|
|
25987
|
-
|
|
26079
|
+
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from (e.g. a generated image)"),
|
|
26080
|
+
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
25988
26081
|
}
|
|
25989
26082
|
},
|
|
25990
|
-
async ({ to, path: path3, caption, dataUrl, fileName }) => withClient(async (c) => {
|
|
26083
|
+
async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
|
|
25991
26084
|
let name, mime, url;
|
|
25992
26085
|
if (path3) {
|
|
25993
26086
|
if (!ctx.local) {
|
|
@@ -26001,15 +26094,18 @@ ${fp}`);
|
|
|
26001
26094
|
const cmd = `curl -s -X POST ${shq(uploadUrl)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
|
|
26002
26095
|
const psCmd = `Invoke-RestMethod -Uri "${uploadUrl}" -Method Post -Headers @{ Authorization = "Bearer ${c.token}" } -ContentType "${fmime}" -InFile "${path3}"`;
|
|
26003
26096
|
return fail(
|
|
26004
|
-
`
|
|
26097
|
+
`This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
|
|
26005
26098
|
|
|
26006
|
-
\
|
|
26007
|
-
${
|
|
26099
|
+
\u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the chat with @${to}:
|
|
26100
|
+
${dropLink}
|
|
26008
26101
|
|
|
26009
|
-
|
|
26010
|
-
${psCmd}
|
|
26102
|
+
(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.)
|
|
26011
26103
|
|
|
26012
|
-
(
|
|
26104
|
+
\u2014 Advanced (only if you prefer a terminal): run one of these to upload it directly \u2014
|
|
26105
|
+
\u2022 macOS / Linux:
|
|
26106
|
+
${cmd}
|
|
26107
|
+
\u2022 Windows (PowerShell):
|
|
26108
|
+
${psCmd}`
|
|
26013
26109
|
);
|
|
26014
26110
|
}
|
|
26015
26111
|
const abs = resolveLocalFile(path3);
|
|
@@ -26024,8 +26120,14 @@ ${psCmd}
|
|
|
26024
26120
|
name = fileName;
|
|
26025
26121
|
mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
|
|
26026
26122
|
url = dataUrl;
|
|
26123
|
+
} else if (fileUrl) {
|
|
26124
|
+
try {
|
|
26125
|
+
({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
|
|
26126
|
+
} catch (e2) {
|
|
26127
|
+
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
26128
|
+
}
|
|
26027
26129
|
} else {
|
|
26028
|
-
return fail("provide a local file `path
|
|
26130
|
+
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
26029
26131
|
}
|
|
26030
26132
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
26031
26133
|
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
@@ -26391,7 +26493,7 @@ ${cmd}`);
|
|
|
26391
26493
|
"jefri_add_doc",
|
|
26392
26494
|
{
|
|
26393
26495
|
title: "Add a document to my memory",
|
|
26394
|
-
description: "Upload a local file into your own document memory. Returns a command to run (reads the file locally and uploads it).",
|
|
26496
|
+
description: "Upload a local file into your own document memory. Returns a command to run (reads the file locally and uploads it). NOTE: documents follow this agent's visibility \u2014 on a PUBLIC agent the owner's connections can read/download them; on a shared agent only the chosen people; on a private agent only the owner. Warn the owner before uploading anything sensitive to a public agent.",
|
|
26395
26497
|
inputSchema: { path: external_exports.string().describe("local file path to upload") }
|
|
26396
26498
|
},
|
|
26397
26499
|
async ({ path: path3 }) => withClient(async (c) => {
|
|
@@ -26536,28 +26638,36 @@ ${cmd}`);
|
|
|
26536
26638
|
"jefri_send_group_file",
|
|
26537
26639
|
{
|
|
26538
26640
|
title: "Send a file to a group",
|
|
26539
|
-
description: "Send a file/image to a GROUP by its group id. Give a local file `path` (works on the local/stdio connector)
|
|
26641
|
+
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.",
|
|
26540
26642
|
inputSchema: {
|
|
26541
26643
|
groupId: external_exports.string().describe("the group's id (from jefri_groups or jefri_inbox)"),
|
|
26542
26644
|
path: external_exports.string().optional().describe("local file path, e.g. ~/Downloads/report.pdf"),
|
|
26543
26645
|
caption: external_exports.string().optional().describe("optional text caption"),
|
|
26544
26646
|
dataUrl: external_exports.string().optional().describe("alternatively, a data: URL of the file (small files only)"),
|
|
26545
|
-
|
|
26647
|
+
fileUrl: external_exports.string().optional().describe("alternatively, a public http(s) URL to fetch the file from"),
|
|
26648
|
+
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
26546
26649
|
}
|
|
26547
26650
|
},
|
|
26548
|
-
async ({ groupId, path: path3, caption, dataUrl, fileName }) => withClient(async (c) => {
|
|
26651
|
+
async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
|
|
26549
26652
|
let name, mime, url;
|
|
26550
26653
|
if (path3) {
|
|
26551
26654
|
if (!ctx.local) {
|
|
26552
26655
|
const hub = ctx.serverUrl.replace(/\/$/, "");
|
|
26656
|
+
const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
|
|
26657
|
+
const dropLink = `${web}/?group=${encodeURIComponent(groupId)}`;
|
|
26553
26658
|
const fname = np3.basename(path3);
|
|
26554
26659
|
const fmime = mimeOf(fname);
|
|
26555
26660
|
const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
|
|
26556
26661
|
const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
|
|
26557
26662
|
return fail(
|
|
26558
|
-
`
|
|
26663
|
+
`This is the cloud connector, so it can't read "${fname}" off your computer. Easiest fix:
|
|
26664
|
+
|
|
26665
|
+
\u2705 SIMPLE \u2014 open the Jefri Chat web app and DRAG "${fname}" into the group chat:
|
|
26666
|
+
${dropLink}
|
|
26559
26667
|
|
|
26560
|
-
|
|
26668
|
+
(If this is an image you GENERATED and have a public URL for, call this tool again with \`fileUrl\` instead of \`path\`.)
|
|
26669
|
+
|
|
26670
|
+
\u2014 Advanced (terminal): ${cmd}`
|
|
26561
26671
|
);
|
|
26562
26672
|
}
|
|
26563
26673
|
const abs = resolveLocalFile(path3);
|
|
@@ -26570,8 +26680,14 @@ ${cmd}`
|
|
|
26570
26680
|
name = fileName;
|
|
26571
26681
|
mime = dataUrl.match(/^data:([^;]+)/)?.[1] ?? mimeOf(fileName);
|
|
26572
26682
|
url = dataUrl;
|
|
26683
|
+
} else if (fileUrl) {
|
|
26684
|
+
try {
|
|
26685
|
+
({ name, mime, url } = await fetchRemoteFile(fileUrl, fileName, MAX_UPLOAD_BYTES));
|
|
26686
|
+
} catch (e2) {
|
|
26687
|
+
return fail(`Couldn't fetch that URL: ${e2?.message ?? e2}`);
|
|
26688
|
+
}
|
|
26573
26689
|
} else {
|
|
26574
|
-
return fail("provide a local file `path
|
|
26690
|
+
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
26575
26691
|
}
|
|
26576
26692
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
26577
26693
|
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
package/package.json
CHANGED