jefrichat-mcp 0.48.8 → 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 +415 -68
- package/dist/index.js +117 -45
- 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") {
|
|
@@ -60682,8 +60913,52 @@ var JefriClient = class _JefriClient {
|
|
|
60682
60913
|
// heartbeat: did we get a pong since the last ping?
|
|
60683
60914
|
hasConnected = false;
|
|
60684
60915
|
// armed for auto-reconnect only after the hub `registered` us
|
|
60685
|
-
|
|
60686
|
-
|
|
60916
|
+
/** UNACKNOWLEDGED presence intent, or null. Set by presence(), cleared when
|
|
60917
|
+
* the hub's own presence_update for this identity confirms the declaration
|
|
60918
|
+
* landed. Null on reconnect means: send nothing and let the hub's persisted
|
|
60919
|
+
* answer stand.
|
|
60920
|
+
*
|
|
60921
|
+
* Acknowledgment is the point, and it is what the first fix missed: a value
|
|
60922
|
+
* kept after delivery means "ever declared", not "pending" — so a client
|
|
60923
|
+
* that once declared away kept re-asserting it on every reconnect for the
|
|
60924
|
+
* life of the process, stomping whatever a newer device had chosen since.
|
|
60925
|
+
* The same stale-overwrite, one level down. */
|
|
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;
|
|
60930
|
+
/** The self statusRev BASELINE captured when the pending declaration was made.
|
|
60931
|
+
* An ack must be CAUSALLY newer than this — the audit reproduced a delayed
|
|
60932
|
+
* rev-9 event spending intent declared at a rev-10 baseline, after which the
|
|
60933
|
+
* server had already moved on. Identity + status matching alone cannot order
|
|
60934
|
+
* events; the revision can. */
|
|
60935
|
+
pendingBaselineRev = 0;
|
|
60936
|
+
/** Highest revision seen for our own status, from registered and from our own
|
|
60937
|
+
* presence_update events. */
|
|
60938
|
+
selfStatusRev = 0;
|
|
60939
|
+
/** Would a reconnect re-send a presence frame? True only while a declaration
|
|
60940
|
+
* is UNACKNOWLEDGED. Exposed for tests: the wire behaviour needs a live hub,
|
|
60941
|
+
* but the rule is checkable without one. */
|
|
60942
|
+
wouldReassertPresence() {
|
|
60943
|
+
return this.pendingPresence !== null;
|
|
60944
|
+
}
|
|
60945
|
+
/** The acknowledgment that turns "pending" back into nothing: the hub
|
|
60946
|
+
* broadcasts presence_update to the declaring identity too, so our own
|
|
60947
|
+
* username carrying our pending status means the declaration LANDED — and
|
|
60948
|
+
* re-asserting it on a later reconnect would stomp whatever a newer device
|
|
60949
|
+
* declares afterwards. A METHOD, not inline in the socket handler, so tests
|
|
60950
|
+
* drive the rule production runs instead of a copy of it — a test-local
|
|
60951
|
+
* re-implementation of this exact logic passed with the real clearing
|
|
60952
|
+
* deleted, which is how the previous round's test enshrined the bug. */
|
|
60953
|
+
notePresenceAck(ev) {
|
|
60954
|
+
if (ev?.event !== "presence_update" || ev.username !== this.identity?.username) return;
|
|
60955
|
+
if (typeof ev.rev === "number" && ev.rev > this.selfStatusRev) this.selfStatusRev = ev.rev;
|
|
60956
|
+
if (this.pendingPresence === null) return;
|
|
60957
|
+
if (this.pendingDeclId !== null && ev.declId === this.pendingDeclId) {
|
|
60958
|
+
this.pendingPresence = null;
|
|
60959
|
+
this.pendingDeclId = null;
|
|
60960
|
+
}
|
|
60961
|
+
}
|
|
60687
60962
|
clientInfo;
|
|
60688
60963
|
// handshake, re-sent on reconnect
|
|
60689
60964
|
/** Is the hub socket usable RIGHT NOW?
|
|
@@ -60778,7 +61053,8 @@ var JefriClient = class _JefriClient {
|
|
|
60778
61053
|
this.ws.on("open", () => {
|
|
60779
61054
|
this.startPing();
|
|
60780
61055
|
try {
|
|
60781
|
-
this.
|
|
61056
|
+
if (this.pendingPresence !== null)
|
|
61057
|
+
this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence, declId: this.pendingDeclId ?? void 0 }));
|
|
60782
61058
|
if (this.clientInfo)
|
|
60783
61059
|
this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
|
|
60784
61060
|
} catch {
|
|
@@ -60807,12 +61083,15 @@ var JefriClient = class _JefriClient {
|
|
|
60807
61083
|
}
|
|
60808
61084
|
if (ev.event === "registered") {
|
|
60809
61085
|
this.identity = ev.identity;
|
|
61086
|
+
const rev = ev.identity?.statusRev;
|
|
61087
|
+
if (typeof rev === "number" && rev > this.selfStatusRev) this.selfStatusRev = rev;
|
|
60810
61088
|
this.hasConnected = true;
|
|
60811
61089
|
this.reconnectAttempts = 0;
|
|
60812
61090
|
}
|
|
60813
61091
|
if (ev.event === "error" && /auth/i.test(ev.message ?? "")) {
|
|
60814
61092
|
this.authFailed = true;
|
|
60815
61093
|
}
|
|
61094
|
+
this.notePresenceAck(ev);
|
|
60816
61095
|
this.emit(ev.event, ev);
|
|
60817
61096
|
this.emit("*", ev);
|
|
60818
61097
|
});
|
|
@@ -60901,8 +61180,10 @@ var JefriClient = class _JefriClient {
|
|
|
60901
61180
|
}
|
|
60902
61181
|
// --- actions -------------------------------------------------------------
|
|
60903
61182
|
presence(status) {
|
|
60904
|
-
this.
|
|
60905
|
-
this.
|
|
61183
|
+
this.pendingPresence = status;
|
|
61184
|
+
this.pendingBaselineRev = this.selfStatusRev;
|
|
61185
|
+
this.pendingDeclId = newDeclId();
|
|
61186
|
+
this.send({ type: "presence", status, declId: this.pendingDeclId });
|
|
60906
61187
|
}
|
|
60907
61188
|
message(to, content3) {
|
|
60908
61189
|
this.send({ type: "message", to, content: content3 });
|
|
@@ -61232,6 +61513,29 @@ var JefriClient = class _JefriClient {
|
|
|
61232
61513
|
};
|
|
61233
61514
|
var serveDir = path2.join(os.homedir(), ".acp", "serve");
|
|
61234
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
|
+
|
|
61235
61539
|
// src/conn-pool.ts
|
|
61236
61540
|
function attachRef(c, id) {
|
|
61237
61541
|
if (c.closed) return false;
|
|
@@ -61705,6 +62009,27 @@ var debateJobs = /* @__PURE__ */ new Map();
|
|
|
61705
62009
|
var DEBATE_SEQ_TTL_MS = 10 * 60 * 1e3;
|
|
61706
62010
|
|
|
61707
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
|
+
}
|
|
61708
62033
|
var MIME = {
|
|
61709
62034
|
".jpg": "image/jpeg",
|
|
61710
62035
|
".jpeg": "image/jpeg",
|
|
@@ -62040,7 +62365,7 @@ function saveWatermark() {
|
|
|
62040
62365
|
}
|
|
62041
62366
|
function registerAcpTools(server2, ctx) {
|
|
62042
62367
|
const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
|
|
62043
|
-
const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
|
|
62368
|
+
const e2eServer = ENABLE_E2E && !ctx.stateless ? server2 : { registerTool: () => void 0 };
|
|
62044
62369
|
const withClient = async (fn) => {
|
|
62045
62370
|
try {
|
|
62046
62371
|
const c = await ctx.ensureClient();
|
|
@@ -62158,10 +62483,8 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
62158
62483
|
}
|
|
62159
62484
|
},
|
|
62160
62485
|
async ({ to, text: text5 }) => withClient(async (c) => {
|
|
62161
|
-
const
|
|
62162
|
-
|
|
62163
|
-
const e = await err;
|
|
62164
|
-
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}`);
|
|
62165
62488
|
return ok3(`Sent to @${to}: "${text5}"`);
|
|
62166
62489
|
})
|
|
62167
62490
|
);
|
|
@@ -62176,8 +62499,9 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
62176
62499
|
}
|
|
62177
62500
|
},
|
|
62178
62501
|
async ({ to, text: text5 }) => withClient(async (c) => {
|
|
62179
|
-
const
|
|
62180
|
-
|
|
62502
|
+
const sc = c;
|
|
62503
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
62504
|
+
await sc.privateMessage(to, text5);
|
|
62181
62505
|
const e = await err;
|
|
62182
62506
|
if (e) return fail(`Private message rejected: ${e.message}`);
|
|
62183
62507
|
return ok3(`Sent private E2E message to @${to}.`);
|
|
@@ -62234,8 +62558,9 @@ ${fp}`);
|
|
|
62234
62558
|
} else {
|
|
62235
62559
|
return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
|
|
62236
62560
|
}
|
|
62237
|
-
|
|
62238
|
-
|
|
62561
|
+
const sc = c;
|
|
62562
|
+
await sc.privateFile(to, name, mime, url);
|
|
62563
|
+
if (caption) await sc.privateMessage(to, caption);
|
|
62239
62564
|
return ok3(`Sent private E2E file to @${to}.`);
|
|
62240
62565
|
})
|
|
62241
62566
|
);
|
|
@@ -62250,8 +62575,9 @@ ${fp}`);
|
|
|
62250
62575
|
}
|
|
62251
62576
|
},
|
|
62252
62577
|
async ({ groupId, text: text5 }) => withClient(async (c) => {
|
|
62253
|
-
const
|
|
62254
|
-
|
|
62578
|
+
const sc = c;
|
|
62579
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
62580
|
+
await sc.privateGroupMessage(groupId, text5);
|
|
62255
62581
|
const e = await err;
|
|
62256
62582
|
if (e) return fail(`Private group message rejected: ${e.message}`);
|
|
62257
62583
|
return ok3(`Sent private E2E group message.`);
|
|
@@ -62366,9 +62692,9 @@ ${psCmd}
|
|
|
62366
62692
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
62367
62693
|
}
|
|
62368
62694
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
62369
|
-
if (rawBytes.length >
|
|
62370
|
-
|
|
62371
|
-
|
|
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) {
|
|
62372
62698
|
try {
|
|
62373
62699
|
await uploadFileHttp(c, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
62374
62700
|
} catch (e2) {
|
|
@@ -62400,9 +62726,9 @@ ${psCmd}
|
|
|
62400
62726
|
const known = mimeOf(fileName);
|
|
62401
62727
|
const mime = known === "application/octet-stream" ? "text/plain" : known;
|
|
62402
62728
|
const contentBytes = Buffer.from(content3, "utf8");
|
|
62403
|
-
if (contentBytes.length >
|
|
62404
|
-
|
|
62405
|
-
|
|
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) {
|
|
62406
62732
|
try {
|
|
62407
62733
|
await uploadFileHttp(c, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
62408
62734
|
} catch (e2) {
|
|
@@ -62533,9 +62859,7 @@ ${psCmd}
|
|
|
62533
62859
|
if (!from && !groupId) return fail("Give `from` (a username, for a DM) or `groupId` (for a group file).");
|
|
62534
62860
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, from);
|
|
62535
62861
|
const where = groupId ? "this group" : `@${from}`;
|
|
62536
|
-
const
|
|
62537
|
-
c.history(convId);
|
|
62538
|
-
const res = await p;
|
|
62862
|
+
const res = await fetchHistory(c, convId);
|
|
62539
62863
|
const files = (res?.messages ?? []).filter((m) => m.kind === "file");
|
|
62540
62864
|
if (!files.length) return fail(`No files in ${where} yet.`);
|
|
62541
62865
|
const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
|
|
@@ -62572,10 +62896,8 @@ It downloads the file to your machine (current folder unless you set a path). Fi
|
|
|
62572
62896
|
inputSchema: { to: external_exports.string().describe("username to connect with") }
|
|
62573
62897
|
},
|
|
62574
62898
|
async ({ to }) => withClient(async (c) => {
|
|
62575
|
-
const
|
|
62576
|
-
|
|
62577
|
-
const e = await err;
|
|
62578
|
-
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}`);
|
|
62579
62901
|
return ok3(`\u{1F91D} Connection request sent to @${to}. They (or their owner) must approve before you can message.`);
|
|
62580
62902
|
})
|
|
62581
62903
|
);
|
|
@@ -62871,10 +63193,8 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62871
63193
|
}
|
|
62872
63194
|
},
|
|
62873
63195
|
async ({ groupId, text: text5 }) => withClient(async (c) => {
|
|
62874
|
-
const
|
|
62875
|
-
|
|
62876
|
-
const e = await err;
|
|
62877
|
-
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}`);
|
|
62878
63198
|
return ok3(`Posted to the group.`);
|
|
62879
63199
|
})
|
|
62880
63200
|
);
|
|
@@ -62934,9 +63254,9 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62934
63254
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
62935
63255
|
}
|
|
62936
63256
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
62937
|
-
if (rawBytes.length >
|
|
62938
|
-
|
|
62939
|
-
|
|
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) {
|
|
62940
63260
|
try {
|
|
62941
63261
|
await uploadFileHttp(c, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
62942
63262
|
} catch (e2) {
|
|
@@ -62967,9 +63287,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
62967
63287
|
if (!other && !groupId) return fail("Give `with` (a username, for a DM) or `groupId` (for a group).");
|
|
62968
63288
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, other);
|
|
62969
63289
|
const label = groupId ? "this group" : `@${other}`;
|
|
62970
|
-
const
|
|
62971
|
-
c.history(convId);
|
|
62972
|
-
const res = await p;
|
|
63290
|
+
const res = await fetchHistory(c, convId);
|
|
62973
63291
|
const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
|
|
62974
63292
|
if (!msgs.length) return ok3(`No messages yet in ${label}.`);
|
|
62975
63293
|
const lines = await Promise.all(msgs.map(async (m) => {
|
|
@@ -63000,10 +63318,14 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63000
63318
|
inputSchema: { query: external_exports.string() }
|
|
63001
63319
|
},
|
|
63002
63320
|
async ({ query }) => withClient(async (c) => {
|
|
63003
|
-
|
|
63004
|
-
c
|
|
63005
|
-
|
|
63006
|
-
|
|
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
|
+
}
|
|
63007
63329
|
if (!results.length) return ok3(`No agents matched "${query}".`);
|
|
63008
63330
|
return ok3(
|
|
63009
63331
|
results.map((r) => `\u2022 @${r.username} \u2014 ${r.displayName} (${r.status})${r.owner ? ` \xB7 by @${r.owner}` : ""} \xB7 ${r.tags.join(", ")}`).join("\n")
|
|
@@ -63020,7 +63342,8 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
63020
63342
|
}
|
|
63021
63343
|
},
|
|
63022
63344
|
async ({ status }) => withClient(async (c) => {
|
|
63023
|
-
c.presence(status);
|
|
63345
|
+
const r = await confirmWrite(c, () => c.presence(status));
|
|
63346
|
+
if (r.error) return fail(`Couldn't set status: ${r.error}`);
|
|
63024
63347
|
return ok3(`Status set to ${status}.`);
|
|
63025
63348
|
})
|
|
63026
63349
|
);
|
|
@@ -63212,7 +63535,8 @@ function sweepIdleSessions(map3) {
|
|
|
63212
63535
|
// src/http.ts
|
|
63213
63536
|
var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
|
|
63214
63537
|
var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
|
|
63215
|
-
var
|
|
63538
|
+
var STATELESS = process.env.JEFRI_STATELESS === "true";
|
|
63539
|
+
var log2 = (...a) => console.error("[jefrichat-mcp-http]", ...a);
|
|
63216
63540
|
var sessions = /* @__PURE__ */ new Map();
|
|
63217
63541
|
var HOST_FRESH_MS = Number(process.env.MCP_HOST_FRESH_MS ?? 10 * 60 * 1e3);
|
|
63218
63542
|
var hubPool = createKeyedPool();
|
|
@@ -63247,7 +63571,7 @@ function closeSession(sid, reason) {
|
|
|
63247
63571
|
if (detachRef(pooled, sid)) teardownPool(s.poolKey, pooled, "last session gone");
|
|
63248
63572
|
else if (s.host) announcePoolHosts(s.poolKey, pooled);
|
|
63249
63573
|
}
|
|
63250
|
-
|
|
63574
|
+
log2(`session ${sid.slice(0, 8)}\u2026 closed (${reason}) (${sessions.size} active)`);
|
|
63251
63575
|
}
|
|
63252
63576
|
function teardownPool(poolKey, pooled, reason) {
|
|
63253
63577
|
pooled.closed = true;
|
|
@@ -63256,9 +63580,10 @@ function teardownPool(poolKey, pooled, reason) {
|
|
|
63256
63580
|
pooled.acp.close();
|
|
63257
63581
|
} catch {
|
|
63258
63582
|
}
|
|
63259
|
-
|
|
63583
|
+
log2(`hub connection ${poolKey.slice(0, 8)}\u2026 closed (${reason})`);
|
|
63260
63584
|
}
|
|
63261
63585
|
function announcePoolHosts(poolKey, pooled) {
|
|
63586
|
+
if (STATELESS) return;
|
|
63262
63587
|
const joined = aggregateFreshHosts(sessions.values(), poolKey, Date.now(), HOST_FRESH_MS);
|
|
63263
63588
|
if (joined === pooled.lastHosts) return;
|
|
63264
63589
|
pooled.lastHosts = joined;
|
|
@@ -63273,6 +63598,12 @@ function closePool(poolKey, reason) {
|
|
|
63273
63598
|
}
|
|
63274
63599
|
async function acquirePooledConn(token, poolKey) {
|
|
63275
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
|
+
}
|
|
63276
63607
|
const acp = await JefriClient.connect({ server: HUB, token, status: "online" });
|
|
63277
63608
|
const inbox = [];
|
|
63278
63609
|
if (acp.identity) attachInbox(acp, inbox, acp.identity.username);
|
|
@@ -63299,7 +63630,10 @@ setInterval(() => {
|
|
|
63299
63630
|
if (seenPools.has(s.poolKey)) continue;
|
|
63300
63631
|
seenPools.add(s.poolKey);
|
|
63301
63632
|
const pooled = hubPool.get(s.poolKey);
|
|
63302
|
-
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"));
|
|
63303
63637
|
}
|
|
63304
63638
|
for (const [ip, e] of initHits) {
|
|
63305
63639
|
if (now > e.resetAt) initHits.delete(ip);
|
|
@@ -63358,6 +63692,7 @@ var health = (_req, res) => res.status(draining ? 503 : 200).json({
|
|
|
63358
63692
|
draining,
|
|
63359
63693
|
service: "jefrichat-mcp-http",
|
|
63360
63694
|
hub: HUB,
|
|
63695
|
+
stateless: STATELESS,
|
|
63361
63696
|
sessions: sessions.size
|
|
63362
63697
|
});
|
|
63363
63698
|
app.get("/health", health);
|
|
@@ -63397,6 +63732,14 @@ app.post("/mcp", async (req, res) => {
|
|
|
63397
63732
|
}
|
|
63398
63733
|
return;
|
|
63399
63734
|
}
|
|
63735
|
+
if (draining && !sessionId && isInitializeRequest(req.body)) {
|
|
63736
|
+
res.setHeader("retry-after", "1");
|
|
63737
|
+
return res.status(503).json({
|
|
63738
|
+
jsonrpc: "2.0",
|
|
63739
|
+
error: { code: -32e3, message: "server is shutting down \u2014 retry; another instance will accept this session" },
|
|
63740
|
+
id: null
|
|
63741
|
+
});
|
|
63742
|
+
}
|
|
63400
63743
|
if (!sessionId && isInitializeRequest(req.body)) {
|
|
63401
63744
|
if (initRateLimited(req)) {
|
|
63402
63745
|
res.status(429).json({
|
|
@@ -63420,7 +63763,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
63420
63763
|
try {
|
|
63421
63764
|
pooled = await acquirePooledConn(token, poolKey);
|
|
63422
63765
|
} catch (e) {
|
|
63423
|
-
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)) {
|
|
63424
63767
|
res.status(401).json({
|
|
63425
63768
|
jsonrpc: "2.0",
|
|
63426
63769
|
error: {
|
|
@@ -63434,6 +63777,10 @@ app.post("/mcp", async (req, res) => {
|
|
|
63434
63777
|
res.status(502).json({ jsonrpc: "2.0", error: { code: -32002, message: `Hub unreachable: ${e?.message ?? e}` }, id: null });
|
|
63435
63778
|
return;
|
|
63436
63779
|
}
|
|
63780
|
+
if (STATELESS && pooled.hbSignedOff) {
|
|
63781
|
+
pooled.hbSignedOff = false;
|
|
63782
|
+
void statelessPresenceBringUp(pooled.acp);
|
|
63783
|
+
}
|
|
63437
63784
|
const acp = pooled.acp;
|
|
63438
63785
|
if (!acp.identity) {
|
|
63439
63786
|
closePool(poolKey, "no identity");
|
|
@@ -63445,7 +63792,7 @@ app.post("/mcp", async (req, res) => {
|
|
|
63445
63792
|
return;
|
|
63446
63793
|
}
|
|
63447
63794
|
const mcp = new McpServer({ name: "jefrichat", version: "0.1.0" });
|
|
63448
|
-
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 });
|
|
63449
63796
|
let mySid;
|
|
63450
63797
|
mcp.server.oninitialized = () => {
|
|
63451
63798
|
try {
|
|
@@ -63457,14 +63804,14 @@ app.post("/mcp", async (req, res) => {
|
|
|
63457
63804
|
}
|
|
63458
63805
|
};
|
|
63459
63806
|
const transport = new StreamableHTTPServerTransport({
|
|
63460
|
-
sessionIdGenerator: () =>
|
|
63807
|
+
sessionIdGenerator: () => randomUUID3(),
|
|
63461
63808
|
onsessioninitialized: (sid) => {
|
|
63462
63809
|
if (pooled.closed) {
|
|
63463
63810
|
try {
|
|
63464
63811
|
transport.close?.();
|
|
63465
63812
|
} catch {
|
|
63466
63813
|
}
|
|
63467
|
-
|
|
63814
|
+
log2(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
|
|
63468
63815
|
return;
|
|
63469
63816
|
}
|
|
63470
63817
|
const meName = acp.identity.username;
|
|
@@ -63489,19 +63836,19 @@ app.post("/mcp", async (req, res) => {
|
|
|
63489
63836
|
transport.close?.();
|
|
63490
63837
|
} catch {
|
|
63491
63838
|
}
|
|
63492
|
-
|
|
63839
|
+
log2(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
|
|
63493
63840
|
return;
|
|
63494
63841
|
}
|
|
63495
63842
|
sessions.set(sid, { transport, acp, lastActive: Date.now(), tokenHash: sha256(token), inflight: 0, poolKey });
|
|
63496
63843
|
mySid = sid;
|
|
63497
|
-
|
|
63844
|
+
log2(`session ${sid.slice(0, 8)}\u2026 open as @${acp.identity.username} (${sessions.size} active, ${pooled.refs.size} on this connection)`);
|
|
63498
63845
|
}
|
|
63499
63846
|
});
|
|
63500
63847
|
transport.onclose = () => {
|
|
63501
63848
|
const sid = transport.sessionId;
|
|
63502
63849
|
if (sid) closeSession(sid, "client closed");
|
|
63503
63850
|
};
|
|
63504
|
-
const reservation = "init:" +
|
|
63851
|
+
const reservation = "init:" + randomUUID3();
|
|
63505
63852
|
pooled.refs.add(reservation);
|
|
63506
63853
|
try {
|
|
63507
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") {
|
|
@@ -38648,8 +38651,52 @@ var JefriClient = class _JefriClient {
|
|
|
38648
38651
|
// heartbeat: did we get a pong since the last ping?
|
|
38649
38652
|
hasConnected = false;
|
|
38650
38653
|
// armed for auto-reconnect only after the hub `registered` us
|
|
38651
|
-
|
|
38652
|
-
|
|
38654
|
+
/** UNACKNOWLEDGED presence intent, or null. Set by presence(), cleared when
|
|
38655
|
+
* the hub's own presence_update for this identity confirms the declaration
|
|
38656
|
+
* landed. Null on reconnect means: send nothing and let the hub's persisted
|
|
38657
|
+
* answer stand.
|
|
38658
|
+
*
|
|
38659
|
+
* Acknowledgment is the point, and it is what the first fix missed: a value
|
|
38660
|
+
* kept after delivery means "ever declared", not "pending" — so a client
|
|
38661
|
+
* that once declared away kept re-asserting it on every reconnect for the
|
|
38662
|
+
* life of the process, stomping whatever a newer device had chosen since.
|
|
38663
|
+
* The same stale-overwrite, one level down. */
|
|
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;
|
|
38668
|
+
/** The self statusRev BASELINE captured when the pending declaration was made.
|
|
38669
|
+
* An ack must be CAUSALLY newer than this — the audit reproduced a delayed
|
|
38670
|
+
* rev-9 event spending intent declared at a rev-10 baseline, after which the
|
|
38671
|
+
* server had already moved on. Identity + status matching alone cannot order
|
|
38672
|
+
* events; the revision can. */
|
|
38673
|
+
pendingBaselineRev = 0;
|
|
38674
|
+
/** Highest revision seen for our own status, from registered and from our own
|
|
38675
|
+
* presence_update events. */
|
|
38676
|
+
selfStatusRev = 0;
|
|
38677
|
+
/** Would a reconnect re-send a presence frame? True only while a declaration
|
|
38678
|
+
* is UNACKNOWLEDGED. Exposed for tests: the wire behaviour needs a live hub,
|
|
38679
|
+
* but the rule is checkable without one. */
|
|
38680
|
+
wouldReassertPresence() {
|
|
38681
|
+
return this.pendingPresence !== null;
|
|
38682
|
+
}
|
|
38683
|
+
/** The acknowledgment that turns "pending" back into nothing: the hub
|
|
38684
|
+
* broadcasts presence_update to the declaring identity too, so our own
|
|
38685
|
+
* username carrying our pending status means the declaration LANDED — and
|
|
38686
|
+
* re-asserting it on a later reconnect would stomp whatever a newer device
|
|
38687
|
+
* declares afterwards. A METHOD, not inline in the socket handler, so tests
|
|
38688
|
+
* drive the rule production runs instead of a copy of it — a test-local
|
|
38689
|
+
* re-implementation of this exact logic passed with the real clearing
|
|
38690
|
+
* deleted, which is how the previous round's test enshrined the bug. */
|
|
38691
|
+
notePresenceAck(ev) {
|
|
38692
|
+
if (ev?.event !== "presence_update" || ev.username !== this.identity?.username) return;
|
|
38693
|
+
if (typeof ev.rev === "number" && ev.rev > this.selfStatusRev) this.selfStatusRev = ev.rev;
|
|
38694
|
+
if (this.pendingPresence === null) return;
|
|
38695
|
+
if (this.pendingDeclId !== null && ev.declId === this.pendingDeclId) {
|
|
38696
|
+
this.pendingPresence = null;
|
|
38697
|
+
this.pendingDeclId = null;
|
|
38698
|
+
}
|
|
38699
|
+
}
|
|
38653
38700
|
clientInfo;
|
|
38654
38701
|
// handshake, re-sent on reconnect
|
|
38655
38702
|
/** Is the hub socket usable RIGHT NOW?
|
|
@@ -38744,7 +38791,8 @@ var JefriClient = class _JefriClient {
|
|
|
38744
38791
|
this.ws.on("open", () => {
|
|
38745
38792
|
this.startPing();
|
|
38746
38793
|
try {
|
|
38747
|
-
this.
|
|
38794
|
+
if (this.pendingPresence !== null)
|
|
38795
|
+
this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence, declId: this.pendingDeclId ?? void 0 }));
|
|
38748
38796
|
if (this.clientInfo)
|
|
38749
38797
|
this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
|
|
38750
38798
|
} catch {
|
|
@@ -38773,12 +38821,15 @@ var JefriClient = class _JefriClient {
|
|
|
38773
38821
|
}
|
|
38774
38822
|
if (ev.event === "registered") {
|
|
38775
38823
|
this.identity = ev.identity;
|
|
38824
|
+
const rev = ev.identity?.statusRev;
|
|
38825
|
+
if (typeof rev === "number" && rev > this.selfStatusRev) this.selfStatusRev = rev;
|
|
38776
38826
|
this.hasConnected = true;
|
|
38777
38827
|
this.reconnectAttempts = 0;
|
|
38778
38828
|
}
|
|
38779
38829
|
if (ev.event === "error" && /auth/i.test(ev.message ?? "")) {
|
|
38780
38830
|
this.authFailed = true;
|
|
38781
38831
|
}
|
|
38832
|
+
this.notePresenceAck(ev);
|
|
38782
38833
|
this.emit(ev.event, ev);
|
|
38783
38834
|
this.emit("*", ev);
|
|
38784
38835
|
});
|
|
@@ -38867,8 +38918,10 @@ var JefriClient = class _JefriClient {
|
|
|
38867
38918
|
}
|
|
38868
38919
|
// --- actions -------------------------------------------------------------
|
|
38869
38920
|
presence(status) {
|
|
38870
|
-
this.
|
|
38871
|
-
this.
|
|
38921
|
+
this.pendingPresence = status;
|
|
38922
|
+
this.pendingBaselineRev = this.selfStatusRev;
|
|
38923
|
+
this.pendingDeclId = newDeclId();
|
|
38924
|
+
this.send({ type: "presence", status, declId: this.pendingDeclId });
|
|
38872
38925
|
}
|
|
38873
38926
|
message(to, content3) {
|
|
38874
38927
|
this.send({ type: "message", to, content: content3 });
|
|
@@ -40488,6 +40541,27 @@ async function runOnceForMessage(m) {
|
|
|
40488
40541
|
}
|
|
40489
40542
|
|
|
40490
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
|
+
}
|
|
40491
40565
|
var MIME = {
|
|
40492
40566
|
".jpg": "image/jpeg",
|
|
40493
40567
|
".jpeg": "image/jpeg",
|
|
@@ -40826,7 +40900,7 @@ function saveWatermark() {
|
|
|
40826
40900
|
}
|
|
40827
40901
|
function registerAcpTools(server2, ctx) {
|
|
40828
40902
|
const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
|
|
40829
|
-
const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
|
|
40903
|
+
const e2eServer = ENABLE_E2E && !ctx.stateless ? server2 : { registerTool: () => void 0 };
|
|
40830
40904
|
const withClient = async (fn) => {
|
|
40831
40905
|
try {
|
|
40832
40906
|
const c2 = await ctx.ensureClient();
|
|
@@ -40944,10 +41018,8 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
40944
41018
|
}
|
|
40945
41019
|
},
|
|
40946
41020
|
async ({ to, text: text5 }) => withClient(async (c2) => {
|
|
40947
|
-
const
|
|
40948
|
-
|
|
40949
|
-
const e = await err;
|
|
40950
|
-
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}`);
|
|
40951
41023
|
return ok3(`Sent to @${to}: "${text5}"`);
|
|
40952
41024
|
})
|
|
40953
41025
|
);
|
|
@@ -40962,8 +41034,9 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
|
|
|
40962
41034
|
}
|
|
40963
41035
|
},
|
|
40964
41036
|
async ({ to, text: text5 }) => withClient(async (c2) => {
|
|
40965
|
-
const
|
|
40966
|
-
|
|
41037
|
+
const sc = c2;
|
|
41038
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
41039
|
+
await sc.privateMessage(to, text5);
|
|
40967
41040
|
const e = await err;
|
|
40968
41041
|
if (e) return fail(`Private message rejected: ${e.message}`);
|
|
40969
41042
|
return ok3(`Sent private E2E message to @${to}.`);
|
|
@@ -41020,8 +41093,9 @@ ${fp}`);
|
|
|
41020
41093
|
} else {
|
|
41021
41094
|
return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
|
|
41022
41095
|
}
|
|
41023
|
-
|
|
41024
|
-
|
|
41096
|
+
const sc = c2;
|
|
41097
|
+
await sc.privateFile(to, name, mime, url);
|
|
41098
|
+
if (caption) await sc.privateMessage(to, caption);
|
|
41025
41099
|
return ok3(`Sent private E2E file to @${to}.`);
|
|
41026
41100
|
})
|
|
41027
41101
|
);
|
|
@@ -41036,8 +41110,9 @@ ${fp}`);
|
|
|
41036
41110
|
}
|
|
41037
41111
|
},
|
|
41038
41112
|
async ({ groupId, text: text5 }) => withClient(async (c2) => {
|
|
41039
|
-
const
|
|
41040
|
-
|
|
41113
|
+
const sc = c2;
|
|
41114
|
+
const err = waitFor(sc, "error", () => true, 800);
|
|
41115
|
+
await sc.privateGroupMessage(groupId, text5);
|
|
41041
41116
|
const e = await err;
|
|
41042
41117
|
if (e) return fail(`Private group message rejected: ${e.message}`);
|
|
41043
41118
|
return ok3(`Sent private E2E group message.`);
|
|
@@ -41152,9 +41227,9 @@ ${psCmd}
|
|
|
41152
41227
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
41153
41228
|
}
|
|
41154
41229
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
41155
|
-
if (rawBytes.length >
|
|
41156
|
-
|
|
41157
|
-
|
|
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) {
|
|
41158
41233
|
try {
|
|
41159
41234
|
await uploadFileHttp(c2, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
|
|
41160
41235
|
} catch (e2) {
|
|
@@ -41186,9 +41261,9 @@ ${psCmd}
|
|
|
41186
41261
|
const known = mimeOf(fileName);
|
|
41187
41262
|
const mime = known === "application/octet-stream" ? "text/plain" : known;
|
|
41188
41263
|
const contentBytes = Buffer.from(content3, "utf8");
|
|
41189
|
-
if (contentBytes.length >
|
|
41190
|
-
|
|
41191
|
-
|
|
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) {
|
|
41192
41267
|
try {
|
|
41193
41268
|
await uploadFileHttp(c2, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
|
|
41194
41269
|
} catch (e2) {
|
|
@@ -41319,9 +41394,7 @@ ${psCmd}
|
|
|
41319
41394
|
if (!from && !groupId) return fail("Give `from` (a username, for a DM) or `groupId` (for a group file).");
|
|
41320
41395
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c2.identity.username, from);
|
|
41321
41396
|
const where = groupId ? "this group" : `@${from}`;
|
|
41322
|
-
const
|
|
41323
|
-
c2.history(convId);
|
|
41324
|
-
const res = await p;
|
|
41397
|
+
const res = await fetchHistory2(c2, convId);
|
|
41325
41398
|
const files = (res?.messages ?? []).filter((m) => m.kind === "file");
|
|
41326
41399
|
if (!files.length) return fail(`No files in ${where} yet.`);
|
|
41327
41400
|
const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
|
|
@@ -41358,10 +41431,8 @@ It downloads the file to your machine (current folder unless you set a path). Fi
|
|
|
41358
41431
|
inputSchema: { to: external_exports.string().describe("username to connect with") }
|
|
41359
41432
|
},
|
|
41360
41433
|
async ({ to }) => withClient(async (c2) => {
|
|
41361
|
-
const
|
|
41362
|
-
|
|
41363
|
-
const e = await err;
|
|
41364
|
-
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}`);
|
|
41365
41436
|
return ok3(`\u{1F91D} Connection request sent to @${to}. They (or their owner) must approve before you can message.`);
|
|
41366
41437
|
})
|
|
41367
41438
|
);
|
|
@@ -41657,10 +41728,8 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41657
41728
|
}
|
|
41658
41729
|
},
|
|
41659
41730
|
async ({ groupId, text: text5 }) => withClient(async (c2) => {
|
|
41660
|
-
const
|
|
41661
|
-
|
|
41662
|
-
const e = await err;
|
|
41663
|
-
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}`);
|
|
41664
41733
|
return ok3(`Posted to the group.`);
|
|
41665
41734
|
})
|
|
41666
41735
|
);
|
|
@@ -41720,9 +41789,9 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41720
41789
|
return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
|
|
41721
41790
|
}
|
|
41722
41791
|
const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
|
|
41723
|
-
if (rawBytes.length >
|
|
41724
|
-
|
|
41725
|
-
|
|
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) {
|
|
41726
41795
|
try {
|
|
41727
41796
|
await uploadFileHttp(c2, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
|
|
41728
41797
|
} catch (e2) {
|
|
@@ -41753,9 +41822,7 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41753
41822
|
if (!other && !groupId) return fail("Give `with` (a username, for a DM) or `groupId` (for a group).");
|
|
41754
41823
|
const convId = groupId ? groupConversationId(groupId) : dmConversationId(c2.identity.username, other);
|
|
41755
41824
|
const label = groupId ? "this group" : `@${other}`;
|
|
41756
|
-
const
|
|
41757
|
-
c2.history(convId);
|
|
41758
|
-
const res = await p;
|
|
41825
|
+
const res = await fetchHistory2(c2, convId);
|
|
41759
41826
|
const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
|
|
41760
41827
|
if (!msgs.length) return ok3(`No messages yet in ${label}.`);
|
|
41761
41828
|
const lines = await Promise.all(msgs.map(async (m) => {
|
|
@@ -41786,10 +41853,14 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41786
41853
|
inputSchema: { query: external_exports.string() }
|
|
41787
41854
|
},
|
|
41788
41855
|
async ({ query }) => withClient(async (c2) => {
|
|
41789
|
-
|
|
41790
|
-
c2
|
|
41791
|
-
|
|
41792
|
-
|
|
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
|
+
}
|
|
41793
41864
|
if (!results.length) return ok3(`No agents matched "${query}".`);
|
|
41794
41865
|
return ok3(
|
|
41795
41866
|
results.map((r) => `\u2022 @${r.username} \u2014 ${r.displayName} (${r.status})${r.owner ? ` \xB7 by @${r.owner}` : ""} \xB7 ${r.tags.join(", ")}`).join("\n")
|
|
@@ -41806,7 +41877,8 @@ Or just upload ${fname} from the agent's page in the web app.`
|
|
|
41806
41877
|
}
|
|
41807
41878
|
},
|
|
41808
41879
|
async ({ status }) => withClient(async (c2) => {
|
|
41809
|
-
c2.presence(status);
|
|
41880
|
+
const r = await confirmWrite(c2, () => c2.presence(status));
|
|
41881
|
+
if (r.error) return fail(`Couldn't set status: ${r.error}`);
|
|
41810
41882
|
return ok3(`Status set to ${status}.`);
|
|
41811
41883
|
})
|
|
41812
41884
|
);
|
package/package.json
CHANGED