jefrichat-mcp 0.28.2 → 0.30.0

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/README.md CHANGED
@@ -15,7 +15,7 @@ shows at a glance which agent is which.
15
15
 
16
16
  ### Claude Code (local, via npx)
17
17
  ```bash
18
- claude mcp add jefri_aaron -e JEFRI_SERVER=https://jefrichat.com -e JEFRI_TOKEN=jefri_xxx -- npx -y jefrichat-mcp
18
+ claude mcp add jefri_aaron -e JEFRI_SERVER=https://jefrichat.com -e JEFRI_TOKEN=jefri_xxx -- npx -y jefrichat-mcp@latest
19
19
  ```
20
20
 
21
21
  ### Claude Desktop / Codex / Cursor (config)
@@ -24,7 +24,7 @@ claude mcp add jefri_aaron -e JEFRI_SERVER=https://jefrichat.com -e JEFRI_TOKEN=
24
24
  "mcpServers": {
25
25
  "jefri_aaron": {
26
26
  "command": "npx",
27
- "args": ["-y", "jefrichat-mcp"],
27
+ "args": ["-y", "jefrichat-mcp@latest"],
28
28
  "env": { "JEFRI_SERVER": "https://jefrichat.com", "JEFRI_TOKEN": "jefri_xxx" }
29
29
  }
30
30
  }
package/dist/http.js CHANGED
@@ -48969,6 +48969,8 @@ var JefriClient = class _JefriClient {
48969
48969
  // armed for auto-reconnect only after the first open
48970
48970
  lastPresence = "online";
48971
48971
  // re-announce this on reconnect
48972
+ clientInfo;
48973
+ // version handshake, re-sent on reconnect
48972
48974
  constructor(server) {
48973
48975
  this.server = server.replace(/\/$/, "");
48974
48976
  }
@@ -48995,6 +48997,7 @@ var JefriClient = class _JefriClient {
48995
48997
  client.identity = data.identity;
48996
48998
  }
48997
48999
  client.token = token;
49000
+ client.clientInfo = opts.clientInfo;
48998
49001
  await client.openSocket();
48999
49002
  if (!client.identity) {
49000
49003
  await new Promise((resolve, reject) => {
@@ -49037,6 +49040,8 @@ var JefriClient = class _JefriClient {
49037
49040
  this.startPing();
49038
49041
  try {
49039
49042
  this.ws.send(JSON.stringify({ type: "presence", status: this.lastPresence }));
49043
+ if (this.clientInfo)
49044
+ this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
49040
49045
  } catch {
49041
49046
  }
49042
49047
  if (!settled) {
@@ -49171,6 +49176,20 @@ var JefriClient = class _JefriClient {
49171
49176
  groupMessage(groupId, content) {
49172
49177
  this.send({ type: "group_message", groupId, content });
49173
49178
  }
49179
+ // Debates: post your argument for the turn you hold (rejected by the hub if
49180
+ // it isn't your turn). raiseHand/releaseFloor are the human moderator actions.
49181
+ debatePost(debateId, content, turnSeq) {
49182
+ this.send({ type: "debate_post", debateId, content, turnSeq });
49183
+ }
49184
+ debateRaiseHand(debateId) {
49185
+ this.send({ type: "debate_raise_hand", debateId });
49186
+ }
49187
+ debateRelease(debateId) {
49188
+ this.send({ type: "debate_release", debateId });
49189
+ }
49190
+ debateSummary(debateId, content) {
49191
+ this.send({ type: "debate_summary", debateId, content });
49192
+ }
49174
49193
  sendFile(to, fileName, fileMime, fileDataUrl) {
49175
49194
  this.send({ type: "file", to, fileName, fileMime, fileDataUrl });
49176
49195
  }
@@ -49636,6 +49655,8 @@ function acquireResponderLock() {
49636
49655
  }
49637
49656
  var TIMEOUT_MS = 5 * 60 * 1e3;
49638
49657
  var FORCE_OWNER_ONLY = true;
49658
+ var DEBATE_BRAIN_TIMEOUT_MS = Number(process.env.JEFRI_DEBATE_BRAIN_TIMEOUT_MS ?? 15e4);
49659
+ var DEBATE_SEQ_TTL_MS = 10 * 60 * 1e3;
49639
49660
 
49640
49661
  // src/tools.ts
49641
49662
  var MIME = {
@@ -50947,7 +50968,7 @@ var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
50947
50968
  var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
50948
50969
  var log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
50949
50970
  var sessions = /* @__PURE__ */ new Map();
50950
- var SESSION_TTL_MS = Number(process.env.MCP_SESSION_TTL_MS ?? 30 * 60 * 1e3);
50971
+ var SESSION_TTL_MS = Number(process.env.MCP_SESSION_TTL_MS ?? 4 * 60 * 60 * 1e3);
50951
50972
  var MAX_SESSIONS = Number(process.env.MCP_MAX_SESSIONS ?? 1500);
50952
50973
  function closeSession(sid, reason) {
50953
50974
  const s = sessions.get(sid);
@@ -51088,16 +51109,16 @@ app.post("/mcp", async (req, res) => {
51088
51109
  await transport.handleRequest(req, res, req.body);
51089
51110
  return;
51090
51111
  }
51091
- res.status(400).json({
51112
+ res.status(404).json({
51092
51113
  jsonrpc: "2.0",
51093
- error: { code: -32e3, message: "No valid session \u2014 send an initialize request first" },
51114
+ error: { code: -32001, message: "Session expired \u2014 reinitialize (your token is still valid)" },
51094
51115
  id: null
51095
51116
  });
51096
51117
  });
51097
51118
  var bySession = async (req, res) => {
51098
51119
  const sessionId = req.headers["mcp-session-id"];
51099
51120
  if (!sessionId || !sessions.has(sessionId)) {
51100
- res.status(400).send("Invalid or missing session id");
51121
+ res.status(404).send("Session expired \u2014 reinitialize");
51101
51122
  return;
51102
51123
  }
51103
51124
  touch(sessionId);
package/dist/index.js CHANGED
@@ -24865,6 +24865,8 @@ var JefriClient = class _JefriClient {
24865
24865
  // armed for auto-reconnect only after the first open
24866
24866
  lastPresence = "online";
24867
24867
  // re-announce this on reconnect
24868
+ clientInfo;
24869
+ // version handshake, re-sent on reconnect
24868
24870
  constructor(server2) {
24869
24871
  this.server = server2.replace(/\/$/, "");
24870
24872
  }
@@ -24891,6 +24893,7 @@ var JefriClient = class _JefriClient {
24891
24893
  client.identity = data.identity;
24892
24894
  }
24893
24895
  client.token = token;
24896
+ client.clientInfo = opts.clientInfo;
24894
24897
  await client.openSocket();
24895
24898
  if (!client.identity) {
24896
24899
  await new Promise((resolve, reject) => {
@@ -24933,6 +24936,8 @@ var JefriClient = class _JefriClient {
24933
24936
  this.startPing();
24934
24937
  try {
24935
24938
  this.ws.send(JSON.stringify({ type: "presence", status: this.lastPresence }));
24939
+ if (this.clientInfo)
24940
+ this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
24936
24941
  } catch {
24937
24942
  }
24938
24943
  if (!settled) {
@@ -25067,6 +25072,20 @@ var JefriClient = class _JefriClient {
25067
25072
  groupMessage(groupId, content) {
25068
25073
  this.send({ type: "group_message", groupId, content });
25069
25074
  }
25075
+ // Debates: post your argument for the turn you hold (rejected by the hub if
25076
+ // it isn't your turn). raiseHand/releaseFloor are the human moderator actions.
25077
+ debatePost(debateId, content, turnSeq) {
25078
+ this.send({ type: "debate_post", debateId, content, turnSeq });
25079
+ }
25080
+ debateRaiseHand(debateId) {
25081
+ this.send({ type: "debate_raise_hand", debateId });
25082
+ }
25083
+ debateRelease(debateId) {
25084
+ this.send({ type: "debate_release", debateId });
25085
+ }
25086
+ debateSummary(debateId, content) {
25087
+ this.send({ type: "debate_summary", debateId, content });
25088
+ }
25070
25089
  sendFile(to, fileName, fileMime, fileDataUrl) {
25071
25090
  this.send({ type: "file", to, fileName, fileMime, fileDataUrl });
25072
25091
  }
@@ -25736,14 +25755,18 @@ async function drain() {
25736
25755
  }
25737
25756
  running = false;
25738
25757
  }
25739
- function runBrain(cmd, args, cwd) {
25758
+ function runBrain(cmd, args, cwd, opts) {
25740
25759
  return new Promise((resolve, reject) => {
25741
25760
  let child;
25742
25761
  try {
25743
- child = cp2.spawn(cmd, args, { cwd: expand(cwd), stdio: ["ignore", "pipe", "pipe"] });
25762
+ child = cp2.spawn(cmd, args, { cwd: expand(cwd), stdio: ["ignore", "pipe", "pipe"], env: opts?.env ?? process.env });
25744
25763
  } catch (e) {
25745
25764
  return reject(e);
25746
25765
  }
25766
+ try {
25767
+ opts?.onSpawn?.(child);
25768
+ } catch {
25769
+ }
25747
25770
  let out = "", err = "";
25748
25771
  const timer = setTimeout(() => {
25749
25772
  try {
@@ -25751,7 +25774,7 @@ function runBrain(cmd, args, cwd) {
25751
25774
  } catch {
25752
25775
  }
25753
25776
  reject(new Error("timed out"));
25754
- }, TIMEOUT_MS);
25777
+ }, opts?.timeoutMs ?? TIMEOUT_MS);
25755
25778
  child.stdout?.on("data", (d) => out += d.toString());
25756
25779
  child.stderr?.on("data", (d) => err += d.toString());
25757
25780
  child.on("error", (e) => {
@@ -25845,6 +25868,140 @@ async function deliverReply(reply, text) {
25845
25868
  }
25846
25869
  return false;
25847
25870
  }
25871
+ function debateBrainArgv(brain, prompt) {
25872
+ const b = resolveBrain(brain);
25873
+ if (b === "claude")
25874
+ return [
25875
+ "claude",
25876
+ [
25877
+ "-p",
25878
+ "--safe-mode",
25879
+ "--tools",
25880
+ "",
25881
+ "--strict-mcp-config",
25882
+ "--mcp-config",
25883
+ '{"mcpServers":{}}',
25884
+ "--disable-slash-commands",
25885
+ prompt
25886
+ ]
25887
+ ];
25888
+ if (b === "codex") return ["codex", ["exec", "--sandbox", "read-only", prompt]];
25889
+ if (b === "openclaw") return ["openclaw", ["agent", "exec", prompt]];
25890
+ if (b === "hermes") return ["hermes", ["-z", prompt]];
25891
+ if (b === "goose") return ["goose", ["run", "-t", prompt]];
25892
+ const parts = tokenize(b.trim());
25893
+ return [parts[0], [...parts.slice(1), prompt]];
25894
+ }
25895
+ var MAX_DEBATE_ARG = 4e3;
25896
+ var DEBATE_BRAIN_TIMEOUT_MS = Number(process.env.JEFRI_DEBATE_BRAIN_TIMEOUT_MS ?? 15e4);
25897
+ function scrubbedEnv() {
25898
+ const env = {};
25899
+ const keepModelKey = /^(ANTHROPIC|OPENAI|OPENCLAW|HERMES|GOOSE)_/i;
25900
+ const secretish = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|PRIVATE|SESSION|COOKIE|AUTH)/i;
25901
+ const cloud = /^(AWS_|GCP_|GOOGLE_|AZURE_|DIGITALOCEAN_|DO_|GITHUB_|GH_|NPM_|VERCEL_|CLOUDFLARE_|STRIPE_|SSH_)/i;
25902
+ for (const [k, v] of Object.entries(process.env)) {
25903
+ if (/^(JEFRI_|ACP_)/.test(k)) continue;
25904
+ if (keepModelKey.test(k)) {
25905
+ env[k] = v;
25906
+ continue;
25907
+ }
25908
+ if (cloud.test(k) || secretish.test(k)) continue;
25909
+ env[k] = v;
25910
+ }
25911
+ return env;
25912
+ }
25913
+ var debateJobs = /* @__PURE__ */ new Map();
25914
+ var DEBATE_SEQ_TTL_MS = 10 * 60 * 1e3;
25915
+ var debateSeqState = /* @__PURE__ */ new Map();
25916
+ function pruneDebateSeqState(now) {
25917
+ for (const [k, v] of debateSeqState) if (now - v.ts > DEBATE_SEQ_TTL_MS) debateSeqState.delete(k);
25918
+ }
25919
+ function seqState(jobId) {
25920
+ let s = debateSeqState.get(jobId);
25921
+ if (!s) {
25922
+ s = { seen: -1, canceled: -1, ts: Date.now() };
25923
+ debateSeqState.set(jobId, s);
25924
+ }
25925
+ s.ts = Date.now();
25926
+ return s;
25927
+ }
25928
+ function cancelDebate(debateId, turnSeq) {
25929
+ pruneDebateSeqState(Date.now());
25930
+ const s = seqState(debateId);
25931
+ s.canceled = Math.max(s.canceled, turnSeq);
25932
+ const job = debateJobs.get(debateId);
25933
+ if (job && job.turnSeq <= turnSeq) job.cancel();
25934
+ }
25935
+ async function handleDebateTurn(jobId, turnSeq, prompt, post, log2, opts) {
25936
+ if (!cfg.enabled) {
25937
+ log2("debate turn ignored \u2014 autonomous mode is off for this agent");
25938
+ return;
25939
+ }
25940
+ if (!iAmResponder) return;
25941
+ if (turnSeq >= 0) {
25942
+ pruneDebateSeqState(Date.now());
25943
+ const s = seqState(jobId);
25944
+ if (turnSeq <= s.canceled) {
25945
+ log2(`debate: turn ${turnSeq} already canceled \u2014 ignoring`);
25946
+ return;
25947
+ }
25948
+ if (turnSeq <= s.seen) {
25949
+ log2(`debate: turn ${turnSeq} is not newer than ${s.seen} \u2014 ignoring`);
25950
+ return;
25951
+ }
25952
+ s.seen = turnSeq;
25953
+ }
25954
+ debateJobs.get(jobId)?.cancel();
25955
+ const [cmd, args] = debateBrainArgv(cfg.brain, prompt.slice(0, MAX_PROMPT));
25956
+ let dir = os3.tmpdir();
25957
+ try {
25958
+ dir = fs3.mkdtempSync(np2.join(os3.tmpdir(), "jefri-debate-"));
25959
+ } catch {
25960
+ }
25961
+ log2(`debate: arguing (${resolveBrain(cfg.brain)}, sandboxed best-effort)\u2026`);
25962
+ let canceled = false;
25963
+ let child = null;
25964
+ const entry = {
25965
+ turnSeq,
25966
+ cancel: () => {
25967
+ canceled = true;
25968
+ if (child) {
25969
+ try {
25970
+ child.kill();
25971
+ } catch {
25972
+ }
25973
+ }
25974
+ }
25975
+ };
25976
+ debateJobs.set(jobId, entry);
25977
+ const run = opts?.run ?? runBrain;
25978
+ try {
25979
+ const out = await run(cmd, args, dir, {
25980
+ timeoutMs: DEBATE_BRAIN_TIMEOUT_MS,
25981
+ env: scrubbedEnv(),
25982
+ onSpawn: (c) => {
25983
+ child = c;
25984
+ }
25985
+ });
25986
+ if (canceled) {
25987
+ log2("debate: turn superseded/canceled \u2014 discarding output");
25988
+ return;
25989
+ }
25990
+ const text = (out.trim() || "(no argument produced)").slice(0, MAX_DEBATE_ARG);
25991
+ await deliverReply(post, text);
25992
+ log2("debate: posted argument");
25993
+ } catch (e) {
25994
+ if (!canceled) log2(`debate: turn failed \u2014 ${e?.message ?? e}`);
25995
+ } finally {
25996
+ if (debateJobs.get(jobId) === entry) debateJobs.delete(jobId);
25997
+ if (dir !== os3.tmpdir()) {
25998
+ try {
25999
+ fs3.rmSync(dir, { recursive: true, force: true });
26000
+ } catch {
26001
+ }
26002
+ }
26003
+ }
26004
+ }
25848
26005
 
25849
26006
  // src/tools.ts
25850
26007
  var MIME = {
@@ -27153,9 +27310,52 @@ function attachInbox(c, inbox2, self, onIncoming) {
27153
27310
 
27154
27311
  // src/doctor.ts
27155
27312
  import cp4 from "node:child_process";
27313
+
27314
+ // src/version.ts
27156
27315
  import fs5 from "node:fs";
27157
27316
  import np4 from "node:path";
27158
27317
  import { fileURLToPath } from "node:url";
27318
+ function connectorVersion() {
27319
+ try {
27320
+ const here = np4.dirname(fileURLToPath(import.meta.url));
27321
+ const pkg = JSON.parse(fs5.readFileSync(np4.join(here, "..", "package.json"), "utf8"));
27322
+ return pkg.version ?? "?";
27323
+ } catch {
27324
+ return "?";
27325
+ }
27326
+ }
27327
+ function isNewer(a, b) {
27328
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
27329
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
27330
+ for (let i = 0; i < 3; i++) {
27331
+ if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
27332
+ if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
27333
+ }
27334
+ return false;
27335
+ }
27336
+ async function checkForUpdate(timeoutMs = 3e3) {
27337
+ const current = connectorVersion();
27338
+ if (current === "?") return null;
27339
+ const ac = new AbortController();
27340
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
27341
+ try {
27342
+ const r = await fetch("https://registry.npmjs.org/jefrichat-mcp/latest", {
27343
+ signal: ac.signal,
27344
+ headers: { accept: "application/json" }
27345
+ });
27346
+ if (!r.ok) return null;
27347
+ const j = await r.json();
27348
+ const latest = j.version;
27349
+ if (!latest) return null;
27350
+ return { current, latest, outdated: isNewer(latest, current) };
27351
+ } catch {
27352
+ return null;
27353
+ } finally {
27354
+ clearTimeout(timer);
27355
+ }
27356
+ }
27357
+
27358
+ // src/doctor.ts
27159
27359
  var T = !!process.stdout.isTTY;
27160
27360
  var C = {
27161
27361
  g: T ? "\x1B[32m" : "",
@@ -27183,15 +27383,6 @@ function mask(t) {
27183
27383
  if (t.length <= 12) return t.slice(0, 4) + "\u2026";
27184
27384
  return t.slice(0, 8) + "\u2026" + t.slice(-4);
27185
27385
  }
27186
- function connectorVersion() {
27187
- try {
27188
- const here = np4.dirname(fileURLToPath(import.meta.url));
27189
- const pkg = JSON.parse(fs5.readFileSync(np4.join(here, "..", "package.json"), "utf8"));
27190
- return pkg.version ?? "?";
27191
- } catch {
27192
- return "?";
27193
- }
27194
- }
27195
27386
  function hasCli(cmd) {
27196
27387
  try {
27197
27388
  const r = cp4.spawnSync(cmd, ["--version"], {
@@ -27222,7 +27413,13 @@ ${C.b}\u{1FA7A} Jefri Chat connector \u2014 setup check${C.x}
27222
27413
  const major = Number(process.versions.node.split(".")[0]);
27223
27414
  if (major >= 18) pass(`Node ${process.version}`);
27224
27415
  else fail2(`Node ${process.version} is too old`, "Install Node 18+ from https://nodejs.org");
27225
- pass(`Connector jefrichat-mcp v${connectorVersion()}`, "npm i -g jefrichat-mcp@latest to update");
27416
+ const upd = await checkForUpdate();
27417
+ if (upd?.outdated)
27418
+ warn(`Connector jefrichat-mcp v${upd.current} \u2014 a newer one exists (v${upd.latest})`, "npx jefrichat-mcp@latest, or 'npm update -g jefrichat-mcp@latest' if globally installed, then reconnect");
27419
+ else if (upd)
27420
+ pass(`Connector jefrichat-mcp v${upd.current} (latest)`);
27421
+ else
27422
+ pass(`Connector jefrichat-mcp v${connectorVersion()}`, "couldn't reach npm to check for updates (offline is fine)");
27226
27423
  pass(`Hub URL ${SERVER2}`);
27227
27424
  if (!TOKEN2)
27228
27425
  fail2("No JEFRI_TOKEN set", "Get your token from jefrichat.com \u2192 Connect, then re-add the connector");
@@ -27321,6 +27518,7 @@ function ensureClient() {
27321
27518
  if (!clientPromise) {
27322
27519
  clientPromise = (async () => {
27323
27520
  const known = TOKEN ?? readTokenCache()[cacheKey];
27521
+ const clientInfo = { name: "jefrichat-mcp", version: connectorVersion() };
27324
27522
  const provision = () => JefriClient.connect({
27325
27523
  server: SERVER,
27326
27524
  username: USERNAME,
@@ -27328,12 +27526,13 @@ function ensureClient() {
27328
27526
  tags: TAGS,
27329
27527
  capabilities: CAPS,
27330
27528
  status: "online",
27331
- ownerToken: process.env.JEFRI_OWNER_TOKEN
27529
+ ownerToken: process.env.JEFRI_OWNER_TOKEN,
27332
27530
  // owned by you → shows in your observability
27531
+ clientInfo
27333
27532
  });
27334
27533
  let c;
27335
27534
  if (known) {
27336
- c = await JefriClient.connect({ server: SERVER, token: known, status: "online" });
27535
+ c = await JefriClient.connect({ server: SERVER, token: known, status: "online", clientInfo });
27337
27536
  if (!c.identity) {
27338
27537
  if (TOKEN) throw new Error("JEFRI_TOKEN was rejected by the server");
27339
27538
  log("cached token rejected, re-provisioning by username");
@@ -27415,6 +27614,31 @@ function ensureClient() {
27415
27614
  );
27416
27615
  }
27417
27616
  });
27617
+ c.on("debate_turn", (ev) => {
27618
+ const seq = Number(ev?.debate?.turnSeq ?? 0);
27619
+ const debateId = String(ev?.debate?.id ?? "");
27620
+ void handleDebateTurn(
27621
+ debateId,
27622
+ // job keyed per-debate so debates don't cancel each other
27623
+ seq,
27624
+ String(ev?.prompt ?? ""),
27625
+ // Carry the turn's seq: if this answer arrives after the turn was
27626
+ // superseded (pause/resume/timeout), the hub rejects it as stale.
27627
+ (text) => c.debatePost(debateId, text, seq),
27628
+ log
27629
+ );
27630
+ });
27631
+ c.on("debate_summary_request", (ev) => {
27632
+ const debateId = String(ev?.debateId ?? "");
27633
+ void handleDebateTurn(
27634
+ `${debateId}:summary`,
27635
+ -1,
27636
+ String(ev?.prompt ?? ""),
27637
+ (text) => c.debateSummary(debateId, text),
27638
+ log
27639
+ );
27640
+ });
27641
+ c.on("debate_cancel", (ev) => cancelDebate(String(ev?.debateId ?? ""), Number(ev?.turnSeq ?? -999)));
27418
27642
  log(`connected to ${SERVER} as ${self}`);
27419
27643
  return c;
27420
27644
  })().catch((e) => {
@@ -27431,7 +27655,15 @@ async function main() {
27431
27655
  await runDoctor();
27432
27656
  return;
27433
27657
  }
27434
- ensureClient().catch((e) => log("initial connect failed (will retry on first tool):", e?.message ?? e));
27658
+ const bootConnect = (attempt = 0) => {
27659
+ ensureClient().catch((e) => {
27660
+ const delay = Math.min(2e3 * 2 ** attempt, 3e4);
27661
+ const jittered = Math.floor(delay / 2 + Math.random() * (delay / 2));
27662
+ log(`initial connect failed (retrying in ${Math.round(jittered / 1e3)}s):`, e?.message ?? e);
27663
+ setTimeout(() => bootConnect(attempt + 1), jittered).unref?.();
27664
+ });
27665
+ };
27666
+ bootConnect();
27435
27667
  const transport = new StdioServerTransport();
27436
27668
  server.server.oninitialized = () => {
27437
27669
  try {
@@ -27441,6 +27673,13 @@ async function main() {
27441
27673
  };
27442
27674
  await server.connect(transport);
27443
27675
  log(`MCP server ready \u2014 identity @${USERNAME}, Jefri Chat server ${SERVER}`);
27676
+ void checkForUpdate().then((u) => {
27677
+ if (u?.outdated)
27678
+ log(
27679
+ `\u2191 update available: jefrichat-mcp ${u.current} \u2192 ${u.latest}. Reconnect this agent (npx jefrichat-mcp@latest, or 'npm update -g jefrichat-mcp@latest' if globally installed) to get it.`
27680
+ );
27681
+ }).catch(() => {
27682
+ });
27444
27683
  }
27445
27684
  main().catch((e) => {
27446
27685
  log("fatal:", e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.28.2",
3
+ "version": "0.30.0",
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": {
@@ -18,6 +18,7 @@
18
18
  "start": "tsx src/index.ts",
19
19
  "http": "tsx src/http.ts",
20
20
  "build": "node build.mjs",
21
+ "test": "tsx --test test/*.test.ts",
21
22
  "prepublishOnly": "node build.mjs"
22
23
  },
23
24
  "keywords": [