jefrichat-mcp 0.8.0 → 0.9.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 +158 -55
  2. package/dist/index.js +272 -57
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -1300,8 +1300,8 @@ var require_node = __commonJS({
1300
1300
  }
1301
1301
  break;
1302
1302
  case "FILE":
1303
- var fs4 = __require("fs");
1304
- stream2 = new fs4.SyncWriteStream(fd2, { autoClose: false });
1303
+ var fs5 = __require("fs");
1304
+ stream2 = new fs5.SyncWriteStream(fd2, { autoClose: false });
1305
1305
  stream2._type = "fs";
1306
1306
  break;
1307
1307
  case "PIPE":
@@ -18437,7 +18437,7 @@ var require_view = __commonJS({
18437
18437
  "use strict";
18438
18438
  var debug = require_src()("express:view");
18439
18439
  var path2 = __require("path");
18440
- var fs4 = __require("fs");
18440
+ var fs5 = __require("fs");
18441
18441
  var dirname = path2.dirname;
18442
18442
  var basename = path2.basename;
18443
18443
  var extname = path2.extname;
@@ -18503,7 +18503,7 @@ var require_view = __commonJS({
18503
18503
  function tryStat(path3) {
18504
18504
  debug('stat "%s"', path3);
18505
18505
  try {
18506
- return fs4.statSync(path3);
18506
+ return fs5.statSync(path3);
18507
18507
  } catch (e) {
18508
18508
  return void 0;
18509
18509
  }
@@ -18858,7 +18858,7 @@ var require_types = __commonJS({
18858
18858
  var require_mime = __commonJS({
18859
18859
  "../../node_modules/.pnpm/mime@1.6.0/node_modules/mime/mime.js"(exports, module) {
18860
18860
  var path2 = __require("path");
18861
- var fs4 = __require("fs");
18861
+ var fs5 = __require("fs");
18862
18862
  function Mime() {
18863
18863
  this.types = /* @__PURE__ */ Object.create(null);
18864
18864
  this.extensions = /* @__PURE__ */ Object.create(null);
@@ -18879,7 +18879,7 @@ var require_mime = __commonJS({
18879
18879
  };
18880
18880
  Mime.prototype.load = function(file) {
18881
18881
  this._loading = file;
18882
- var map = {}, content = fs4.readFileSync(file, "ascii"), lines = content.split(/[\r\n]+/);
18882
+ var map = {}, content = fs5.readFileSync(file, "ascii"), lines = content.split(/[\r\n]+/);
18883
18883
  lines.forEach(function(line) {
18884
18884
  var fields = line.replace(/\s*#.*|^\s*|\s*$/g, "").split(/\s+/);
18885
18885
  map[fields.shift()] = fields;
@@ -19117,7 +19117,7 @@ var require_send = __commonJS({
19117
19117
  var escapeHtml = require_escape_html();
19118
19118
  var etag = require_etag();
19119
19119
  var fresh = require_fresh();
19120
- var fs4 = __require("fs");
19120
+ var fs5 = __require("fs");
19121
19121
  var mime = require_mime();
19122
19122
  var ms = require_ms2();
19123
19123
  var onFinished = require_on_finished();
@@ -19450,7 +19450,7 @@ var require_send = __commonJS({
19450
19450
  var i = 0;
19451
19451
  var self = this;
19452
19452
  debug('stat "%s"', path3);
19453
- fs4.stat(path3, function onstat(err, stat) {
19453
+ fs5.stat(path3, function onstat(err, stat) {
19454
19454
  if (err && err.code === "ENOENT" && !extname(path3) && path3[path3.length - 1] !== sep) {
19455
19455
  return next(err);
19456
19456
  }
@@ -19465,7 +19465,7 @@ var require_send = __commonJS({
19465
19465
  }
19466
19466
  var p = path3 + "." + self._extensions[i++];
19467
19467
  debug('stat "%s"', p);
19468
- fs4.stat(p, function(err2, stat) {
19468
+ fs5.stat(p, function(err2, stat) {
19469
19469
  if (err2) return next(err2);
19470
19470
  if (stat.isDirectory()) return next();
19471
19471
  self.emit("file", p, stat);
@@ -19483,7 +19483,7 @@ var require_send = __commonJS({
19483
19483
  }
19484
19484
  var p = join(path3, self._index[i]);
19485
19485
  debug('stat "%s"', p);
19486
- fs4.stat(p, function(err2, stat) {
19486
+ fs5.stat(p, function(err2, stat) {
19487
19487
  if (err2) return next(err2);
19488
19488
  if (stat.isDirectory()) return next();
19489
19489
  self.emit("file", p, stat);
@@ -19495,7 +19495,7 @@ var require_send = __commonJS({
19495
19495
  SendStream.prototype.stream = function stream(path3, options) {
19496
19496
  var self = this;
19497
19497
  var res = this.res;
19498
- var stream2 = fs4.createReadStream(path3, options);
19498
+ var stream2 = fs5.createReadStream(path3, options);
19499
19499
  this.emit("stream", stream2);
19500
19500
  stream2.pipe(res);
19501
19501
  function cleanup() {
@@ -29739,12 +29739,12 @@ var require_dist = __commonJS({
29739
29739
  throw new Error(`Unknown format "${name}"`);
29740
29740
  return f;
29741
29741
  };
29742
- function addFormats(ajv, list, fs4, exportName) {
29742
+ function addFormats(ajv, list, fs5, exportName) {
29743
29743
  var _a;
29744
29744
  var _b;
29745
29745
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
29746
29746
  for (const f of list)
29747
- ajv.addFormat(f, fs4[f]);
29747
+ ajv.addFormat(f, fs5[f]);
29748
29748
  }
29749
29749
  module.exports = exports = formatsPlugin;
29750
29750
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -48938,6 +48938,8 @@ var AcpClient = class _AcpClient {
48938
48938
  closing = false;
48939
48939
  authFailed = false;
48940
48940
  // a bad token must NOT trigger a reconnect storm
48941
+ isAlive = true;
48942
+ // heartbeat: did we get a pong since the last ping?
48941
48943
  constructor(server) {
48942
48944
  this.server = server.replace(/\/$/, "");
48943
48945
  }
@@ -49038,15 +49040,45 @@ var AcpClient = class _AcpClient {
49038
49040
  });
49039
49041
  });
49040
49042
  }
49041
- // Keep the hub connection alive through idle-timeout proxies.
49043
+ // Heartbeat: keep the connection warm through idle proxies AND detect a
49044
+ // half-open (silently dead) socket. A dropped Wi-Fi / VPN / sleep can leave
49045
+ // the socket readyState=OPEN forever while no data flows, so `close` never
49046
+ // fires and we'd never reconnect. So each tick we send a WS-level ping (the
49047
+ // server's ws auto-pongs) and require a pong to have arrived since the last
49048
+ // tick — if not, the link is dead, so terminate() it, which fires `close` and
49049
+ // triggers the existing reconnect.
49042
49050
  startPing() {
49043
49051
  this.stopPing();
49052
+ this.isAlive = true;
49053
+ try {
49054
+ this.ws.on("pong", () => {
49055
+ this.isAlive = true;
49056
+ });
49057
+ } catch {
49058
+ }
49044
49059
  this.pingTimer = setInterval(() => {
49060
+ if (this.ws.readyState !== this.ws.OPEN) return;
49061
+ if (!this.isAlive) {
49062
+ try {
49063
+ this.ws.terminate();
49064
+ } catch {
49065
+ try {
49066
+ this.ws.close();
49067
+ } catch {
49068
+ }
49069
+ }
49070
+ return;
49071
+ }
49072
+ this.isAlive = false;
49073
+ try {
49074
+ this.ws.ping();
49075
+ } catch {
49076
+ }
49045
49077
  try {
49046
- if (this.ws.readyState === this.ws.OPEN) this.ws.send(JSON.stringify({ type: "ping" }));
49078
+ this.ws.send(JSON.stringify({ type: "ping" }));
49047
49079
  } catch {
49048
49080
  }
49049
- }, 25e3);
49081
+ }, 15e3);
49050
49082
  }
49051
49083
  stopPing() {
49052
49084
  if (this.pingTimer) clearInterval(this.pingTimer);
@@ -49373,10 +49405,10 @@ var AcpClient = class _AcpClient {
49373
49405
  };
49374
49406
 
49375
49407
  // src/tools.ts
49376
- import fs3 from "node:fs";
49377
- import np2 from "node:path";
49408
+ import fs4 from "node:fs";
49409
+ import np3 from "node:path";
49378
49410
  import cp2 from "node:child_process";
49379
- import os3 from "node:os";
49411
+ import os4 from "node:os";
49380
49412
 
49381
49413
  // src/notify.ts
49382
49414
  import cp from "node:child_process";
@@ -49435,6 +49467,39 @@ function setPrefs(patch) {
49435
49467
  return { ...prefs };
49436
49468
  }
49437
49469
 
49470
+ // src/autonomous.ts
49471
+ import fs3 from "node:fs";
49472
+ import np2 from "node:path";
49473
+ import os3 from "node:os";
49474
+ var DEFAULTS2 = {
49475
+ enabled: false,
49476
+ brain: "claude",
49477
+ workdir: process.cwd(),
49478
+ replyMode: "mentions",
49479
+ persona: "",
49480
+ replyToBots: false
49481
+ };
49482
+ var DIR = np2.join(os3.homedir(), ".jefri");
49483
+ var FILE = np2.join(DIR, "autonomous.json");
49484
+ function loadCfg() {
49485
+ try {
49486
+ return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(FILE, "utf8")) };
49487
+ } catch {
49488
+ return { ...DEFAULTS2 };
49489
+ }
49490
+ }
49491
+ var cfg = loadCfg();
49492
+ function setAuto(patch) {
49493
+ cfg = { ...cfg, ...patch };
49494
+ try {
49495
+ fs3.mkdirSync(DIR, { recursive: true });
49496
+ fs3.writeFileSync(FILE, JSON.stringify(cfg, null, 2));
49497
+ } catch {
49498
+ }
49499
+ return { ...cfg };
49500
+ }
49501
+ var TIMEOUT_MS = 5 * 60 * 1e3;
49502
+
49438
49503
  // src/tools.ts
49439
49504
  var MIME = {
49440
49505
  ".jpg": "image/jpeg",
@@ -49455,21 +49520,21 @@ var MIME = {
49455
49520
  ".json": "application/json",
49456
49521
  ".zip": "application/zip"
49457
49522
  };
49458
- var mimeOf = (name) => MIME[np2.extname(name).toLowerCase()] ?? "application/octet-stream";
49523
+ var mimeOf = (name) => MIME[np3.extname(name).toLowerCase()] ?? "application/octet-stream";
49459
49524
  function screenshotDir() {
49460
49525
  try {
49461
49526
  const out = cp2.execFileSync("defaults", ["read", "com.apple.screencapture", "location"], { encoding: "utf8", timeout: 2e3 }).trim();
49462
- if (out) return out.startsWith("~") ? np2.join(os3.homedir(), out.slice(1)) : out;
49527
+ if (out) return out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
49463
49528
  } catch {
49464
49529
  }
49465
- const desktop = np2.join(os3.homedir(), "Desktop");
49466
- return fs3.existsSync(desktop) ? desktop : null;
49530
+ const desktop = np3.join(os4.homedir(), "Desktop");
49531
+ return fs4.existsSync(desktop) ? desktop : null;
49467
49532
  }
49468
49533
  function resolveLocalFile(rawPath) {
49469
- const expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
49534
+ const expand = (p) => p.startsWith("~") ? np3.join(os4.homedir(), p.slice(1)) : p;
49470
49535
  const exists = (p) => {
49471
49536
  try {
49472
- return fs3.existsSync(p) ? p : null;
49537
+ return fs4.existsSync(p) ? p : null;
49473
49538
  } catch {
49474
49539
  return null;
49475
49540
  }
@@ -49482,26 +49547,26 @@ function resolveLocalFile(rawPath) {
49482
49547
  hit = exists(v);
49483
49548
  if (hit) return hit;
49484
49549
  }
49485
- const base = np2.basename(abs);
49550
+ const base = np3.basename(abs);
49486
49551
  const looksLikeShot = /screencaptureui/i.test(abs) || /^Screenshot[\s  ]/i.test(base);
49487
49552
  if (looksLikeShot) {
49488
49553
  const dir = screenshotDir();
49489
49554
  if (dir) {
49490
49555
  let files = [];
49491
49556
  try {
49492
- files = fs3.readdirSync(dir);
49557
+ files = fs4.readdirSync(dir);
49493
49558
  } catch {
49494
49559
  }
49495
49560
  const norm = (s) => s.replace(/[    ⁠\s]+/g, " ").trim().toLowerCase();
49496
49561
  const want = norm(base);
49497
49562
  for (const f of files) if (norm(f) === want) {
49498
- hit = exists(np2.join(dir, f));
49563
+ hit = exists(np3.join(dir, f));
49499
49564
  if (hit) return hit;
49500
49565
  }
49501
49566
  const shots = files.filter((f) => /^Screenshot[\s  ].*\.(png|jpe?g)$/i.test(f)).map((f) => {
49502
- const p = np2.join(dir, f);
49567
+ const p = np3.join(dir, f);
49503
49568
  try {
49504
- return { p, m: fs3.statSync(p).mtimeMs };
49569
+ return { p, m: fs4.statSync(p).mtimeMs };
49505
49570
  } catch {
49506
49571
  return { p, m: 0 };
49507
49572
  }
@@ -49661,8 +49726,8 @@ ${fp}`);
49661
49726
  `I can't find a file at ${path2} on this machine.`
49662
49727
  );
49663
49728
  }
49664
- const buf = fs3.readFileSync(abs);
49665
- name = np2.basename(abs);
49729
+ const buf = fs4.readFileSync(abs);
49730
+ name = np3.basename(abs);
49666
49731
  mime = mimeOf(name);
49667
49732
  url = `data:${mime};base64,${buf.toString("base64")}`;
49668
49733
  } else if (dataUrl && fileName) {
@@ -49722,8 +49787,8 @@ ${fp}`);
49722
49787
  `I can't find a file at ${path2} on this machine.`
49723
49788
  );
49724
49789
  }
49725
- const buf = fs3.readFileSync(abs);
49726
- name = np2.basename(abs);
49790
+ const buf = fs4.readFileSync(abs);
49791
+ name = np3.basename(abs);
49727
49792
  mime = mimeOf(name);
49728
49793
  url = `data:${mime};base64,${buf.toString("base64")}`;
49729
49794
  } else if (dataUrl && fileName) {
@@ -49755,7 +49820,7 @@ ${fp}`);
49755
49820
  let name, mime, url;
49756
49821
  if (path2) {
49757
49822
  if (!ctx.local) {
49758
- const fname = np2.basename(path2);
49823
+ const fname = np3.basename(path2);
49759
49824
  const fmime = mimeOf(fname);
49760
49825
  const hub = ctx.serverUrl.replace(/\/$/, "");
49761
49826
  const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
@@ -49780,8 +49845,8 @@ ${psCmd}
49780
49845
  if (!abs) {
49781
49846
  return fail(`I can't find a file at ${path2} on this machine.`);
49782
49847
  }
49783
- const buf = fs3.readFileSync(abs);
49784
- name = np2.basename(abs);
49848
+ const buf = fs4.readFileSync(abs);
49849
+ name = np3.basename(abs);
49785
49850
  mime = mimeOf(name);
49786
49851
  url = `data:${mime};base64,${buf.toString("base64")}`;
49787
49852
  } else if (dataUrl && fileName) {
@@ -49862,14 +49927,14 @@ ${psCmd}
49862
49927
  `Sending a folder reads files off disk, which this connector can't do \u2014 it runs in the cloud. Run jefri_send_folder from a LOCAL agent (Claude Code with the stdio connector) inside the project.`
49863
49928
  );
49864
49929
  const rawDir = path2 && path2.trim() ? path2 : process.cwd();
49865
- const dir = rawDir.startsWith("~") ? np2.join(os3.homedir(), rawDir.slice(1)) : rawDir;
49866
- if (!fs3.existsSync(dir))
49930
+ const dir = rawDir.startsWith("~") ? np3.join(os4.homedir(), rawDir.slice(1)) : rawDir;
49931
+ if (!fs4.existsSync(dir))
49867
49932
  return fail(
49868
49933
  `I can't see "${rawDir}" from here. Sending a folder reads it off disk, which only works on the LOCAL (stdio) connector \u2014 run this from Claude Code (or another local agent) inside the project.`
49869
49934
  );
49870
49935
  let stat;
49871
49936
  try {
49872
- stat = fs3.statSync(dir);
49937
+ stat = fs4.statSync(dir);
49873
49938
  } catch {
49874
49939
  return fail(`Cannot read "${rawDir}".`);
49875
49940
  }
@@ -49880,8 +49945,8 @@ ${psCmd}
49880
49945
  } catch {
49881
49946
  return fail(`The 'zip' command isn't available on this machine, and I need it to package the folder.`);
49882
49947
  }
49883
- const base = np2.basename(np2.resolve(dir)) || "project";
49884
- const tmpZip = np2.join(os3.tmpdir(), `acp-${base}-${Date.now()}.zip`);
49948
+ const base = np3.basename(np3.resolve(dir)) || "project";
49949
+ const tmpZip = np3.join(os4.tmpdir(), `acp-${base}-${Date.now()}.zip`);
49885
49950
  let isRepo = false;
49886
49951
  try {
49887
49952
  cp2.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
@@ -49903,16 +49968,16 @@ ${psCmd}
49903
49968
  }
49904
49969
  } catch (e) {
49905
49970
  try {
49906
- fs3.rmSync(tmpZip, { force: true });
49971
+ fs4.rmSync(tmpZip, { force: true });
49907
49972
  } catch {
49908
49973
  }
49909
49974
  return fail(`Could not zip "${rawDir}": ${e?.message ?? e}`);
49910
49975
  }
49911
- if (!fs3.existsSync(tmpZip))
49976
+ if (!fs4.existsSync(tmpZip))
49912
49977
  return fail(`Nothing to send \u2014 "${rawDir}" produced an empty archive (all files ignored or folder empty).`);
49913
- const size = fs3.statSync(tmpZip).size;
49978
+ const size = fs4.statSync(tmpZip).size;
49914
49979
  if (size > MAX_UPLOAD_BYTES) {
49915
- fs3.rmSync(tmpZip, { force: true });
49980
+ fs4.rmSync(tmpZip, { force: true });
49916
49981
  return fail(
49917
49982
  `That archive is ${MB(size)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit. Send a subfolder (e.g. path=\u2026/src) or remove large files, then try again.`
49918
49983
  );
@@ -49924,7 +49989,7 @@ ${psCmd}
49924
49989
  const res = await fetch(`${hub}/api/files?${q}`, {
49925
49990
  method: "POST",
49926
49991
  headers: { Authorization: `Bearer ${c.token}`, "Content-Type": "application/zip" },
49927
- body: fs3.readFileSync(tmpZip)
49992
+ body: fs4.readFileSync(tmpZip)
49928
49993
  });
49929
49994
  if (!res.ok) {
49930
49995
  const t = await res.text().catch(() => "");
@@ -49934,7 +49999,7 @@ ${psCmd}
49934
49999
  return fail(`Upload failed: ${e?.message ?? e}`);
49935
50000
  } finally {
49936
50001
  try {
49937
- fs3.rmSync(tmpZip, { force: true });
50002
+ fs4.rmSync(tmpZip, { force: true });
49938
50003
  } catch {
49939
50004
  }
49940
50005
  }
@@ -49969,12 +50034,12 @@ ${psCmd}
49969
50034
  const hub = ctx.serverUrl.replace(/\/$/, "");
49970
50035
  const out = savePath || match.fileName || "downloaded.file";
49971
50036
  if (ctx.local) {
49972
- const outPath = out.startsWith("~") ? np2.join(os3.homedir(), out.slice(1)) : out;
50037
+ const outPath = out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
49973
50038
  try {
49974
50039
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
49975
50040
  if (!r.ok) throw new Error(`download failed (${r.status})`);
49976
50041
  const buf = Buffer.from(await r.arrayBuffer());
49977
- fs3.writeFileSync(outPath, buf);
50042
+ fs4.writeFileSync(outPath, buf);
49978
50043
  return ok(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
49979
50044
  } catch (e) {
49980
50045
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
@@ -50075,7 +50140,7 @@ ${cmd}`);
50075
50140
  }
50076
50141
  },
50077
50142
  async ({ departmentId, path: path2, kind }) => withClient(async (c) => {
50078
- const fname = np2.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50143
+ const fname = np3.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50079
50144
  const mime = mimeOf(fname);
50080
50145
  const q = `name=${encodeURIComponent(fname)}&kind=${kind ?? "file"}`;
50081
50146
  const cmd = `curl -s -X POST ${shq(`${hubBase()}/api/departments/${departmentId}/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${mime}" --data-binary @${shq(path2)}`;
@@ -50159,7 +50224,7 @@ ${cmd}`);
50159
50224
  inputSchema: { path: external_exports.string().describe("local file path to upload") }
50160
50225
  },
50161
50226
  async ({ path: path2 }) => withClient(async (c) => {
50162
- const fname = np2.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50227
+ const fname = np3.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50163
50228
  const mime = mimeOf(fname);
50164
50229
  const cmd = `curl -s -X POST ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c.identity.username)}/docs?name=${encodeURIComponent(fname)}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${mime}" --data-binary @${shq(path2)}`;
50165
50230
  return ok(`To add ${fname} to your document memory, run in the terminal:
@@ -50311,7 +50376,7 @@ ${cmd}`);
50311
50376
  if (path2) {
50312
50377
  if (!ctx.local) {
50313
50378
  const hub = ctx.serverUrl.replace(/\/$/, "");
50314
- const fname = np2.basename(path2);
50379
+ const fname = np3.basename(path2);
50315
50380
  const fmime = mimeOf(fname);
50316
50381
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
50317
50382
  const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path2)}`;
@@ -50323,8 +50388,8 @@ ${cmd}`
50323
50388
  }
50324
50389
  const abs = resolveLocalFile(path2);
50325
50390
  if (!abs) return fail(`I can't find a file at ${path2} on this machine.`);
50326
- const buf = fs3.readFileSync(abs);
50327
- name = np2.basename(abs);
50391
+ const buf = fs4.readFileSync(abs);
50392
+ name = np3.basename(abs);
50328
50393
  mime = mimeOf(name);
50329
50394
  url = `data:${mime};base64,${buf.toString("base64")}`;
50330
50395
  } else if (dataUrl && fileName) {
@@ -50532,6 +50597,44 @@ ${cmd}`
50532
50597
  return ok(summary);
50533
50598
  })
50534
50599
  );
50600
+ server.registerTool(
50601
+ "jefri_autonomous",
50602
+ {
50603
+ title: "Autonomous mode",
50604
+ 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.",
50605
+ inputSchema: {
50606
+ enabled: external_exports.boolean().optional().describe("turn autonomous replies on/off"),
50607
+ brain: external_exports.string().optional().describe("'claude', 'codex', or a full custom command (e.g. for OpenClaw)"),
50608
+ workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
50609
+ replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
50610
+ persona: external_exports.string().optional().describe("role/instructions for the agent"),
50611
+ replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)")
50612
+ }
50613
+ },
50614
+ async (args) => withClient(async () => {
50615
+ const patch = {};
50616
+ if (typeof args.enabled === "boolean") patch.enabled = args.enabled;
50617
+ if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
50618
+ if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim();
50619
+ if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
50620
+ if (typeof args.persona === "string") patch.persona = args.persona;
50621
+ if (args.replyMode) patch.replyMode = args.replyMode;
50622
+ const c = setAuto(patch);
50623
+ const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
50624
+ const lines = [
50625
+ `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"}`,
50626
+ ` Brain: ${c.brain}`,
50627
+ ` Work folder: ${c.workdir}`,
50628
+ ` Replies to: ${modeLabel}`,
50629
+ ` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
50630
+ ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`
50631
+ ];
50632
+ if (c.enabled)
50633
+ lines.push(`
50634
+ \u26A0\uFE0F Incoming messages now drive "${c.brain}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
50635
+ return ok(lines.join("\n"));
50636
+ })
50637
+ );
50535
50638
  }
50536
50639
  }
50537
50640
  function attachInbox(c, inbox, self, onIncoming) {
package/dist/index.js CHANGED
@@ -6883,12 +6883,12 @@ var require_dist = __commonJS({
6883
6883
  throw new Error(`Unknown format "${name}"`);
6884
6884
  return f;
6885
6885
  };
6886
- function addFormats(ajv, list, fs5, exportName) {
6886
+ function addFormats(ajv, list, fs6, exportName) {
6887
6887
  var _a;
6888
6888
  var _b;
6889
6889
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6890
6890
  for (const f of list)
6891
- ajv.addFormat(f, fs5[f]);
6891
+ ajv.addFormat(f, fs6[f]);
6892
6892
  }
6893
6893
  module.exports = exports = formatsPlugin;
6894
6894
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -24788,8 +24788,8 @@ var StdioServerTransport = class {
24788
24788
  };
24789
24789
 
24790
24790
  // src/index.ts
24791
- import fs4 from "node:fs";
24792
- import os4 from "node:os";
24791
+ import fs5 from "node:fs";
24792
+ import os5 from "node:os";
24793
24793
  import path2 from "node:path";
24794
24794
 
24795
24795
  // ../../node_modules/.pnpm/ws@8.21.0/node_modules/ws/wrapper.mjs
@@ -24846,6 +24846,8 @@ var AcpClient = class _AcpClient {
24846
24846
  closing = false;
24847
24847
  authFailed = false;
24848
24848
  // a bad token must NOT trigger a reconnect storm
24849
+ isAlive = true;
24850
+ // heartbeat: did we get a pong since the last ping?
24849
24851
  constructor(server2) {
24850
24852
  this.server = server2.replace(/\/$/, "");
24851
24853
  }
@@ -24946,15 +24948,45 @@ var AcpClient = class _AcpClient {
24946
24948
  });
24947
24949
  });
24948
24950
  }
24949
- // Keep the hub connection alive through idle-timeout proxies.
24951
+ // Heartbeat: keep the connection warm through idle proxies AND detect a
24952
+ // half-open (silently dead) socket. A dropped Wi-Fi / VPN / sleep can leave
24953
+ // the socket readyState=OPEN forever while no data flows, so `close` never
24954
+ // fires and we'd never reconnect. So each tick we send a WS-level ping (the
24955
+ // server's ws auto-pongs) and require a pong to have arrived since the last
24956
+ // tick — if not, the link is dead, so terminate() it, which fires `close` and
24957
+ // triggers the existing reconnect.
24950
24958
  startPing() {
24951
24959
  this.stopPing();
24960
+ this.isAlive = true;
24961
+ try {
24962
+ this.ws.on("pong", () => {
24963
+ this.isAlive = true;
24964
+ });
24965
+ } catch {
24966
+ }
24952
24967
  this.pingTimer = setInterval(() => {
24968
+ if (this.ws.readyState !== this.ws.OPEN) return;
24969
+ if (!this.isAlive) {
24970
+ try {
24971
+ this.ws.terminate();
24972
+ } catch {
24973
+ try {
24974
+ this.ws.close();
24975
+ } catch {
24976
+ }
24977
+ }
24978
+ return;
24979
+ }
24980
+ this.isAlive = false;
24953
24981
  try {
24954
- if (this.ws.readyState === this.ws.OPEN) this.ws.send(JSON.stringify({ type: "ping" }));
24982
+ this.ws.ping();
24955
24983
  } catch {
24956
24984
  }
24957
- }, 25e3);
24985
+ try {
24986
+ this.ws.send(JSON.stringify({ type: "ping" }));
24987
+ } catch {
24988
+ }
24989
+ }, 15e3);
24958
24990
  }
24959
24991
  stopPing() {
24960
24992
  if (this.pingTimer) clearInterval(this.pingTimer);
@@ -25281,10 +25313,10 @@ var AcpClient = class _AcpClient {
25281
25313
  };
25282
25314
 
25283
25315
  // src/tools.ts
25284
- import fs3 from "node:fs";
25285
- import np2 from "node:path";
25286
- import cp2 from "node:child_process";
25287
- import os3 from "node:os";
25316
+ import fs4 from "node:fs";
25317
+ import np3 from "node:path";
25318
+ import cp3 from "node:child_process";
25319
+ import os4 from "node:os";
25288
25320
 
25289
25321
  // src/notify.ts
25290
25322
  import cp from "node:child_process";
@@ -25394,6 +25426,124 @@ function notifyIncoming(m) {
25394
25426
  }
25395
25427
  }
25396
25428
 
25429
+ // src/autonomous.ts
25430
+ import cp2 from "node:child_process";
25431
+ import fs3 from "node:fs";
25432
+ import np2 from "node:path";
25433
+ import os3 from "node:os";
25434
+ var DEFAULTS2 = {
25435
+ enabled: false,
25436
+ brain: "claude",
25437
+ workdir: process.cwd(),
25438
+ replyMode: "mentions",
25439
+ persona: "",
25440
+ replyToBots: false
25441
+ };
25442
+ var DIR = np2.join(os3.homedir(), ".jefri");
25443
+ var FILE = np2.join(DIR, "autonomous.json");
25444
+ function loadCfg() {
25445
+ try {
25446
+ return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(FILE, "utf8")) };
25447
+ } catch {
25448
+ return { ...DEFAULTS2 };
25449
+ }
25450
+ }
25451
+ var cfg = loadCfg();
25452
+ function getAuto() {
25453
+ return { ...cfg };
25454
+ }
25455
+ function setAuto(patch) {
25456
+ cfg = { ...cfg, ...patch };
25457
+ try {
25458
+ fs3.mkdirSync(DIR, { recursive: true });
25459
+ fs3.writeFileSync(FILE, JSON.stringify(cfg, null, 2));
25460
+ } catch {
25461
+ }
25462
+ return { ...cfg };
25463
+ }
25464
+ var expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
25465
+ function brainArgv(brain, prompt) {
25466
+ if (brain === "claude") return ["claude", ["-p", "--permission-mode", "acceptEdits", prompt]];
25467
+ if (brain === "codex") return ["codex", ["exec", "--full-auto", prompt]];
25468
+ const parts = brain.trim().split(/\s+/);
25469
+ return [parts[0], [...parts.slice(1), prompt]];
25470
+ }
25471
+ var MAX_PROMPT = 6e3;
25472
+ var MAX_REPLY = 7900;
25473
+ var TIMEOUT_MS = 5 * 60 * 1e3;
25474
+ function shouldHandle(m) {
25475
+ if (!cfg.enabled) return false;
25476
+ if (m.senderIsBot && !cfg.replyToBots) return false;
25477
+ if (!m.isGroup) return true;
25478
+ if (cfg.replyMode === "all") return true;
25479
+ if (cfg.replyMode === "mentions") return m.isMention;
25480
+ return false;
25481
+ }
25482
+ var running = false;
25483
+ var queue = [];
25484
+ async function drain() {
25485
+ if (running) return;
25486
+ running = true;
25487
+ while (queue.length) {
25488
+ const job = queue.shift();
25489
+ try {
25490
+ await job();
25491
+ } catch {
25492
+ }
25493
+ }
25494
+ running = false;
25495
+ }
25496
+ function runBrain(cmd, args, cwd) {
25497
+ return new Promise((resolve, reject) => {
25498
+ let child;
25499
+ try {
25500
+ child = cp2.spawn(cmd, args, { cwd: expand(cwd), stdio: ["ignore", "pipe", "pipe"] });
25501
+ } catch (e) {
25502
+ return reject(e);
25503
+ }
25504
+ let out = "", err = "";
25505
+ const timer = setTimeout(() => {
25506
+ try {
25507
+ child.kill();
25508
+ } catch {
25509
+ }
25510
+ reject(new Error("timed out"));
25511
+ }, TIMEOUT_MS);
25512
+ child.stdout?.on("data", (d) => out += d.toString());
25513
+ child.stderr?.on("data", (d) => err += d.toString());
25514
+ child.on("error", (e) => {
25515
+ clearTimeout(timer);
25516
+ reject(e);
25517
+ });
25518
+ child.on("close", (code) => {
25519
+ clearTimeout(timer);
25520
+ if (code === 0 || out.trim()) resolve(out);
25521
+ else reject(new Error(err.trim().slice(0, 200) || `exit ${code}`));
25522
+ });
25523
+ });
25524
+ }
25525
+ function handleAutonomous(m, reply, log2) {
25526
+ if (!shouldHandle(m)) return;
25527
+ queue.push(async () => {
25528
+ const persona = cfg.persona ? cfg.persona.trim() + "\n\n" : "";
25529
+ const where = m.isGroup ? " in a group" : "";
25530
+ const prompt = `${persona}You are an autonomous agent on Jefri Chat. @${m.sender} sent you a message${where}. Do what it asks (you can use your tools/files) and then give a concise reply to send back.
25531
+
25532
+ Message from @${m.sender}: ${m.content}`.slice(0, MAX_PROMPT);
25533
+ const [cmd, args] = brainArgv(cfg.brain, prompt);
25534
+ log2(`autonomous: @${m.sender} \u2192 running "${cmd}"\u2026`);
25535
+ try {
25536
+ const out = await runBrain(cmd, args, cfg.workdir);
25537
+ reply((out.trim() || "(done)").slice(0, MAX_REPLY));
25538
+ log2(`autonomous: replied to @${m.sender}`);
25539
+ } catch (e) {
25540
+ reply(`(couldn't finish autonomously: ${e?.message ?? e})`.slice(0, MAX_REPLY));
25541
+ log2(`autonomous: failed for @${m.sender}: ${e?.message ?? e}`);
25542
+ }
25543
+ });
25544
+ void drain();
25545
+ }
25546
+
25397
25547
  // src/tools.ts
25398
25548
  var MIME = {
25399
25549
  ".jpg": "image/jpeg",
@@ -25414,26 +25564,26 @@ var MIME = {
25414
25564
  ".json": "application/json",
25415
25565
  ".zip": "application/zip"
25416
25566
  };
25417
- var mimeOf = (name) => MIME[np2.extname(name).toLowerCase()] ?? "application/octet-stream";
25567
+ var mimeOf = (name) => MIME[np3.extname(name).toLowerCase()] ?? "application/octet-stream";
25418
25568
  function screenshotDir() {
25419
25569
  try {
25420
- const out = cp2.execFileSync("defaults", ["read", "com.apple.screencapture", "location"], { encoding: "utf8", timeout: 2e3 }).trim();
25421
- if (out) return out.startsWith("~") ? np2.join(os3.homedir(), out.slice(1)) : out;
25570
+ const out = cp3.execFileSync("defaults", ["read", "com.apple.screencapture", "location"], { encoding: "utf8", timeout: 2e3 }).trim();
25571
+ if (out) return out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
25422
25572
  } catch {
25423
25573
  }
25424
- const desktop = np2.join(os3.homedir(), "Desktop");
25425
- return fs3.existsSync(desktop) ? desktop : null;
25574
+ const desktop = np3.join(os4.homedir(), "Desktop");
25575
+ return fs4.existsSync(desktop) ? desktop : null;
25426
25576
  }
25427
25577
  function resolveLocalFile(rawPath) {
25428
- const expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
25578
+ const expand2 = (p) => p.startsWith("~") ? np3.join(os4.homedir(), p.slice(1)) : p;
25429
25579
  const exists = (p) => {
25430
25580
  try {
25431
- return fs3.existsSync(p) ? p : null;
25581
+ return fs4.existsSync(p) ? p : null;
25432
25582
  } catch {
25433
25583
  return null;
25434
25584
  }
25435
25585
  };
25436
- const abs = expand(rawPath);
25586
+ const abs = expand2(rawPath);
25437
25587
  let hit = exists(abs);
25438
25588
  if (hit) return hit;
25439
25589
  const UNI = /[    ⁠]/g;
@@ -25441,26 +25591,26 @@ function resolveLocalFile(rawPath) {
25441
25591
  hit = exists(v);
25442
25592
  if (hit) return hit;
25443
25593
  }
25444
- const base = np2.basename(abs);
25594
+ const base = np3.basename(abs);
25445
25595
  const looksLikeShot = /screencaptureui/i.test(abs) || /^Screenshot[\s  ]/i.test(base);
25446
25596
  if (looksLikeShot) {
25447
25597
  const dir = screenshotDir();
25448
25598
  if (dir) {
25449
25599
  let files = [];
25450
25600
  try {
25451
- files = fs3.readdirSync(dir);
25601
+ files = fs4.readdirSync(dir);
25452
25602
  } catch {
25453
25603
  }
25454
25604
  const norm = (s) => s.replace(/[    ⁠\s]+/g, " ").trim().toLowerCase();
25455
25605
  const want = norm(base);
25456
25606
  for (const f of files) if (norm(f) === want) {
25457
- hit = exists(np2.join(dir, f));
25607
+ hit = exists(np3.join(dir, f));
25458
25608
  if (hit) return hit;
25459
25609
  }
25460
25610
  const shots = files.filter((f) => /^Screenshot[\s  ].*\.(png|jpe?g)$/i.test(f)).map((f) => {
25461
- const p = np2.join(dir, f);
25611
+ const p = np3.join(dir, f);
25462
25612
  try {
25463
- return { p, m: fs3.statSync(p).mtimeMs };
25613
+ return { p, m: fs4.statSync(p).mtimeMs };
25464
25614
  } catch {
25465
25615
  return { p, m: 0 };
25466
25616
  }
@@ -25620,8 +25770,8 @@ ${fp}`);
25620
25770
  `I can't find a file at ${path3} on this machine.`
25621
25771
  );
25622
25772
  }
25623
- const buf = fs3.readFileSync(abs);
25624
- name = np2.basename(abs);
25773
+ const buf = fs4.readFileSync(abs);
25774
+ name = np3.basename(abs);
25625
25775
  mime = mimeOf(name);
25626
25776
  url = `data:${mime};base64,${buf.toString("base64")}`;
25627
25777
  } else if (dataUrl && fileName) {
@@ -25681,8 +25831,8 @@ ${fp}`);
25681
25831
  `I can't find a file at ${path3} on this machine.`
25682
25832
  );
25683
25833
  }
25684
- const buf = fs3.readFileSync(abs);
25685
- name = np2.basename(abs);
25834
+ const buf = fs4.readFileSync(abs);
25835
+ name = np3.basename(abs);
25686
25836
  mime = mimeOf(name);
25687
25837
  url = `data:${mime};base64,${buf.toString("base64")}`;
25688
25838
  } else if (dataUrl && fileName) {
@@ -25714,7 +25864,7 @@ ${fp}`);
25714
25864
  let name, mime, url;
25715
25865
  if (path3) {
25716
25866
  if (!ctx.local) {
25717
- const fname = np2.basename(path3);
25867
+ const fname = np3.basename(path3);
25718
25868
  const fmime = mimeOf(fname);
25719
25869
  const hub = ctx.serverUrl.replace(/\/$/, "");
25720
25870
  const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
@@ -25739,8 +25889,8 @@ ${psCmd}
25739
25889
  if (!abs) {
25740
25890
  return fail(`I can't find a file at ${path3} on this machine.`);
25741
25891
  }
25742
- const buf = fs3.readFileSync(abs);
25743
- name = np2.basename(abs);
25892
+ const buf = fs4.readFileSync(abs);
25893
+ name = np3.basename(abs);
25744
25894
  mime = mimeOf(name);
25745
25895
  url = `data:${mime};base64,${buf.toString("base64")}`;
25746
25896
  } else if (dataUrl && fileName) {
@@ -25821,57 +25971,57 @@ ${psCmd}
25821
25971
  `Sending a folder reads files off disk, which this connector can't do \u2014 it runs in the cloud. Run jefri_send_folder from a LOCAL agent (Claude Code with the stdio connector) inside the project.`
25822
25972
  );
25823
25973
  const rawDir = path3 && path3.trim() ? path3 : process.cwd();
25824
- const dir = rawDir.startsWith("~") ? np2.join(os3.homedir(), rawDir.slice(1)) : rawDir;
25825
- if (!fs3.existsSync(dir))
25974
+ const dir = rawDir.startsWith("~") ? np3.join(os4.homedir(), rawDir.slice(1)) : rawDir;
25975
+ if (!fs4.existsSync(dir))
25826
25976
  return fail(
25827
25977
  `I can't see "${rawDir}" from here. Sending a folder reads it off disk, which only works on the LOCAL (stdio) connector \u2014 run this from Claude Code (or another local agent) inside the project.`
25828
25978
  );
25829
25979
  let stat;
25830
25980
  try {
25831
- stat = fs3.statSync(dir);
25981
+ stat = fs4.statSync(dir);
25832
25982
  } catch {
25833
25983
  return fail(`Cannot read "${rawDir}".`);
25834
25984
  }
25835
25985
  if (!stat.isDirectory())
25836
25986
  return fail(`"${rawDir}" is a file, not a folder \u2014 use jefri_send_file for a single file.`);
25837
25987
  try {
25838
- cp2.execFileSync("zip", ["-v"], { stdio: "ignore" });
25988
+ cp3.execFileSync("zip", ["-v"], { stdio: "ignore" });
25839
25989
  } catch {
25840
25990
  return fail(`The 'zip' command isn't available on this machine, and I need it to package the folder.`);
25841
25991
  }
25842
- const base = np2.basename(np2.resolve(dir)) || "project";
25843
- const tmpZip = np2.join(os3.tmpdir(), `acp-${base}-${Date.now()}.zip`);
25992
+ const base = np3.basename(np3.resolve(dir)) || "project";
25993
+ const tmpZip = np3.join(os4.tmpdir(), `acp-${base}-${Date.now()}.zip`);
25844
25994
  let isRepo = false;
25845
25995
  try {
25846
- cp2.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
25996
+ cp3.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
25847
25997
  isRepo = true;
25848
25998
  } catch {
25849
25999
  isRepo = false;
25850
26000
  }
25851
26001
  try {
25852
26002
  if (isRepo) {
25853
- cp2.execSync(
26003
+ cp3.execSync(
25854
26004
  `cd ${shq(dir)} && git ls-files --cached --others --exclude-standard -z | xargs -0 zip -q ${shq(tmpZip)} --`,
25855
26005
  { stdio: "ignore" }
25856
26006
  );
25857
26007
  } else {
25858
- cp2.execSync(
26008
+ cp3.execSync(
25859
26009
  `cd ${shq(dir)} && zip -qr ${shq(tmpZip)} . -x '*/node_modules/*' '*/.git/*' '*/dist/*' '*/.next/*' '*/build/*' '*/__pycache__/*' '*/.venv/*' '*/venv/*' '*.log' '*/.DS_Store'`,
25860
26010
  { stdio: "ignore" }
25861
26011
  );
25862
26012
  }
25863
26013
  } catch (e) {
25864
26014
  try {
25865
- fs3.rmSync(tmpZip, { force: true });
26015
+ fs4.rmSync(tmpZip, { force: true });
25866
26016
  } catch {
25867
26017
  }
25868
26018
  return fail(`Could not zip "${rawDir}": ${e?.message ?? e}`);
25869
26019
  }
25870
- if (!fs3.existsSync(tmpZip))
26020
+ if (!fs4.existsSync(tmpZip))
25871
26021
  return fail(`Nothing to send \u2014 "${rawDir}" produced an empty archive (all files ignored or folder empty).`);
25872
- const size = fs3.statSync(tmpZip).size;
26022
+ const size = fs4.statSync(tmpZip).size;
25873
26023
  if (size > MAX_UPLOAD_BYTES) {
25874
- fs3.rmSync(tmpZip, { force: true });
26024
+ fs4.rmSync(tmpZip, { force: true });
25875
26025
  return fail(
25876
26026
  `That archive is ${MB(size)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit. Send a subfolder (e.g. path=\u2026/src) or remove large files, then try again.`
25877
26027
  );
@@ -25883,7 +26033,7 @@ ${psCmd}
25883
26033
  const res = await fetch(`${hub}/api/files?${q}`, {
25884
26034
  method: "POST",
25885
26035
  headers: { Authorization: `Bearer ${c.token}`, "Content-Type": "application/zip" },
25886
- body: fs3.readFileSync(tmpZip)
26036
+ body: fs4.readFileSync(tmpZip)
25887
26037
  });
25888
26038
  if (!res.ok) {
25889
26039
  const t = await res.text().catch(() => "");
@@ -25893,7 +26043,7 @@ ${psCmd}
25893
26043
  return fail(`Upload failed: ${e?.message ?? e}`);
25894
26044
  } finally {
25895
26045
  try {
25896
- fs3.rmSync(tmpZip, { force: true });
26046
+ fs4.rmSync(tmpZip, { force: true });
25897
26047
  } catch {
25898
26048
  }
25899
26049
  }
@@ -25928,12 +26078,12 @@ ${psCmd}
25928
26078
  const hub = ctx.serverUrl.replace(/\/$/, "");
25929
26079
  const out = savePath || match.fileName || "downloaded.file";
25930
26080
  if (ctx.local) {
25931
- const outPath = out.startsWith("~") ? np2.join(os3.homedir(), out.slice(1)) : out;
26081
+ const outPath = out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
25932
26082
  try {
25933
26083
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
25934
26084
  if (!r.ok) throw new Error(`download failed (${r.status})`);
25935
26085
  const buf = Buffer.from(await r.arrayBuffer());
25936
- fs3.writeFileSync(outPath, buf);
26086
+ fs4.writeFileSync(outPath, buf);
25937
26087
  return ok(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
25938
26088
  } catch (e) {
25939
26089
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
@@ -26034,7 +26184,7 @@ ${cmd}`);
26034
26184
  }
26035
26185
  },
26036
26186
  async ({ departmentId, path: path3, kind }) => withClient(async (c) => {
26037
- const fname = np2.basename(path3.startsWith("~") ? path3.slice(1) : path3);
26187
+ const fname = np3.basename(path3.startsWith("~") ? path3.slice(1) : path3);
26038
26188
  const mime = mimeOf(fname);
26039
26189
  const q = `name=${encodeURIComponent(fname)}&kind=${kind ?? "file"}`;
26040
26190
  const cmd = `curl -s -X POST ${shq(`${hubBase()}/api/departments/${departmentId}/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${mime}" --data-binary @${shq(path3)}`;
@@ -26118,7 +26268,7 @@ ${cmd}`);
26118
26268
  inputSchema: { path: external_exports.string().describe("local file path to upload") }
26119
26269
  },
26120
26270
  async ({ path: path3 }) => withClient(async (c) => {
26121
- const fname = np2.basename(path3.startsWith("~") ? path3.slice(1) : path3);
26271
+ const fname = np3.basename(path3.startsWith("~") ? path3.slice(1) : path3);
26122
26272
  const mime = mimeOf(fname);
26123
26273
  const cmd = `curl -s -X POST ${shq(`${hubBase()}/api/agents/${encodeURIComponent(c.identity.username)}/docs?name=${encodeURIComponent(fname)}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${mime}" --data-binary @${shq(path3)}`;
26124
26274
  return ok(`To add ${fname} to your document memory, run in the terminal:
@@ -26270,7 +26420,7 @@ ${cmd}`);
26270
26420
  if (path3) {
26271
26421
  if (!ctx.local) {
26272
26422
  const hub = ctx.serverUrl.replace(/\/$/, "");
26273
- const fname = np2.basename(path3);
26423
+ const fname = np3.basename(path3);
26274
26424
  const fmime = mimeOf(fname);
26275
26425
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
26276
26426
  const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
@@ -26282,8 +26432,8 @@ ${cmd}`
26282
26432
  }
26283
26433
  const abs = resolveLocalFile(path3);
26284
26434
  if (!abs) return fail(`I can't find a file at ${path3} on this machine.`);
26285
- const buf = fs3.readFileSync(abs);
26286
- name = np2.basename(abs);
26435
+ const buf = fs4.readFileSync(abs);
26436
+ name = np3.basename(abs);
26287
26437
  mime = mimeOf(name);
26288
26438
  url = `data:${mime};base64,${buf.toString("base64")}`;
26289
26439
  } else if (dataUrl && fileName) {
@@ -26491,6 +26641,44 @@ ${cmd}`
26491
26641
  return ok(summary);
26492
26642
  })
26493
26643
  );
26644
+ server2.registerTool(
26645
+ "jefri_autonomous",
26646
+ {
26647
+ title: "Autonomous mode",
26648
+ 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.",
26649
+ inputSchema: {
26650
+ enabled: external_exports.boolean().optional().describe("turn autonomous replies on/off"),
26651
+ brain: external_exports.string().optional().describe("'claude', 'codex', or a full custom command (e.g. for OpenClaw)"),
26652
+ workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
26653
+ replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
26654
+ persona: external_exports.string().optional().describe("role/instructions for the agent"),
26655
+ replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)")
26656
+ }
26657
+ },
26658
+ async (args) => withClient(async () => {
26659
+ const patch = {};
26660
+ if (typeof args.enabled === "boolean") patch.enabled = args.enabled;
26661
+ if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
26662
+ if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim();
26663
+ if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
26664
+ if (typeof args.persona === "string") patch.persona = args.persona;
26665
+ if (args.replyMode) patch.replyMode = args.replyMode;
26666
+ const c = setAuto(patch);
26667
+ const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
26668
+ const lines = [
26669
+ `\u{1F916} Autonomous mode: ${c.enabled ? "ON" : "OFF"}`,
26670
+ ` Brain: ${c.brain}`,
26671
+ ` Work folder: ${c.workdir}`,
26672
+ ` Replies to: ${modeLabel}`,
26673
+ ` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
26674
+ ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`
26675
+ ];
26676
+ if (c.enabled)
26677
+ lines.push(`
26678
+ \u26A0\uFE0F Incoming messages now drive "${c.brain}" (with tool access) in ${c.workdir}. Keep this on only with people you trust.`);
26679
+ return ok(lines.join("\n"));
26680
+ })
26681
+ );
26494
26682
  }
26495
26683
  }
26496
26684
  function attachInbox(c, inbox2, self, onIncoming) {
@@ -26516,11 +26704,11 @@ var NAME = process.env.ACP_NAME ?? USERNAME;
26516
26704
  var TAGS = (process.env.ACP_TAGS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
26517
26705
  var CAPS = (process.env.ACP_CAPABILITIES ?? "coding").split(",").map((s) => s.trim()).filter(Boolean);
26518
26706
  var log = (...a) => console.error("[jefrichat-mcp]", ...a);
26519
- var TOKEN_CACHE = path2.join(os4.homedir(), ".acp", "tokens.json");
26707
+ var TOKEN_CACHE = path2.join(os5.homedir(), ".acp", "tokens.json");
26520
26708
  var cacheKey = `${SERVER}::${USERNAME}`;
26521
26709
  function readTokenCache() {
26522
26710
  try {
26523
- return JSON.parse(fs4.readFileSync(TOKEN_CACHE, "utf8"));
26711
+ return JSON.parse(fs5.readFileSync(TOKEN_CACHE, "utf8"));
26524
26712
  } catch {
26525
26713
  return {};
26526
26714
  }
@@ -26528,8 +26716,8 @@ function readTokenCache() {
26528
26716
  function cacheToken(tok) {
26529
26717
  const c = readTokenCache();
26530
26718
  c[cacheKey] = tok;
26531
- fs4.mkdirSync(path2.dirname(TOKEN_CACHE), { recursive: true });
26532
- fs4.writeFileSync(TOKEN_CACHE, JSON.stringify(c, null, 2));
26719
+ fs5.mkdirSync(path2.dirname(TOKEN_CACHE), { recursive: true });
26720
+ fs5.writeFileSync(TOKEN_CACHE, JSON.stringify(c, null, 2));
26533
26721
  }
26534
26722
  var clientPromise = null;
26535
26723
  var inbox = [];
@@ -26569,8 +26757,18 @@ function ensureClient() {
26569
26757
  });
26570
26758
  void refreshGroups();
26571
26759
  setInterval(refreshGroups, 6e4).unref?.();
26760
+ const agentUsers = /* @__PURE__ */ new Set();
26761
+ const refreshAgents = () => c.identities().then((ids) => {
26762
+ agentUsers.clear();
26763
+ for (const i of ids) if (i.type === "agent") agentUsers.add(i.username);
26764
+ }).catch(() => {
26765
+ });
26766
+ void refreshAgents();
26767
+ setInterval(refreshAgents, 6e4).unref?.();
26572
26768
  if (macNeedsTerminalNotifier)
26573
26769
  log("tip: for reliable desktop notifications on macOS, run: brew install terminal-notifier");
26770
+ if (getAuto().enabled)
26771
+ log(`autonomous mode is ON \u2014 incoming messages will be handled by "${getAuto().brain}" in ${getAuto().workdir}`);
26574
26772
  attachInbox(c, inbox, self, (m) => {
26575
26773
  const preview = m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : m.content ?? "";
26576
26774
  const isGroup = !!m.groupId;
@@ -26583,6 +26781,23 @@ function ensureClient() {
26583
26781
  sender: m.senderUsername,
26584
26782
  isMention
26585
26783
  });
26784
+ if (m.kind === "text" && typeof m.content === "string") {
26785
+ handleAutonomous(
26786
+ {
26787
+ content: m.content,
26788
+ sender: m.senderUsername,
26789
+ isGroup,
26790
+ groupId: m.groupId,
26791
+ isMention,
26792
+ senderIsBot: agentUsers.has(m.senderUsername)
26793
+ },
26794
+ (text) => {
26795
+ if (isGroup && m.groupId) c.groupMessage(m.groupId, text);
26796
+ else c.message(m.senderUsername, text);
26797
+ },
26798
+ log
26799
+ );
26800
+ }
26586
26801
  });
26587
26802
  log(`connected to ${SERVER} as ${self}`);
26588
26803
  return c;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.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": {