jefrichat-mcp 0.21.0 → 0.25.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.
Files changed (3) hide show
  1. package/dist/http.js +103 -17
  2. package/dist/index.js +156 -25
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -49520,7 +49520,8 @@ import os3 from "node:os";
49520
49520
  var DEFAULT_TEMPLATE = '{persona}You are @{me}, an autonomous agent on Jefri Chat. A message came in{where}. It may address several agents by @name (e.g. "@alice: do X @bob: do Y") \u2014 do ONLY the part addressed to you (@{me}); ignore parts meant for other agents. If nothing is addressed to you, reply with nothing to do. You can do ANYTHING (research, writing, analysis, code, running tools) \u2014 not just code. If a CLAUDE.md or relevant files are in this folder, read them first to get oriented. If the request is unclear, reply with ONE short clarifying question and stop (only your owner will answer). Otherwise do your part and reply concisely with the result.\n\n{context}Latest message from @{sender}: {message}';
49521
49521
  var DEFAULTS2 = {
49522
49522
  enabled: false,
49523
- brain: "claude",
49523
+ brain: "auto",
49524
+ // "auto" = match the HOST app (Codex→codex, Claude Code→claude)
49524
49525
  workdir: process.cwd(),
49525
49526
  replyMode: "mentions",
49526
49527
  persona: "",
@@ -49528,29 +49529,109 @@ var DEFAULTS2 = {
49528
49529
  ownerOnly: true,
49529
49530
  // SECURITY: default to only the owner can direct an autonomous agent
49530
49531
  contextMessages: 12,
49532
+ showWorking: true,
49533
+ // visibility: the headless brain is invisible, so tell the chat it's working
49531
49534
  promptTemplate: DEFAULT_TEMPLATE
49532
49535
  };
49533
49536
  var DIR = np2.join(os3.homedir(), ".jefri");
49534
- var FILE = np2.join(DIR, "autonomous.json");
49537
+ var AUTO_DIR = np2.join(DIR, "autonomous");
49538
+ var LEGACY_FILE = np2.join(DIR, "autonomous.json");
49535
49539
  var LOG_FILE2 = np2.join(DIR, "autonomous.log");
49536
- function loadCfg() {
49540
+ var safeUser = (u) => (u || "unknown").replace(/[^a-zA-Z0-9_.-]/g, "_");
49541
+ function cfgPath() {
49542
+ return np2.join(AUTO_DIR, `${safeUser(selfName)}.json`);
49543
+ }
49544
+ function lockPath() {
49545
+ return np2.join(AUTO_DIR, `${safeUser(selfName)}.lock`);
49546
+ }
49547
+ function logLine(line) {
49537
49548
  try {
49538
- return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(FILE, "utf8")) };
49549
+ fs3.mkdirSync(DIR, { recursive: true });
49550
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
49551
+ fs3.appendFileSync(LOG_FILE2, `[${ts}] ${line}
49552
+ `);
49539
49553
  } catch {
49540
- return { ...DEFAULTS2 };
49541
49554
  }
49542
49555
  }
49543
- var cfg = loadCfg();
49556
+ var cfg = { ...DEFAULTS2 };
49557
+ function withDerived(c) {
49558
+ return { ...c, resolvedBrain: resolveBrain(c.brain), ownerOnlyEffective: c.ownerOnly || FORCE_OWNER_ONLY, ownerOnlyEnforced: FORCE_OWNER_ONLY };
49559
+ }
49544
49560
  function setAuto(patch) {
49545
49561
  cfg = { ...cfg, ...patch };
49546
49562
  try {
49547
- fs3.mkdirSync(DIR, { recursive: true });
49548
- fs3.writeFileSync(FILE, JSON.stringify(cfg, null, 2));
49563
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
49564
+ fs3.writeFileSync(cfgPath(), JSON.stringify(cfg, null, 2));
49549
49565
  } catch {
49550
49566
  }
49551
- return { ...cfg };
49567
+ if (patch.enabled) acquireResponderLock();
49568
+ return withDerived(cfg);
49569
+ }
49570
+ var hostBrain = "";
49571
+ var hostName = "";
49572
+ function resolveBrain(brain) {
49573
+ if (brain && brain !== "auto") return brain;
49574
+ return hostBrain || "claude";
49575
+ }
49576
+ var selfName = "";
49577
+ var LOCK_STALE_MS = 9e4;
49578
+ var iAmResponder = false;
49579
+ var heartbeat = null;
49580
+ function pidAlive(pid) {
49581
+ try {
49582
+ process.kill(pid, 0);
49583
+ return true;
49584
+ } catch (e) {
49585
+ return e?.code === "EPERM";
49586
+ }
49587
+ }
49588
+ function acquireResponderLock() {
49589
+ try {
49590
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
49591
+ const now = Date.now();
49592
+ let cur = null;
49593
+ try {
49594
+ cur = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
49595
+ } catch {
49596
+ }
49597
+ const heldByOther = cur && cur.pid !== process.pid && pidAlive(cur.pid) && now - cur.ts < LOCK_STALE_MS;
49598
+ if (heldByOther) {
49599
+ if (iAmResponder) logLine(`yielded responder lock for @${selfName} to pid ${cur.pid} (${cur.host})`);
49600
+ iAmResponder = false;
49601
+ } else {
49602
+ fs3.writeFileSync(lockPath(), JSON.stringify({ pid: process.pid, host: hostName || "unknown", ts: now }));
49603
+ if (!iAmResponder) logLine(`acquired responder lock for @${selfName} (pid ${process.pid})`);
49604
+ iAmResponder = true;
49605
+ }
49606
+ } catch {
49607
+ iAmResponder = false;
49608
+ }
49609
+ if (!heartbeat) {
49610
+ heartbeat = setInterval(() => {
49611
+ if (cfg.enabled) acquireResponderLock();
49612
+ }, 3e4);
49613
+ heartbeat.unref?.();
49614
+ const release = () => {
49615
+ try {
49616
+ const c = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
49617
+ if (c.pid === process.pid) fs3.unlinkSync(lockPath());
49618
+ } catch {
49619
+ }
49620
+ };
49621
+ process.once("exit", release);
49622
+ process.once("SIGINT", () => {
49623
+ release();
49624
+ process.exit(0);
49625
+ });
49626
+ process.once("SIGTERM", () => {
49627
+ release();
49628
+ process.exit(0);
49629
+ });
49630
+ }
49631
+ return iAmResponder;
49552
49632
  }
49553
49633
  var TIMEOUT_MS = 5 * 60 * 1e3;
49634
+ var FORCE_OWNER_ONLY = true;
49554
49635
 
49555
49636
  // src/tools.ts
49556
49637
  var MIME = {
@@ -50793,14 +50874,15 @@ ${cmd}`);
50793
50874
  description: "Turn autonomous replies on/off for THIS agent. When ON, each incoming message is handled by a 'brain' (claude / codex / a custom command) that does the work in a folder and replies on its own \u2014 so you can, e.g., leave it on and it builds what people ask and answers them. Only works for your own agent (this connector's token). Call with NO arguments to see current settings. \u26A0\uFE0F When on, remote messages drive a tool-enabled agent on your machine \u2014 only keep it on with people you trust, and scope the work folder.",
50794
50875
  inputSchema: {
50795
50876
  enabled: external_exports.boolean().optional().describe("turn autonomous replies on/off"),
50796
- brain: external_exports.string().optional().describe("'claude', 'codex', or a full custom command (e.g. for OpenClaw)"),
50877
+ brain: external_exports.string().optional().describe("'auto' (default \u2014 matches the host app you launched from: Codex\u2192codex, Claude Code\u2192claude), or force 'claude' / 'codex' / a full custom command"),
50797
50878
  workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
50798
50879
  replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
50799
50880
  persona: external_exports.string().optional().describe("role/instructions for the agent (injected as {persona})"),
50800
50881
  promptTemplate: external_exports.string().optional().describe("advanced: fully customize how the message is framed to the brain. Placeholders: {persona} {me} {sender} {where} {context} {message}. Pass 'default' to reset."),
50801
- ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER \u2014 ignore other people (great for a private agent team). Default off."),
50882
+ ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER. NOTE: during beta this is FORCE-ENABLED for safety (a remote message drives a tool-agent on your machine) \u2014 setting false is accepted but has no effect yet."),
50802
50883
  contextMessages: external_exports.number().optional().describe("how many recent messages of the conversation to give the brain for memory (0 = stateless, default 12)"),
50803
- replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)")
50884
+ replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)"),
50885
+ showWorking: external_exports.boolean().optional().describe("post a quick '\u{1F427} on it\u2026' when the brain starts, so the invisible headless run is visible in chat (default on)")
50804
50886
  }
50805
50887
  },
50806
50888
  async (args) => withClient(async () => {
@@ -50809,7 +50891,8 @@ ${cmd}`);
50809
50891
  if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
50810
50892
  if (typeof args.ownerOnly === "boolean") patch.ownerOnly = args.ownerOnly;
50811
50893
  if (typeof args.contextMessages === "number") patch.contextMessages = Math.max(0, Math.min(40, Math.floor(args.contextMessages)));
50812
- if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim();
50894
+ if (typeof args.showWorking === "boolean") patch.showWorking = args.showWorking;
50895
+ if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim().toLowerCase() === "auto" ? "auto" : args.brain.trim();
50813
50896
  if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
50814
50897
  if (typeof args.persona === "string") patch.persona = args.persona;
50815
50898
  if (typeof args.promptTemplate === "string" && args.promptTemplate.trim())
@@ -50817,21 +50900,24 @@ ${cmd}`);
50817
50900
  if (args.replyMode) patch.replyMode = args.replyMode;
50818
50901
  const c = setAuto(patch);
50819
50902
  const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
50903
+ const ownerLabel = c.ownerOnlyEffective ? `yes \u2014 only you direct me${c.ownerOnlyEnforced && !c.ownerOnly ? " (force-enabled during beta)" : ""}` : "no (anyone who @mentions me)";
50904
+ const brainLabel = c.brain === "auto" ? `auto \u2192 ${c.resolvedBrain ?? "claude"} (this host)` : c.brain;
50820
50905
  const lines = [
50821
- `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"}`,
50822
- ` Brain: ${c.brain}`,
50906
+ `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"} \xB7 this agent replies from ONE place (this connector); the same agent open elsewhere stays silent`,
50907
+ ` Brain: ${brainLabel}`,
50823
50908
  ` Work folder: ${c.workdir}`,
50824
50909
  ` Replies to: ${modeLabel}`,
50825
- ` Owner-only: ${c.ownerOnly ? "yes (only you direct me)" : "no (anyone who @mentions me)"}`,
50910
+ ` Owner-only: ${ownerLabel}`,
50826
50911
  ` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
50827
50912
  ` Conversation memory: ${c.contextMessages > 0 ? `last ${c.contextMessages} messages` : "off"}`,
50913
+ ` Shows "on it\u2026" while working: ${c.showWorking ? "yes" : "no"}`,
50828
50914
  ` Prompt: ${c.promptTemplate === DEFAULT_TEMPLATE ? "default (@name routing + ask-if-unclear)" : "custom"}`,
50829
50915
  ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`,
50830
50916
  ` Activity log: ~/.jefri/autonomous.log (watch it: tail -f ~/.jefri/autonomous.log)`
50831
50917
  ];
50832
50918
  if (c.enabled)
50833
50919
  lines.push(`
50834
- \u26A0\uFE0F Incoming messages now drive "${c.brain}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
50920
+ \u26A0\uFE0F Incoming messages now drive "${brainLabel}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
50835
50921
  return ok(lines.join("\n"));
50836
50922
  })
50837
50923
  );
package/dist/index.js CHANGED
@@ -25494,7 +25494,8 @@ import os3 from "node:os";
25494
25494
  var DEFAULT_TEMPLATE = '{persona}You are @{me}, an autonomous agent on Jefri Chat. A message came in{where}. It may address several agents by @name (e.g. "@alice: do X @bob: do Y") \u2014 do ONLY the part addressed to you (@{me}); ignore parts meant for other agents. If nothing is addressed to you, reply with nothing to do. You can do ANYTHING (research, writing, analysis, code, running tools) \u2014 not just code. If a CLAUDE.md or relevant files are in this folder, read them first to get oriented. If the request is unclear, reply with ONE short clarifying question and stop (only your owner will answer). Otherwise do your part and reply concisely with the result.\n\n{context}Latest message from @{sender}: {message}';
25495
25495
  var DEFAULTS2 = {
25496
25496
  enabled: false,
25497
- brain: "claude",
25497
+ brain: "auto",
25498
+ // "auto" = match the HOST app (Codex→codex, Claude Code→claude)
25498
25499
  workdir: process.cwd(),
25499
25500
  replyMode: "mentions",
25500
25501
  persona: "",
@@ -25502,11 +25503,21 @@ var DEFAULTS2 = {
25502
25503
  ownerOnly: true,
25503
25504
  // SECURITY: default to only the owner can direct an autonomous agent
25504
25505
  contextMessages: 12,
25506
+ showWorking: true,
25507
+ // visibility: the headless brain is invisible, so tell the chat it's working
25505
25508
  promptTemplate: DEFAULT_TEMPLATE
25506
25509
  };
25507
25510
  var DIR = np2.join(os3.homedir(), ".jefri");
25508
- var FILE = np2.join(DIR, "autonomous.json");
25511
+ var AUTO_DIR = np2.join(DIR, "autonomous");
25512
+ var LEGACY_FILE = np2.join(DIR, "autonomous.json");
25509
25513
  var LOG_FILE2 = np2.join(DIR, "autonomous.log");
25514
+ var safeUser = (u) => (u || "unknown").replace(/[^a-zA-Z0-9_.-]/g, "_");
25515
+ function cfgPath() {
25516
+ return np2.join(AUTO_DIR, `${safeUser(selfName)}.json`);
25517
+ }
25518
+ function lockPath() {
25519
+ return np2.join(AUTO_DIR, `${safeUser(selfName)}.lock`);
25520
+ }
25510
25521
  function logLine(line) {
25511
25522
  try {
25512
25523
  fs3.mkdirSync(DIR, { recursive: true });
@@ -25518,23 +25529,47 @@ function logLine(line) {
25518
25529
  }
25519
25530
  function loadCfg() {
25520
25531
  try {
25521
- return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(FILE, "utf8")) };
25532
+ return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(cfgPath(), "utf8")) };
25533
+ } catch {
25534
+ }
25535
+ try {
25536
+ const legacy = { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(LEGACY_FILE, "utf8")) };
25537
+ logLine(`migrated legacy autonomous.json \u2192 ${safeUser(selfName)}.json`);
25538
+ return legacy;
25522
25539
  } catch {
25523
25540
  return { ...DEFAULTS2 };
25524
25541
  }
25525
25542
  }
25526
- var cfg = loadCfg();
25543
+ var cfg = { ...DEFAULTS2 };
25544
+ function withDerived(c) {
25545
+ return { ...c, resolvedBrain: resolveBrain(c.brain), ownerOnlyEffective: c.ownerOnly || FORCE_OWNER_ONLY, ownerOnlyEnforced: FORCE_OWNER_ONLY };
25546
+ }
25527
25547
  function getAuto() {
25528
- return { ...cfg };
25548
+ return withDerived(cfg);
25529
25549
  }
25530
25550
  function setAuto(patch) {
25531
25551
  cfg = { ...cfg, ...patch };
25532
25552
  try {
25533
- fs3.mkdirSync(DIR, { recursive: true });
25534
- fs3.writeFileSync(FILE, JSON.stringify(cfg, null, 2));
25553
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
25554
+ fs3.writeFileSync(cfgPath(), JSON.stringify(cfg, null, 2));
25535
25555
  } catch {
25536
25556
  }
25537
- return { ...cfg };
25557
+ if (patch.enabled) acquireResponderLock();
25558
+ return withDerived(cfg);
25559
+ }
25560
+ var hostBrain = "";
25561
+ var hostName = "";
25562
+ function setAutoHost(clientName) {
25563
+ hostName = clientName ?? "";
25564
+ const n = hostName.toLowerCase();
25565
+ if (n.includes("codex")) hostBrain = "codex";
25566
+ else if (n.includes("claude")) hostBrain = "claude";
25567
+ else hostBrain = "";
25568
+ logLine(`host detected: "${hostName}" \u2192 brain ${hostBrain || "(unknown, default claude)"}`);
25569
+ }
25570
+ function resolveBrain(brain) {
25571
+ if (brain && brain !== "auto") return brain;
25572
+ return hostBrain || "claude";
25538
25573
  }
25539
25574
  var selfName = "";
25540
25575
  var ownerName = "";
@@ -25543,12 +25578,78 @@ function configureAuto(opts) {
25543
25578
  selfName = opts.self;
25544
25579
  ownerName = opts.owner ?? "";
25545
25580
  fetchHistory = opts.getHistory;
25581
+ cfg = loadCfg();
25582
+ if (cfg.enabled) acquireResponderLock();
25583
+ }
25584
+ var LOCK_STALE_MS = 9e4;
25585
+ var iAmResponder = false;
25586
+ var heartbeat = null;
25587
+ function pidAlive(pid) {
25588
+ try {
25589
+ process.kill(pid, 0);
25590
+ return true;
25591
+ } catch (e) {
25592
+ return e?.code === "EPERM";
25593
+ }
25594
+ }
25595
+ function acquireResponderLock() {
25596
+ try {
25597
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
25598
+ const now = Date.now();
25599
+ let cur = null;
25600
+ try {
25601
+ cur = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
25602
+ } catch {
25603
+ }
25604
+ const heldByOther = cur && cur.pid !== process.pid && pidAlive(cur.pid) && now - cur.ts < LOCK_STALE_MS;
25605
+ if (heldByOther) {
25606
+ if (iAmResponder) logLine(`yielded responder lock for @${selfName} to pid ${cur.pid} (${cur.host})`);
25607
+ iAmResponder = false;
25608
+ } else {
25609
+ fs3.writeFileSync(lockPath(), JSON.stringify({ pid: process.pid, host: hostName || "unknown", ts: now }));
25610
+ if (!iAmResponder) logLine(`acquired responder lock for @${selfName} (pid ${process.pid})`);
25611
+ iAmResponder = true;
25612
+ }
25613
+ } catch {
25614
+ iAmResponder = false;
25615
+ }
25616
+ if (!heartbeat) {
25617
+ heartbeat = setInterval(() => {
25618
+ if (cfg.enabled) acquireResponderLock();
25619
+ }, 3e4);
25620
+ heartbeat.unref?.();
25621
+ const release = () => {
25622
+ try {
25623
+ const c = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
25624
+ if (c.pid === process.pid) fs3.unlinkSync(lockPath());
25625
+ } catch {
25626
+ }
25627
+ };
25628
+ process.once("exit", release);
25629
+ process.once("SIGINT", () => {
25630
+ release();
25631
+ process.exit(0);
25632
+ });
25633
+ process.once("SIGTERM", () => {
25634
+ release();
25635
+ process.exit(0);
25636
+ });
25637
+ }
25638
+ return iAmResponder;
25546
25639
  }
25547
25640
  var expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
25641
+ function tokenize(cmd) {
25642
+ const out = [];
25643
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
25644
+ let m;
25645
+ while ((m = re.exec(cmd)) !== null) out.push(m[1] ?? m[2] ?? m[3]);
25646
+ return out;
25647
+ }
25548
25648
  function brainArgv(brain, prompt) {
25549
- if (brain === "claude") return ["claude", ["-p", "--permission-mode", "acceptEdits", prompt]];
25550
- if (brain === "codex") return ["codex", ["exec", "--full-auto", prompt]];
25551
- const parts = brain.trim().split(/\s+/);
25649
+ const b = resolveBrain(brain);
25650
+ if (b === "claude") return ["claude", ["-p", "--permission-mode", "acceptEdits", prompt]];
25651
+ if (b === "codex") return ["codex", ["exec", "--full-auto", prompt]];
25652
+ const parts = tokenize(b.trim());
25552
25653
  return [parts[0], [...parts.slice(1), prompt]];
25553
25654
  }
25554
25655
  var MAX_PROMPT = 12e3;
@@ -25557,6 +25658,7 @@ var TIMEOUT_MS = 5 * 60 * 1e3;
25557
25658
  var FORCE_OWNER_ONLY = true;
25558
25659
  function shouldHandle(m) {
25559
25660
  if (!cfg.enabled) return false;
25661
+ if (!iAmResponder) return false;
25560
25662
  const fromOwner = !!ownerName && m.sender === ownerName;
25561
25663
  const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY;
25562
25664
  if (ownerOnly && !fromOwner) return false;
@@ -25637,23 +25739,41 @@ function handleAutonomous(m, reply, log2) {
25637
25739
  const context = await buildContext(m.conversationId);
25638
25740
  const prompt = (cfg.promptTemplate || DEFAULT_TEMPLATE).replace(/\{persona\}/g, persona).replace(/\{me\}/g, selfName).replace(/\{sender\}/g, m.sender).replace(/\{where\}/g, where).replace(/\{context\}/g, context).replace(/\{message\}/g, m.content).slice(0, MAX_PROMPT);
25639
25741
  const [cmd, args] = brainArgv(cfg.brain, prompt);
25640
- log2(`autonomous: @${m.sender} \u2192 running "${cfg.brain}"\u2026`);
25641
- logLine(` running: ${cfg.brain} in ${cfg.workdir}`);
25742
+ const shownBrain = resolveBrain(cfg.brain) + (cfg.brain === "auto" ? " (auto \u2192 host)" : "");
25743
+ log2(`autonomous: @${m.sender} \u2192 running "${shownBrain}"\u2026`);
25744
+ logLine(` running: ${shownBrain} in ${cfg.workdir}`);
25745
+ if (cfg.showWorking) {
25746
+ try {
25747
+ reply(`\u{1F427} on it\u2026 (${resolveBrain(cfg.brain)})`);
25748
+ } catch {
25749
+ }
25750
+ }
25642
25751
  try {
25643
25752
  const out = await runBrain(cmd, args, cfg.workdir);
25644
25753
  const text = (out.trim() || "(done)").slice(0, MAX_REPLY);
25645
- reply(text);
25646
- log2(`autonomous: replied to @${m.sender}`);
25647
- logLine(` \u2192 replied: ${text.replace(/\s+/g, " ").slice(0, 300)}`);
25754
+ const delivered = await deliverReply(reply, text);
25755
+ log2(`autonomous: ${delivered ? "replied to" : "reply FAILED (socket down) for"} @${m.sender}`);
25756
+ logLine(` ${delivered ? "\u2192 replied" : "\u2717 reply lost (socket down after retries)"}: ${text.replace(/\s+/g, " ").slice(0, 300)}`);
25648
25757
  } catch (e) {
25649
25758
  const msg = `(couldn't finish autonomously: ${e?.message ?? e})`.slice(0, MAX_REPLY);
25650
- reply(msg);
25759
+ await deliverReply(reply, msg);
25651
25760
  log2(`autonomous: failed for @${m.sender}: ${e?.message ?? e}`);
25652
25761
  logLine(` \u2717 failed: ${e?.message ?? e}`);
25653
25762
  }
25654
25763
  });
25655
25764
  void drain();
25656
25765
  }
25766
+ async function deliverReply(reply, text) {
25767
+ for (let attempt = 0; attempt < 10; attempt++) {
25768
+ try {
25769
+ reply(text);
25770
+ return true;
25771
+ } catch {
25772
+ await new Promise((r) => setTimeout(r, 3e3));
25773
+ }
25774
+ }
25775
+ return false;
25776
+ }
25657
25777
 
25658
25778
  // src/tools.ts
25659
25779
  var MIME = {
@@ -26896,14 +27016,15 @@ ${cmd}`);
26896
27016
  description: "Turn autonomous replies on/off for THIS agent. When ON, each incoming message is handled by a 'brain' (claude / codex / a custom command) that does the work in a folder and replies on its own \u2014 so you can, e.g., leave it on and it builds what people ask and answers them. Only works for your own agent (this connector's token). Call with NO arguments to see current settings. \u26A0\uFE0F When on, remote messages drive a tool-enabled agent on your machine \u2014 only keep it on with people you trust, and scope the work folder.",
26897
27017
  inputSchema: {
26898
27018
  enabled: external_exports.boolean().optional().describe("turn autonomous replies on/off"),
26899
- brain: external_exports.string().optional().describe("'claude', 'codex', or a full custom command (e.g. for OpenClaw)"),
27019
+ brain: external_exports.string().optional().describe("'auto' (default \u2014 matches the host app you launched from: Codex\u2192codex, Claude Code\u2192claude), or force 'claude' / 'codex' / a full custom command"),
26900
27020
  workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
26901
27021
  replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
26902
27022
  persona: external_exports.string().optional().describe("role/instructions for the agent (injected as {persona})"),
26903
27023
  promptTemplate: external_exports.string().optional().describe("advanced: fully customize how the message is framed to the brain. Placeholders: {persona} {me} {sender} {where} {context} {message}. Pass 'default' to reset."),
26904
- ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER \u2014 ignore other people (great for a private agent team). Default off."),
27024
+ ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER. NOTE: during beta this is FORCE-ENABLED for safety (a remote message drives a tool-agent on your machine) \u2014 setting false is accepted but has no effect yet."),
26905
27025
  contextMessages: external_exports.number().optional().describe("how many recent messages of the conversation to give the brain for memory (0 = stateless, default 12)"),
26906
- replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)")
27026
+ replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)"),
27027
+ showWorking: external_exports.boolean().optional().describe("post a quick '\u{1F427} on it\u2026' when the brain starts, so the invisible headless run is visible in chat (default on)")
26907
27028
  }
26908
27029
  },
26909
27030
  async (args) => withClient(async () => {
@@ -26912,7 +27033,8 @@ ${cmd}`);
26912
27033
  if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
26913
27034
  if (typeof args.ownerOnly === "boolean") patch.ownerOnly = args.ownerOnly;
26914
27035
  if (typeof args.contextMessages === "number") patch.contextMessages = Math.max(0, Math.min(40, Math.floor(args.contextMessages)));
26915
- if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim();
27036
+ if (typeof args.showWorking === "boolean") patch.showWorking = args.showWorking;
27037
+ if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim().toLowerCase() === "auto" ? "auto" : args.brain.trim();
26916
27038
  if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
26917
27039
  if (typeof args.persona === "string") patch.persona = args.persona;
26918
27040
  if (typeof args.promptTemplate === "string" && args.promptTemplate.trim())
@@ -26920,21 +27042,24 @@ ${cmd}`);
26920
27042
  if (args.replyMode) patch.replyMode = args.replyMode;
26921
27043
  const c = setAuto(patch);
26922
27044
  const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
27045
+ const ownerLabel = c.ownerOnlyEffective ? `yes \u2014 only you direct me${c.ownerOnlyEnforced && !c.ownerOnly ? " (force-enabled during beta)" : ""}` : "no (anyone who @mentions me)";
27046
+ const brainLabel = c.brain === "auto" ? `auto \u2192 ${c.resolvedBrain ?? "claude"} (this host)` : c.brain;
26923
27047
  const lines = [
26924
- `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"}`,
26925
- ` Brain: ${c.brain}`,
27048
+ `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"} \xB7 this agent replies from ONE place (this connector); the same agent open elsewhere stays silent`,
27049
+ ` Brain: ${brainLabel}`,
26926
27050
  ` Work folder: ${c.workdir}`,
26927
27051
  ` Replies to: ${modeLabel}`,
26928
- ` Owner-only: ${c.ownerOnly ? "yes (only you direct me)" : "no (anyone who @mentions me)"}`,
27052
+ ` Owner-only: ${ownerLabel}`,
26929
27053
  ` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
26930
27054
  ` Conversation memory: ${c.contextMessages > 0 ? `last ${c.contextMessages} messages` : "off"}`,
27055
+ ` Shows "on it\u2026" while working: ${c.showWorking ? "yes" : "no"}`,
26931
27056
  ` Prompt: ${c.promptTemplate === DEFAULT_TEMPLATE ? "default (@name routing + ask-if-unclear)" : "custom"}`,
26932
27057
  ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`,
26933
27058
  ` Activity log: ~/.jefri/autonomous.log (watch it: tail -f ~/.jefri/autonomous.log)`
26934
27059
  ];
26935
27060
  if (c.enabled)
26936
27061
  lines.push(`
26937
- \u26A0\uFE0F Incoming messages now drive "${c.brain}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
27062
+ \u26A0\uFE0F Incoming messages now drive "${brainLabel}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
26938
27063
  return ok(lines.join("\n"));
26939
27064
  })
26940
27065
  );
@@ -27229,6 +27354,12 @@ async function main() {
27229
27354
  }
27230
27355
  ensureClient().catch((e) => log("initial connect failed (will retry on first tool):", e?.message ?? e));
27231
27356
  const transport = new StdioServerTransport();
27357
+ server.server.oninitialized = () => {
27358
+ try {
27359
+ setAutoHost(server.server.getClientVersion()?.name);
27360
+ } catch {
27361
+ }
27362
+ };
27232
27363
  await server.connect(transport);
27233
27364
  log(`MCP server ready \u2014 identity @${USERNAME}, Jefri Chat server ${SERVER}`);
27234
27365
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.21.0",
3
+ "version": "0.25.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": {