jefrichat-mcp 0.48.9 → 0.48.11
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 +592 -89
- package/dist/index.js +163 -61
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -98,7 +98,7 @@ var require_depd = __commonJS({
|
|
|
98
98
|
var site = callSiteLocation(stack[1]);
|
|
99
99
|
var file = site[0];
|
|
100
100
|
function deprecate(message) {
|
|
101
|
-
|
|
101
|
+
log3.call(deprecate, message);
|
|
102
102
|
}
|
|
103
103
|
deprecate._file = file;
|
|
104
104
|
deprecate._ignored = isignored(namespace);
|
|
@@ -127,7 +127,7 @@ var require_depd = __commonJS({
|
|
|
127
127
|
var str = process.env.TRACE_DEPRECATION || "";
|
|
128
128
|
return containsNamespace(str, namespace);
|
|
129
129
|
}
|
|
130
|
-
function
|
|
130
|
+
function log3(message, site) {
|
|
131
131
|
var haslisteners = eehaslisteners(process, "deprecation");
|
|
132
132
|
if (!haslisteners && this._ignored) {
|
|
133
133
|
return;
|
|
@@ -267,7 +267,7 @@ var require_depd = __commonJS({
|
|
|
267
267
|
"message",
|
|
268
268
|
"site",
|
|
269
269
|
'"use strict"\nreturn function (' + args + ") {log.call(deprecate, message, site)\nreturn fn.apply(this, arguments)\n}"
|
|
270
|
-
)(fn,
|
|
270
|
+
)(fn, log3, this, message, site);
|
|
271
271
|
return deprecatedfn;
|
|
272
272
|
}
|
|
273
273
|
function wrapproperty(obj, prop, message) {
|
|
@@ -292,13 +292,13 @@ var require_depd = __commonJS({
|
|
|
292
292
|
var set = descriptor.set;
|
|
293
293
|
if (typeof get === "function") {
|
|
294
294
|
descriptor.get = function getter() {
|
|
295
|
-
|
|
295
|
+
log3.call(deprecate, message, site);
|
|
296
296
|
return get.apply(this, arguments);
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
if (typeof set === "function") {
|
|
300
300
|
descriptor.set = function setter() {
|
|
301
|
-
|
|
301
|
+
log3.call(deprecate, message, site);
|
|
302
302
|
return set.apply(this, arguments);
|
|
303
303
|
};
|
|
304
304
|
}
|
|
@@ -1135,7 +1135,7 @@ var require_debug = __commonJS({
|
|
|
1135
1135
|
var require_browser = __commonJS({
|
|
1136
1136
|
"../../node_modules/.pnpm/debug@2.6.9/node_modules/debug/src/browser.js"(exports, module) {
|
|
1137
1137
|
exports = module.exports = require_debug();
|
|
1138
|
-
exports.log =
|
|
1138
|
+
exports.log = log3;
|
|
1139
1139
|
exports.formatArgs = formatArgs;
|
|
1140
1140
|
exports.save = save;
|
|
1141
1141
|
exports.load = load;
|
|
@@ -1183,7 +1183,7 @@ var require_browser = __commonJS({
|
|
|
1183
1183
|
});
|
|
1184
1184
|
args.splice(lastC, 0, c);
|
|
1185
1185
|
}
|
|
1186
|
-
function
|
|
1186
|
+
function log3() {
|
|
1187
1187
|
return "object" === typeof console && console.log && Function.prototype.apply.call(console.log, console, arguments);
|
|
1188
1188
|
}
|
|
1189
1189
|
function save(namespaces) {
|
|
@@ -1224,7 +1224,7 @@ var require_node = __commonJS({
|
|
|
1224
1224
|
var util2 = __require("util");
|
|
1225
1225
|
exports = module.exports = require_debug();
|
|
1226
1226
|
exports.init = init;
|
|
1227
|
-
exports.log =
|
|
1227
|
+
exports.log = log3;
|
|
1228
1228
|
exports.formatArgs = formatArgs;
|
|
1229
1229
|
exports.save = save;
|
|
1230
1230
|
exports.load = load;
|
|
@@ -1275,7 +1275,7 @@ var require_node = __commonJS({
|
|
|
1275
1275
|
args[0] = (/* @__PURE__ */ new Date()).toUTCString() + " " + name + " " + args[0];
|
|
1276
1276
|
}
|
|
1277
1277
|
}
|
|
1278
|
-
function
|
|
1278
|
+
function log3() {
|
|
1279
1279
|
return stream.write(util2.format.apply(util2, arguments) + "\n");
|
|
1280
1280
|
}
|
|
1281
1281
|
function save(namespaces) {
|
|
@@ -27279,11 +27279,11 @@ var require_core = __commonJS({
|
|
|
27279
27279
|
Ajv2.ValidationError = validation_error_1.default;
|
|
27280
27280
|
Ajv2.MissingRefError = ref_error_1.default;
|
|
27281
27281
|
exports.default = Ajv2;
|
|
27282
|
-
function checkOptions(checkOpts, options, msg,
|
|
27282
|
+
function checkOptions(checkOpts, options, msg, log3 = "error") {
|
|
27283
27283
|
for (const key in checkOpts) {
|
|
27284
27284
|
const opt = key;
|
|
27285
27285
|
if (opt in options)
|
|
27286
|
-
this.logger[
|
|
27286
|
+
this.logger[log3](`${msg}: option ${key}. ${checkOpts[opt]}`);
|
|
27287
27287
|
}
|
|
27288
27288
|
}
|
|
27289
27289
|
function getSchEnv(keyRef) {
|
|
@@ -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",
|
|
@@ -33549,7 +33549,7 @@ for (const [k, v] of snapshot) {
|
|
|
33549
33549
|
|
|
33550
33550
|
// src/http.ts
|
|
33551
33551
|
import http from "node:http";
|
|
33552
|
-
import { randomUUID } from "node:crypto";
|
|
33552
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
33553
33553
|
|
|
33554
33554
|
// src/session-auth.ts
|
|
33555
33555
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
@@ -49051,7 +49051,7 @@ var wrapper_default = import_websocket.default;
|
|
|
49051
49051
|
import fs from "node:fs";
|
|
49052
49052
|
import os from "node:os";
|
|
49053
49053
|
import path2 from "node:path";
|
|
49054
|
-
import { webcrypto } from "node:crypto";
|
|
49054
|
+
import { webcrypto, randomUUID as randomUUID2 } from "node:crypto";
|
|
49055
49055
|
|
|
49056
49056
|
// ../../node_modules/.pnpm/bail@2.0.2/node_modules/bail/index.js
|
|
49057
49057
|
function bail(error2) {
|
|
@@ -60649,6 +60649,238 @@ function groupConversationId(groupId) {
|
|
|
60649
60649
|
return "group:" + groupId;
|
|
60650
60650
|
}
|
|
60651
60651
|
|
|
60652
|
+
// ../sdk/src/http-client.ts
|
|
60653
|
+
import { randomUUID } from "node:crypto";
|
|
60654
|
+
function idem(key) {
|
|
60655
|
+
return { "idempotency-key": key || `sdk-${randomUUID()}` };
|
|
60656
|
+
}
|
|
60657
|
+
var JefriHttpError = class extends Error {
|
|
60658
|
+
constructor(message, status, body) {
|
|
60659
|
+
super(message);
|
|
60660
|
+
this.status = status;
|
|
60661
|
+
this.body = body;
|
|
60662
|
+
this.name = "JefriHttpError";
|
|
60663
|
+
}
|
|
60664
|
+
status;
|
|
60665
|
+
body;
|
|
60666
|
+
/** The hub's machine-readable state field, when it sent one. */
|
|
60667
|
+
get state() {
|
|
60668
|
+
return this.body?.state;
|
|
60669
|
+
}
|
|
60670
|
+
};
|
|
60671
|
+
var UnsupportedOverHttp = class extends Error {
|
|
60672
|
+
constructor(what, why) {
|
|
60673
|
+
super(`${what} is not available without a live connection \u2014 ${why}`);
|
|
60674
|
+
this.name = "UnsupportedOverHttp";
|
|
60675
|
+
}
|
|
60676
|
+
};
|
|
60677
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
60678
|
+
var JefriHttpClient = class {
|
|
60679
|
+
base;
|
|
60680
|
+
/** Discriminant for callers that accept either client: `stateless` is true
|
|
60681
|
+
* here and absent on the socket client, so shared code can branch without
|
|
60682
|
+
* instanceof (which breaks across duplicated package instances). */
|
|
60683
|
+
stateless = true;
|
|
60684
|
+
/** PUBLIC, like JefriClient.token: the connector's HTTP upload/download
|
|
60685
|
+
* helpers authenticate raw fetches with it. */
|
|
60686
|
+
token;
|
|
60687
|
+
f;
|
|
60688
|
+
timeoutMs;
|
|
60689
|
+
/** Populated by whoami(); mirrors JefriClient.identity for callers that read it. */
|
|
60690
|
+
identity = null;
|
|
60691
|
+
constructor(opts) {
|
|
60692
|
+
this.base = opts.server.replace(/\/+$/, "");
|
|
60693
|
+
this.token = opts.token;
|
|
60694
|
+
this.f = opts.fetchImpl ?? fetch;
|
|
60695
|
+
const t = Number(opts.timeoutMs);
|
|
60696
|
+
this.timeoutMs = Number.isFinite(t) && t > 0 ? t : DEFAULT_TIMEOUT_MS;
|
|
60697
|
+
}
|
|
60698
|
+
async call(method, path3, body, query, extraHeaders) {
|
|
60699
|
+
const url = new URL(this.base + path3);
|
|
60700
|
+
for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0 && v !== "") url.searchParams.set(k, v);
|
|
60701
|
+
const ac = new AbortController();
|
|
60702
|
+
const timer = setTimeout(() => ac.abort(), this.timeoutMs);
|
|
60703
|
+
let res;
|
|
60704
|
+
try {
|
|
60705
|
+
res = await this.f(url.toString(), {
|
|
60706
|
+
method,
|
|
60707
|
+
signal: ac.signal,
|
|
60708
|
+
headers: {
|
|
60709
|
+
authorization: `Bearer ${this.token}`,
|
|
60710
|
+
...body !== void 0 ? { "content-type": "application/json" } : {},
|
|
60711
|
+
...extraHeaders ?? {}
|
|
60712
|
+
},
|
|
60713
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
60714
|
+
});
|
|
60715
|
+
} catch (e) {
|
|
60716
|
+
clearTimeout(timer);
|
|
60717
|
+
if (e?.name === "AbortError") throw new Error(`${method} ${path3} timed out after ${this.timeoutMs}ms`);
|
|
60718
|
+
throw e;
|
|
60719
|
+
}
|
|
60720
|
+
let text5;
|
|
60721
|
+
try {
|
|
60722
|
+
text5 = await res.text();
|
|
60723
|
+
} catch (e) {
|
|
60724
|
+
if (e?.name === "AbortError") throw new Error(`${method} ${path3} timed out after ${this.timeoutMs}ms reading the body`);
|
|
60725
|
+
throw e;
|
|
60726
|
+
} finally {
|
|
60727
|
+
clearTimeout(timer);
|
|
60728
|
+
}
|
|
60729
|
+
let parsed = null;
|
|
60730
|
+
try {
|
|
60731
|
+
parsed = text5 ? JSON.parse(text5) : null;
|
|
60732
|
+
} catch {
|
|
60733
|
+
}
|
|
60734
|
+
if (!res.ok) {
|
|
60735
|
+
throw new JefriHttpError(parsed?.error ?? `${method} ${path3} failed with ${res.status}`, res.status, parsed);
|
|
60736
|
+
}
|
|
60737
|
+
return parsed;
|
|
60738
|
+
}
|
|
60739
|
+
// --- identity ------------------------------------------------------------
|
|
60740
|
+
/** /api/me answers `{ identity, online }`, NOT a bare Identity — unwrapping is
|
|
60741
|
+
* the caller's job and getting it wrong is silent: every field reads as
|
|
60742
|
+
* undefined and the object still looks like an Identity. Caught by running
|
|
60743
|
+
* this against the real hub; a stubbed test would have returned whatever
|
|
60744
|
+
* shape the stub was written to expect. */
|
|
60745
|
+
async whoami() {
|
|
60746
|
+
const body = await this.call("GET", "/api/me");
|
|
60747
|
+
const me = body?.identity ?? body;
|
|
60748
|
+
if (!me?.username) throw new Error("unexpected /api/me response shape");
|
|
60749
|
+
this.identity = me;
|
|
60750
|
+
return me;
|
|
60751
|
+
}
|
|
60752
|
+
// --- reads ---------------------------------------------------------------
|
|
60753
|
+
identities() {
|
|
60754
|
+
return this.call("GET", "/api/identities");
|
|
60755
|
+
}
|
|
60756
|
+
groups() {
|
|
60757
|
+
return this.call("GET", "/api/groups");
|
|
60758
|
+
}
|
|
60759
|
+
inbox(since) {
|
|
60760
|
+
return this.call("GET", "/api/inbox", void 0, { since });
|
|
60761
|
+
}
|
|
60762
|
+
search(query) {
|
|
60763
|
+
return this.call("GET", "/api/search", void 0, { q: query });
|
|
60764
|
+
}
|
|
60765
|
+
history(conversationId, before) {
|
|
60766
|
+
return this.call("GET", "/api/history", void 0, { conversationId, before });
|
|
60767
|
+
}
|
|
60768
|
+
// --- writes --------------------------------------------------------------
|
|
60769
|
+
/** Sends carry an idempotency key.
|
|
60770
|
+
*
|
|
60771
|
+
* The hub persists the key in the SAME transaction as the message, so a retry
|
|
60772
|
+
* is exactly-once rather than at-least-once. This matters far more for a
|
|
60773
|
+
* stateless client than a socket one: a timed-out or connection-reset POST
|
|
60774
|
+
* gives no evidence of whether the send landed, and the natural response — retry
|
|
60775
|
+
* — is precisely what produces duplicates during a deployment.
|
|
60776
|
+
*
|
|
60777
|
+
* Callers may pass their own key when a retry must reuse it; otherwise one is
|
|
60778
|
+
* minted per call. */
|
|
60779
|
+
message(to, content3, idempotencyKey) {
|
|
60780
|
+
return this.call("POST", "/api/messages", { to, content: content3 }, void 0, idem(idempotencyKey));
|
|
60781
|
+
}
|
|
60782
|
+
groupMessage(groupId, content3, idempotencyKey) {
|
|
60783
|
+
return this.call("POST", "/api/messages", { groupId, content: content3 }, void 0, idem(idempotencyKey));
|
|
60784
|
+
}
|
|
60785
|
+
/** DECLARE a status — a user-level choice that hits every connection the
|
|
60786
|
+
* identity holds. The returned status is the SETTLED one, which may differ
|
|
60787
|
+
* from the request if another lease outvoted it. */
|
|
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 });
|
|
60794
|
+
}
|
|
60795
|
+
/** KEEPALIVE — "still here", declaring nothing. This is the call for a timer.
|
|
60796
|
+
* Heartbeating through presence("online") instead is a declaration on a
|
|
60797
|
+
* timer, and it silently undoes an appear-offline chosen elsewhere.
|
|
60798
|
+
*
|
|
60799
|
+
* REJECTS (409, "signed off") when the identity has signed off: the beat was
|
|
60800
|
+
* refused and no lease exists, so the timer should STOP and the client must
|
|
60801
|
+
* declare a presence to resume. The refusal used to arrive as ok:true with a
|
|
60802
|
+
* full TTL — a lease lifetime that did not exist. */
|
|
60803
|
+
async heartbeat() {
|
|
60804
|
+
try {
|
|
60805
|
+
return await this.call("POST", "/api/presence", { keepalive: true });
|
|
60806
|
+
} catch (e) {
|
|
60807
|
+
if (e instanceof JefriHttpError && e.status === 409 && e.state === "signedOff")
|
|
60808
|
+
return { ok: false, state: "signedOff", status: String(e.body?.status ?? "offline"), expiresInMs: 0 };
|
|
60809
|
+
throw e;
|
|
60810
|
+
}
|
|
60811
|
+
}
|
|
60812
|
+
addFriend(to) {
|
|
60813
|
+
return this.call("POST", "/api/friends/request", { to });
|
|
60814
|
+
}
|
|
60815
|
+
/** Upload and deliver a file. The hub's raw-body route takes the bytes
|
|
60816
|
+
* directly, so there is no base64 round-trip. */
|
|
60817
|
+
async sendFile(target, fileName, mime, bytes, idempotencyKey) {
|
|
60818
|
+
const url = new URL(this.base + "/api/files");
|
|
60819
|
+
if (target.to) url.searchParams.set("to", target.to);
|
|
60820
|
+
if (target.groupId) url.searchParams.set("groupId", target.groupId);
|
|
60821
|
+
url.searchParams.set("fileName", fileName);
|
|
60822
|
+
if (target.caption) url.searchParams.set("caption", target.caption);
|
|
60823
|
+
const uploadTimeout = Math.max(this.timeoutMs, 3e4 + bytes.length / 20);
|
|
60824
|
+
const ac = new AbortController();
|
|
60825
|
+
const timer = setTimeout(() => ac.abort(), uploadTimeout);
|
|
60826
|
+
let res;
|
|
60827
|
+
try {
|
|
60828
|
+
res = await this.f(url.toString(), {
|
|
60829
|
+
method: "POST",
|
|
60830
|
+
signal: ac.signal,
|
|
60831
|
+
headers: {
|
|
60832
|
+
authorization: `Bearer ${this.token}`,
|
|
60833
|
+
"content-type": mime || "application/octet-stream",
|
|
60834
|
+
...idem(idempotencyKey)
|
|
60835
|
+
},
|
|
60836
|
+
body: bytes
|
|
60837
|
+
});
|
|
60838
|
+
} catch (e) {
|
|
60839
|
+
clearTimeout(timer);
|
|
60840
|
+
if (e?.name === "AbortError") throw new Error(`file upload timed out after ${Math.round(uploadTimeout)}ms`);
|
|
60841
|
+
throw e;
|
|
60842
|
+
}
|
|
60843
|
+
let text5;
|
|
60844
|
+
try {
|
|
60845
|
+
text5 = await res.text();
|
|
60846
|
+
} catch (e) {
|
|
60847
|
+
if (e?.name === "AbortError") throw new Error(`file upload timed out after ${Math.round(uploadTimeout)}ms reading the response`);
|
|
60848
|
+
throw e;
|
|
60849
|
+
} finally {
|
|
60850
|
+
clearTimeout(timer);
|
|
60851
|
+
}
|
|
60852
|
+
if (!res.ok) {
|
|
60853
|
+
let msg = `upload failed with ${res.status}`;
|
|
60854
|
+
try {
|
|
60855
|
+
msg = JSON.parse(text5)?.error ?? msg;
|
|
60856
|
+
} catch {
|
|
60857
|
+
}
|
|
60858
|
+
throw new Error(msg);
|
|
60859
|
+
}
|
|
60860
|
+
return text5 ? JSON.parse(text5) : null;
|
|
60861
|
+
}
|
|
60862
|
+
// --- deliberately unsupported -------------------------------------------
|
|
60863
|
+
/** Realtime delivery needs a socket. A stateless caller polls /api/inbox. */
|
|
60864
|
+
on() {
|
|
60865
|
+
throw new UnsupportedOverHttp("Realtime events", "this client holds no socket; poll the inbox instead");
|
|
60866
|
+
}
|
|
60867
|
+
off() {
|
|
60868
|
+
throw new UnsupportedOverHttp("Realtime events", "this client holds no socket; poll the inbox instead");
|
|
60869
|
+
}
|
|
60870
|
+
/** E2E sends are excluded until key custody is durable — see the file header.
|
|
60871
|
+
* Throwing names the reason so the next person does not "fix" it by wiring the
|
|
60872
|
+
* call through and shipping unreadable ciphertext. */
|
|
60873
|
+
privateMessage() {
|
|
60874
|
+
throw new UnsupportedOverHttp("End-to-end encrypted send", "private keys are not durably held; use the stateful connector");
|
|
60875
|
+
}
|
|
60876
|
+
privateGroupMessage() {
|
|
60877
|
+
throw new UnsupportedOverHttp("End-to-end encrypted send", "private keys are not durably held; use the stateful connector");
|
|
60878
|
+
}
|
|
60879
|
+
/** Nothing to close — kept so callers can treat both clients alike. */
|
|
60880
|
+
close() {
|
|
60881
|
+
}
|
|
60882
|
+
};
|
|
60883
|
+
|
|
60652
60884
|
// ../sdk/src/index.ts
|
|
60653
60885
|
var subtle = webcrypto.subtle;
|
|
60654
60886
|
var enc = new TextEncoder();
|
|
@@ -60659,6 +60891,9 @@ var b64 = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8
|
|
|
60659
60891
|
var fromB64 = (s) => new Uint8Array(Buffer.from(s, "base64"));
|
|
60660
60892
|
var safeName = (s) => s.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
60661
60893
|
var canonicalJwk = (jwk) => JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
|
|
60894
|
+
function newDeclId() {
|
|
60895
|
+
return "d-" + randomUUID2();
|
|
60896
|
+
}
|
|
60662
60897
|
var JefriAuthError = class extends Error {
|
|
60663
60898
|
code = "ACP_AUTH_FAILED";
|
|
60664
60899
|
constructor(message = "invalid or expired Jefri Chat token") {
|
|
@@ -60693,6 +60928,9 @@ var JefriClient = class _JefriClient {
|
|
|
60693
60928
|
* life of the process, stomping whatever a newer device had chosen since.
|
|
60694
60929
|
* The same stale-overwrite, one level down. */
|
|
60695
60930
|
pendingPresence = null;
|
|
60931
|
+
/** The id minted for the pending declaration. Its ECHO on a presence_update
|
|
60932
|
+
* is the only genuine acknowledgment — see notePresenceAck. */
|
|
60933
|
+
pendingDeclId = null;
|
|
60696
60934
|
/** The self statusRev BASELINE captured when the pending declaration was made.
|
|
60697
60935
|
* An ack must be CAUSALLY newer than this — the audit reproduced a delayed
|
|
60698
60936
|
* rev-9 event spending intent declared at a rev-10 baseline, after which the
|
|
@@ -60718,11 +60956,12 @@ var JefriClient = class _JefriClient {
|
|
|
60718
60956
|
* deleted, which is how the previous round's test enshrined the bug. */
|
|
60719
60957
|
notePresenceAck(ev) {
|
|
60720
60958
|
if (ev?.event !== "presence_update" || ev.username !== this.identity?.username) return;
|
|
60721
|
-
const frontier = this.selfStatusRev;
|
|
60722
60959
|
if (typeof ev.rev === "number" && ev.rev > this.selfStatusRev) this.selfStatusRev = ev.rev;
|
|
60723
|
-
if (this.pendingPresence === null
|
|
60724
|
-
if (
|
|
60725
|
-
|
|
60960
|
+
if (this.pendingPresence === null) return;
|
|
60961
|
+
if (this.pendingDeclId !== null && ev.declId === this.pendingDeclId) {
|
|
60962
|
+
this.pendingPresence = null;
|
|
60963
|
+
this.pendingDeclId = null;
|
|
60964
|
+
}
|
|
60726
60965
|
}
|
|
60727
60966
|
clientInfo;
|
|
60728
60967
|
// handshake, re-sent on reconnect
|
|
@@ -60819,7 +61058,7 @@ var JefriClient = class _JefriClient {
|
|
|
60819
61058
|
this.startPing();
|
|
60820
61059
|
try {
|
|
60821
61060
|
if (this.pendingPresence !== null)
|
|
60822
|
-
this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence }));
|
|
61061
|
+
this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence, declId: this.pendingDeclId ?? void 0 }));
|
|
60823
61062
|
if (this.clientInfo)
|
|
60824
61063
|
this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
|
|
60825
61064
|
} catch {
|
|
@@ -60947,7 +61186,8 @@ var JefriClient = class _JefriClient {
|
|
|
60947
61186
|
presence(status) {
|
|
60948
61187
|
this.pendingPresence = status;
|
|
60949
61188
|
this.pendingBaselineRev = this.selfStatusRev;
|
|
60950
|
-
this.
|
|
61189
|
+
this.pendingDeclId = newDeclId();
|
|
61190
|
+
this.send({ type: "presence", status, declId: this.pendingDeclId });
|
|
60951
61191
|
}
|
|
60952
61192
|
message(to, content3) {
|
|
60953
61193
|
this.send({ type: "message", to, content: content3 });
|
|
@@ -61277,6 +61517,96 @@ var JefriClient = class _JefriClient {
|
|
|
61277
61517
|
};
|
|
61278
61518
|
var serveDir = path2.join(os.homedir(), ".acp", "serve");
|
|
61279
61519
|
|
|
61520
|
+
// src/stateless-presence.ts
|
|
61521
|
+
var log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
|
|
61522
|
+
async function statelessPresenceBringUp(acp) {
|
|
61523
|
+
try {
|
|
61524
|
+
const hb = await acp.heartbeat();
|
|
61525
|
+
if (hb.state === "signedOff") await acp.presence("online");
|
|
61526
|
+
return true;
|
|
61527
|
+
} catch (e) {
|
|
61528
|
+
log("stateless presence bring-up failed (the sweep retries it):", e?.message ?? e);
|
|
61529
|
+
return false;
|
|
61530
|
+
}
|
|
61531
|
+
}
|
|
61532
|
+
async function statelessSweepBeat(acp, pool, onAuthRevoked) {
|
|
61533
|
+
if (pool.closed || pool.hbSignedOff) return;
|
|
61534
|
+
try {
|
|
61535
|
+
const hb = await acp.heartbeat();
|
|
61536
|
+
if (hb.state === "signedOff") pool.hbSignedOff = true;
|
|
61537
|
+
} catch (e) {
|
|
61538
|
+
if (e instanceof JefriHttpError && (e.status === 401 || e.status === 403)) {
|
|
61539
|
+
onAuthRevoked();
|
|
61540
|
+
return;
|
|
61541
|
+
}
|
|
61542
|
+
}
|
|
61543
|
+
}
|
|
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
|
+
|
|
61280
61610
|
// src/conn-pool.ts
|
|
61281
61611
|
function attachRef(c, id) {
|
|
61282
61612
|
if (c.closed) return false;
|
|
@@ -61315,7 +61645,8 @@ function createKeyedPool() {
|
|
|
61315
61645
|
return v;
|
|
61316
61646
|
},
|
|
61317
61647
|
has: (key) => ready.has(key),
|
|
61318
|
-
size: () => ready.size
|
|
61648
|
+
size: () => ready.size,
|
|
61649
|
+
keys: () => [...ready.keys()]
|
|
61319
61650
|
};
|
|
61320
61651
|
}
|
|
61321
61652
|
|
|
@@ -61343,6 +61674,7 @@ import cp3 from "node:child_process";
|
|
|
61343
61674
|
import os4 from "node:os";
|
|
61344
61675
|
import nnet from "node:net";
|
|
61345
61676
|
import ndns from "node:dns/promises";
|
|
61677
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
61346
61678
|
import nhttp from "node:http";
|
|
61347
61679
|
import nhttps from "node:https";
|
|
61348
61680
|
|
|
@@ -61750,6 +62082,35 @@ var debateJobs = /* @__PURE__ */ new Map();
|
|
|
61750
62082
|
var DEBATE_SEQ_TTL_MS = 10 * 60 * 1e3;
|
|
61751
62083
|
|
|
61752
62084
|
// src/tools.ts
|
|
62085
|
+
var isStateless = (c) => c.stateless === true;
|
|
62086
|
+
async function confirmWrite(c, fire) {
|
|
62087
|
+
if (isStateless(c)) {
|
|
62088
|
+
try {
|
|
62089
|
+
await fire();
|
|
62090
|
+
return {};
|
|
62091
|
+
} catch (e2) {
|
|
62092
|
+
return { error: e2?.message ?? String(e2) };
|
|
62093
|
+
}
|
|
62094
|
+
}
|
|
62095
|
+
const err = waitFor(c, "error", () => true, 800);
|
|
62096
|
+
fire();
|
|
62097
|
+
const e = await err;
|
|
62098
|
+
return e ? { error: e.message } : {};
|
|
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
|
+
const bucket = Math.floor(Date.now() / 6e5);
|
|
62106
|
+
return "mcp-" + createHash2("sha256").update(JSON.stringify([bucket, ua, rid, ...parts])).digest("hex").slice(0, 40);
|
|
62107
|
+
}
|
|
62108
|
+
async function fetchHistory(c, convId) {
|
|
62109
|
+
if (isStateless(c)) return c.history(convId);
|
|
62110
|
+
const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
|
|
62111
|
+
c.history(convId);
|
|
62112
|
+
return p;
|
|
62113
|
+
}
|
|
61753
62114
|
var MIME = {
|
|
61754
62115
|
".jpg": "image/jpeg",
|
|
61755
62116
|
".jpeg": "image/jpeg",
|
|
@@ -62085,7 +62446,7 @@ function saveWatermark() {
|
|
|
62085
62446
|
}
|
|
62086
62447
|
function registerAcpTools(server2, ctx) {
|
|
62087
62448
|
const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
|
|
62088
|
-
const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
|
|
62449
|
+
const e2eServer = ENABLE_E2E && !ctx.stateless ? server2 : { registerTool: () => void 0 };
|
|
62089
62450
|
const withClient = async (fn) => {
|
|
62090
62451
|
try {
|
|
62091
62452
|
const c = await ctx.ensureClient();
|
|
@@ -62202,11 +62563,12 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
62202
62563
|
text: external_exports.string().describe("the message body")
|
|
62203
62564
|
}
|
|
62204
62565
|
},
|
|
62205
|
-
async ({ to, text: text5 }) => withClient(async (c) => {
|
|
62206
|
-
const
|
|
62207
|
-
|
|
62208
|
-
|
|
62209
|
-
|
|
62566
|
+
async ({ to, text: text5 }, extra) => withClient(async (c) => {
|
|
62567
|
+
const r = await confirmWrite(
|
|
62568
|
+
c,
|
|
62569
|
+
() => isStateless(c) ? c.message(to, text5, retryKey(extra, ["message", to, text5])) : c.message(to, text5)
|
|
62570
|
+
);
|
|
62571
|
+
if (r.error) return fail(`Message rejected: ${r.error}`);
|
|
62210
62572
|
return ok3(`Sent to @${to}: "${text5}"`);
|
|
62211
62573
|
})
|
|
62212
62574
|
);
|
|
@@ -62221,8 +62583,9 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
62221
62583
|
}
|
|
62222
62584
|
},
|
|
62223
62585
|
async ({ to, text: text5 }) => withClient(async (c) => {
|
|
62224
|
-
const
|
|
62225
|
-
|
|
62586
|
+
const sc = c;
|
|
62587
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
62588
|
+
await sc.privateMessage(to, text5);
|
|
62226
62589
|
const e = await err;
|
|
62227
62590
|
if (e) return fail(`Private message rejected: ${e.message}`);
|
|
62228
62591
|
return ok3(`Sent private E2E message to @${to}.`);
|
|
@@ -62279,8 +62642,9 @@ ${fp}`);
|
|
|
62279
62642
|
} else {
|
|
62280
62643
|
return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
|
|
62281
62644
|
}
|
|
62282
|
-
|
|
62283
|
-
|
|
62645
|
+
const sc = c;
|
|
62646
|
+
await sc.privateFile(to, name, mime, url);
|
|
62647
|
+
if (caption) await sc.privateMessage(to, caption);
|
|
62284
62648
|
return ok3(`Sent private E2E file to @${to}.`);
|
|
62285
62649
|
})
|
|
62286
62650
|
);
|
|
@@ -62295,8 +62659,9 @@ ${fp}`);
|
|
|
62295
62659
|
}
|
|
62296
62660
|
},
|
|
62297
62661
|
async ({ groupId, text: text5 }) => withClient(async (c) => {
|
|
62298
|
-
const
|
|
62299
|
-
|
|
62662
|
+
const sc = c;
|
|
62663
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
62664
|
+
await sc.privateGroupMessage(groupId, text5);
|
|
62300
62665
|
const e = await err;
|
|
62301
62666
|
if (e) return fail(`Private group message rejected: ${e.message}`);
|
|
62302
62667
|
return ok3(`Sent private E2E group message.`);
|
|
@@ -62359,7 +62724,7 @@ ${fp}`);
|
|
|
62359
62724
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
62360
62725
|
}
|
|
62361
62726
|
},
|
|
62362
|
-
async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
|
|
62727
|
+
async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
62363
62728
|
let name, mime, url;
|
|
62364
62729
|
if (path3) {
|
|
62365
62730
|
if (!ctx.local) {
|
|
@@ -62411,9 +62776,23 @@ ${psCmd}
|
|
|
62411
62776
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
62412
62777
|
}
|
|
62413
62778
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
62779
|
+
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
62780
|
+
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62781
|
+
if (isStateless(c)) {
|
|
62782
|
+
try {
|
|
62783
|
+
await c.sendFile(
|
|
62784
|
+
{ to, caption },
|
|
62785
|
+
name,
|
|
62786
|
+
mime,
|
|
62787
|
+
rawBytes,
|
|
62788
|
+
retryKey(extra, ["file", to, name, createHash2("sha256").update(rawBytes).digest("hex")])
|
|
62789
|
+
);
|
|
62790
|
+
} catch (e2) {
|
|
62791
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
62792
|
+
}
|
|
62793
|
+
return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
|
|
62794
|
+
}
|
|
62414
62795
|
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
62415
|
-
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
62416
|
-
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62417
62796
|
try {
|
|
62418
62797
|
await uploadFileHttp(c, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
62419
62798
|
} catch (e2) {
|
|
@@ -62441,13 +62820,27 @@ ${psCmd}
|
|
|
62441
62820
|
caption: external_exports.string().optional()
|
|
62442
62821
|
}
|
|
62443
62822
|
},
|
|
62444
|
-
async ({ to, fileName, content: content3, caption }) => withClient(async (c) => {
|
|
62823
|
+
async ({ to, fileName, content: content3, caption }, extra) => withClient(async (c) => {
|
|
62445
62824
|
const known = mimeOf(fileName);
|
|
62446
62825
|
const mime = known === "application/octet-stream" ? "text/plain" : known;
|
|
62447
62826
|
const contentBytes = Buffer.from(content3, "utf8");
|
|
62827
|
+
if (contentBytes.length > MAX_UPLOAD_BYTES)
|
|
62828
|
+
return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62829
|
+
if (isStateless(c)) {
|
|
62830
|
+
try {
|
|
62831
|
+
await c.sendFile(
|
|
62832
|
+
{ to, caption },
|
|
62833
|
+
fileName,
|
|
62834
|
+
mime,
|
|
62835
|
+
contentBytes,
|
|
62836
|
+
retryKey(extra, ["file", to, fileName, createHash2("sha256").update(contentBytes).digest("hex")])
|
|
62837
|
+
);
|
|
62838
|
+
} catch (e2) {
|
|
62839
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
62840
|
+
}
|
|
62841
|
+
return ok3(`Sent \u{1F4C4} ${fileName} (${MB(contentBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
|
|
62842
|
+
}
|
|
62448
62843
|
if (contentBytes.length > WS_SAFE_FILE_BYTES) {
|
|
62449
|
-
if (contentBytes.length > MAX_UPLOAD_BYTES)
|
|
62450
|
-
return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62451
62844
|
try {
|
|
62452
62845
|
await uploadFileHttp(c, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
62453
62846
|
} catch (e2) {
|
|
@@ -62578,9 +62971,7 @@ ${psCmd}
|
|
|
62578
62971
|
if (!from && !groupId) return fail("Give `from` (a username, for a DM) or `groupId` (for a group file).");
|
|
62579
62972
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, from);
|
|
62580
62973
|
const where = groupId ? "this group" : `@${from}`;
|
|
62581
|
-
const
|
|
62582
|
-
c.history(convId);
|
|
62583
|
-
const res = await p;
|
|
62974
|
+
const res = await fetchHistory(c, convId);
|
|
62584
62975
|
const files = (res?.messages ?? []).filter((m) => m.kind === "file");
|
|
62585
62976
|
if (!files.length) return fail(`No files in ${where} yet.`);
|
|
62586
62977
|
const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
|
|
@@ -62617,10 +63008,8 @@ It downloads the file to your machine (current folder unless you set a path). Fi
|
|
|
62617
63008
|
inputSchema: { to: external_exports.string().describe("username to connect with") }
|
|
62618
63009
|
},
|
|
62619
63010
|
async ({ to }) => withClient(async (c) => {
|
|
62620
|
-
const
|
|
62621
|
-
|
|
62622
|
-
const e = await err;
|
|
62623
|
-
if (e) return fail(`Request rejected: ${e.message}`);
|
|
63011
|
+
const r = await confirmWrite(c, () => c.addFriend(to));
|
|
63012
|
+
if (r.error) return fail(`Request rejected: ${r.error}`);
|
|
62624
63013
|
return ok3(`\u{1F91D} Connection request sent to @${to}. They (or their owner) must approve before you can message.`);
|
|
62625
63014
|
})
|
|
62626
63015
|
);
|
|
@@ -62842,7 +63231,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62842
63231
|
},
|
|
62843
63232
|
async () => withClient(async (c) => {
|
|
62844
63233
|
const me = c.identity.username;
|
|
62845
|
-
const since = inboxWatermark.get(me);
|
|
63234
|
+
const since = ctx.stateless ? "stored" : inboxWatermark.get(me);
|
|
62846
63235
|
const url = `${ctx.serverUrl.replace(/\/$/, "")}/api/inbox` + (since ? `?since=${encodeURIComponent(since)}` : "");
|
|
62847
63236
|
let msgs = [];
|
|
62848
63237
|
try {
|
|
@@ -62856,8 +63245,24 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62856
63245
|
}
|
|
62857
63246
|
if (!msgs.length) return ok3("\u{1F4ED} No new messages.");
|
|
62858
63247
|
msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
|
|
62859
|
-
|
|
62860
|
-
|
|
63248
|
+
if (ctx.stateless) {
|
|
63249
|
+
const last = msgs[msgs.length - 1];
|
|
63250
|
+
const ack = async () => {
|
|
63251
|
+
try {
|
|
63252
|
+
await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
|
|
63253
|
+
method: "POST",
|
|
63254
|
+
headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
|
|
63255
|
+
body: JSON.stringify({ until: last.createdAt, untilId: last.id })
|
|
63256
|
+
});
|
|
63257
|
+
} catch {
|
|
63258
|
+
}
|
|
63259
|
+
};
|
|
63260
|
+
if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
|
|
63261
|
+
else await ack();
|
|
63262
|
+
} else {
|
|
63263
|
+
inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
|
|
63264
|
+
saveWatermark();
|
|
63265
|
+
}
|
|
62861
63266
|
const groupNames = /* @__PURE__ */ new Map();
|
|
62862
63267
|
if (msgs.some((m) => m.groupId)) {
|
|
62863
63268
|
try {
|
|
@@ -62915,11 +63320,12 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62915
63320
|
text: external_exports.string().describe("the message to post to the group")
|
|
62916
63321
|
}
|
|
62917
63322
|
},
|
|
62918
|
-
async ({ groupId, text: text5 }) => withClient(async (c) => {
|
|
62919
|
-
const
|
|
62920
|
-
|
|
62921
|
-
|
|
62922
|
-
|
|
63323
|
+
async ({ groupId, text: text5 }, extra) => withClient(async (c) => {
|
|
63324
|
+
const r = await confirmWrite(
|
|
63325
|
+
c,
|
|
63326
|
+
() => isStateless(c) ? c.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5])) : c.groupMessage(groupId, text5)
|
|
63327
|
+
);
|
|
63328
|
+
if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
|
|
62923
63329
|
return ok3(`Posted to the group.`);
|
|
62924
63330
|
})
|
|
62925
63331
|
);
|
|
@@ -62937,7 +63343,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62937
63343
|
fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
|
|
62938
63344
|
}
|
|
62939
63345
|
},
|
|
62940
|
-
async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
|
|
63346
|
+
async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
|
|
62941
63347
|
let name, mime, url;
|
|
62942
63348
|
if (path3) {
|
|
62943
63349
|
if (!ctx.local) {
|
|
@@ -62979,9 +63385,23 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62979
63385
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
62980
63386
|
}
|
|
62981
63387
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
63388
|
+
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
63389
|
+
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
63390
|
+
if (isStateless(c)) {
|
|
63391
|
+
try {
|
|
63392
|
+
await c.sendFile(
|
|
63393
|
+
{ groupId, caption },
|
|
63394
|
+
name,
|
|
63395
|
+
mime,
|
|
63396
|
+
rawBytes,
|
|
63397
|
+
retryKey(extra, ["file", groupId, name, createHash2("sha256").update(rawBytes).digest("hex")])
|
|
63398
|
+
);
|
|
63399
|
+
} catch (e2) {
|
|
63400
|
+
return fail(`Rejected: ${e2?.message ?? e2}`);
|
|
63401
|
+
}
|
|
63402
|
+
return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to the group.`);
|
|
63403
|
+
}
|
|
62982
63404
|
if (rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
62983
|
-
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
62984
|
-
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62985
63405
|
try {
|
|
62986
63406
|
await uploadFileHttp(c, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
62987
63407
|
} catch (e2) {
|
|
@@ -63012,9 +63432,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63012
63432
|
if (!other && !groupId) return fail("Give `with` (a username, for a DM) or `groupId` (for a group).");
|
|
63013
63433
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, other);
|
|
63014
63434
|
const label = groupId ? "this group" : `@${other}`;
|
|
63015
|
-
const
|
|
63016
|
-
c.history(convId);
|
|
63017
|
-
const res = await p;
|
|
63435
|
+
const res = await fetchHistory(c, convId);
|
|
63018
63436
|
const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
|
|
63019
63437
|
if (!msgs.length) return ok3(`No messages yet in ${label}.`);
|
|
63020
63438
|
const lines = await Promise.all(msgs.map(async (m) => {
|
|
@@ -63045,10 +63463,14 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63045
63463
|
inputSchema: { query: external_exports.string() }
|
|
63046
63464
|
},
|
|
63047
63465
|
async ({ query }) => withClient(async (c) => {
|
|
63048
|
-
|
|
63049
|
-
c
|
|
63050
|
-
|
|
63051
|
-
|
|
63466
|
+
let results;
|
|
63467
|
+
if (isStateless(c)) {
|
|
63468
|
+
results = (await c.search(query))?.results ?? [];
|
|
63469
|
+
} else {
|
|
63470
|
+
const res = waitFor(c, "search_results", () => true);
|
|
63471
|
+
c.search(query);
|
|
63472
|
+
results = (await res)?.results ?? [];
|
|
63473
|
+
}
|
|
63052
63474
|
if (!results.length) return ok3(`No agents matched "${query}".`);
|
|
63053
63475
|
return ok3(
|
|
63054
63476
|
results.map((r) => `\u2022 @${r.username} \u2014 ${r.displayName} (${r.status})${r.owner ? ` \xB7 by @${r.owner}` : ""} \xB7 ${r.tags.join(", ")}`).join("\n")
|
|
@@ -63064,8 +63486,11 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63064
63486
|
status: external_exports.enum(["online", "away", "busy", "coding", "reviewing", "planning", "offline"])
|
|
63065
63487
|
}
|
|
63066
63488
|
},
|
|
63067
|
-
async ({ status }) => withClient(async (c) => {
|
|
63068
|
-
c
|
|
63489
|
+
async ({ status }, extra) => withClient(async (c) => {
|
|
63490
|
+
const declId = isStateless(c) ? retryKey(extra, ["presence", status])?.replace(/^mcp-/, "d-") : void 0;
|
|
63491
|
+
const r = await confirmWrite(c, () => isStateless(c) ? c.presence(status, declId) : c.presence(status));
|
|
63492
|
+
if (r.error) return fail(`Couldn't set status: ${r.error}`);
|
|
63493
|
+
ctx.onPresenceDeclared?.(status);
|
|
63069
63494
|
return ok3(`Status set to ${status}.`);
|
|
63070
63495
|
})
|
|
63071
63496
|
);
|
|
@@ -63257,11 +63682,13 @@ function sweepIdleSessions(map3) {
|
|
|
63257
63682
|
// src/http.ts
|
|
63258
63683
|
var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
|
|
63259
63684
|
var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
|
|
63260
|
-
var
|
|
63685
|
+
var STATELESS = process.env.JEFRI_STATELESS === "true";
|
|
63686
|
+
var log2 = (...a) => console.error("[jefrichat-mcp-http]", ...a);
|
|
63261
63687
|
var sessions = /* @__PURE__ */ new Map();
|
|
63262
63688
|
var HOST_FRESH_MS = Number(process.env.MCP_HOST_FRESH_MS ?? 10 * 60 * 1e3);
|
|
63263
63689
|
var hubPool = createKeyedPool();
|
|
63264
63690
|
var SESSION_TTL_MS = Number(process.env.MCP_SESSION_TTL_MS ?? 4 * 60 * 60 * 1e3);
|
|
63691
|
+
var STATELESS_POOL_IDLE_MS = Number(process.env.MCP_STATELESS_POOL_IDLE_MS ?? 10 * 60 * 1e3);
|
|
63265
63692
|
var MAX_SESSIONS = Number(process.env.MCP_MAX_SESSIONS ?? 1500);
|
|
63266
63693
|
var MAX_SESSIONS_PER_IDENTITY = Number(process.env.MCP_MAX_SESSIONS_PER_IDENTITY ?? 20);
|
|
63267
63694
|
var INIT_WINDOW_MS = 6e4;
|
|
@@ -63292,7 +63719,7 @@ function closeSession(sid, reason) {
|
|
|
63292
63719
|
if (detachRef(pooled, sid)) teardownPool(s.poolKey, pooled, "last session gone");
|
|
63293
63720
|
else if (s.host) announcePoolHosts(s.poolKey, pooled);
|
|
63294
63721
|
}
|
|
63295
|
-
|
|
63722
|
+
log2(`session ${sid.slice(0, 8)}\u2026 closed (${reason}) (${sessions.size} active)`);
|
|
63296
63723
|
}
|
|
63297
63724
|
function teardownPool(poolKey, pooled, reason) {
|
|
63298
63725
|
pooled.closed = true;
|
|
@@ -63301,9 +63728,10 @@ function teardownPool(poolKey, pooled, reason) {
|
|
|
63301
63728
|
pooled.acp.close();
|
|
63302
63729
|
} catch {
|
|
63303
63730
|
}
|
|
63304
|
-
|
|
63731
|
+
log2(`hub connection ${poolKey.slice(0, 8)}\u2026 closed (${reason})`);
|
|
63305
63732
|
}
|
|
63306
63733
|
function announcePoolHosts(poolKey, pooled) {
|
|
63734
|
+
if (STATELESS) return;
|
|
63307
63735
|
const joined = aggregateFreshHosts(sessions.values(), poolKey, Date.now(), HOST_FRESH_MS);
|
|
63308
63736
|
if (joined === pooled.lastHosts) return;
|
|
63309
63737
|
pooled.lastHosts = joined;
|
|
@@ -63318,6 +63746,12 @@ function closePool(poolKey, reason) {
|
|
|
63318
63746
|
}
|
|
63319
63747
|
async function acquirePooledConn(token, poolKey) {
|
|
63320
63748
|
return hubPool.acquire(poolKey, async () => {
|
|
63749
|
+
if (STATELESS) {
|
|
63750
|
+
const acp2 = new JefriHttpClient({ server: HUB, token });
|
|
63751
|
+
await acp2.whoami();
|
|
63752
|
+
const broughtUp = await statelessPresenceBringUp(acp2);
|
|
63753
|
+
return { acp: acp2, inbox: [], refs: /* @__PURE__ */ new Set(), closed: false, lastSeen: Date.now(), needsBringUp: !broughtUp };
|
|
63754
|
+
}
|
|
63321
63755
|
const acp = await JefriClient.connect({ server: HUB, token, status: "online" });
|
|
63322
63756
|
const inbox = [];
|
|
63323
63757
|
if (acp.identity) attachInbox(acp, inbox, acp.identity.username);
|
|
@@ -63339,12 +63773,30 @@ setInterval(() => {
|
|
|
63339
63773
|
for (const [sid, s] of sessions) {
|
|
63340
63774
|
if (now - s.lastActive > SESSION_TTL_MS) closeSession(sid, "idle");
|
|
63341
63775
|
}
|
|
63342
|
-
|
|
63343
|
-
|
|
63344
|
-
|
|
63345
|
-
|
|
63346
|
-
|
|
63347
|
-
|
|
63776
|
+
if (STATELESS) {
|
|
63777
|
+
for (const key of hubPool.keys()) {
|
|
63778
|
+
const pooled = hubPool.get(key);
|
|
63779
|
+
if (!pooled) continue;
|
|
63780
|
+
if (now - (pooled.lastSeen ?? 0) > STATELESS_POOL_IDLE_MS) {
|
|
63781
|
+
hubPool.drop(key);
|
|
63782
|
+
continue;
|
|
63783
|
+
}
|
|
63784
|
+
if (pooled.needsBringUp) {
|
|
63785
|
+
void statelessPresenceBringUp(pooled.acp).then((ok4) => {
|
|
63786
|
+
if (ok4) pooled.needsBringUp = false;
|
|
63787
|
+
});
|
|
63788
|
+
} else {
|
|
63789
|
+
void statelessSweepBeat(pooled.acp, pooled, () => closePool(key, "auth revoked"));
|
|
63790
|
+
}
|
|
63791
|
+
}
|
|
63792
|
+
} else {
|
|
63793
|
+
const seenPools = /* @__PURE__ */ new Set();
|
|
63794
|
+
for (const s of sessions.values()) {
|
|
63795
|
+
if (seenPools.has(s.poolKey)) continue;
|
|
63796
|
+
seenPools.add(s.poolKey);
|
|
63797
|
+
const pooled = hubPool.get(s.poolKey);
|
|
63798
|
+
if (pooled) announcePoolHosts(s.poolKey, pooled);
|
|
63799
|
+
}
|
|
63348
63800
|
}
|
|
63349
63801
|
for (const [ip, e] of initHits) {
|
|
63350
63802
|
if (now > e.resetAt) initHits.delete(ip);
|
|
@@ -63403,11 +63855,12 @@ var health = (_req, res) => res.status(draining ? 503 : 200).json({
|
|
|
63403
63855
|
draining,
|
|
63404
63856
|
service: "jefrichat-mcp-http",
|
|
63405
63857
|
hub: HUB,
|
|
63858
|
+
stateless: STATELESS,
|
|
63406
63859
|
sessions: sessions.size
|
|
63407
63860
|
});
|
|
63408
63861
|
app.get("/health", health);
|
|
63409
63862
|
app.get("/mcp/health", health);
|
|
63410
|
-
|
|
63863
|
+
var statefulPost = async (req, res) => {
|
|
63411
63864
|
const sessionId = req.headers["mcp-session-id"];
|
|
63412
63865
|
if (sessionId && sessions.has(sessionId)) {
|
|
63413
63866
|
const s = sessions.get(sessionId);
|
|
@@ -63473,7 +63926,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
63473
63926
|
try {
|
|
63474
63927
|
pooled = await acquirePooledConn(token, poolKey);
|
|
63475
63928
|
} catch (e) {
|
|
63476
|
-
if (e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED") {
|
|
63929
|
+
if (e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED" || e instanceof JefriHttpError && (e.status === 401 || e.status === 403)) {
|
|
63477
63930
|
res.status(401).json({
|
|
63478
63931
|
jsonrpc: "2.0",
|
|
63479
63932
|
error: {
|
|
@@ -63498,7 +63951,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
63498
63951
|
return;
|
|
63499
63952
|
}
|
|
63500
63953
|
const mcp = new McpServer({ name: "jefrichat", version: "0.1.0" });
|
|
63501
|
-
registerAcpTools(mcp, { ensureClient: async () => acp, inbox: pooled.inbox, serverUrl: HUB, local: false });
|
|
63954
|
+
registerAcpTools(mcp, { ensureClient: async () => acp, inbox: pooled.inbox, serverUrl: HUB, local: false, stateless: STATELESS });
|
|
63502
63955
|
let mySid;
|
|
63503
63956
|
mcp.server.oninitialized = () => {
|
|
63504
63957
|
try {
|
|
@@ -63510,14 +63963,14 @@ app.post("/mcp", async (req, res) => {
|
|
|
63510
63963
|
}
|
|
63511
63964
|
};
|
|
63512
63965
|
const transport = new StreamableHTTPServerTransport({
|
|
63513
|
-
sessionIdGenerator: () =>
|
|
63966
|
+
sessionIdGenerator: () => randomUUID3(),
|
|
63514
63967
|
onsessioninitialized: (sid) => {
|
|
63515
63968
|
if (pooled.closed) {
|
|
63516
63969
|
try {
|
|
63517
63970
|
transport.close?.();
|
|
63518
63971
|
} catch {
|
|
63519
63972
|
}
|
|
63520
|
-
|
|
63973
|
+
log2(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
|
|
63521
63974
|
return;
|
|
63522
63975
|
}
|
|
63523
63976
|
const meName = acp.identity.username;
|
|
@@ -63542,19 +63995,19 @@ app.post("/mcp", async (req, res) => {
|
|
|
63542
63995
|
transport.close?.();
|
|
63543
63996
|
} catch {
|
|
63544
63997
|
}
|
|
63545
|
-
|
|
63998
|
+
log2(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
|
|
63546
63999
|
return;
|
|
63547
64000
|
}
|
|
63548
64001
|
sessions.set(sid, { transport, acp, lastActive: Date.now(), tokenHash: sha256(token), inflight: 0, poolKey });
|
|
63549
64002
|
mySid = sid;
|
|
63550
|
-
|
|
64003
|
+
log2(`session ${sid.slice(0, 8)}\u2026 open as @${acp.identity.username} (${sessions.size} active, ${pooled.refs.size} on this connection)`);
|
|
63551
64004
|
}
|
|
63552
64005
|
});
|
|
63553
64006
|
transport.onclose = () => {
|
|
63554
64007
|
const sid = transport.sessionId;
|
|
63555
64008
|
if (sid) closeSession(sid, "client closed");
|
|
63556
64009
|
};
|
|
63557
|
-
const reservation = "init:" +
|
|
64010
|
+
const reservation = "init:" + randomUUID3();
|
|
63558
64011
|
pooled.refs.add(reservation);
|
|
63559
64012
|
try {
|
|
63560
64013
|
await mcp.connect(transport);
|
|
@@ -63570,7 +64023,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
63570
64023
|
error: { code: -32001, message: "Session expired \u2014 reinitialize (your token is still valid)" },
|
|
63571
64024
|
id: null
|
|
63572
64025
|
});
|
|
63573
|
-
}
|
|
64026
|
+
};
|
|
63574
64027
|
var bySession = async (req, res) => {
|
|
63575
64028
|
const sessionId = req.headers["mcp-session-id"];
|
|
63576
64029
|
const s = sessionId ? sessions.get(sessionId) : void 0;
|
|
@@ -63588,8 +64041,58 @@ var bySession = async (req, res) => {
|
|
|
63588
64041
|
touch(sessionId);
|
|
63589
64042
|
await s.transport.handleRequest(req, res);
|
|
63590
64043
|
};
|
|
63591
|
-
|
|
63592
|
-
app.
|
|
64044
|
+
if (STATELESS) {
|
|
64045
|
+
app.post(
|
|
64046
|
+
"/mcp",
|
|
64047
|
+
statelessMcpHandler({
|
|
64048
|
+
bearer,
|
|
64049
|
+
acquire: async (token) => {
|
|
64050
|
+
const poolKey = sha256(token).toString("hex");
|
|
64051
|
+
const pooled = await acquirePooledConn(token, poolKey);
|
|
64052
|
+
pooled.lastSeen = Date.now();
|
|
64053
|
+
return pooled;
|
|
64054
|
+
},
|
|
64055
|
+
// registerTools below closes over the POOLED entry via acp — the presence
|
|
64056
|
+
// resume/stop signal needs the pool, so it is looked up by identity of
|
|
64057
|
+
// the shared client (one pool owns exactly one client instance).
|
|
64058
|
+
classifyError: (e) => e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED" || e instanceof JefriHttpError && (e.status === 401 || e.status === 403) ? "auth" : "hub",
|
|
64059
|
+
registerTools: (mcp, acp, hooks) => registerAcpTools(mcp, {
|
|
64060
|
+
ensureClient: async () => acp,
|
|
64061
|
+
inbox: [],
|
|
64062
|
+
serverUrl: HUB,
|
|
64063
|
+
local: false,
|
|
64064
|
+
stateless: true,
|
|
64065
|
+
deferUntilDelivered: hooks.deferUntilDelivered,
|
|
64066
|
+
// Round 26 #4a: a successful declaration RESUMES heartbeats (the
|
|
64067
|
+
// user came back — the hub cleared its sign-off marker, the pool
|
|
64068
|
+
// must clear its own), and a sign-off STOPS them proactively.
|
|
64069
|
+
onPresenceDeclared: (status) => {
|
|
64070
|
+
for (const key of hubPool.keys()) {
|
|
64071
|
+
const pooled = hubPool.get(key);
|
|
64072
|
+
if (pooled && pooled.acp === acp) {
|
|
64073
|
+
if (status === "offline") pooled.hbSignedOff = true;
|
|
64074
|
+
else {
|
|
64075
|
+
pooled.hbSignedOff = false;
|
|
64076
|
+
pooled.needsBringUp = false;
|
|
64077
|
+
}
|
|
64078
|
+
break;
|
|
64079
|
+
}
|
|
64080
|
+
}
|
|
64081
|
+
}
|
|
64082
|
+
}),
|
|
64083
|
+
isDraining: () => draining,
|
|
64084
|
+
tryAcquireInflight: (token) => tryAcquire(tokenInflight, sha256(token).toString("hex"), MAX_INFLIGHT_PER_TOKEN),
|
|
64085
|
+
releaseInflight: (token) => release(tokenInflight, sha256(token).toString("hex")),
|
|
64086
|
+
log: log2
|
|
64087
|
+
})
|
|
64088
|
+
);
|
|
64089
|
+
app.get("/mcp", statelessMethodNotAllowed);
|
|
64090
|
+
app.delete("/mcp", statelessMethodNotAllowed);
|
|
64091
|
+
} else {
|
|
64092
|
+
app.post("/mcp", statefulPost);
|
|
64093
|
+
app.get("/mcp", bySession);
|
|
64094
|
+
app.delete("/mcp", bySession);
|
|
64095
|
+
}
|
|
63593
64096
|
var server = http.createServer(app).listen(PORT, () => {
|
|
63594
64097
|
console.error(`
|
|
63595
64098
|
\u{1F310} Jefri Chat remote MCP (HTTP) on http://localhost:${PORT}/mcp`);
|