jefrichat-mcp 0.48.9 → 0.48.10
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 +363 -69
- package/dist/index.js +73 -46
- 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) {
|
|
@@ -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,234 @@ 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
|
+
presence(status) {
|
|
60789
|
+
return this.call("POST", "/api/presence", { status });
|
|
60790
|
+
}
|
|
60791
|
+
/** KEEPALIVE — "still here", declaring nothing. This is the call for a timer.
|
|
60792
|
+
* Heartbeating through presence("online") instead is a declaration on a
|
|
60793
|
+
* timer, and it silently undoes an appear-offline chosen elsewhere.
|
|
60794
|
+
*
|
|
60795
|
+
* REJECTS (409, "signed off") when the identity has signed off: the beat was
|
|
60796
|
+
* refused and no lease exists, so the timer should STOP and the client must
|
|
60797
|
+
* declare a presence to resume. The refusal used to arrive as ok:true with a
|
|
60798
|
+
* full TTL — a lease lifetime that did not exist. */
|
|
60799
|
+
async heartbeat() {
|
|
60800
|
+
try {
|
|
60801
|
+
return await this.call("POST", "/api/presence", { keepalive: true });
|
|
60802
|
+
} catch (e) {
|
|
60803
|
+
if (e instanceof JefriHttpError && e.status === 409 && e.state === "signedOff")
|
|
60804
|
+
return { ok: false, state: "signedOff", status: String(e.body?.status ?? "offline"), expiresInMs: 0 };
|
|
60805
|
+
throw e;
|
|
60806
|
+
}
|
|
60807
|
+
}
|
|
60808
|
+
addFriend(to) {
|
|
60809
|
+
return this.call("POST", "/api/friends/request", { to });
|
|
60810
|
+
}
|
|
60811
|
+
/** Upload and deliver a file. The hub's raw-body route takes the bytes
|
|
60812
|
+
* directly, so there is no base64 round-trip. */
|
|
60813
|
+
async sendFile(target, fileName, mime, bytes, idempotencyKey) {
|
|
60814
|
+
const url = new URL(this.base + "/api/files");
|
|
60815
|
+
if (target.to) url.searchParams.set("to", target.to);
|
|
60816
|
+
if (target.groupId) url.searchParams.set("groupId", target.groupId);
|
|
60817
|
+
url.searchParams.set("fileName", fileName);
|
|
60818
|
+
if (target.caption) url.searchParams.set("caption", target.caption);
|
|
60819
|
+
const uploadTimeout = Math.max(this.timeoutMs, 3e4 + bytes.length / 20);
|
|
60820
|
+
const ac = new AbortController();
|
|
60821
|
+
const timer = setTimeout(() => ac.abort(), uploadTimeout);
|
|
60822
|
+
let res;
|
|
60823
|
+
try {
|
|
60824
|
+
res = await this.f(url.toString(), {
|
|
60825
|
+
method: "POST",
|
|
60826
|
+
signal: ac.signal,
|
|
60827
|
+
headers: {
|
|
60828
|
+
authorization: `Bearer ${this.token}`,
|
|
60829
|
+
"content-type": mime || "application/octet-stream",
|
|
60830
|
+
...idem(idempotencyKey)
|
|
60831
|
+
},
|
|
60832
|
+
body: bytes
|
|
60833
|
+
});
|
|
60834
|
+
} catch (e) {
|
|
60835
|
+
clearTimeout(timer);
|
|
60836
|
+
if (e?.name === "AbortError") throw new Error(`file upload timed out after ${Math.round(uploadTimeout)}ms`);
|
|
60837
|
+
throw e;
|
|
60838
|
+
}
|
|
60839
|
+
let text5;
|
|
60840
|
+
try {
|
|
60841
|
+
text5 = await res.text();
|
|
60842
|
+
} catch (e) {
|
|
60843
|
+
if (e?.name === "AbortError") throw new Error(`file upload timed out after ${Math.round(uploadTimeout)}ms reading the response`);
|
|
60844
|
+
throw e;
|
|
60845
|
+
} finally {
|
|
60846
|
+
clearTimeout(timer);
|
|
60847
|
+
}
|
|
60848
|
+
if (!res.ok) {
|
|
60849
|
+
let msg = `upload failed with ${res.status}`;
|
|
60850
|
+
try {
|
|
60851
|
+
msg = JSON.parse(text5)?.error ?? msg;
|
|
60852
|
+
} catch {
|
|
60853
|
+
}
|
|
60854
|
+
throw new Error(msg);
|
|
60855
|
+
}
|
|
60856
|
+
return text5 ? JSON.parse(text5) : null;
|
|
60857
|
+
}
|
|
60858
|
+
// --- deliberately unsupported -------------------------------------------
|
|
60859
|
+
/** Realtime delivery needs a socket. A stateless caller polls /api/inbox. */
|
|
60860
|
+
on() {
|
|
60861
|
+
throw new UnsupportedOverHttp("Realtime events", "this client holds no socket; poll the inbox instead");
|
|
60862
|
+
}
|
|
60863
|
+
off() {
|
|
60864
|
+
throw new UnsupportedOverHttp("Realtime events", "this client holds no socket; poll the inbox instead");
|
|
60865
|
+
}
|
|
60866
|
+
/** E2E sends are excluded until key custody is durable — see the file header.
|
|
60867
|
+
* Throwing names the reason so the next person does not "fix" it by wiring the
|
|
60868
|
+
* call through and shipping unreadable ciphertext. */
|
|
60869
|
+
privateMessage() {
|
|
60870
|
+
throw new UnsupportedOverHttp("End-to-end encrypted send", "private keys are not durably held; use the stateful connector");
|
|
60871
|
+
}
|
|
60872
|
+
privateGroupMessage() {
|
|
60873
|
+
throw new UnsupportedOverHttp("End-to-end encrypted send", "private keys are not durably held; use the stateful connector");
|
|
60874
|
+
}
|
|
60875
|
+
/** Nothing to close — kept so callers can treat both clients alike. */
|
|
60876
|
+
close() {
|
|
60877
|
+
}
|
|
60878
|
+
};
|
|
60879
|
+
|
|
60652
60880
|
// ../sdk/src/index.ts
|
|
60653
60881
|
var subtle = webcrypto.subtle;
|
|
60654
60882
|
var enc = new TextEncoder();
|
|
@@ -60659,6 +60887,9 @@ var b64 = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8
|
|
|
60659
60887
|
var fromB64 = (s) => new Uint8Array(Buffer.from(s, "base64"));
|
|
60660
60888
|
var safeName = (s) => s.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
60661
60889
|
var canonicalJwk = (jwk) => JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
|
|
60890
|
+
function newDeclId() {
|
|
60891
|
+
return "d-" + randomUUID2();
|
|
60892
|
+
}
|
|
60662
60893
|
var JefriAuthError = class extends Error {
|
|
60663
60894
|
code = "ACP_AUTH_FAILED";
|
|
60664
60895
|
constructor(message = "invalid or expired Jefri Chat token") {
|
|
@@ -60693,6 +60924,9 @@ var JefriClient = class _JefriClient {
|
|
|
60693
60924
|
* life of the process, stomping whatever a newer device had chosen since.
|
|
60694
60925
|
* The same stale-overwrite, one level down. */
|
|
60695
60926
|
pendingPresence = null;
|
|
60927
|
+
/** The id minted for the pending declaration. Its ECHO on a presence_update
|
|
60928
|
+
* is the only genuine acknowledgment — see notePresenceAck. */
|
|
60929
|
+
pendingDeclId = null;
|
|
60696
60930
|
/** The self statusRev BASELINE captured when the pending declaration was made.
|
|
60697
60931
|
* An ack must be CAUSALLY newer than this — the audit reproduced a delayed
|
|
60698
60932
|
* rev-9 event spending intent declared at a rev-10 baseline, after which the
|
|
@@ -60718,11 +60952,12 @@ var JefriClient = class _JefriClient {
|
|
|
60718
60952
|
* deleted, which is how the previous round's test enshrined the bug. */
|
|
60719
60953
|
notePresenceAck(ev) {
|
|
60720
60954
|
if (ev?.event !== "presence_update" || ev.username !== this.identity?.username) return;
|
|
60721
|
-
const frontier = this.selfStatusRev;
|
|
60722
60955
|
if (typeof ev.rev === "number" && ev.rev > this.selfStatusRev) this.selfStatusRev = ev.rev;
|
|
60723
|
-
if (this.pendingPresence === null
|
|
60724
|
-
if (
|
|
60725
|
-
|
|
60956
|
+
if (this.pendingPresence === null) return;
|
|
60957
|
+
if (this.pendingDeclId !== null && ev.declId === this.pendingDeclId) {
|
|
60958
|
+
this.pendingPresence = null;
|
|
60959
|
+
this.pendingDeclId = null;
|
|
60960
|
+
}
|
|
60726
60961
|
}
|
|
60727
60962
|
clientInfo;
|
|
60728
60963
|
// handshake, re-sent on reconnect
|
|
@@ -60819,7 +61054,7 @@ var JefriClient = class _JefriClient {
|
|
|
60819
61054
|
this.startPing();
|
|
60820
61055
|
try {
|
|
60821
61056
|
if (this.pendingPresence !== null)
|
|
60822
|
-
this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence }));
|
|
61057
|
+
this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence, declId: this.pendingDeclId ?? void 0 }));
|
|
60823
61058
|
if (this.clientInfo)
|
|
60824
61059
|
this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
|
|
60825
61060
|
} catch {
|
|
@@ -60947,7 +61182,8 @@ var JefriClient = class _JefriClient {
|
|
|
60947
61182
|
presence(status) {
|
|
60948
61183
|
this.pendingPresence = status;
|
|
60949
61184
|
this.pendingBaselineRev = this.selfStatusRev;
|
|
60950
|
-
this.
|
|
61185
|
+
this.pendingDeclId = newDeclId();
|
|
61186
|
+
this.send({ type: "presence", status, declId: this.pendingDeclId });
|
|
60951
61187
|
}
|
|
60952
61188
|
message(to, content3) {
|
|
60953
61189
|
this.send({ type: "message", to, content: content3 });
|
|
@@ -61277,6 +61513,29 @@ var JefriClient = class _JefriClient {
|
|
|
61277
61513
|
};
|
|
61278
61514
|
var serveDir = path2.join(os.homedir(), ".acp", "serve");
|
|
61279
61515
|
|
|
61516
|
+
// src/stateless-presence.ts
|
|
61517
|
+
var log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
|
|
61518
|
+
async function statelessPresenceBringUp(acp) {
|
|
61519
|
+
try {
|
|
61520
|
+
const hb = await acp.heartbeat();
|
|
61521
|
+
if (hb.state === "signedOff") await acp.presence("online");
|
|
61522
|
+
} catch (e) {
|
|
61523
|
+
log("stateless presence bring-up failed (will retry on sweep):", e?.message ?? e);
|
|
61524
|
+
}
|
|
61525
|
+
}
|
|
61526
|
+
async function statelessSweepBeat(acp, pool, onAuthRevoked) {
|
|
61527
|
+
if (pool.closed || pool.hbSignedOff) return;
|
|
61528
|
+
try {
|
|
61529
|
+
const hb = await acp.heartbeat();
|
|
61530
|
+
if (hb.state === "signedOff") pool.hbSignedOff = true;
|
|
61531
|
+
} catch (e) {
|
|
61532
|
+
if (e instanceof JefriHttpError && (e.status === 401 || e.status === 403)) {
|
|
61533
|
+
onAuthRevoked();
|
|
61534
|
+
return;
|
|
61535
|
+
}
|
|
61536
|
+
}
|
|
61537
|
+
}
|
|
61538
|
+
|
|
61280
61539
|
// src/conn-pool.ts
|
|
61281
61540
|
function attachRef(c, id) {
|
|
61282
61541
|
if (c.closed) return false;
|
|
@@ -61750,6 +62009,27 @@ var debateJobs = /* @__PURE__ */ new Map();
|
|
|
61750
62009
|
var DEBATE_SEQ_TTL_MS = 10 * 60 * 1e3;
|
|
61751
62010
|
|
|
61752
62011
|
// src/tools.ts
|
|
62012
|
+
var isStateless = (c) => c.stateless === true;
|
|
62013
|
+
async function confirmWrite(c, fire) {
|
|
62014
|
+
if (isStateless(c)) {
|
|
62015
|
+
try {
|
|
62016
|
+
await fire();
|
|
62017
|
+
return {};
|
|
62018
|
+
} catch (e2) {
|
|
62019
|
+
return { error: e2?.message ?? String(e2) };
|
|
62020
|
+
}
|
|
62021
|
+
}
|
|
62022
|
+
const err = waitFor(c, "error", () => true, 800);
|
|
62023
|
+
fire();
|
|
62024
|
+
const e = await err;
|
|
62025
|
+
return e ? { error: e.message } : {};
|
|
62026
|
+
}
|
|
62027
|
+
async function fetchHistory(c, convId) {
|
|
62028
|
+
if (isStateless(c)) return c.history(convId);
|
|
62029
|
+
const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
|
|
62030
|
+
c.history(convId);
|
|
62031
|
+
return p;
|
|
62032
|
+
}
|
|
61753
62033
|
var MIME = {
|
|
61754
62034
|
".jpg": "image/jpeg",
|
|
61755
62035
|
".jpeg": "image/jpeg",
|
|
@@ -62085,7 +62365,7 @@ function saveWatermark() {
|
|
|
62085
62365
|
}
|
|
62086
62366
|
function registerAcpTools(server2, ctx) {
|
|
62087
62367
|
const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
|
|
62088
|
-
const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
|
|
62368
|
+
const e2eServer = ENABLE_E2E && !ctx.stateless ? server2 : { registerTool: () => void 0 };
|
|
62089
62369
|
const withClient = async (fn) => {
|
|
62090
62370
|
try {
|
|
62091
62371
|
const c = await ctx.ensureClient();
|
|
@@ -62203,10 +62483,8 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
62203
62483
|
}
|
|
62204
62484
|
},
|
|
62205
62485
|
async ({ to, text: text5 }) => withClient(async (c) => {
|
|
62206
|
-
const
|
|
62207
|
-
|
|
62208
|
-
const e = await err;
|
|
62209
|
-
if (e) return fail(`Message rejected: ${e.message}`);
|
|
62486
|
+
const r = await confirmWrite(c, () => c.message(to, text5));
|
|
62487
|
+
if (r.error) return fail(`Message rejected: ${r.error}`);
|
|
62210
62488
|
return ok3(`Sent to @${to}: "${text5}"`);
|
|
62211
62489
|
})
|
|
62212
62490
|
);
|
|
@@ -62221,8 +62499,9 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
62221
62499
|
}
|
|
62222
62500
|
},
|
|
62223
62501
|
async ({ to, text: text5 }) => withClient(async (c) => {
|
|
62224
|
-
const
|
|
62225
|
-
|
|
62502
|
+
const sc = c;
|
|
62503
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
62504
|
+
await sc.privateMessage(to, text5);
|
|
62226
62505
|
const e = await err;
|
|
62227
62506
|
if (e) return fail(`Private message rejected: ${e.message}`);
|
|
62228
62507
|
return ok3(`Sent private E2E message to @${to}.`);
|
|
@@ -62279,8 +62558,9 @@ ${fp}`);
|
|
|
62279
62558
|
} else {
|
|
62280
62559
|
return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
|
|
62281
62560
|
}
|
|
62282
|
-
|
|
62283
|
-
|
|
62561
|
+
const sc = c;
|
|
62562
|
+
await sc.privateFile(to, name, mime, url);
|
|
62563
|
+
if (caption) await sc.privateMessage(to, caption);
|
|
62284
62564
|
return ok3(`Sent private E2E file to @${to}.`);
|
|
62285
62565
|
})
|
|
62286
62566
|
);
|
|
@@ -62295,8 +62575,9 @@ ${fp}`);
|
|
|
62295
62575
|
}
|
|
62296
62576
|
},
|
|
62297
62577
|
async ({ groupId, text: text5 }) => withClient(async (c) => {
|
|
62298
|
-
const
|
|
62299
|
-
|
|
62578
|
+
const sc = c;
|
|
62579
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
62580
|
+
await sc.privateGroupMessage(groupId, text5);
|
|
62300
62581
|
const e = await err;
|
|
62301
62582
|
if (e) return fail(`Private group message rejected: ${e.message}`);
|
|
62302
62583
|
return ok3(`Sent private E2E group message.`);
|
|
@@ -62411,9 +62692,9 @@ ${psCmd}
|
|
|
62411
62692
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
62412
62693
|
}
|
|
62413
62694
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
62414
|
-
if (rawBytes.length >
|
|
62415
|
-
|
|
62416
|
-
|
|
62695
|
+
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
62696
|
+
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62697
|
+
if (isStateless(c) || rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
62417
62698
|
try {
|
|
62418
62699
|
await uploadFileHttp(c, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
62419
62700
|
} catch (e2) {
|
|
@@ -62445,9 +62726,9 @@ ${psCmd}
|
|
|
62445
62726
|
const known = mimeOf(fileName);
|
|
62446
62727
|
const mime = known === "application/octet-stream" ? "text/plain" : known;
|
|
62447
62728
|
const contentBytes = Buffer.from(content3, "utf8");
|
|
62448
|
-
if (contentBytes.length >
|
|
62449
|
-
|
|
62450
|
-
|
|
62729
|
+
if (contentBytes.length > MAX_UPLOAD_BYTES)
|
|
62730
|
+
return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
62731
|
+
if (isStateless(c) || contentBytes.length > WS_SAFE_FILE_BYTES) {
|
|
62451
62732
|
try {
|
|
62452
62733
|
await uploadFileHttp(c, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
62453
62734
|
} catch (e2) {
|
|
@@ -62578,9 +62859,7 @@ ${psCmd}
|
|
|
62578
62859
|
if (!from && !groupId) return fail("Give `from` (a username, for a DM) or `groupId` (for a group file).");
|
|
62579
62860
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, from);
|
|
62580
62861
|
const where = groupId ? "this group" : `@${from}`;
|
|
62581
|
-
const
|
|
62582
|
-
c.history(convId);
|
|
62583
|
-
const res = await p;
|
|
62862
|
+
const res = await fetchHistory(c, convId);
|
|
62584
62863
|
const files = (res?.messages ?? []).filter((m) => m.kind === "file");
|
|
62585
62864
|
if (!files.length) return fail(`No files in ${where} yet.`);
|
|
62586
62865
|
const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
|
|
@@ -62617,10 +62896,8 @@ It downloads the file to your machine (current folder unless you set a path). Fi
|
|
|
62617
62896
|
inputSchema: { to: external_exports.string().describe("username to connect with") }
|
|
62618
62897
|
},
|
|
62619
62898
|
async ({ to }) => withClient(async (c) => {
|
|
62620
|
-
const
|
|
62621
|
-
|
|
62622
|
-
const e = await err;
|
|
62623
|
-
if (e) return fail(`Request rejected: ${e.message}`);
|
|
62899
|
+
const r = await confirmWrite(c, () => c.addFriend(to));
|
|
62900
|
+
if (r.error) return fail(`Request rejected: ${r.error}`);
|
|
62624
62901
|
return ok3(`\u{1F91D} Connection request sent to @${to}. They (or their owner) must approve before you can message.`);
|
|
62625
62902
|
})
|
|
62626
62903
|
);
|
|
@@ -62916,10 +63193,8 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62916
63193
|
}
|
|
62917
63194
|
},
|
|
62918
63195
|
async ({ groupId, text: text5 }) => withClient(async (c) => {
|
|
62919
|
-
const
|
|
62920
|
-
|
|
62921
|
-
const e = await err;
|
|
62922
|
-
if (e) return fail(`Couldn't post to the group: ${e.message}`);
|
|
63196
|
+
const r = await confirmWrite(c, () => c.groupMessage(groupId, text5));
|
|
63197
|
+
if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
|
|
62923
63198
|
return ok3(`Posted to the group.`);
|
|
62924
63199
|
})
|
|
62925
63200
|
);
|
|
@@ -62979,9 +63254,9 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62979
63254
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
62980
63255
|
}
|
|
62981
63256
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
62982
|
-
if (rawBytes.length >
|
|
62983
|
-
|
|
62984
|
-
|
|
63257
|
+
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
63258
|
+
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
63259
|
+
if (isStateless(c) || rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
62985
63260
|
try {
|
|
62986
63261
|
await uploadFileHttp(c, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
62987
63262
|
} catch (e2) {
|
|
@@ -63012,9 +63287,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63012
63287
|
if (!other && !groupId) return fail("Give `with` (a username, for a DM) or `groupId` (for a group).");
|
|
63013
63288
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, other);
|
|
63014
63289
|
const label = groupId ? "this group" : `@${other}`;
|
|
63015
|
-
const
|
|
63016
|
-
c.history(convId);
|
|
63017
|
-
const res = await p;
|
|
63290
|
+
const res = await fetchHistory(c, convId);
|
|
63018
63291
|
const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
|
|
63019
63292
|
if (!msgs.length) return ok3(`No messages yet in ${label}.`);
|
|
63020
63293
|
const lines = await Promise.all(msgs.map(async (m) => {
|
|
@@ -63045,10 +63318,14 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63045
63318
|
inputSchema: { query: external_exports.string() }
|
|
63046
63319
|
},
|
|
63047
63320
|
async ({ query }) => withClient(async (c) => {
|
|
63048
|
-
|
|
63049
|
-
c
|
|
63050
|
-
|
|
63051
|
-
|
|
63321
|
+
let results;
|
|
63322
|
+
if (isStateless(c)) {
|
|
63323
|
+
results = (await c.search(query))?.results ?? [];
|
|
63324
|
+
} else {
|
|
63325
|
+
const res = waitFor(c, "search_results", () => true);
|
|
63326
|
+
c.search(query);
|
|
63327
|
+
results = (await res)?.results ?? [];
|
|
63328
|
+
}
|
|
63052
63329
|
if (!results.length) return ok3(`No agents matched "${query}".`);
|
|
63053
63330
|
return ok3(
|
|
63054
63331
|
results.map((r) => `\u2022 @${r.username} \u2014 ${r.displayName} (${r.status})${r.owner ? ` \xB7 by @${r.owner}` : ""} \xB7 ${r.tags.join(", ")}`).join("\n")
|
|
@@ -63065,7 +63342,8 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63065
63342
|
}
|
|
63066
63343
|
},
|
|
63067
63344
|
async ({ status }) => withClient(async (c) => {
|
|
63068
|
-
c.presence(status);
|
|
63345
|
+
const r = await confirmWrite(c, () => c.presence(status));
|
|
63346
|
+
if (r.error) return fail(`Couldn't set status: ${r.error}`);
|
|
63069
63347
|
return ok3(`Status set to ${status}.`);
|
|
63070
63348
|
})
|
|
63071
63349
|
);
|
|
@@ -63257,7 +63535,8 @@ function sweepIdleSessions(map3) {
|
|
|
63257
63535
|
// src/http.ts
|
|
63258
63536
|
var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
|
|
63259
63537
|
var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
|
|
63260
|
-
var
|
|
63538
|
+
var STATELESS = process.env.JEFRI_STATELESS === "true";
|
|
63539
|
+
var log2 = (...a) => console.error("[jefrichat-mcp-http]", ...a);
|
|
63261
63540
|
var sessions = /* @__PURE__ */ new Map();
|
|
63262
63541
|
var HOST_FRESH_MS = Number(process.env.MCP_HOST_FRESH_MS ?? 10 * 60 * 1e3);
|
|
63263
63542
|
var hubPool = createKeyedPool();
|
|
@@ -63292,7 +63571,7 @@ function closeSession(sid, reason) {
|
|
|
63292
63571
|
if (detachRef(pooled, sid)) teardownPool(s.poolKey, pooled, "last session gone");
|
|
63293
63572
|
else if (s.host) announcePoolHosts(s.poolKey, pooled);
|
|
63294
63573
|
}
|
|
63295
|
-
|
|
63574
|
+
log2(`session ${sid.slice(0, 8)}\u2026 closed (${reason}) (${sessions.size} active)`);
|
|
63296
63575
|
}
|
|
63297
63576
|
function teardownPool(poolKey, pooled, reason) {
|
|
63298
63577
|
pooled.closed = true;
|
|
@@ -63301,9 +63580,10 @@ function teardownPool(poolKey, pooled, reason) {
|
|
|
63301
63580
|
pooled.acp.close();
|
|
63302
63581
|
} catch {
|
|
63303
63582
|
}
|
|
63304
|
-
|
|
63583
|
+
log2(`hub connection ${poolKey.slice(0, 8)}\u2026 closed (${reason})`);
|
|
63305
63584
|
}
|
|
63306
63585
|
function announcePoolHosts(poolKey, pooled) {
|
|
63586
|
+
if (STATELESS) return;
|
|
63307
63587
|
const joined = aggregateFreshHosts(sessions.values(), poolKey, Date.now(), HOST_FRESH_MS);
|
|
63308
63588
|
if (joined === pooled.lastHosts) return;
|
|
63309
63589
|
pooled.lastHosts = joined;
|
|
@@ -63318,6 +63598,12 @@ function closePool(poolKey, reason) {
|
|
|
63318
63598
|
}
|
|
63319
63599
|
async function acquirePooledConn(token, poolKey) {
|
|
63320
63600
|
return hubPool.acquire(poolKey, async () => {
|
|
63601
|
+
if (STATELESS) {
|
|
63602
|
+
const acp2 = new JefriHttpClient({ server: HUB, token });
|
|
63603
|
+
await acp2.whoami();
|
|
63604
|
+
await statelessPresenceBringUp(acp2);
|
|
63605
|
+
return { acp: acp2, inbox: [], refs: /* @__PURE__ */ new Set(), closed: false };
|
|
63606
|
+
}
|
|
63321
63607
|
const acp = await JefriClient.connect({ server: HUB, token, status: "online" });
|
|
63322
63608
|
const inbox = [];
|
|
63323
63609
|
if (acp.identity) attachInbox(acp, inbox, acp.identity.username);
|
|
@@ -63344,7 +63630,10 @@ setInterval(() => {
|
|
|
63344
63630
|
if (seenPools.has(s.poolKey)) continue;
|
|
63345
63631
|
seenPools.add(s.poolKey);
|
|
63346
63632
|
const pooled = hubPool.get(s.poolKey);
|
|
63347
|
-
if (pooled)
|
|
63633
|
+
if (!pooled) continue;
|
|
63634
|
+
announcePoolHosts(s.poolKey, pooled);
|
|
63635
|
+
if (STATELESS)
|
|
63636
|
+
void statelessSweepBeat(pooled.acp, pooled, () => closePool(s.poolKey, "auth revoked"));
|
|
63348
63637
|
}
|
|
63349
63638
|
for (const [ip, e] of initHits) {
|
|
63350
63639
|
if (now > e.resetAt) initHits.delete(ip);
|
|
@@ -63403,6 +63692,7 @@ var health = (_req, res) => res.status(draining ? 503 : 200).json({
|
|
|
63403
63692
|
draining,
|
|
63404
63693
|
service: "jefrichat-mcp-http",
|
|
63405
63694
|
hub: HUB,
|
|
63695
|
+
stateless: STATELESS,
|
|
63406
63696
|
sessions: sessions.size
|
|
63407
63697
|
});
|
|
63408
63698
|
app.get("/health", health);
|
|
@@ -63473,7 +63763,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
63473
63763
|
try {
|
|
63474
63764
|
pooled = await acquirePooledConn(token, poolKey);
|
|
63475
63765
|
} catch (e) {
|
|
63476
|
-
if (e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED") {
|
|
63766
|
+
if (e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED" || e instanceof JefriHttpError && (e.status === 401 || e.status === 403)) {
|
|
63477
63767
|
res.status(401).json({
|
|
63478
63768
|
jsonrpc: "2.0",
|
|
63479
63769
|
error: {
|
|
@@ -63487,6 +63777,10 @@ app.post("/mcp", async (req, res) => {
|
|
|
63487
63777
|
res.status(502).json({ jsonrpc: "2.0", error: { code: -32002, message: `Hub unreachable: ${e?.message ?? e}` }, id: null });
|
|
63488
63778
|
return;
|
|
63489
63779
|
}
|
|
63780
|
+
if (STATELESS && pooled.hbSignedOff) {
|
|
63781
|
+
pooled.hbSignedOff = false;
|
|
63782
|
+
void statelessPresenceBringUp(pooled.acp);
|
|
63783
|
+
}
|
|
63490
63784
|
const acp = pooled.acp;
|
|
63491
63785
|
if (!acp.identity) {
|
|
63492
63786
|
closePool(poolKey, "no identity");
|
|
@@ -63498,7 +63792,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
63498
63792
|
return;
|
|
63499
63793
|
}
|
|
63500
63794
|
const mcp = new McpServer({ name: "jefrichat", version: "0.1.0" });
|
|
63501
|
-
registerAcpTools(mcp, { ensureClient: async () => acp, inbox: pooled.inbox, serverUrl: HUB, local: false });
|
|
63795
|
+
registerAcpTools(mcp, { ensureClient: async () => acp, inbox: pooled.inbox, serverUrl: HUB, local: false, stateless: STATELESS });
|
|
63502
63796
|
let mySid;
|
|
63503
63797
|
mcp.server.oninitialized = () => {
|
|
63504
63798
|
try {
|
|
@@ -63510,14 +63804,14 @@ app.post("/mcp", async (req, res) => {
|
|
|
63510
63804
|
}
|
|
63511
63805
|
};
|
|
63512
63806
|
const transport = new StreamableHTTPServerTransport({
|
|
63513
|
-
sessionIdGenerator: () =>
|
|
63807
|
+
sessionIdGenerator: () => randomUUID3(),
|
|
63514
63808
|
onsessioninitialized: (sid) => {
|
|
63515
63809
|
if (pooled.closed) {
|
|
63516
63810
|
try {
|
|
63517
63811
|
transport.close?.();
|
|
63518
63812
|
} catch {
|
|
63519
63813
|
}
|
|
63520
|
-
|
|
63814
|
+
log2(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
|
|
63521
63815
|
return;
|
|
63522
63816
|
}
|
|
63523
63817
|
const meName = acp.identity.username;
|
|
@@ -63542,19 +63836,19 @@ app.post("/mcp", async (req, res) => {
|
|
|
63542
63836
|
transport.close?.();
|
|
63543
63837
|
} catch {
|
|
63544
63838
|
}
|
|
63545
|
-
|
|
63839
|
+
log2(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
|
|
63546
63840
|
return;
|
|
63547
63841
|
}
|
|
63548
63842
|
sessions.set(sid, { transport, acp, lastActive: Date.now(), tokenHash: sha256(token), inflight: 0, poolKey });
|
|
63549
63843
|
mySid = sid;
|
|
63550
|
-
|
|
63844
|
+
log2(`session ${sid.slice(0, 8)}\u2026 open as @${acp.identity.username} (${sessions.size} active, ${pooled.refs.size} on this connection)`);
|
|
63551
63845
|
}
|
|
63552
63846
|
});
|
|
63553
63847
|
transport.onclose = () => {
|
|
63554
63848
|
const sid = transport.sessionId;
|
|
63555
63849
|
if (sid) closeSession(sid, "client closed");
|
|
63556
63850
|
};
|
|
63557
|
-
const reservation = "init:" +
|
|
63851
|
+
const reservation = "init:" + randomUUID3();
|
|
63558
63852
|
pooled.refs.add(reservation);
|
|
63559
63853
|
try {
|
|
63560
63854
|
await mcp.connect(transport);
|
package/dist/index.js
CHANGED
|
@@ -26890,7 +26890,7 @@ var wrapper_default = import_websocket.default;
|
|
|
26890
26890
|
import fs from "node:fs";
|
|
26891
26891
|
import os from "node:os";
|
|
26892
26892
|
import path2 from "node:path";
|
|
26893
|
-
import { webcrypto } from "node:crypto";
|
|
26893
|
+
import { webcrypto, randomUUID } from "node:crypto";
|
|
26894
26894
|
|
|
26895
26895
|
// ../../node_modules/.pnpm/bail@2.0.2/node_modules/bail/index.js
|
|
26896
26896
|
function bail(error2) {
|
|
@@ -38625,6 +38625,9 @@ var b64 = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8
|
|
|
38625
38625
|
var fromB64 = (s) => new Uint8Array(Buffer.from(s, "base64"));
|
|
38626
38626
|
var safeName = (s) => s.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
38627
38627
|
var canonicalJwk = (jwk) => JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
|
|
38628
|
+
function newDeclId() {
|
|
38629
|
+
return "d-" + randomUUID();
|
|
38630
|
+
}
|
|
38628
38631
|
var JefriAuthError = class extends Error {
|
|
38629
38632
|
code = "ACP_AUTH_FAILED";
|
|
38630
38633
|
constructor(message = "invalid or expired Jefri Chat token") {
|
|
@@ -38659,6 +38662,9 @@ var JefriClient = class _JefriClient {
|
|
|
38659
38662
|
* life of the process, stomping whatever a newer device had chosen since.
|
|
38660
38663
|
* The same stale-overwrite, one level down. */
|
|
38661
38664
|
pendingPresence = null;
|
|
38665
|
+
/** The id minted for the pending declaration. Its ECHO on a presence_update
|
|
38666
|
+
* is the only genuine acknowledgment — see notePresenceAck. */
|
|
38667
|
+
pendingDeclId = null;
|
|
38662
38668
|
/** The self statusRev BASELINE captured when the pending declaration was made.
|
|
38663
38669
|
* An ack must be CAUSALLY newer than this — the audit reproduced a delayed
|
|
38664
38670
|
* rev-9 event spending intent declared at a rev-10 baseline, after which the
|
|
@@ -38684,11 +38690,12 @@ var JefriClient = class _JefriClient {
|
|
|
38684
38690
|
* deleted, which is how the previous round's test enshrined the bug. */
|
|
38685
38691
|
notePresenceAck(ev) {
|
|
38686
38692
|
if (ev?.event !== "presence_update" || ev.username !== this.identity?.username) return;
|
|
38687
|
-
const frontier = this.selfStatusRev;
|
|
38688
38693
|
if (typeof ev.rev === "number" && ev.rev > this.selfStatusRev) this.selfStatusRev = ev.rev;
|
|
38689
|
-
if (this.pendingPresence === null
|
|
38690
|
-
if (
|
|
38691
|
-
|
|
38694
|
+
if (this.pendingPresence === null) return;
|
|
38695
|
+
if (this.pendingDeclId !== null && ev.declId === this.pendingDeclId) {
|
|
38696
|
+
this.pendingPresence = null;
|
|
38697
|
+
this.pendingDeclId = null;
|
|
38698
|
+
}
|
|
38692
38699
|
}
|
|
38693
38700
|
clientInfo;
|
|
38694
38701
|
// handshake, re-sent on reconnect
|
|
@@ -38785,7 +38792,7 @@ var JefriClient = class _JefriClient {
|
|
|
38785
38792
|
this.startPing();
|
|
38786
38793
|
try {
|
|
38787
38794
|
if (this.pendingPresence !== null)
|
|
38788
|
-
this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence }));
|
|
38795
|
+
this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence, declId: this.pendingDeclId ?? void 0 }));
|
|
38789
38796
|
if (this.clientInfo)
|
|
38790
38797
|
this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
|
|
38791
38798
|
} catch {
|
|
@@ -38913,7 +38920,8 @@ var JefriClient = class _JefriClient {
|
|
|
38913
38920
|
presence(status) {
|
|
38914
38921
|
this.pendingPresence = status;
|
|
38915
38922
|
this.pendingBaselineRev = this.selfStatusRev;
|
|
38916
|
-
this.
|
|
38923
|
+
this.pendingDeclId = newDeclId();
|
|
38924
|
+
this.send({ type: "presence", status, declId: this.pendingDeclId });
|
|
38917
38925
|
}
|
|
38918
38926
|
message(to, content3) {
|
|
38919
38927
|
this.send({ type: "message", to, content: content3 });
|
|
@@ -40533,6 +40541,27 @@ async function runOnceForMessage(m) {
|
|
|
40533
40541
|
}
|
|
40534
40542
|
|
|
40535
40543
|
// src/tools.ts
|
|
40544
|
+
var isStateless = (c2) => c2.stateless === true;
|
|
40545
|
+
async function confirmWrite(c2, fire) {
|
|
40546
|
+
if (isStateless(c2)) {
|
|
40547
|
+
try {
|
|
40548
|
+
await fire();
|
|
40549
|
+
return {};
|
|
40550
|
+
} catch (e2) {
|
|
40551
|
+
return { error: e2?.message ?? String(e2) };
|
|
40552
|
+
}
|
|
40553
|
+
}
|
|
40554
|
+
const err = waitFor(c2, "error", () => true, 800);
|
|
40555
|
+
fire();
|
|
40556
|
+
const e = await err;
|
|
40557
|
+
return e ? { error: e.message } : {};
|
|
40558
|
+
}
|
|
40559
|
+
async function fetchHistory2(c2, convId) {
|
|
40560
|
+
if (isStateless(c2)) return c2.history(convId);
|
|
40561
|
+
const p = waitFor(c2, "history", (e) => e.conversationId === convId, 4e3);
|
|
40562
|
+
c2.history(convId);
|
|
40563
|
+
return p;
|
|
40564
|
+
}
|
|
40536
40565
|
var MIME = {
|
|
40537
40566
|
".jpg": "image/jpeg",
|
|
40538
40567
|
".jpeg": "image/jpeg",
|
|
@@ -40871,7 +40900,7 @@ function saveWatermark() {
|
|
|
40871
40900
|
}
|
|
40872
40901
|
function registerAcpTools(server2, ctx) {
|
|
40873
40902
|
const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
|
|
40874
|
-
const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
|
|
40903
|
+
const e2eServer = ENABLE_E2E && !ctx.stateless ? server2 : { registerTool: () => void 0 };
|
|
40875
40904
|
const withClient = async (fn) => {
|
|
40876
40905
|
try {
|
|
40877
40906
|
const c2 = await ctx.ensureClient();
|
|
@@ -40989,10 +41018,8 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
40989
41018
|
}
|
|
40990
41019
|
},
|
|
40991
41020
|
async ({ to, text: text5 }) => withClient(async (c2) => {
|
|
40992
|
-
const
|
|
40993
|
-
|
|
40994
|
-
const e = await err;
|
|
40995
|
-
if (e) return fail(`Message rejected: ${e.message}`);
|
|
41021
|
+
const r = await confirmWrite(c2, () => c2.message(to, text5));
|
|
41022
|
+
if (r.error) return fail(`Message rejected: ${r.error}`);
|
|
40996
41023
|
return ok3(`Sent to @${to}: "${text5}"`);
|
|
40997
41024
|
})
|
|
40998
41025
|
);
|
|
@@ -41007,8 +41034,9 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
41007
41034
|
}
|
|
41008
41035
|
},
|
|
41009
41036
|
async ({ to, text: text5 }) => withClient(async (c2) => {
|
|
41010
|
-
const
|
|
41011
|
-
|
|
41037
|
+
const sc = c2;
|
|
41038
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
41039
|
+
await sc.privateMessage(to, text5);
|
|
41012
41040
|
const e = await err;
|
|
41013
41041
|
if (e) return fail(`Private message rejected: ${e.message}`);
|
|
41014
41042
|
return ok3(`Sent private E2E message to @${to}.`);
|
|
@@ -41065,8 +41093,9 @@ ${fp}`);
|
|
|
41065
41093
|
} else {
|
|
41066
41094
|
return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
|
|
41067
41095
|
}
|
|
41068
|
-
|
|
41069
|
-
|
|
41096
|
+
const sc = c2;
|
|
41097
|
+
await sc.privateFile(to, name, mime, url);
|
|
41098
|
+
if (caption) await sc.privateMessage(to, caption);
|
|
41070
41099
|
return ok3(`Sent private E2E file to @${to}.`);
|
|
41071
41100
|
})
|
|
41072
41101
|
);
|
|
@@ -41081,8 +41110,9 @@ ${fp}`);
|
|
|
41081
41110
|
}
|
|
41082
41111
|
},
|
|
41083
41112
|
async ({ groupId, text: text5 }) => withClient(async (c2) => {
|
|
41084
|
-
const
|
|
41085
|
-
|
|
41113
|
+
const sc = c2;
|
|
41114
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
41115
|
+
await sc.privateGroupMessage(groupId, text5);
|
|
41086
41116
|
const e = await err;
|
|
41087
41117
|
if (e) return fail(`Private group message rejected: ${e.message}`);
|
|
41088
41118
|
return ok3(`Sent private E2E group message.`);
|
|
@@ -41197,9 +41227,9 @@ ${psCmd}
|
|
|
41197
41227
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
41198
41228
|
}
|
|
41199
41229
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
41200
|
-
if (rawBytes.length >
|
|
41201
|
-
|
|
41202
|
-
|
|
41230
|
+
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
41231
|
+
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
41232
|
+
if (isStateless(c2) || rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
41203
41233
|
try {
|
|
41204
41234
|
await uploadFileHttp(c2, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
41205
41235
|
} catch (e2) {
|
|
@@ -41231,9 +41261,9 @@ ${psCmd}
|
|
|
41231
41261
|
const known = mimeOf(fileName);
|
|
41232
41262
|
const mime = known === "application/octet-stream" ? "text/plain" : known;
|
|
41233
41263
|
const contentBytes = Buffer.from(content3, "utf8");
|
|
41234
|
-
if (contentBytes.length >
|
|
41235
|
-
|
|
41236
|
-
|
|
41264
|
+
if (contentBytes.length > MAX_UPLOAD_BYTES)
|
|
41265
|
+
return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
41266
|
+
if (isStateless(c2) || contentBytes.length > WS_SAFE_FILE_BYTES) {
|
|
41237
41267
|
try {
|
|
41238
41268
|
await uploadFileHttp(c2, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
41239
41269
|
} catch (e2) {
|
|
@@ -41364,9 +41394,7 @@ ${psCmd}
|
|
|
41364
41394
|
if (!from && !groupId) return fail("Give `from` (a username, for a DM) or `groupId` (for a group file).");
|
|
41365
41395
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c2.identity.username, from);
|
|
41366
41396
|
const where = groupId ? "this group" : `@${from}`;
|
|
41367
|
-
const
|
|
41368
|
-
c2.history(convId);
|
|
41369
|
-
const res = await p;
|
|
41397
|
+
const res = await fetchHistory2(c2, convId);
|
|
41370
41398
|
const files = (res?.messages ?? []).filter((m) => m.kind === "file");
|
|
41371
41399
|
if (!files.length) return fail(`No files in ${where} yet.`);
|
|
41372
41400
|
const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
|
|
@@ -41403,10 +41431,8 @@ It downloads the file to your machine (current folder unless you set a path). Fi
|
|
|
41403
41431
|
inputSchema: { to: external_exports.string().describe("username to connect with") }
|
|
41404
41432
|
},
|
|
41405
41433
|
async ({ to }) => withClient(async (c2) => {
|
|
41406
|
-
const
|
|
41407
|
-
|
|
41408
|
-
const e = await err;
|
|
41409
|
-
if (e) return fail(`Request rejected: ${e.message}`);
|
|
41434
|
+
const r = await confirmWrite(c2, () => c2.addFriend(to));
|
|
41435
|
+
if (r.error) return fail(`Request rejected: ${r.error}`);
|
|
41410
41436
|
return ok3(`\u{1F91D} Connection request sent to @${to}. They (or their owner) must approve before you can message.`);
|
|
41411
41437
|
})
|
|
41412
41438
|
);
|
|
@@ -41702,10 +41728,8 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41702
41728
|
}
|
|
41703
41729
|
},
|
|
41704
41730
|
async ({ groupId, text: text5 }) => withClient(async (c2) => {
|
|
41705
|
-
const
|
|
41706
|
-
|
|
41707
|
-
const e = await err;
|
|
41708
|
-
if (e) return fail(`Couldn't post to the group: ${e.message}`);
|
|
41731
|
+
const r = await confirmWrite(c2, () => c2.groupMessage(groupId, text5));
|
|
41732
|
+
if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
|
|
41709
41733
|
return ok3(`Posted to the group.`);
|
|
41710
41734
|
})
|
|
41711
41735
|
);
|
|
@@ -41765,9 +41789,9 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41765
41789
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
41766
41790
|
}
|
|
41767
41791
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
41768
|
-
if (rawBytes.length >
|
|
41769
|
-
|
|
41770
|
-
|
|
41792
|
+
if (rawBytes.length > MAX_UPLOAD_BYTES)
|
|
41793
|
+
return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
41794
|
+
if (isStateless(c2) || rawBytes.length > WS_SAFE_FILE_BYTES) {
|
|
41771
41795
|
try {
|
|
41772
41796
|
await uploadFileHttp(c2, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
41773
41797
|
} catch (e2) {
|
|
@@ -41798,9 +41822,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41798
41822
|
if (!other && !groupId) return fail("Give `with` (a username, for a DM) or `groupId` (for a group).");
|
|
41799
41823
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c2.identity.username, other);
|
|
41800
41824
|
const label = groupId ? "this group" : `@${other}`;
|
|
41801
|
-
const
|
|
41802
|
-
c2.history(convId);
|
|
41803
|
-
const res = await p;
|
|
41825
|
+
const res = await fetchHistory2(c2, convId);
|
|
41804
41826
|
const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
|
|
41805
41827
|
if (!msgs.length) return ok3(`No messages yet in ${label}.`);
|
|
41806
41828
|
const lines = await Promise.all(msgs.map(async (m) => {
|
|
@@ -41831,10 +41853,14 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41831
41853
|
inputSchema: { query: external_exports.string() }
|
|
41832
41854
|
},
|
|
41833
41855
|
async ({ query }) => withClient(async (c2) => {
|
|
41834
|
-
|
|
41835
|
-
c2
|
|
41836
|
-
|
|
41837
|
-
|
|
41856
|
+
let results;
|
|
41857
|
+
if (isStateless(c2)) {
|
|
41858
|
+
results = (await c2.search(query))?.results ?? [];
|
|
41859
|
+
} else {
|
|
41860
|
+
const res = waitFor(c2, "search_results", () => true);
|
|
41861
|
+
c2.search(query);
|
|
41862
|
+
results = (await res)?.results ?? [];
|
|
41863
|
+
}
|
|
41838
41864
|
if (!results.length) return ok3(`No agents matched "${query}".`);
|
|
41839
41865
|
return ok3(
|
|
41840
41866
|
results.map((r) => `\u2022 @${r.username} \u2014 ${r.displayName} (${r.status})${r.owner ? ` \xB7 by @${r.owner}` : ""} \xB7 ${r.tags.join(", ")}`).join("\n")
|
|
@@ -41851,7 +41877,8 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41851
41877
|
}
|
|
41852
41878
|
},
|
|
41853
41879
|
async ({ status }) => withClient(async (c2) => {
|
|
41854
|
-
c2.presence(status);
|
|
41880
|
+
const r = await confirmWrite(c2, () => c2.presence(status));
|
|
41881
|
+
if (r.error) return fail(`Couldn't set status: ${r.error}`);
|
|
41855
41882
|
return ok3(`Status set to ${status}.`);
|
|
41856
41883
|
})
|
|
41857
41884
|
);
|
package/package.json
CHANGED