jefrichat-mcp 0.48.10 → 0.48.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http.js +290 -42
- package/dist/index.js +138 -24
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -32012,7 +32012,7 @@ var require_websocket = __commonJS({
|
|
|
32012
32012
|
var http2 = __require("http");
|
|
32013
32013
|
var net = __require("net");
|
|
32014
32014
|
var tls = __require("tls");
|
|
32015
|
-
var { randomBytes, createHash:
|
|
32015
|
+
var { randomBytes, createHash: createHash3 } = __require("crypto");
|
|
32016
32016
|
var { Duplex, Readable: Readable2 } = __require("stream");
|
|
32017
32017
|
var { URL: URL2 } = __require("url");
|
|
32018
32018
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -32680,7 +32680,7 @@ var require_websocket = __commonJS({
|
|
|
32680
32680
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
32681
32681
|
return;
|
|
32682
32682
|
}
|
|
32683
|
-
const digest =
|
|
32683
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
32684
32684
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
32685
32685
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
32686
32686
|
return;
|
|
@@ -33049,7 +33049,7 @@ var require_websocket_server = __commonJS({
|
|
|
33049
33049
|
var EventEmitter = __require("events");
|
|
33050
33050
|
var http2 = __require("http");
|
|
33051
33051
|
var { Duplex } = __require("stream");
|
|
33052
|
-
var { createHash:
|
|
33052
|
+
var { createHash: createHash3 } = __require("crypto");
|
|
33053
33053
|
var extension3 = require_extension();
|
|
33054
33054
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
33055
33055
|
var subprotocol2 = require_subprotocol();
|
|
@@ -33356,7 +33356,7 @@ var require_websocket_server = __commonJS({
|
|
|
33356
33356
|
);
|
|
33357
33357
|
}
|
|
33358
33358
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
33359
|
-
const digest =
|
|
33359
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
33360
33360
|
const headers = [
|
|
33361
33361
|
"HTTP/1.1 101 Switching Protocols",
|
|
33362
33362
|
"Upgrade: websocket",
|
|
@@ -60785,8 +60785,12 @@ var JefriHttpClient = class {
|
|
|
60785
60785
|
/** DECLARE a status — a user-level choice that hits every connection the
|
|
60786
60786
|
* identity holds. The returned status is the SETTLED one, which may differ
|
|
60787
60787
|
* from the request if another lease outvoted it. */
|
|
60788
|
-
|
|
60789
|
-
|
|
60788
|
+
/** `declId` (optional) makes the declaration retry-safe: a re-POST of the
|
|
60789
|
+
* same id replays — answering the CURRENT settled state — instead of
|
|
60790
|
+
* reapplying, so a stale retry can never overwrite a status another device
|
|
60791
|
+
* chose in between. Reusing an id with a DIFFERENT status is a 409. */
|
|
60792
|
+
presence(status, declId) {
|
|
60793
|
+
return this.call("POST", "/api/presence", declId ? { status, declId } : { status });
|
|
60790
60794
|
}
|
|
60791
60795
|
/** KEEPALIVE — "still here", declaring nothing. This is the call for a timer.
|
|
60792
60796
|
* Heartbeating through presence("online") instead is a declaration on a
|
|
@@ -61519,8 +61523,10 @@ async function statelessPresenceBringUp(acp) {
|
|
|
61519
61523
|
try {
|
|
61520
61524
|
const hb = await acp.heartbeat();
|
|
61521
61525
|
if (hb.state === "signedOff") await acp.presence("online");
|
|
61526
|
+
return true;
|
|
61522
61527
|
} catch (e) {
|
|
61523
|
-
log("stateless presence bring-up failed (
|
|
61528
|
+
log("stateless presence bring-up failed (the sweep retries it):", e?.message ?? e);
|
|
61529
|
+
return false;
|
|
61524
61530
|
}
|
|
61525
61531
|
}
|
|
61526
61532
|
async function statelessSweepBeat(acp, pool, onAuthRevoked) {
|
|
@@ -61536,6 +61542,71 @@ async function statelessSweepBeat(acp, pool, onAuthRevoked) {
|
|
|
61536
61542
|
}
|
|
61537
61543
|
}
|
|
61538
61544
|
|
|
61545
|
+
// src/stateless-mcp.ts
|
|
61546
|
+
var rpcError = (res, status, code3, message) => res.status(status).json({ jsonrpc: "2.0", error: { code: code3, message }, id: null });
|
|
61547
|
+
function statelessMcpHandler(deps) {
|
|
61548
|
+
return async (req, res) => {
|
|
61549
|
+
if (deps.isDraining()) {
|
|
61550
|
+
res.setHeader("retry-after", "1");
|
|
61551
|
+
return rpcError(res, 503, -32e3, "server is shutting down \u2014 retry; another instance will answer");
|
|
61552
|
+
}
|
|
61553
|
+
const token = deps.bearer(req);
|
|
61554
|
+
if (!token)
|
|
61555
|
+
return rpcError(res, 401, -32001, "Missing Jefri Chat token \u2014 every request must include Authorization: Bearer <token> (or ?token=).");
|
|
61556
|
+
if (!deps.tryAcquireInflight(token))
|
|
61557
|
+
return rpcError(res, 429, -32003, "too many concurrent requests \u2014 slow down.");
|
|
61558
|
+
try {
|
|
61559
|
+
let acp;
|
|
61560
|
+
try {
|
|
61561
|
+
({ acp } = await deps.acquire(token));
|
|
61562
|
+
} catch (e) {
|
|
61563
|
+
if (deps.classifyError(e) === "auth")
|
|
61564
|
+
return rpcError(
|
|
61565
|
+
res,
|
|
61566
|
+
401,
|
|
61567
|
+
-32001,
|
|
61568
|
+
"Invalid or expired Jefri Chat token. Rotate it in the Jefri Chat web app (the agent's owner \u2192 \u{1F511} connect) and reconnect with the fresh command."
|
|
61569
|
+
);
|
|
61570
|
+
return rpcError(res, 502, -32002, `Hub unreachable: ${e?.message ?? e}`);
|
|
61571
|
+
}
|
|
61572
|
+
const mcp = new McpServer({ name: "jefrichat", version: "0.1.0" });
|
|
61573
|
+
const deferred = [];
|
|
61574
|
+
deps.registerTools(mcp, acp, { deferUntilDelivered: (fn) => deferred.push(fn) });
|
|
61575
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
61576
|
+
res.on("finish", () => {
|
|
61577
|
+
for (const fn of deferred) {
|
|
61578
|
+
try {
|
|
61579
|
+
void Promise.resolve(fn()).catch((e) => deps.log?.("deferred post-delivery work failed:", e?.message ?? e));
|
|
61580
|
+
} catch (e) {
|
|
61581
|
+
deps.log?.("deferred post-delivery work failed:", e?.message ?? e);
|
|
61582
|
+
}
|
|
61583
|
+
}
|
|
61584
|
+
deferred.length = 0;
|
|
61585
|
+
});
|
|
61586
|
+
res.on("close", () => {
|
|
61587
|
+
try {
|
|
61588
|
+
transport.close();
|
|
61589
|
+
} catch {
|
|
61590
|
+
}
|
|
61591
|
+
try {
|
|
61592
|
+
void mcp.close();
|
|
61593
|
+
} catch {
|
|
61594
|
+
}
|
|
61595
|
+
});
|
|
61596
|
+
await mcp.connect(transport);
|
|
61597
|
+
await transport.handleRequest(req, res, req.body);
|
|
61598
|
+
} catch (e) {
|
|
61599
|
+
deps.log?.("stateless request failed:", e?.message ?? e);
|
|
61600
|
+
if (!res.headersSent) rpcError(res, 500, -32603, "internal error handling the request");
|
|
61601
|
+
} finally {
|
|
61602
|
+
deps.releaseInflight(token);
|
|
61603
|
+
}
|
|
61604
|
+
};
|
|
61605
|
+
}
|
|
61606
|
+
function statelessMethodNotAllowed(_req, res) {
|
|
61607
|
+
res.status(405).json({ jsonrpc: "2.0", error: { code: -32e3, message: "stateless server: POST only (no SSE stream, no session to terminate)" }, id: null });
|
|
61608
|
+
}
|
|
61609
|
+
|
|
61539
61610
|
// src/conn-pool.ts
|
|
61540
61611
|
function attachRef(c, id) {
|
|
61541
61612
|
if (c.closed) return false;
|
|
@@ -61574,7 +61645,8 @@ function createKeyedPool() {
|
|
|
61574
61645
|
return v;
|
|
61575
61646
|
},
|
|
61576
61647
|
has: (key) => ready.has(key),
|
|
61577
|
-
size: () => ready.size
|
|
61648
|
+
size: () => ready.size,
|
|
61649
|
+
keys: () => [...ready.keys()]
|
|
61578
61650
|
};
|
|
61579
61651
|
}
|
|
61580
61652
|
|
|
@@ -61602,6 +61674,7 @@ import cp3 from "node:child_process";
|
|
|
61602
61674
|
import os4 from "node:os";
|
|
61603
61675
|
import nnet from "node:net";
|
|
61604
61676
|
import ndns from "node:dns/promises";
|
|
61677
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
61605
61678
|
import nhttp from "node:http";
|
|
61606
61679
|
import nhttps from "node:https";
|
|
61607
61680
|
|
|
@@ -62024,6 +62097,22 @@ async function confirmWrite(c, fire) {
|
|
|
62024
62097
|
const e = await err;
|
|
62025
62098
|
return e ? { error: e.message } : {};
|
|
62026
62099
|
}
|
|
62100
|
+
function retryKey(extra, parts) {
|
|
62101
|
+
const rid = extra?.requestId;
|
|
62102
|
+
if (rid === void 0 || rid === null) return void 0;
|
|
62103
|
+
const h = extra?.requestInfo?.headers;
|
|
62104
|
+
const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
|
|
62105
|
+
if (!ua) return void 0;
|
|
62106
|
+
const bucket = Math.floor(Date.now() / 75e3);
|
|
62107
|
+
return "mcp-" + createHash2("sha256").update(JSON.stringify([bucket, ua, rid, ...parts])).digest("hex").slice(0, 40);
|
|
62108
|
+
}
|
|
62109
|
+
function declKey(extra, parts) {
|
|
62110
|
+
const rid = extra?.requestId;
|
|
62111
|
+
if (rid === void 0 || rid === null) return void 0;
|
|
62112
|
+
const h = extra?.requestInfo?.headers;
|
|
62113
|
+
const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
|
|
62114
|
+
return "d-" + createHash2("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
|
|
62115
|
+
}
|
|
62027
62116
|
async function fetchHistory(c, convId) {
|
|
62028
62117
|
if (isStateless(c)) return c.history(convId);
|
|
62029
62118
|
const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
|
|
@@ -62482,7 +62571,17 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
62482
62571
|
text: external_exports.string().describe("the message body")
|
|
62483
62572
|
}
|
|
62484
62573
|
},
|
|
62485
|
-
async ({ to, text: text5 }) => withClient(async (c) => {
|
|
62574
|
+
async ({ to, text: text5 }, extra) => withClient(async (c) => {
|
|
62575
|
+
if (isStateless(c)) {
|
|
62576
|
+
try {
|
|
62577
|
+
const res = await c.message(to, text5, retryKey(extra, ["message", to, text5]));
|
|
62578
|
+
if (res?.deduplicated)
|
|
62579
|
+
return ok3(`Already delivered to @${to} \u2014 this request matched a recent identical send and was suppressed as a retry. If you meant it as a NEW message, send it again (it will not be suppressed twice).`);
|
|
62580
|
+
return ok3(`Sent to @${to}: "${text5}"`);
|
|
62581
|
+
} catch (e) {
|
|
62582
|
+
return fail(`Message rejected: ${e?.message ?? e}`);
|
|
62583
|
+
}
|
|
62584
|
+
}
|
|
62486
62585
|
const r = await confirmWrite(c, () => c.message(to, text5));
|
|
62487
62586
|
if (r.error) return fail(`Message rejected: ${r.error}`);
|
|
62488
62587
|
return ok3(`Sent to @${to}: "${text5}"`);
|
|
@@ -62640,7 +62739,7 @@ ${fp}`);
|
|
|
62640
62739
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
62641
62740
|
}
|
|
62642
62741
|
},
|
|
62643
|
-
async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
|
|
62742
|
+
async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
62644
62743
|
let name, mime, url;
|
|
62645
62744
|
if (path3) {
|
|
62646
62745
|
if (!ctx.local) {
|
|
@@ -62694,7 +62793,23 @@ ${psCmd}
|
|
|
62694
62793
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
62695
62794
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
62696
62795
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62697
|
-
if (isStateless(c)
|
|
62796
|
+
if (isStateless(c)) {
|
|
62797
|
+
try {
|
|
62798
|
+
const res = await c.sendFile(
|
|
62799
|
+
{ to, caption },
|
|
62800
|
+
name,
|
|
62801
|
+
mime,
|
|
62802
|
+
rawBytes,
|
|
62803
|
+
retryKey(extra, ["file", to, name, createHash2("sha256").update(rawBytes).digest("hex")])
|
|
62804
|
+
);
|
|
62805
|
+
if (res?.deduplicated)
|
|
62806
|
+
return ok3(`Already delivered \u2014 ${name} matched a recent identical upload and was suppressed as a retry (round 28 #2: suppression is SAID, never a false "Sent"). If you meant a new copy, send it again.`);
|
|
62807
|
+
} catch (e2) {
|
|
62808
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
62809
|
+
}
|
|
62810
|
+
return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
|
|
62811
|
+
}
|
|
62812
|
+
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
62698
62813
|
try {
|
|
62699
62814
|
await uploadFileHttp(c, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
62700
62815
|
} catch (e2) {
|
|
@@ -62722,13 +62837,29 @@ ${psCmd}
|
|
|
62722
62837
|
caption: external_exports.string().optional()
|
|
62723
62838
|
}
|
|
62724
62839
|
},
|
|
62725
|
-
async ({ to, fileName, content: content3, caption }) => withClient(async (c) => {
|
|
62840
|
+
async ({ to, fileName, content: content3, caption }, extra) => withClient(async (c) => {
|
|
62726
62841
|
const known = mimeOf(fileName);
|
|
62727
62842
|
const mime = known === "application/octet-stream" ? "text/plain" : known;
|
|
62728
62843
|
const contentBytes = Buffer.from(content3, "utf8");
|
|
62729
62844
|
if (contentBytes.length > MAX_UPLOAD_BYTES)
|
|
62730
62845
|
return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62731
|
-
if (isStateless(c)
|
|
62846
|
+
if (isStateless(c)) {
|
|
62847
|
+
try {
|
|
62848
|
+
const res = await c.sendFile(
|
|
62849
|
+
{ to, caption },
|
|
62850
|
+
fileName,
|
|
62851
|
+
mime,
|
|
62852
|
+
contentBytes,
|
|
62853
|
+
retryKey(extra, ["file", to, fileName, createHash2("sha256").update(contentBytes).digest("hex")])
|
|
62854
|
+
);
|
|
62855
|
+
if (res?.deduplicated)
|
|
62856
|
+
return ok3(`Already delivered \u2014 ${fileName} matched a recent identical upload and was suppressed as a retry. If you meant a new copy, send it again.`);
|
|
62857
|
+
} catch (e2) {
|
|
62858
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
62859
|
+
}
|
|
62860
|
+
return ok3(`Sent \u{1F4C4} ${fileName} (${MB(contentBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
|
|
62861
|
+
}
|
|
62862
|
+
if (contentBytes.length > WS_SAFE_FILE_BYTES) {
|
|
62732
62863
|
try {
|
|
62733
62864
|
await uploadFileHttp(c, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
62734
62865
|
} catch (e2) {
|
|
@@ -63119,7 +63250,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63119
63250
|
},
|
|
63120
63251
|
async () => withClient(async (c) => {
|
|
63121
63252
|
const me = c.identity.username;
|
|
63122
|
-
const since = inboxWatermark.get(me);
|
|
63253
|
+
const since = ctx.stateless ? "stored" : inboxWatermark.get(me);
|
|
63123
63254
|
const url = `${ctx.serverUrl.replace(/\/$/, "")}/api/inbox` + (since ? `?since=${encodeURIComponent(since)}` : "");
|
|
63124
63255
|
let msgs = [];
|
|
63125
63256
|
try {
|
|
@@ -63132,9 +63263,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63132
63263
|
ctx.inbox.length = 0;
|
|
63133
63264
|
}
|
|
63134
63265
|
if (!msgs.length) return ok3("\u{1F4ED} No new messages.");
|
|
63135
|
-
msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
|
|
63136
|
-
inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
|
|
63137
|
-
saveWatermark();
|
|
63266
|
+
msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : a.id < b.id ? -1 : 1);
|
|
63138
63267
|
const groupNames = /* @__PURE__ */ new Map();
|
|
63139
63268
|
if (msgs.some((m) => m.groupId)) {
|
|
63140
63269
|
try {
|
|
@@ -63162,8 +63291,27 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63162
63291
|
const body = m.kind === "file" ? `\u{1F4CE} sent a file: ${m.fileName} \u2014 to save it call jefri_download_file(from: "${m.senderUsername}"${m.fileName ? `, fileName: "${m.fileName}"` : ""})` : m.content;
|
|
63163
63292
|
return `${at}@${m.senderUsername}${where(m)}: ${body}`;
|
|
63164
63293
|
}));
|
|
63165
|
-
|
|
63166
|
-
` + lines.join("\n")
|
|
63294
|
+
const rendered = `\u{1F4E8} ${lines.length} message(s):
|
|
63295
|
+
` + lines.join("\n");
|
|
63296
|
+
if (ctx.stateless) {
|
|
63297
|
+
const renderedIds = msgs.map((m) => m.id);
|
|
63298
|
+
const ack = async () => {
|
|
63299
|
+
try {
|
|
63300
|
+
await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
|
|
63301
|
+
method: "POST",
|
|
63302
|
+
headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
|
|
63303
|
+
body: JSON.stringify({ messageIds: renderedIds })
|
|
63304
|
+
});
|
|
63305
|
+
} catch {
|
|
63306
|
+
}
|
|
63307
|
+
};
|
|
63308
|
+
if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
|
|
63309
|
+
else await ack();
|
|
63310
|
+
} else {
|
|
63311
|
+
inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
|
|
63312
|
+
saveWatermark();
|
|
63313
|
+
}
|
|
63314
|
+
return ok3(rendered);
|
|
63167
63315
|
})
|
|
63168
63316
|
);
|
|
63169
63317
|
server2.registerTool(
|
|
@@ -63192,7 +63340,17 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63192
63340
|
text: external_exports.string().describe("the message to post to the group")
|
|
63193
63341
|
}
|
|
63194
63342
|
},
|
|
63195
|
-
async ({ groupId, text: text5 }) => withClient(async (c) => {
|
|
63343
|
+
async ({ groupId, text: text5 }, extra) => withClient(async (c) => {
|
|
63344
|
+
if (isStateless(c)) {
|
|
63345
|
+
try {
|
|
63346
|
+
const res = await c.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5]));
|
|
63347
|
+
if (res?.deduplicated)
|
|
63348
|
+
return ok3(`Already posted \u2014 this request matched a recent identical post and was suppressed as a retry. If you meant it as a NEW message, send it again.`);
|
|
63349
|
+
return ok3(`Posted to the group.`);
|
|
63350
|
+
} catch (e) {
|
|
63351
|
+
return fail(`Couldn't post to the group: ${e?.message ?? e}`);
|
|
63352
|
+
}
|
|
63353
|
+
}
|
|
63196
63354
|
const r = await confirmWrite(c, () => c.groupMessage(groupId, text5));
|
|
63197
63355
|
if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
|
|
63198
63356
|
return ok3(`Posted to the group.`);
|
|
@@ -63212,7 +63370,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63212
63370
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
63213
63371
|
}
|
|
63214
63372
|
},
|
|
63215
|
-
async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
|
|
63373
|
+
async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
63216
63374
|
let name, mime, url;
|
|
63217
63375
|
if (path3) {
|
|
63218
63376
|
if (!ctx.local) {
|
|
@@ -63256,7 +63414,23 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63256
63414
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
63257
63415
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
63258
63416
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
63259
|
-
if (isStateless(c)
|
|
63417
|
+
if (isStateless(c)) {
|
|
63418
|
+
try {
|
|
63419
|
+
const res = await c.sendFile(
|
|
63420
|
+
{ groupId, caption },
|
|
63421
|
+
name,
|
|
63422
|
+
mime,
|
|
63423
|
+
rawBytes,
|
|
63424
|
+
retryKey(extra, ["file", groupId, name, createHash2("sha256").update(rawBytes).digest("hex")])
|
|
63425
|
+
);
|
|
63426
|
+
if (res?.deduplicated)
|
|
63427
|
+
return ok3(`Already delivered \u2014 ${name} matched a recent identical upload and was suppressed as a retry. If you meant a new copy, send it again.`);
|
|
63428
|
+
} catch (e2) {
|
|
63429
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
63430
|
+
}
|
|
63431
|
+
return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to the group.`);
|
|
63432
|
+
}
|
|
63433
|
+
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
63260
63434
|
try {
|
|
63261
63435
|
await uploadFileHttp(c, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
63262
63436
|
} catch (e2) {
|
|
@@ -63341,7 +63515,19 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63341
63515
|
status: external_exports.enum(["online", "away", "busy", "coding", "reviewing", "planning", "offline"])
|
|
63342
63516
|
}
|
|
63343
63517
|
},
|
|
63344
|
-
async ({ status }) => withClient(async (c) => {
|
|
63518
|
+
async ({ status }, extra) => withClient(async (c) => {
|
|
63519
|
+
if (isStateless(c)) {
|
|
63520
|
+
const declId = declKey(extra, ["presence", status]);
|
|
63521
|
+
try {
|
|
63522
|
+
const res = await c.presence(status, declId);
|
|
63523
|
+
ctx.onPresenceDeclared?.(res.status);
|
|
63524
|
+
if (res.replayed && res.status !== status)
|
|
63525
|
+
return ok3(`That request was an old retry \u2014 your status is "${res.status}" (a newer declaration stands).`);
|
|
63526
|
+
return ok3(`Status set to ${res.status}.`);
|
|
63527
|
+
} catch (e) {
|
|
63528
|
+
return fail(`Couldn't set status: ${e?.message ?? e}`);
|
|
63529
|
+
}
|
|
63530
|
+
}
|
|
63345
63531
|
const r = await confirmWrite(c, () => c.presence(status));
|
|
63346
63532
|
if (r.error) return fail(`Couldn't set status: ${r.error}`);
|
|
63347
63533
|
return ok3(`Status set to ${status}.`);
|
|
@@ -63541,6 +63727,7 @@ var sessions = /* @__PURE__ */ new Map();
|
|
|
63541
63727
|
var HOST_FRESH_MS = Number(process.env.MCP_HOST_FRESH_MS ?? 10 * 60 * 1e3);
|
|
63542
63728
|
var hubPool = createKeyedPool();
|
|
63543
63729
|
var SESSION_TTL_MS = Number(process.env.MCP_SESSION_TTL_MS ?? 4 * 60 * 60 * 1e3);
|
|
63730
|
+
var STATELESS_POOL_IDLE_MS = Number(process.env.MCP_STATELESS_POOL_IDLE_MS ?? 10 * 60 * 1e3);
|
|
63544
63731
|
var MAX_SESSIONS = Number(process.env.MCP_MAX_SESSIONS ?? 1500);
|
|
63545
63732
|
var MAX_SESSIONS_PER_IDENTITY = Number(process.env.MCP_MAX_SESSIONS_PER_IDENTITY ?? 20);
|
|
63546
63733
|
var INIT_WINDOW_MS = 6e4;
|
|
@@ -63601,8 +63788,8 @@ async function acquirePooledConn(token, poolKey) {
|
|
|
63601
63788
|
if (STATELESS) {
|
|
63602
63789
|
const acp2 = new JefriHttpClient({ server: HUB, token });
|
|
63603
63790
|
await acp2.whoami();
|
|
63604
|
-
await statelessPresenceBringUp(acp2);
|
|
63605
|
-
return { acp: acp2, inbox: [], refs: /* @__PURE__ */ new Set(), closed: false };
|
|
63791
|
+
const broughtUp = await statelessPresenceBringUp(acp2);
|
|
63792
|
+
return { acp: acp2, inbox: [], refs: /* @__PURE__ */ new Set(), closed: false, lastSeen: Date.now(), needsBringUp: !broughtUp };
|
|
63606
63793
|
}
|
|
63607
63794
|
const acp = await JefriClient.connect({ server: HUB, token, status: "online" });
|
|
63608
63795
|
const inbox = [];
|
|
@@ -63625,15 +63812,30 @@ setInterval(() => {
|
|
|
63625
63812
|
for (const [sid, s] of sessions) {
|
|
63626
63813
|
if (now - s.lastActive > SESSION_TTL_MS) closeSession(sid, "idle");
|
|
63627
63814
|
}
|
|
63628
|
-
|
|
63629
|
-
|
|
63630
|
-
|
|
63631
|
-
|
|
63632
|
-
|
|
63633
|
-
|
|
63634
|
-
|
|
63635
|
-
|
|
63636
|
-
|
|
63815
|
+
if (STATELESS) {
|
|
63816
|
+
for (const key of hubPool.keys()) {
|
|
63817
|
+
const pooled = hubPool.get(key);
|
|
63818
|
+
if (!pooled) continue;
|
|
63819
|
+
if (now - (pooled.lastSeen ?? 0) > STATELESS_POOL_IDLE_MS) {
|
|
63820
|
+
hubPool.drop(key);
|
|
63821
|
+
continue;
|
|
63822
|
+
}
|
|
63823
|
+
if (pooled.needsBringUp) {
|
|
63824
|
+
void statelessPresenceBringUp(pooled.acp).then((ok4) => {
|
|
63825
|
+
if (ok4) pooled.needsBringUp = false;
|
|
63826
|
+
});
|
|
63827
|
+
} else {
|
|
63828
|
+
void statelessSweepBeat(pooled.acp, pooled, () => closePool(key, "auth revoked"));
|
|
63829
|
+
}
|
|
63830
|
+
}
|
|
63831
|
+
} else {
|
|
63832
|
+
const seenPools = /* @__PURE__ */ new Set();
|
|
63833
|
+
for (const s of sessions.values()) {
|
|
63834
|
+
if (seenPools.has(s.poolKey)) continue;
|
|
63835
|
+
seenPools.add(s.poolKey);
|
|
63836
|
+
const pooled = hubPool.get(s.poolKey);
|
|
63837
|
+
if (pooled) announcePoolHosts(s.poolKey, pooled);
|
|
63838
|
+
}
|
|
63637
63839
|
}
|
|
63638
63840
|
for (const [ip, e] of initHits) {
|
|
63639
63841
|
if (now > e.resetAt) initHits.delete(ip);
|
|
@@ -63697,7 +63899,7 @@ var health = (_req, res) => res.status(draining ? 503 : 200).json({
|
|
|
63697
63899
|
});
|
|
63698
63900
|
app.get("/health", health);
|
|
63699
63901
|
app.get("/mcp/health", health);
|
|
63700
|
-
|
|
63902
|
+
var statefulPost = async (req, res) => {
|
|
63701
63903
|
const sessionId = req.headers["mcp-session-id"];
|
|
63702
63904
|
if (sessionId && sessions.has(sessionId)) {
|
|
63703
63905
|
const s = sessions.get(sessionId);
|
|
@@ -63777,10 +63979,6 @@ app.post("/mcp", async (req, res) => {
|
|
|
63777
63979
|
res.status(502).json({ jsonrpc: "2.0", error: { code: -32002, message: `Hub unreachable: ${e?.message ?? e}` }, id: null });
|
|
63778
63980
|
return;
|
|
63779
63981
|
}
|
|
63780
|
-
if (STATELESS && pooled.hbSignedOff) {
|
|
63781
|
-
pooled.hbSignedOff = false;
|
|
63782
|
-
void statelessPresenceBringUp(pooled.acp);
|
|
63783
|
-
}
|
|
63784
63982
|
const acp = pooled.acp;
|
|
63785
63983
|
if (!acp.identity) {
|
|
63786
63984
|
closePool(poolKey, "no identity");
|
|
@@ -63864,7 +64062,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
63864
64062
|
error: { code: -32001, message: "Session expired \u2014 reinitialize (your token is still valid)" },
|
|
63865
64063
|
id: null
|
|
63866
64064
|
});
|
|
63867
|
-
}
|
|
64065
|
+
};
|
|
63868
64066
|
var bySession = async (req, res) => {
|
|
63869
64067
|
const sessionId = req.headers["mcp-session-id"];
|
|
63870
64068
|
const s = sessionId ? sessions.get(sessionId) : void 0;
|
|
@@ -63882,8 +64080,58 @@ var bySession = async (req, res) => {
|
|
|
63882
64080
|
touch(sessionId);
|
|
63883
64081
|
await s.transport.handleRequest(req, res);
|
|
63884
64082
|
};
|
|
63885
|
-
|
|
63886
|
-
app.
|
|
64083
|
+
if (STATELESS) {
|
|
64084
|
+
app.post(
|
|
64085
|
+
"/mcp",
|
|
64086
|
+
statelessMcpHandler({
|
|
64087
|
+
bearer,
|
|
64088
|
+
acquire: async (token) => {
|
|
64089
|
+
const poolKey = sha256(token).toString("hex");
|
|
64090
|
+
const pooled = await acquirePooledConn(token, poolKey);
|
|
64091
|
+
pooled.lastSeen = Date.now();
|
|
64092
|
+
return pooled;
|
|
64093
|
+
},
|
|
64094
|
+
// registerTools below closes over the POOLED entry via acp — the presence
|
|
64095
|
+
// resume/stop signal needs the pool, so it is looked up by identity of
|
|
64096
|
+
// the shared client (one pool owns exactly one client instance).
|
|
64097
|
+
classifyError: (e) => e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED" || e instanceof JefriHttpError && (e.status === 401 || e.status === 403) ? "auth" : "hub",
|
|
64098
|
+
registerTools: (mcp, acp, hooks) => registerAcpTools(mcp, {
|
|
64099
|
+
ensureClient: async () => acp,
|
|
64100
|
+
inbox: [],
|
|
64101
|
+
serverUrl: HUB,
|
|
64102
|
+
local: false,
|
|
64103
|
+
stateless: true,
|
|
64104
|
+
deferUntilDelivered: hooks.deferUntilDelivered,
|
|
64105
|
+
// Round 26 #4a: a successful declaration RESUMES heartbeats (the
|
|
64106
|
+
// user came back — the hub cleared its sign-off marker, the pool
|
|
64107
|
+
// must clear its own), and a sign-off STOPS them proactively.
|
|
64108
|
+
onPresenceDeclared: (status) => {
|
|
64109
|
+
for (const key of hubPool.keys()) {
|
|
64110
|
+
const pooled = hubPool.get(key);
|
|
64111
|
+
if (pooled && pooled.acp === acp) {
|
|
64112
|
+
if (status === "offline") pooled.hbSignedOff = true;
|
|
64113
|
+
else {
|
|
64114
|
+
pooled.hbSignedOff = false;
|
|
64115
|
+
pooled.needsBringUp = false;
|
|
64116
|
+
}
|
|
64117
|
+
break;
|
|
64118
|
+
}
|
|
64119
|
+
}
|
|
64120
|
+
}
|
|
64121
|
+
}),
|
|
64122
|
+
isDraining: () => draining,
|
|
64123
|
+
tryAcquireInflight: (token) => tryAcquire(tokenInflight, sha256(token).toString("hex"), MAX_INFLIGHT_PER_TOKEN),
|
|
64124
|
+
releaseInflight: (token) => release(tokenInflight, sha256(token).toString("hex")),
|
|
64125
|
+
log: log2
|
|
64126
|
+
})
|
|
64127
|
+
);
|
|
64128
|
+
app.get("/mcp", statelessMethodNotAllowed);
|
|
64129
|
+
app.delete("/mcp", statelessMethodNotAllowed);
|
|
64130
|
+
} else {
|
|
64131
|
+
app.post("/mcp", statefulPost);
|
|
64132
|
+
app.get("/mcp", bySession);
|
|
64133
|
+
app.delete("/mcp", bySession);
|
|
64134
|
+
}
|
|
63887
64135
|
var server = http.createServer(app).listen(PORT, () => {
|
|
63888
64136
|
console.error(`
|
|
63889
64137
|
\u{1F310} Jefri Chat remote MCP (HTTP) on http://localhost:${PORT}/mcp`);
|
package/dist/index.js
CHANGED
|
@@ -9152,7 +9152,7 @@ var require_websocket = __commonJS({
|
|
|
9152
9152
|
var http = __require("http");
|
|
9153
9153
|
var net4 = __require("net");
|
|
9154
9154
|
var tls = __require("tls");
|
|
9155
|
-
var { randomBytes: randomBytes3, createHash:
|
|
9155
|
+
var { randomBytes: randomBytes3, createHash: createHash4 } = __require("crypto");
|
|
9156
9156
|
var { Duplex, Readable } = __require("stream");
|
|
9157
9157
|
var { URL: URL2 } = __require("url");
|
|
9158
9158
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -9820,7 +9820,7 @@ var require_websocket = __commonJS({
|
|
|
9820
9820
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
9821
9821
|
return;
|
|
9822
9822
|
}
|
|
9823
|
-
const digest =
|
|
9823
|
+
const digest = createHash4("sha1").update(key + GUID).digest("base64");
|
|
9824
9824
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
9825
9825
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
9826
9826
|
return;
|
|
@@ -10189,7 +10189,7 @@ var require_websocket_server = __commonJS({
|
|
|
10189
10189
|
var EventEmitter = __require("events");
|
|
10190
10190
|
var http = __require("http");
|
|
10191
10191
|
var { Duplex } = __require("stream");
|
|
10192
|
-
var { createHash:
|
|
10192
|
+
var { createHash: createHash4 } = __require("crypto");
|
|
10193
10193
|
var extension3 = require_extension();
|
|
10194
10194
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
10195
10195
|
var subprotocol2 = require_subprotocol();
|
|
@@ -10496,7 +10496,7 @@ var require_websocket_server = __commonJS({
|
|
|
10496
10496
|
);
|
|
10497
10497
|
}
|
|
10498
10498
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
10499
|
-
const digest =
|
|
10499
|
+
const digest = createHash4("sha1").update(key + GUID).digest("base64");
|
|
10500
10500
|
const headers = [
|
|
10501
10501
|
"HTTP/1.1 101 Switching Protocols",
|
|
10502
10502
|
"Upgrade: websocket",
|
|
@@ -10891,7 +10891,7 @@ import fs6 from "node:fs";
|
|
|
10891
10891
|
import net from "node:net";
|
|
10892
10892
|
import os5 from "node:os";
|
|
10893
10893
|
import np5 from "node:path";
|
|
10894
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
10894
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
10895
10895
|
function parseInstanceName(name) {
|
|
10896
10896
|
if (name.startsWith(".")) return null;
|
|
10897
10897
|
const lastDot = name.lastIndexOf(".");
|
|
@@ -10922,7 +10922,7 @@ function safeUserParts(u) {
|
|
|
10922
10922
|
const raw = u || "unknown";
|
|
10923
10923
|
return {
|
|
10924
10924
|
base: raw.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 40) || "agent",
|
|
10925
|
-
hash:
|
|
10925
|
+
hash: createHash2("sha1").update(raw).digest("hex").slice(0, 8)
|
|
10926
10926
|
};
|
|
10927
10927
|
}
|
|
10928
10928
|
function safeUser2(u) {
|
|
@@ -10935,7 +10935,7 @@ function socketPathFor(username, home = os5.homedir(), platform = process.platfo
|
|
|
10935
10935
|
const preferred = np5.join(home, ".jefri", "sock", `${safe}.sock`);
|
|
10936
10936
|
if (Buffer.byteLength(preferred) <= 100) return preferred;
|
|
10937
10937
|
const uid = typeof process.getuid === "function" ? String(process.getuid()) : process.env.USERNAME ?? "";
|
|
10938
|
-
const scope =
|
|
10938
|
+
const scope = createHash2("sha1").update(`${home}\0${uid}`).digest("hex").slice(0, 8);
|
|
10939
10939
|
const { base, hash } = safeUserParts(username);
|
|
10940
10940
|
return np5.join(tmp, `jefri-${base.slice(0, 16)}-${hash}-${scope}.sock`);
|
|
10941
10941
|
}
|
|
@@ -12118,7 +12118,7 @@ __export(run_exports, {
|
|
|
12118
12118
|
tmuxRunArgv: () => tmuxRunArgv
|
|
12119
12119
|
});
|
|
12120
12120
|
import cp9 from "node:child_process";
|
|
12121
|
-
import { createHash as
|
|
12121
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
12122
12122
|
import readline2 from "node:readline";
|
|
12123
12123
|
function tmuxRunArgv(agentCmd, sessionName, cwd, envPairs = []) {
|
|
12124
12124
|
const env = envPairs.flatMap((e) => ["-e", e]);
|
|
@@ -12127,7 +12127,7 @@ function tmuxRunArgv(agentCmd, sessionName, cwd, envPairs = []) {
|
|
|
12127
12127
|
function sessionNameFor(agentCmd, cwd = process.cwd(), experimental = false) {
|
|
12128
12128
|
const exe = (agentCmd[0] || "agent").replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 32);
|
|
12129
12129
|
const key = agentCmd.join("\0") + "\0" + cwd;
|
|
12130
|
-
const h =
|
|
12130
|
+
const h = createHash3("sha1").update(key + (experimental ? " x" : "")).digest("hex").slice(0, 8);
|
|
12131
12131
|
return `jefri-${exe}-${h}`;
|
|
12132
12132
|
}
|
|
12133
12133
|
function isShellExe(cmd) {
|
|
@@ -39391,6 +39391,7 @@ import cp3 from "node:child_process";
|
|
|
39391
39391
|
import os4 from "node:os";
|
|
39392
39392
|
import nnet from "node:net";
|
|
39393
39393
|
import ndns from "node:dns/promises";
|
|
39394
|
+
import { createHash } from "node:crypto";
|
|
39394
39395
|
import nhttp from "node:http";
|
|
39395
39396
|
import nhttps from "node:https";
|
|
39396
39397
|
|
|
@@ -40556,6 +40557,22 @@ async function confirmWrite(c2, fire) {
|
|
|
40556
40557
|
const e = await err;
|
|
40557
40558
|
return e ? { error: e.message } : {};
|
|
40558
40559
|
}
|
|
40560
|
+
function retryKey(extra, parts) {
|
|
40561
|
+
const rid = extra?.requestId;
|
|
40562
|
+
if (rid === void 0 || rid === null) return void 0;
|
|
40563
|
+
const h = extra?.requestInfo?.headers;
|
|
40564
|
+
const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
|
|
40565
|
+
if (!ua) return void 0;
|
|
40566
|
+
const bucket = Math.floor(Date.now() / 75e3);
|
|
40567
|
+
return "mcp-" + createHash("sha256").update(JSON.stringify([bucket, ua, rid, ...parts])).digest("hex").slice(0, 40);
|
|
40568
|
+
}
|
|
40569
|
+
function declKey(extra, parts) {
|
|
40570
|
+
const rid = extra?.requestId;
|
|
40571
|
+
if (rid === void 0 || rid === null) return void 0;
|
|
40572
|
+
const h = extra?.requestInfo?.headers;
|
|
40573
|
+
const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
|
|
40574
|
+
return "d-" + createHash("sha256").update(JSON.stringify([ua, rid, ...parts])).digest("hex").slice(0, 40);
|
|
40575
|
+
}
|
|
40559
40576
|
async function fetchHistory2(c2, convId) {
|
|
40560
40577
|
if (isStateless(c2)) return c2.history(convId);
|
|
40561
40578
|
const p = waitFor(c2, "history", (e) => e.conversationId === convId, 4e3);
|
|
@@ -41017,7 +41034,17 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
41017
41034
|
text: external_exports.string().describe("the message body")
|
|
41018
41035
|
}
|
|
41019
41036
|
},
|
|
41020
|
-
async ({ to, text: text5 }) => withClient(async (c2) => {
|
|
41037
|
+
async ({ to, text: text5 }, extra) => withClient(async (c2) => {
|
|
41038
|
+
if (isStateless(c2)) {
|
|
41039
|
+
try {
|
|
41040
|
+
const res = await c2.message(to, text5, retryKey(extra, ["message", to, text5]));
|
|
41041
|
+
if (res?.deduplicated)
|
|
41042
|
+
return ok3(`Already delivered to @${to} \u2014 this request matched a recent identical send and was suppressed as a retry. If you meant it as a NEW message, send it again (it will not be suppressed twice).`);
|
|
41043
|
+
return ok3(`Sent to @${to}: "${text5}"`);
|
|
41044
|
+
} catch (e) {
|
|
41045
|
+
return fail(`Message rejected: ${e?.message ?? e}`);
|
|
41046
|
+
}
|
|
41047
|
+
}
|
|
41021
41048
|
const r = await confirmWrite(c2, () => c2.message(to, text5));
|
|
41022
41049
|
if (r.error) return fail(`Message rejected: ${r.error}`);
|
|
41023
41050
|
return ok3(`Sent to @${to}: "${text5}"`);
|
|
@@ -41175,7 +41202,7 @@ ${fp}`);
|
|
|
41175
41202
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
41176
41203
|
}
|
|
41177
41204
|
},
|
|
41178
|
-
async ({ to, path: path4, caption, dataUrl, fileUrl, fileName }) => withClient(async (c2) => {
|
|
41205
|
+
async ({ to, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
|
|
41179
41206
|
let name, mime, url;
|
|
41180
41207
|
if (path4) {
|
|
41181
41208
|
if (!ctx.local) {
|
|
@@ -41229,7 +41256,23 @@ ${psCmd}
|
|
|
41229
41256
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
41230
41257
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
41231
41258
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
41232
|
-
if (isStateless(c2)
|
|
41259
|
+
if (isStateless(c2)) {
|
|
41260
|
+
try {
|
|
41261
|
+
const res = await c2.sendFile(
|
|
41262
|
+
{ to, caption },
|
|
41263
|
+
name,
|
|
41264
|
+
mime,
|
|
41265
|
+
rawBytes,
|
|
41266
|
+
retryKey(extra, ["file", to, name, createHash("sha256").update(rawBytes).digest("hex")])
|
|
41267
|
+
);
|
|
41268
|
+
if (res?.deduplicated)
|
|
41269
|
+
return ok3(`Already delivered \u2014 ${name} matched a recent identical upload and was suppressed as a retry (round 28 #2: suppression is SAID, never a false "Sent"). If you meant a new copy, send it again.`);
|
|
41270
|
+
} catch (e2) {
|
|
41271
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
41272
|
+
}
|
|
41273
|
+
return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
|
|
41274
|
+
}
|
|
41275
|
+
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
41233
41276
|
try {
|
|
41234
41277
|
await uploadFileHttp(c2, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
41235
41278
|
} catch (e2) {
|
|
@@ -41257,13 +41300,29 @@ ${psCmd}
|
|
|
41257
41300
|
caption: external_exports.string().optional()
|
|
41258
41301
|
}
|
|
41259
41302
|
},
|
|
41260
|
-
async ({ to, fileName, content: content3, caption }) => withClient(async (c2) => {
|
|
41303
|
+
async ({ to, fileName, content: content3, caption }, extra) => withClient(async (c2) => {
|
|
41261
41304
|
const known = mimeOf(fileName);
|
|
41262
41305
|
const mime = known === "application/octet-stream" ? "text/plain" : known;
|
|
41263
41306
|
const contentBytes = Buffer.from(content3, "utf8");
|
|
41264
41307
|
if (contentBytes.length > MAX_UPLOAD_BYTES)
|
|
41265
41308
|
return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
41266
|
-
if (isStateless(c2)
|
|
41309
|
+
if (isStateless(c2)) {
|
|
41310
|
+
try {
|
|
41311
|
+
const res = await c2.sendFile(
|
|
41312
|
+
{ to, caption },
|
|
41313
|
+
fileName,
|
|
41314
|
+
mime,
|
|
41315
|
+
contentBytes,
|
|
41316
|
+
retryKey(extra, ["file", to, fileName, createHash("sha256").update(contentBytes).digest("hex")])
|
|
41317
|
+
);
|
|
41318
|
+
if (res?.deduplicated)
|
|
41319
|
+
return ok3(`Already delivered \u2014 ${fileName} matched a recent identical upload and was suppressed as a retry. If you meant a new copy, send it again.`);
|
|
41320
|
+
} catch (e2) {
|
|
41321
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
41322
|
+
}
|
|
41323
|
+
return ok3(`Sent \u{1F4C4} ${fileName} (${MB(contentBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
|
|
41324
|
+
}
|
|
41325
|
+
if (contentBytes.length > WS_SAFE_FILE_BYTES) {
|
|
41267
41326
|
try {
|
|
41268
41327
|
await uploadFileHttp(c2, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
41269
41328
|
} catch (e2) {
|
|
@@ -41654,7 +41713,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41654
41713
|
},
|
|
41655
41714
|
async () => withClient(async (c2) => {
|
|
41656
41715
|
const me = c2.identity.username;
|
|
41657
|
-
const since = inboxWatermark.get(me);
|
|
41716
|
+
const since = ctx.stateless ? "stored" : inboxWatermark.get(me);
|
|
41658
41717
|
const url = `${ctx.serverUrl.replace(/\/$/, "")}/api/inbox` + (since ? `?since=${encodeURIComponent(since)}` : "");
|
|
41659
41718
|
let msgs = [];
|
|
41660
41719
|
try {
|
|
@@ -41667,9 +41726,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41667
41726
|
ctx.inbox.length = 0;
|
|
41668
41727
|
}
|
|
41669
41728
|
if (!msgs.length) return ok3("\u{1F4ED} No new messages.");
|
|
41670
|
-
msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
|
|
41671
|
-
inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
|
|
41672
|
-
saveWatermark();
|
|
41729
|
+
msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : a.id < b.id ? -1 : 1);
|
|
41673
41730
|
const groupNames = /* @__PURE__ */ new Map();
|
|
41674
41731
|
if (msgs.some((m) => m.groupId)) {
|
|
41675
41732
|
try {
|
|
@@ -41697,8 +41754,27 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41697
41754
|
const body = m.kind === "file" ? `\u{1F4CE} sent a file: ${m.fileName} \u2014 to save it call jefri_download_file(from: "${m.senderUsername}"${m.fileName ? `, fileName: "${m.fileName}"` : ""})` : m.content;
|
|
41698
41755
|
return `${at}@${m.senderUsername}${where(m)}: ${body}`;
|
|
41699
41756
|
}));
|
|
41700
|
-
|
|
41701
|
-
` + lines.join("\n")
|
|
41757
|
+
const rendered = `\u{1F4E8} ${lines.length} message(s):
|
|
41758
|
+
` + lines.join("\n");
|
|
41759
|
+
if (ctx.stateless) {
|
|
41760
|
+
const renderedIds = msgs.map((m) => m.id);
|
|
41761
|
+
const ack = async () => {
|
|
41762
|
+
try {
|
|
41763
|
+
await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
|
|
41764
|
+
method: "POST",
|
|
41765
|
+
headers: { authorization: `Bearer ${c2.token}`, "content-type": "application/json" },
|
|
41766
|
+
body: JSON.stringify({ messageIds: renderedIds })
|
|
41767
|
+
});
|
|
41768
|
+
} catch {
|
|
41769
|
+
}
|
|
41770
|
+
};
|
|
41771
|
+
if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
|
|
41772
|
+
else await ack();
|
|
41773
|
+
} else {
|
|
41774
|
+
inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
|
|
41775
|
+
saveWatermark();
|
|
41776
|
+
}
|
|
41777
|
+
return ok3(rendered);
|
|
41702
41778
|
})
|
|
41703
41779
|
);
|
|
41704
41780
|
server2.registerTool(
|
|
@@ -41727,7 +41803,17 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41727
41803
|
text: external_exports.string().describe("the message to post to the group")
|
|
41728
41804
|
}
|
|
41729
41805
|
},
|
|
41730
|
-
async ({ groupId, text: text5 }) => withClient(async (c2) => {
|
|
41806
|
+
async ({ groupId, text: text5 }, extra) => withClient(async (c2) => {
|
|
41807
|
+
if (isStateless(c2)) {
|
|
41808
|
+
try {
|
|
41809
|
+
const res = await c2.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5]));
|
|
41810
|
+
if (res?.deduplicated)
|
|
41811
|
+
return ok3(`Already posted \u2014 this request matched a recent identical post and was suppressed as a retry. If you meant it as a NEW message, send it again.`);
|
|
41812
|
+
return ok3(`Posted to the group.`);
|
|
41813
|
+
} catch (e) {
|
|
41814
|
+
return fail(`Couldn't post to the group: ${e?.message ?? e}`);
|
|
41815
|
+
}
|
|
41816
|
+
}
|
|
41731
41817
|
const r = await confirmWrite(c2, () => c2.groupMessage(groupId, text5));
|
|
41732
41818
|
if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
|
|
41733
41819
|
return ok3(`Posted to the group.`);
|
|
@@ -41747,7 +41833,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41747
41833
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
41748
41834
|
}
|
|
41749
41835
|
},
|
|
41750
|
-
async ({ groupId, path: path4, caption, dataUrl, fileUrl, fileName }) => withClient(async (c2) => {
|
|
41836
|
+
async ({ groupId, path: path4, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c2) => {
|
|
41751
41837
|
let name, mime, url;
|
|
41752
41838
|
if (path4) {
|
|
41753
41839
|
if (!ctx.local) {
|
|
@@ -41791,7 +41877,23 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41791
41877
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
41792
41878
|
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
41793
41879
|
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
41794
|
-
if (isStateless(c2)
|
|
41880
|
+
if (isStateless(c2)) {
|
|
41881
|
+
try {
|
|
41882
|
+
const res = await c2.sendFile(
|
|
41883
|
+
{ groupId, caption },
|
|
41884
|
+
name,
|
|
41885
|
+
mime,
|
|
41886
|
+
rawBytes,
|
|
41887
|
+
retryKey(extra, ["file", groupId, name, createHash("sha256").update(rawBytes).digest("hex")])
|
|
41888
|
+
);
|
|
41889
|
+
if (res?.deduplicated)
|
|
41890
|
+
return ok3(`Already delivered \u2014 ${name} matched a recent identical upload and was suppressed as a retry. If you meant a new copy, send it again.`);
|
|
41891
|
+
} catch (e2) {
|
|
41892
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
41893
|
+
}
|
|
41894
|
+
return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to the group.`);
|
|
41895
|
+
}
|
|
41896
|
+
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
41795
41897
|
try {
|
|
41796
41898
|
await uploadFileHttp(c2, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
41797
41899
|
} catch (e2) {
|
|
@@ -41876,7 +41978,19 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41876
41978
|
status: external_exports.enum(["online", "away", "busy", "coding", "reviewing", "planning", "offline"])
|
|
41877
41979
|
}
|
|
41878
41980
|
},
|
|
41879
|
-
async ({ status }) => withClient(async (c2) => {
|
|
41981
|
+
async ({ status }, extra) => withClient(async (c2) => {
|
|
41982
|
+
if (isStateless(c2)) {
|
|
41983
|
+
const declId = declKey(extra, ["presence", status]);
|
|
41984
|
+
try {
|
|
41985
|
+
const res = await c2.presence(status, declId);
|
|
41986
|
+
ctx.onPresenceDeclared?.(res.status);
|
|
41987
|
+
if (res.replayed && res.status !== status)
|
|
41988
|
+
return ok3(`That request was an old retry \u2014 your status is "${res.status}" (a newer declaration stands).`);
|
|
41989
|
+
return ok3(`Status set to ${res.status}.`);
|
|
41990
|
+
} catch (e) {
|
|
41991
|
+
return fail(`Couldn't set status: ${e?.message ?? e}`);
|
|
41992
|
+
}
|
|
41993
|
+
}
|
|
41880
41994
|
const r = await confirmWrite(c2, () => c2.presence(status));
|
|
41881
41995
|
if (r.error) return fail(`Couldn't set status: ${r.error}`);
|
|
41882
41996
|
return ok3(`Status set to ${status}.`);
|
package/package.json
CHANGED