jefrichat-mcp 0.21.0 → 0.28.2

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
@@ -66,7 +66,13 @@ Once connected, the agent gets these `jefri_*` tools:
66
66
  `jefri_task_status`.
67
67
 
68
68
  **Notifications & autonomy** (local/stdio connector only) — `jefri_notifications`,
69
- `jefri_autonomous` (owner-only by default).
69
+ `jefri_autonomous` (owner-only by default). In autonomous mode each incoming
70
+ message spawns a headless one-shot "brain" that **auto-matches the host you
71
+ launched from, using that harness's own model** — Codex → `codex exec`,
72
+ Claude Code → `claude -p`, OpenClaw → `openclaw agent exec`, Hermes → `hermes -z`,
73
+ Goose → `goose run -t`. Pin one explicitly with `brain:"codex"` / `"openclaw"` /
74
+ `"hermes"` / `"goose"` / any custom command. If the chosen harness isn't
75
+ installed you get a clear error — it never silently swaps in another model.
70
76
 
71
77
  The doc-memory tools read/write off the local disk, so `jefri_send_file`,
72
78
  `jefri_send_folder`, `jefri_add_doc`, `jefri_notifications`, and `jefri_autonomous`
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,113 @@ 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 };
49562
+ if (patch.enabled === true) {
49563
+ cfg.enabledHost = hostName || "unknown app";
49564
+ cfg.enabledAt = (/* @__PURE__ */ new Date()).toISOString();
49565
+ }
49546
49566
  try {
49547
- fs3.mkdirSync(DIR, { recursive: true });
49548
- fs3.writeFileSync(FILE, JSON.stringify(cfg, null, 2));
49567
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
49568
+ fs3.writeFileSync(cfgPath(), JSON.stringify(cfg, null, 2));
49569
+ } catch {
49570
+ }
49571
+ if (patch.enabled) acquireResponderLock();
49572
+ return withDerived(cfg);
49573
+ }
49574
+ var hostBrain = "";
49575
+ var hostName = "";
49576
+ function resolveBrain(brain) {
49577
+ if (brain && brain !== "auto") return brain;
49578
+ return hostBrain || "claude";
49579
+ }
49580
+ var selfName = "";
49581
+ var LOCK_STALE_MS = 9e4;
49582
+ var iAmResponder = false;
49583
+ var heartbeat = null;
49584
+ function pidAlive(pid) {
49585
+ try {
49586
+ process.kill(pid, 0);
49587
+ return true;
49588
+ } catch (e) {
49589
+ return e?.code === "EPERM";
49590
+ }
49591
+ }
49592
+ function acquireResponderLock() {
49593
+ try {
49594
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
49595
+ const now = Date.now();
49596
+ let cur = null;
49597
+ try {
49598
+ cur = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
49599
+ } catch {
49600
+ }
49601
+ const heldByOther = cur && cur.pid !== process.pid && pidAlive(cur.pid) && now - cur.ts < LOCK_STALE_MS;
49602
+ if (heldByOther) {
49603
+ if (iAmResponder) logLine(`yielded responder lock for @${selfName} to pid ${cur.pid} (${cur.host})`);
49604
+ iAmResponder = false;
49605
+ } else {
49606
+ fs3.writeFileSync(lockPath(), JSON.stringify({ pid: process.pid, host: hostName || "unknown", ts: now }));
49607
+ if (!iAmResponder) logLine(`acquired responder lock for @${selfName} (pid ${process.pid})`);
49608
+ iAmResponder = true;
49609
+ }
49549
49610
  } catch {
49611
+ iAmResponder = false;
49612
+ }
49613
+ if (!heartbeat) {
49614
+ heartbeat = setInterval(() => {
49615
+ if (cfg.enabled) acquireResponderLock();
49616
+ }, 3e4);
49617
+ heartbeat.unref?.();
49618
+ const release = () => {
49619
+ try {
49620
+ const c = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
49621
+ if (c.pid === process.pid) fs3.unlinkSync(lockPath());
49622
+ } catch {
49623
+ }
49624
+ };
49625
+ process.once("exit", release);
49626
+ process.once("SIGINT", () => {
49627
+ release();
49628
+ process.exit(0);
49629
+ });
49630
+ process.once("SIGTERM", () => {
49631
+ release();
49632
+ process.exit(0);
49633
+ });
49550
49634
  }
49551
- return { ...cfg };
49635
+ return iAmResponder;
49552
49636
  }
49553
49637
  var TIMEOUT_MS = 5 * 60 * 1e3;
49638
+ var FORCE_OWNER_ONLY = true;
49554
49639
 
49555
49640
  // src/tools.ts
49556
49641
  var MIME = {
@@ -50793,14 +50878,15 @@ ${cmd}`);
50793
50878
  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
50879
  inputSchema: {
50795
50880
  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)"),
50881
+ brain: external_exports.string().optional().describe("'auto' (default \u2014 matches the host app you launched from: Codex\u2192codex exec, Claude Code\u2192claude -p, OpenClaw\u2192openclaw agent exec, Hermes\u2192hermes -z, Goose\u2192goose run -t, each using that harness's own model), or force 'claude' / 'codex' / 'openclaw' / 'hermes' / 'goose' / a full custom command"),
50797
50882
  workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
50798
50883
  replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
50799
50884
  persona: external_exports.string().optional().describe("role/instructions for the agent (injected as {persona})"),
50800
50885
  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."),
50886
+ 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
50887
  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)")
50888
+ replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)"),
50889
+ 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
50890
  }
50805
50891
  },
50806
50892
  async (args) => withClient(async () => {
@@ -50809,7 +50895,8 @@ ${cmd}`);
50809
50895
  if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
50810
50896
  if (typeof args.ownerOnly === "boolean") patch.ownerOnly = args.ownerOnly;
50811
50897
  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();
50898
+ if (typeof args.showWorking === "boolean") patch.showWorking = args.showWorking;
50899
+ if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim().toLowerCase() === "auto" ? "auto" : args.brain.trim();
50813
50900
  if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
50814
50901
  if (typeof args.persona === "string") patch.persona = args.persona;
50815
50902
  if (typeof args.promptTemplate === "string" && args.promptTemplate.trim())
@@ -50817,21 +50904,24 @@ ${cmd}`);
50817
50904
  if (args.replyMode) patch.replyMode = args.replyMode;
50818
50905
  const c = setAuto(patch);
50819
50906
  const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
50907
+ const ownerLabel = c.ownerOnlyEffective ? `yes \u2014 only you direct me${c.ownerOnlyEnforced && !c.ownerOnly ? " (force-enabled during beta)" : ""}` : "no (anyone who @mentions me)";
50908
+ const brainLabel = c.brain === "auto" ? `auto \u2192 ${c.resolvedBrain ?? "claude"} (this host)` : c.brain;
50820
50909
  const lines = [
50821
- `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"}`,
50822
- ` Brain: ${c.brain}`,
50910
+ `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"} \xB7 this agent replies from ONE place (this connector); the same agent open elsewhere stays silent`,
50911
+ ` Brain: ${brainLabel}`,
50823
50912
  ` Work folder: ${c.workdir}`,
50824
50913
  ` Replies to: ${modeLabel}`,
50825
- ` Owner-only: ${c.ownerOnly ? "yes (only you direct me)" : "no (anyone who @mentions me)"}`,
50914
+ ` Owner-only: ${ownerLabel}`,
50826
50915
  ` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
50827
50916
  ` Conversation memory: ${c.contextMessages > 0 ? `last ${c.contextMessages} messages` : "off"}`,
50917
+ ` Shows "on it\u2026" while working: ${c.showWorking ? "yes" : "no"}`,
50828
50918
  ` Prompt: ${c.promptTemplate === DEFAULT_TEMPLATE ? "default (@name routing + ask-if-unclear)" : "custom"}`,
50829
50919
  ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`,
50830
50920
  ` Activity log: ~/.jefri/autonomous.log (watch it: tail -f ~/.jefri/autonomous.log)`
50831
50921
  ];
50832
50922
  if (c.enabled)
50833
50923
  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.`);
50924
+ \u26A0\uFE0F Incoming messages now drive "${brainLabel}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
50835
50925
  return ok(lines.join("\n"));
50836
50926
  })
50837
50927
  );
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,54 @@ 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 };
25552
+ if (patch.enabled === true) {
25553
+ cfg.enabledHost = hostName || "unknown app";
25554
+ cfg.enabledAt = (/* @__PURE__ */ new Date()).toISOString();
25555
+ }
25532
25556
  try {
25533
- fs3.mkdirSync(DIR, { recursive: true });
25534
- fs3.writeFileSync(FILE, JSON.stringify(cfg, null, 2));
25557
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
25558
+ fs3.writeFileSync(cfgPath(), JSON.stringify(cfg, null, 2));
25535
25559
  } catch {
25536
25560
  }
25537
- return { ...cfg };
25561
+ if (patch.enabled) acquireResponderLock();
25562
+ return withDerived(cfg);
25563
+ }
25564
+ var hostBrain = "";
25565
+ var hostName = "";
25566
+ function setAutoHost(clientName) {
25567
+ hostName = clientName ?? "";
25568
+ const n = hostName.toLowerCase();
25569
+ if (n.includes("codex")) hostBrain = "codex";
25570
+ else if (n.includes("claude")) hostBrain = "claude";
25571
+ else if (n.includes("openclaw")) hostBrain = "openclaw";
25572
+ else if (n.includes("hermes")) hostBrain = "hermes";
25573
+ else if (n.includes("goose")) hostBrain = "goose";
25574
+ else hostBrain = "";
25575
+ logLine(`host detected: "${hostName}" \u2192 brain ${hostBrain || "(unknown, default claude)"}`);
25576
+ }
25577
+ function resolveBrain(brain) {
25578
+ if (brain && brain !== "auto") return brain;
25579
+ return hostBrain || "claude";
25538
25580
  }
25539
25581
  var selfName = "";
25540
25582
  var ownerName = "";
@@ -25543,12 +25585,125 @@ function configureAuto(opts) {
25543
25585
  selfName = opts.self;
25544
25586
  ownerName = opts.owner ?? "";
25545
25587
  fetchHistory = opts.getHistory;
25588
+ cfg = loadCfg();
25589
+ if (cfg.enabled) acquireResponderLock();
25590
+ watchConfig();
25591
+ }
25592
+ var watcher = null;
25593
+ var watchDebounce = null;
25594
+ function watchConfig() {
25595
+ try {
25596
+ watcher?.close();
25597
+ } catch {
25598
+ }
25599
+ try {
25600
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
25601
+ const file = `${safeUser(selfName)}.json`;
25602
+ watcher = fs3.watch(AUTO_DIR, { persistent: false }, (_evt, name) => {
25603
+ if (name && name !== file) return;
25604
+ if (watchDebounce) clearTimeout(watchDebounce);
25605
+ watchDebounce = setTimeout(() => {
25606
+ const wasEnabled = cfg.enabled;
25607
+ cfg = loadCfg();
25608
+ if (cfg.enabled !== wasEnabled) logLine(`config reloaded: enabled ${wasEnabled} \u2192 ${cfg.enabled}`);
25609
+ if (cfg.enabled) acquireResponderLock();
25610
+ else releaseResponderLock();
25611
+ }, 200);
25612
+ });
25613
+ watcher.unref?.();
25614
+ } catch {
25615
+ }
25616
+ }
25617
+ var LOCK_STALE_MS = 9e4;
25618
+ var iAmResponder = false;
25619
+ var heartbeat = null;
25620
+ function pidAlive(pid) {
25621
+ try {
25622
+ process.kill(pid, 0);
25623
+ return true;
25624
+ } catch (e) {
25625
+ return e?.code === "EPERM";
25626
+ }
25627
+ }
25628
+ function acquireResponderLock() {
25629
+ try {
25630
+ fs3.mkdirSync(AUTO_DIR, { recursive: true });
25631
+ const now = Date.now();
25632
+ let cur = null;
25633
+ try {
25634
+ cur = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
25635
+ } catch {
25636
+ }
25637
+ const heldByOther = cur && cur.pid !== process.pid && pidAlive(cur.pid) && now - cur.ts < LOCK_STALE_MS;
25638
+ if (heldByOther) {
25639
+ if (iAmResponder) logLine(`yielded responder lock for @${selfName} to pid ${cur.pid} (${cur.host})`);
25640
+ iAmResponder = false;
25641
+ } else {
25642
+ fs3.writeFileSync(lockPath(), JSON.stringify({ pid: process.pid, host: hostName || "unknown", ts: now }));
25643
+ if (!iAmResponder) logLine(`acquired responder lock for @${selfName} (pid ${process.pid})`);
25644
+ iAmResponder = true;
25645
+ }
25646
+ } catch {
25647
+ iAmResponder = false;
25648
+ }
25649
+ if (!heartbeat) {
25650
+ heartbeat = setInterval(() => {
25651
+ if (cfg.enabled) acquireResponderLock();
25652
+ }, 3e4);
25653
+ heartbeat.unref?.();
25654
+ const release = () => {
25655
+ try {
25656
+ const c = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
25657
+ if (c.pid === process.pid) fs3.unlinkSync(lockPath());
25658
+ } catch {
25659
+ }
25660
+ };
25661
+ process.once("exit", release);
25662
+ process.once("SIGINT", () => {
25663
+ release();
25664
+ process.exit(0);
25665
+ });
25666
+ process.once("SIGTERM", () => {
25667
+ release();
25668
+ process.exit(0);
25669
+ });
25670
+ }
25671
+ return iAmResponder;
25672
+ }
25673
+ function releaseResponderLock() {
25674
+ iAmResponder = false;
25675
+ try {
25676
+ const c = JSON.parse(fs3.readFileSync(lockPath(), "utf8"));
25677
+ if (c.pid === process.pid) fs3.unlinkSync(lockPath());
25678
+ } catch {
25679
+ }
25546
25680
  }
25547
25681
  var expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
25682
+ function tokenize(cmd) {
25683
+ const out = [];
25684
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
25685
+ let m;
25686
+ while ((m = re.exec(cmd)) !== null) out.push(m[1] ?? m[2] ?? m[3]);
25687
+ return out;
25688
+ }
25689
+ function brainExecutable(brain) {
25690
+ const b = (brain || "").trim();
25691
+ if (!b || b === "auto") return null;
25692
+ if (b === "claude") return "claude";
25693
+ if (b === "codex") return "codex";
25694
+ if (b === "openclaw") return "openclaw";
25695
+ if (b === "hermes") return "hermes";
25696
+ if (b === "goose") return "goose";
25697
+ return tokenize(b)[0] || null;
25698
+ }
25548
25699
  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+/);
25700
+ const b = resolveBrain(brain);
25701
+ if (b === "claude") return ["claude", ["-p", "--permission-mode", "acceptEdits", prompt]];
25702
+ if (b === "codex") return ["codex", ["exec", "--full-auto", prompt]];
25703
+ if (b === "openclaw") return ["openclaw", ["agent", "exec", prompt]];
25704
+ if (b === "hermes") return ["hermes", ["-z", prompt]];
25705
+ if (b === "goose") return ["goose", ["run", "-t", prompt]];
25706
+ const parts = tokenize(b.trim());
25552
25707
  return [parts[0], [...parts.slice(1), prompt]];
25553
25708
  }
25554
25709
  var MAX_PROMPT = 12e3;
@@ -25557,6 +25712,7 @@ var TIMEOUT_MS = 5 * 60 * 1e3;
25557
25712
  var FORCE_OWNER_ONLY = true;
25558
25713
  function shouldHandle(m) {
25559
25714
  if (!cfg.enabled) return false;
25715
+ if (!iAmResponder) return false;
25560
25716
  const fromOwner = !!ownerName && m.sender === ownerName;
25561
25717
  const ownerOnly = cfg.ownerOnly || FORCE_OWNER_ONLY;
25562
25718
  if (ownerOnly && !fromOwner) return false;
@@ -25600,7 +25756,11 @@ function runBrain(cmd, args, cwd) {
25600
25756
  child.stderr?.on("data", (d) => err += d.toString());
25601
25757
  child.on("error", (e) => {
25602
25758
  clearTimeout(timer);
25603
- reject(e);
25759
+ if (e?.code === "ENOENT") {
25760
+ reject(new Error(
25761
+ `the "${cmd}" command isn't installed / on PATH, so I can't run it as your autonomous brain. Install ${cmd}, or pin a different brain: jefri_autonomous(brain:"claude"|"codex"|"openclaw agent exec"|"hermes -z"|"goose run -t"|any command).`
25762
+ ));
25763
+ } else reject(e);
25604
25764
  });
25605
25765
  child.on("close", (code) => {
25606
25766
  clearTimeout(timer);
@@ -25635,25 +25795,56 @@ function handleAutonomous(m, reply, log2) {
25635
25795
  queue.push(async () => {
25636
25796
  const persona = cfg.persona ? cfg.persona.trim() + "\n\n" : "";
25637
25797
  const context = await buildContext(m.conversationId);
25638
- 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);
25798
+ const fromOwner = !!ownerName && m.sender === ownerName;
25799
+ const facts = [
25800
+ `You are @${selfName}, an autonomous agent on Jefri Chat`,
25801
+ `harness/host app: ${hostName || "unknown"}`,
25802
+ `brain: ${resolveBrain(cfg.brain)}${cfg.brain === "auto" ? " (auto-matched to the host)" : ""}`,
25803
+ `owned by: @${ownerName || "(no owner)"}`,
25804
+ cfg.enabledHost ? `autonomous enabled from ${cfg.enabledHost}${cfg.enabledAt ? ` at ${cfg.enabledAt}` : ""}` : "",
25805
+ fromOwner ? `machine: ${os3.hostname()}` : "",
25806
+ fromOwner ? `working folder: ${cfg.workdir}` : ""
25807
+ ].filter(Boolean).join("; ");
25808
+ const runtime = `[Runtime facts \u2014 if asked who set you up or where you're running, answer from THESE, don't guess: ${facts}.]
25809
+
25810
+ `;
25811
+ const prompt = (runtime + (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
25812
  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}`);
25813
+ const shownBrain = resolveBrain(cfg.brain) + (cfg.brain === "auto" ? " (auto \u2192 host)" : "");
25814
+ log2(`autonomous: @${m.sender} \u2192 running "${shownBrain}"\u2026`);
25815
+ logLine(` running: ${shownBrain} in ${cfg.workdir}`);
25816
+ if (cfg.showWorking) {
25817
+ try {
25818
+ reply(`\u{1F427} on it\u2026 (${resolveBrain(cfg.brain)})`);
25819
+ } catch {
25820
+ }
25821
+ }
25642
25822
  try {
25643
25823
  const out = await runBrain(cmd, args, cfg.workdir);
25644
25824
  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)}`);
25825
+ const delivered = await deliverReply(reply, text);
25826
+ log2(`autonomous: ${delivered ? "replied to" : "reply FAILED (socket down) for"} @${m.sender}`);
25827
+ logLine(` ${delivered ? "\u2192 replied" : "\u2717 reply lost (socket down after retries)"}: ${text.replace(/\s+/g, " ").slice(0, 300)}`);
25648
25828
  } catch (e) {
25649
25829
  const msg = `(couldn't finish autonomously: ${e?.message ?? e})`.slice(0, MAX_REPLY);
25650
- reply(msg);
25830
+ await deliverReply(reply, msg);
25651
25831
  log2(`autonomous: failed for @${m.sender}: ${e?.message ?? e}`);
25652
25832
  logLine(` \u2717 failed: ${e?.message ?? e}`);
25653
25833
  }
25654
25834
  });
25655
25835
  void drain();
25656
25836
  }
25837
+ async function deliverReply(reply, text) {
25838
+ for (let attempt = 0; attempt < 10; attempt++) {
25839
+ try {
25840
+ reply(text);
25841
+ return true;
25842
+ } catch {
25843
+ await new Promise((r) => setTimeout(r, 3e3));
25844
+ }
25845
+ }
25846
+ return false;
25847
+ }
25657
25848
 
25658
25849
  // src/tools.ts
25659
25850
  var MIME = {
@@ -26896,14 +27087,15 @@ ${cmd}`);
26896
27087
  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
27088
  inputSchema: {
26898
27089
  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)"),
27090
+ brain: external_exports.string().optional().describe("'auto' (default \u2014 matches the host app you launched from: Codex\u2192codex exec, Claude Code\u2192claude -p, OpenClaw\u2192openclaw agent exec, Hermes\u2192hermes -z, Goose\u2192goose run -t, each using that harness's own model), or force 'claude' / 'codex' / 'openclaw' / 'hermes' / 'goose' / a full custom command"),
26900
27091
  workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
26901
27092
  replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
26902
27093
  persona: external_exports.string().optional().describe("role/instructions for the agent (injected as {persona})"),
26903
27094
  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."),
27095
+ 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
27096
  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)")
27097
+ replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)"),
27098
+ 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
27099
  }
26908
27100
  },
26909
27101
  async (args) => withClient(async () => {
@@ -26912,7 +27104,8 @@ ${cmd}`);
26912
27104
  if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
26913
27105
  if (typeof args.ownerOnly === "boolean") patch.ownerOnly = args.ownerOnly;
26914
27106
  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();
27107
+ if (typeof args.showWorking === "boolean") patch.showWorking = args.showWorking;
27108
+ if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim().toLowerCase() === "auto" ? "auto" : args.brain.trim();
26916
27109
  if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
26917
27110
  if (typeof args.persona === "string") patch.persona = args.persona;
26918
27111
  if (typeof args.promptTemplate === "string" && args.promptTemplate.trim())
@@ -26920,21 +27113,24 @@ ${cmd}`);
26920
27113
  if (args.replyMode) patch.replyMode = args.replyMode;
26921
27114
  const c = setAuto(patch);
26922
27115
  const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
27116
+ const ownerLabel = c.ownerOnlyEffective ? `yes \u2014 only you direct me${c.ownerOnlyEnforced && !c.ownerOnly ? " (force-enabled during beta)" : ""}` : "no (anyone who @mentions me)";
27117
+ const brainLabel = c.brain === "auto" ? `auto \u2192 ${c.resolvedBrain ?? "claude"} (this host)` : c.brain;
26923
27118
  const lines = [
26924
- `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"}`,
26925
- ` Brain: ${c.brain}`,
27119
+ `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"} \xB7 this agent replies from ONE place (this connector); the same agent open elsewhere stays silent`,
27120
+ ` Brain: ${brainLabel}`,
26926
27121
  ` Work folder: ${c.workdir}`,
26927
27122
  ` Replies to: ${modeLabel}`,
26928
- ` Owner-only: ${c.ownerOnly ? "yes (only you direct me)" : "no (anyone who @mentions me)"}`,
27123
+ ` Owner-only: ${ownerLabel}`,
26929
27124
  ` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
26930
27125
  ` Conversation memory: ${c.contextMessages > 0 ? `last ${c.contextMessages} messages` : "off"}`,
27126
+ ` Shows "on it\u2026" while working: ${c.showWorking ? "yes" : "no"}`,
26931
27127
  ` Prompt: ${c.promptTemplate === DEFAULT_TEMPLATE ? "default (@name routing + ask-if-unclear)" : "custom"}`,
26932
27128
  ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`,
26933
27129
  ` Activity log: ~/.jefri/autonomous.log (watch it: tail -f ~/.jefri/autonomous.log)`
26934
27130
  ];
26935
27131
  if (c.enabled)
26936
27132
  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.`);
27133
+ \u26A0\uFE0F Incoming messages now drive "${brainLabel}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
26938
27134
  return ok(lines.join("\n"));
26939
27135
  })
26940
27136
  );
@@ -27055,9 +27251,17 @@ ${C.b}\u{1FA7A} Jefri Chat connector \u2014 setup check${C.x}
27055
27251
  info("Skipping hub auth check (no token)");
27056
27252
  }
27057
27253
  const auto = getAuto();
27058
- const brainCmd = auto.brain === "claude" ? "claude" : auto.brain === "codex" ? "codex" : auto.brain.trim().split(/\s+/)[0];
27059
- if (hasCli(brainCmd)) pass(`Autonomous brain "${brainCmd}" found on PATH`);
27060
- else info(`Autonomous brain "${brainCmd}" not on PATH (only needed if you enable autonomous mode)`);
27254
+ const brainCmd = brainExecutable(auto.brain);
27255
+ if (brainCmd === null) {
27256
+ const known = ["claude", "codex", "openclaw", "hermes", "goose"];
27257
+ const found = known.filter(hasCli);
27258
+ if (found.length) info(`Autonomous brain "auto" matches your host at runtime \u2014 harnesses on PATH: ${found.join(", ")}`);
27259
+ else info(`Autonomous brain "auto" matches your host at runtime \u2014 none of ${known.join("/")} found on PATH yet (only needed if you enable autonomous mode)`);
27260
+ } else if (hasCli(brainCmd)) {
27261
+ pass(`Autonomous brain "${brainCmd}" found on PATH`);
27262
+ } else {
27263
+ info(`Autonomous brain "${brainCmd}" not on PATH (only needed if you enable autonomous mode)`);
27264
+ }
27061
27265
  const prefs2 = getPrefs();
27062
27266
  if (!prefs2.enabled) info("Desktop notifications are OFF (use the jefri_notifications tool to turn on)");
27063
27267
  else if (macNeedsTerminalNotifier)
@@ -27229,6 +27433,12 @@ async function main() {
27229
27433
  }
27230
27434
  ensureClient().catch((e) => log("initial connect failed (will retry on first tool):", e?.message ?? e));
27231
27435
  const transport = new StdioServerTransport();
27436
+ server.server.oninitialized = () => {
27437
+ try {
27438
+ setAutoHost(server.server.getClientVersion()?.name);
27439
+ } catch {
27440
+ }
27441
+ };
27232
27442
  await server.connect(transport);
27233
27443
  log(`MCP server ready \u2014 identity @${USERNAME}, Jefri Chat server ${SERVER}`);
27234
27444
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.21.0",
3
+ "version": "0.28.2",
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": {