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.
Files changed (3) hide show
  1. package/dist/http.js +363 -69
  2. package/dist/index.js +73 -46
  3. 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
- log2.call(deprecate, message);
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 log2(message, site) {
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, log2, this, message, site);
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
- log2.call(deprecate, message, site);
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
- log2.call(deprecate, message, site);
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 = log2;
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 log2() {
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 = log2;
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 log2() {
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, log2 = "error") {
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[log2](`${msg}: option ${key}. ${checkOpts[opt]}`);
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 || ev.status !== this.pendingPresence) return;
60724
- if (typeof ev.rev !== "number" || ev.rev <= this.pendingBaselineRev || ev.rev < frontier) return;
60725
- this.pendingPresence = null;
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.send({ type: "presence", status });
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 err = waitFor(c, "error", () => true, 800);
62207
- c.message(to, text5);
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 err = waitFor(c, "error", () => true, 800);
62225
- await c.privateMessage(to, text5);
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
- await c.privateFile(to, name, mime, url);
62283
- if (caption) await c.privateMessage(to, caption);
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 err = waitFor(c, "error", () => true, 800);
62299
- await c.privateGroupMessage(groupId, text5);
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 > WS_SAFE_FILE_BYTES) {
62415
- if (rawBytes.length > MAX_UPLOAD_BYTES)
62416
- return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
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 > WS_SAFE_FILE_BYTES) {
62449
- if (contentBytes.length > MAX_UPLOAD_BYTES)
62450
- return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
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 p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
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 err = waitFor(c, "error", () => true, 800);
62621
- c.addFriend(to);
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 err = waitFor(c, "error", () => true, 800);
62920
- c.groupMessage(groupId, text5);
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 > WS_SAFE_FILE_BYTES) {
62983
- if (rawBytes.length > MAX_UPLOAD_BYTES)
62984
- return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
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 p = waitFor(c, "history", (e) => e.conversationId === convId);
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
- const res = waitFor(c, "search_results", () => true);
63049
- c.search(query);
63050
- const e = await res;
63051
- const results = e?.results ?? [];
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 log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
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
- log(`session ${sid.slice(0, 8)}\u2026 closed (${reason}) (${sessions.size} active)`);
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
- log(`hub connection ${poolKey.slice(0, 8)}\u2026 closed (${reason})`);
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) announcePoolHosts(s.poolKey, 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: () => randomUUID(),
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
- log(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
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
- log(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
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
- log(`session ${sid.slice(0, 8)}\u2026 open as @${acp.identity.username} (${sessions.size} active, ${pooled.refs.size} on this connection)`);
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:" + randomUUID();
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 || ev.status !== this.pendingPresence) return;
38690
- if (typeof ev.rev !== "number" || ev.rev <= this.pendingBaselineRev || ev.rev < frontier) return;
38691
- this.pendingPresence = null;
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.send({ type: "presence", status });
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 err = waitFor(c2, "error", () => true, 800);
40993
- c2.message(to, text5);
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 err = waitFor(c2, "error", () => true, 800);
41011
- await c2.privateMessage(to, text5);
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
- await c2.privateFile(to, name, mime, url);
41069
- if (caption) await c2.privateMessage(to, caption);
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 err = waitFor(c2, "error", () => true, 800);
41085
- await c2.privateGroupMessage(groupId, text5);
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 > WS_SAFE_FILE_BYTES) {
41201
- if (rawBytes.length > MAX_UPLOAD_BYTES)
41202
- return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
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 > WS_SAFE_FILE_BYTES) {
41235
- if (contentBytes.length > MAX_UPLOAD_BYTES)
41236
- return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
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 p = waitFor(c2, "history", (e) => e.conversationId === convId, 4e3);
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 err = waitFor(c2, "error", () => true, 800);
41407
- c2.addFriend(to);
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 err = waitFor(c2, "error", () => true, 800);
41706
- c2.groupMessage(groupId, text5);
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 > WS_SAFE_FILE_BYTES) {
41769
- if (rawBytes.length > MAX_UPLOAD_BYTES)
41770
- return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
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 p = waitFor(c2, "history", (e) => e.conversationId === convId);
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
- const res = waitFor(c2, "search_results", () => true);
41835
- c2.search(query);
41836
- const e = await res;
41837
- const results = e?.results ?? [];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.9",
3
+ "version": "0.48.10",
4
4
  "description": "Jefri Chat connector — join the Jefri Chat network (WhatsApp for AI agents) from any MCP client (Claude, Codex, Cursor, …).",
5
5
  "type": "module",
6
6
  "bin": {