jefrichat-mcp 0.29.0 → 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/dist/http.js CHANGED
@@ -49176,6 +49176,20 @@ var JefriClient = class _JefriClient {
49176
49176
  groupMessage(groupId, content) {
49177
49177
  this.send({ type: "group_message", groupId, content });
49178
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
+ }
49179
49193
  sendFile(to, fileName, fileMime, fileDataUrl) {
49180
49194
  this.send({ type: "file", to, fileName, fileMime, fileDataUrl });
49181
49195
  }
@@ -49641,6 +49655,8 @@ function acquireResponderLock() {
49641
49655
  }
49642
49656
  var TIMEOUT_MS = 5 * 60 * 1e3;
49643
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;
49644
49660
 
49645
49661
  // src/tools.ts
49646
49662
  var MIME = {
@@ -50952,7 +50968,7 @@ var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
50952
50968
  var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
50953
50969
  var log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
50954
50970
  var sessions = /* @__PURE__ */ new Map();
50955
- 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);
50956
50972
  var MAX_SESSIONS = Number(process.env.MCP_MAX_SESSIONS ?? 1500);
50957
50973
  function closeSession(sid, reason) {
50958
50974
  const s = sessions.get(sid);
@@ -51093,16 +51109,16 @@ app.post("/mcp", async (req, res) => {
51093
51109
  await transport.handleRequest(req, res, req.body);
51094
51110
  return;
51095
51111
  }
51096
- res.status(400).json({
51112
+ res.status(404).json({
51097
51113
  jsonrpc: "2.0",
51098
- 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)" },
51099
51115
  id: null
51100
51116
  });
51101
51117
  });
51102
51118
  var bySession = async (req, res) => {
51103
51119
  const sessionId = req.headers["mcp-session-id"];
51104
51120
  if (!sessionId || !sessions.has(sessionId)) {
51105
- res.status(400).send("Invalid or missing session id");
51121
+ res.status(404).send("Session expired \u2014 reinitialize");
51106
51122
  return;
51107
51123
  }
51108
51124
  touch(sessionId);
package/dist/index.js CHANGED
@@ -25072,6 +25072,20 @@ var JefriClient = class _JefriClient {
25072
25072
  groupMessage(groupId, content) {
25073
25073
  this.send({ type: "group_message", groupId, content });
25074
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
+ }
25075
25089
  sendFile(to, fileName, fileMime, fileDataUrl) {
25076
25090
  this.send({ type: "file", to, fileName, fileMime, fileDataUrl });
25077
25091
  }
@@ -25741,14 +25755,18 @@ async function drain() {
25741
25755
  }
25742
25756
  running = false;
25743
25757
  }
25744
- function runBrain(cmd, args, cwd) {
25758
+ function runBrain(cmd, args, cwd, opts) {
25745
25759
  return new Promise((resolve, reject) => {
25746
25760
  let child;
25747
25761
  try {
25748
- 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 });
25749
25763
  } catch (e) {
25750
25764
  return reject(e);
25751
25765
  }
25766
+ try {
25767
+ opts?.onSpawn?.(child);
25768
+ } catch {
25769
+ }
25752
25770
  let out = "", err = "";
25753
25771
  const timer = setTimeout(() => {
25754
25772
  try {
@@ -25756,7 +25774,7 @@ function runBrain(cmd, args, cwd) {
25756
25774
  } catch {
25757
25775
  }
25758
25776
  reject(new Error("timed out"));
25759
- }, TIMEOUT_MS);
25777
+ }, opts?.timeoutMs ?? TIMEOUT_MS);
25760
25778
  child.stdout?.on("data", (d) => out += d.toString());
25761
25779
  child.stderr?.on("data", (d) => err += d.toString());
25762
25780
  child.on("error", (e) => {
@@ -25850,6 +25868,140 @@ async function deliverReply(reply, text) {
25850
25868
  }
25851
25869
  return false;
25852
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
+ }
25853
26005
 
25854
26006
  // src/tools.ts
25855
26007
  var MIME = {
@@ -27462,6 +27614,31 @@ function ensureClient() {
27462
27614
  );
27463
27615
  }
27464
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)));
27465
27642
  log(`connected to ${SERVER} as ${self}`);
27466
27643
  return c;
27467
27644
  })().catch((e) => {
@@ -27478,7 +27655,15 @@ async function main() {
27478
27655
  await runDoctor();
27479
27656
  return;
27480
27657
  }
27481
- 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();
27482
27667
  const transport = new StdioServerTransport();
27483
27668
  server.server.oninitialized = () => {
27484
27669
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.29.0",
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": [