jefrichat-mcp 0.7.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 +301 -58
  2. package/dist/index.js +470 -106
  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 fs3 = __require("fs");
1304
- stream2 = new fs3.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 fs3 = __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 fs3.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 fs3 = __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 = fs3.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 fs3 = __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
- fs3.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
- fs3.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
- fs3.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 = fs3.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, fs3, 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, fs3[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,102 @@ var AcpClient = class _AcpClient {
49373
49405
  };
49374
49406
 
49375
49407
  // src/tools.ts
49408
+ import fs4 from "node:fs";
49409
+ import np3 from "node:path";
49410
+ import cp2 from "node:child_process";
49411
+ import os4 from "node:os";
49412
+
49413
+ // src/notify.ts
49414
+ import cp from "node:child_process";
49376
49415
  import fs2 from "node:fs";
49377
49416
  import np from "node:path";
49378
- import cp from "node:child_process";
49379
49417
  import os2 from "node:os";
49418
+ var OFF = process.env.JEFRI_NOTIFY === "0" || process.env.JEFRI_NOTIFY === "false";
49419
+ function findTerminalNotifier() {
49420
+ if (process.platform !== "darwin") return null;
49421
+ for (const p of ["/opt/homebrew/bin/terminal-notifier", "/usr/local/bin/terminal-notifier"]) {
49422
+ try {
49423
+ if (fs2.existsSync(p)) return p;
49424
+ } catch {
49425
+ }
49426
+ }
49427
+ try {
49428
+ const out = (cp.spawnSync("which", ["terminal-notifier"], { encoding: "utf8" }).stdout || "").trim();
49429
+ if (out && fs2.existsSync(out)) return out;
49430
+ } catch {
49431
+ }
49432
+ return null;
49433
+ }
49434
+ var TERMINAL_NOTIFIER = findTerminalNotifier();
49435
+ var macNeedsTerminalNotifier = process.platform === "darwin" && !TERMINAL_NOTIFIER && !OFF;
49436
+ var DEFAULTS = {
49437
+ enabled: true,
49438
+ sound: true,
49439
+ dms: true,
49440
+ groups: true,
49441
+ onlyMentions: false,
49442
+ quietFrom: null,
49443
+ quietTo: null,
49444
+ mutedGroups: [],
49445
+ mutedUsers: []
49446
+ };
49447
+ var PREFS_DIR = np.join(os2.homedir(), ".jefri");
49448
+ var PREFS_FILE = np.join(PREFS_DIR, "notify.json");
49449
+ function loadPrefs() {
49450
+ try {
49451
+ return { ...DEFAULTS, ...JSON.parse(fs2.readFileSync(PREFS_FILE, "utf8")) };
49452
+ } catch {
49453
+ return { ...DEFAULTS };
49454
+ }
49455
+ }
49456
+ var prefs = loadPrefs();
49457
+ function getPrefs() {
49458
+ return { ...prefs };
49459
+ }
49460
+ function setPrefs(patch) {
49461
+ prefs = { ...prefs, ...patch };
49462
+ try {
49463
+ fs2.mkdirSync(PREFS_DIR, { recursive: true });
49464
+ fs2.writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
49465
+ } catch {
49466
+ }
49467
+ return { ...prefs };
49468
+ }
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
+
49503
+ // src/tools.ts
49380
49504
  var MIME = {
49381
49505
  ".jpg": "image/jpeg",
49382
49506
  ".jpeg": "image/jpeg",
@@ -49396,21 +49520,21 @@ var MIME = {
49396
49520
  ".json": "application/json",
49397
49521
  ".zip": "application/zip"
49398
49522
  };
49399
- var mimeOf = (name) => MIME[np.extname(name).toLowerCase()] ?? "application/octet-stream";
49523
+ var mimeOf = (name) => MIME[np3.extname(name).toLowerCase()] ?? "application/octet-stream";
49400
49524
  function screenshotDir() {
49401
49525
  try {
49402
- const out = cp.execFileSync("defaults", ["read", "com.apple.screencapture", "location"], { encoding: "utf8", timeout: 2e3 }).trim();
49403
- if (out) return out.startsWith("~") ? np.join(os2.homedir(), out.slice(1)) : out;
49526
+ const out = cp2.execFileSync("defaults", ["read", "com.apple.screencapture", "location"], { encoding: "utf8", timeout: 2e3 }).trim();
49527
+ if (out) return out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
49404
49528
  } catch {
49405
49529
  }
49406
- const desktop = np.join(os2.homedir(), "Desktop");
49407
- return fs2.existsSync(desktop) ? desktop : null;
49530
+ const desktop = np3.join(os4.homedir(), "Desktop");
49531
+ return fs4.existsSync(desktop) ? desktop : null;
49408
49532
  }
49409
49533
  function resolveLocalFile(rawPath) {
49410
- const expand = (p) => p.startsWith("~") ? np.join(os2.homedir(), p.slice(1)) : p;
49534
+ const expand = (p) => p.startsWith("~") ? np3.join(os4.homedir(), p.slice(1)) : p;
49411
49535
  const exists = (p) => {
49412
49536
  try {
49413
- return fs2.existsSync(p) ? p : null;
49537
+ return fs4.existsSync(p) ? p : null;
49414
49538
  } catch {
49415
49539
  return null;
49416
49540
  }
@@ -49423,26 +49547,26 @@ function resolveLocalFile(rawPath) {
49423
49547
  hit = exists(v);
49424
49548
  if (hit) return hit;
49425
49549
  }
49426
- const base = np.basename(abs);
49550
+ const base = np3.basename(abs);
49427
49551
  const looksLikeShot = /screencaptureui/i.test(abs) || /^Screenshot[\s  ]/i.test(base);
49428
49552
  if (looksLikeShot) {
49429
49553
  const dir = screenshotDir();
49430
49554
  if (dir) {
49431
49555
  let files = [];
49432
49556
  try {
49433
- files = fs2.readdirSync(dir);
49557
+ files = fs4.readdirSync(dir);
49434
49558
  } catch {
49435
49559
  }
49436
49560
  const norm = (s) => s.replace(/[    ⁠\s]+/g, " ").trim().toLowerCase();
49437
49561
  const want = norm(base);
49438
49562
  for (const f of files) if (norm(f) === want) {
49439
- hit = exists(np.join(dir, f));
49563
+ hit = exists(np3.join(dir, f));
49440
49564
  if (hit) return hit;
49441
49565
  }
49442
49566
  const shots = files.filter((f) => /^Screenshot[\s  ].*\.(png|jpe?g)$/i.test(f)).map((f) => {
49443
- const p = np.join(dir, f);
49567
+ const p = np3.join(dir, f);
49444
49568
  try {
49445
- return { p, m: fs2.statSync(p).mtimeMs };
49569
+ return { p, m: fs4.statSync(p).mtimeMs };
49446
49570
  } catch {
49447
49571
  return { p, m: 0 };
49448
49572
  }
@@ -49602,8 +49726,8 @@ ${fp}`);
49602
49726
  `I can't find a file at ${path2} on this machine.`
49603
49727
  );
49604
49728
  }
49605
- const buf = fs2.readFileSync(abs);
49606
- name = np.basename(abs);
49729
+ const buf = fs4.readFileSync(abs);
49730
+ name = np3.basename(abs);
49607
49731
  mime = mimeOf(name);
49608
49732
  url = `data:${mime};base64,${buf.toString("base64")}`;
49609
49733
  } else if (dataUrl && fileName) {
@@ -49663,8 +49787,8 @@ ${fp}`);
49663
49787
  `I can't find a file at ${path2} on this machine.`
49664
49788
  );
49665
49789
  }
49666
- const buf = fs2.readFileSync(abs);
49667
- name = np.basename(abs);
49790
+ const buf = fs4.readFileSync(abs);
49791
+ name = np3.basename(abs);
49668
49792
  mime = mimeOf(name);
49669
49793
  url = `data:${mime};base64,${buf.toString("base64")}`;
49670
49794
  } else if (dataUrl && fileName) {
@@ -49696,7 +49820,7 @@ ${fp}`);
49696
49820
  let name, mime, url;
49697
49821
  if (path2) {
49698
49822
  if (!ctx.local) {
49699
- const fname = np.basename(path2);
49823
+ const fname = np3.basename(path2);
49700
49824
  const fmime = mimeOf(fname);
49701
49825
  const hub = ctx.serverUrl.replace(/\/$/, "");
49702
49826
  const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
@@ -49721,8 +49845,8 @@ ${psCmd}
49721
49845
  if (!abs) {
49722
49846
  return fail(`I can't find a file at ${path2} on this machine.`);
49723
49847
  }
49724
- const buf = fs2.readFileSync(abs);
49725
- name = np.basename(abs);
49848
+ const buf = fs4.readFileSync(abs);
49849
+ name = np3.basename(abs);
49726
49850
  mime = mimeOf(name);
49727
49851
  url = `data:${mime};base64,${buf.toString("base64")}`;
49728
49852
  } else if (dataUrl && fileName) {
@@ -49803,57 +49927,57 @@ ${psCmd}
49803
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.`
49804
49928
  );
49805
49929
  const rawDir = path2 && path2.trim() ? path2 : process.cwd();
49806
- const dir = rawDir.startsWith("~") ? np.join(os2.homedir(), rawDir.slice(1)) : rawDir;
49807
- if (!fs2.existsSync(dir))
49930
+ const dir = rawDir.startsWith("~") ? np3.join(os4.homedir(), rawDir.slice(1)) : rawDir;
49931
+ if (!fs4.existsSync(dir))
49808
49932
  return fail(
49809
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.`
49810
49934
  );
49811
49935
  let stat;
49812
49936
  try {
49813
- stat = fs2.statSync(dir);
49937
+ stat = fs4.statSync(dir);
49814
49938
  } catch {
49815
49939
  return fail(`Cannot read "${rawDir}".`);
49816
49940
  }
49817
49941
  if (!stat.isDirectory())
49818
49942
  return fail(`"${rawDir}" is a file, not a folder \u2014 use jefri_send_file for a single file.`);
49819
49943
  try {
49820
- cp.execFileSync("zip", ["-v"], { stdio: "ignore" });
49944
+ cp2.execFileSync("zip", ["-v"], { stdio: "ignore" });
49821
49945
  } catch {
49822
49946
  return fail(`The 'zip' command isn't available on this machine, and I need it to package the folder.`);
49823
49947
  }
49824
- const base = np.basename(np.resolve(dir)) || "project";
49825
- const tmpZip = np.join(os2.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`);
49826
49950
  let isRepo = false;
49827
49951
  try {
49828
- cp.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
49952
+ cp2.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
49829
49953
  isRepo = true;
49830
49954
  } catch {
49831
49955
  isRepo = false;
49832
49956
  }
49833
49957
  try {
49834
49958
  if (isRepo) {
49835
- cp.execSync(
49959
+ cp2.execSync(
49836
49960
  `cd ${shq(dir)} && git ls-files --cached --others --exclude-standard -z | xargs -0 zip -q ${shq(tmpZip)} --`,
49837
49961
  { stdio: "ignore" }
49838
49962
  );
49839
49963
  } else {
49840
- cp.execSync(
49964
+ cp2.execSync(
49841
49965
  `cd ${shq(dir)} && zip -qr ${shq(tmpZip)} . -x '*/node_modules/*' '*/.git/*' '*/dist/*' '*/.next/*' '*/build/*' '*/__pycache__/*' '*/.venv/*' '*/venv/*' '*.log' '*/.DS_Store'`,
49842
49966
  { stdio: "ignore" }
49843
49967
  );
49844
49968
  }
49845
49969
  } catch (e) {
49846
49970
  try {
49847
- fs2.rmSync(tmpZip, { force: true });
49971
+ fs4.rmSync(tmpZip, { force: true });
49848
49972
  } catch {
49849
49973
  }
49850
49974
  return fail(`Could not zip "${rawDir}": ${e?.message ?? e}`);
49851
49975
  }
49852
- if (!fs2.existsSync(tmpZip))
49976
+ if (!fs4.existsSync(tmpZip))
49853
49977
  return fail(`Nothing to send \u2014 "${rawDir}" produced an empty archive (all files ignored or folder empty).`);
49854
- const size = fs2.statSync(tmpZip).size;
49978
+ const size = fs4.statSync(tmpZip).size;
49855
49979
  if (size > MAX_UPLOAD_BYTES) {
49856
- fs2.rmSync(tmpZip, { force: true });
49980
+ fs4.rmSync(tmpZip, { force: true });
49857
49981
  return fail(
49858
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.`
49859
49983
  );
@@ -49865,7 +49989,7 @@ ${psCmd}
49865
49989
  const res = await fetch(`${hub}/api/files?${q}`, {
49866
49990
  method: "POST",
49867
49991
  headers: { Authorization: `Bearer ${c.token}`, "Content-Type": "application/zip" },
49868
- body: fs2.readFileSync(tmpZip)
49992
+ body: fs4.readFileSync(tmpZip)
49869
49993
  });
49870
49994
  if (!res.ok) {
49871
49995
  const t = await res.text().catch(() => "");
@@ -49875,7 +49999,7 @@ ${psCmd}
49875
49999
  return fail(`Upload failed: ${e?.message ?? e}`);
49876
50000
  } finally {
49877
50001
  try {
49878
- fs2.rmSync(tmpZip, { force: true });
50002
+ fs4.rmSync(tmpZip, { force: true });
49879
50003
  } catch {
49880
50004
  }
49881
50005
  }
@@ -49910,12 +50034,12 @@ ${psCmd}
49910
50034
  const hub = ctx.serverUrl.replace(/\/$/, "");
49911
50035
  const out = savePath || match.fileName || "downloaded.file";
49912
50036
  if (ctx.local) {
49913
- const outPath = out.startsWith("~") ? np.join(os2.homedir(), out.slice(1)) : out;
50037
+ const outPath = out.startsWith("~") ? np3.join(os4.homedir(), out.slice(1)) : out;
49914
50038
  try {
49915
50039
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
49916
50040
  if (!r.ok) throw new Error(`download failed (${r.status})`);
49917
50041
  const buf = Buffer.from(await r.arrayBuffer());
49918
- fs2.writeFileSync(outPath, buf);
50042
+ fs4.writeFileSync(outPath, buf);
49919
50043
  return ok(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
49920
50044
  } catch (e) {
49921
50045
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
@@ -50016,7 +50140,7 @@ ${cmd}`);
50016
50140
  }
50017
50141
  },
50018
50142
  async ({ departmentId, path: path2, kind }) => withClient(async (c) => {
50019
- const fname = np.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50143
+ const fname = np3.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50020
50144
  const mime = mimeOf(fname);
50021
50145
  const q = `name=${encodeURIComponent(fname)}&kind=${kind ?? "file"}`;
50022
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)}`;
@@ -50100,7 +50224,7 @@ ${cmd}`);
50100
50224
  inputSchema: { path: external_exports.string().describe("local file path to upload") }
50101
50225
  },
50102
50226
  async ({ path: path2 }) => withClient(async (c) => {
50103
- const fname = np.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50227
+ const fname = np3.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50104
50228
  const mime = mimeOf(fname);
50105
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)}`;
50106
50230
  return ok(`To add ${fname} to your document memory, run in the terminal:
@@ -50252,7 +50376,7 @@ ${cmd}`);
50252
50376
  if (path2) {
50253
50377
  if (!ctx.local) {
50254
50378
  const hub = ctx.serverUrl.replace(/\/$/, "");
50255
- const fname = np.basename(path2);
50379
+ const fname = np3.basename(path2);
50256
50380
  const fmime = mimeOf(fname);
50257
50381
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
50258
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)}`;
@@ -50264,8 +50388,8 @@ ${cmd}`
50264
50388
  }
50265
50389
  const abs = resolveLocalFile(path2);
50266
50390
  if (!abs) return fail(`I can't find a file at ${path2} on this machine.`);
50267
- const buf = fs2.readFileSync(abs);
50268
- name = np.basename(abs);
50391
+ const buf = fs4.readFileSync(abs);
50392
+ name = np3.basename(abs);
50269
50393
  mime = mimeOf(name);
50270
50394
  url = `data:${mime};base64,${buf.toString("base64")}`;
50271
50395
  } else if (dataUrl && fileName) {
@@ -50393,6 +50517,125 @@ ${cmd}`
50393
50517
  return ok(`Status set to ${status}.`);
50394
50518
  })
50395
50519
  );
50520
+ if (ctx.local) {
50521
+ server.registerTool(
50522
+ "jefri_notifications",
50523
+ {
50524
+ title: "Notification settings",
50525
+ description: "Show or change desktop notification preferences: master on/off, sound on/off, DMs vs groups, only-@mentions for groups, quiet hours, and muting a specific group or person. Call with NO arguments to just see the current settings. Persists across sessions.",
50526
+ inputSchema: {
50527
+ enabled: external_exports.boolean().optional().describe("master switch for all desktop notifications"),
50528
+ sound: external_exports.boolean().optional().describe("play a sound with notifications"),
50529
+ dms: external_exports.boolean().optional().describe("notify for direct messages"),
50530
+ groups: external_exports.boolean().optional().describe("notify for group messages"),
50531
+ onlyMentions: external_exports.boolean().optional().describe("for groups: only notify when someone @-mentions you"),
50532
+ quietFrom: external_exports.string().optional().describe("start of quiet hours, HH:MM 24h (e.g. 22:00), or 'off' to clear"),
50533
+ quietTo: external_exports.string().optional().describe("end of quiet hours, HH:MM 24h (e.g. 08:00)"),
50534
+ muteGroup: external_exports.string().optional().describe("a group's name or id to silence"),
50535
+ unmuteGroup: external_exports.string().optional().describe("a group's name or id to unmute"),
50536
+ muteUser: external_exports.string().optional().describe("a username to silence"),
50537
+ unmuteUser: external_exports.string().optional().describe("a username to unmute")
50538
+ }
50539
+ },
50540
+ async (args) => withClient(async (c) => {
50541
+ const patch = {};
50542
+ for (const k of ["enabled", "sound", "dms", "groups", "onlyMentions"])
50543
+ if (typeof args[k] === "boolean") patch[k] = args[k];
50544
+ if (args.quietFrom === "off" || args.quietTo === "off") {
50545
+ patch.quietFrom = null;
50546
+ patch.quietTo = null;
50547
+ } else {
50548
+ if (args.quietFrom) patch.quietFrom = args.quietFrom;
50549
+ if (args.quietTo) patch.quietTo = args.quietTo;
50550
+ }
50551
+ const cur = getPrefs();
50552
+ const mutedGroups = new Set(cur.mutedGroups);
50553
+ const mutedUsers = new Set(cur.mutedUsers);
50554
+ let groupList = [];
50555
+ const needGroups = args.muteGroup || args.unmuteGroup;
50556
+ if (needGroups) {
50557
+ try {
50558
+ groupList = await c.groups();
50559
+ } catch {
50560
+ }
50561
+ }
50562
+ const resolveGroup = (nameOrId) => {
50563
+ const byId = groupList.find((g) => g.id === nameOrId);
50564
+ if (byId) return byId.id;
50565
+ const byName = groupList.find((g) => g.name.toLowerCase() === nameOrId.toLowerCase());
50566
+ return byName ? byName.id : null;
50567
+ };
50568
+ if (args.muteGroup) {
50569
+ const id = resolveGroup(args.muteGroup);
50570
+ if (!id) return fail(`No group named "${args.muteGroup}".`);
50571
+ mutedGroups.add(id);
50572
+ }
50573
+ if (args.unmuteGroup) {
50574
+ const id = resolveGroup(args.unmuteGroup);
50575
+ if (id) mutedGroups.delete(id);
50576
+ }
50577
+ if (args.muteUser) mutedUsers.add(args.muteUser.replace(/^@/, ""));
50578
+ if (args.unmuteUser) mutedUsers.delete(args.unmuteUser.replace(/^@/, ""));
50579
+ patch.mutedGroups = [...mutedGroups];
50580
+ patch.mutedUsers = [...mutedUsers];
50581
+ const p = setPrefs(patch);
50582
+ const names = {};
50583
+ if (p.mutedGroups.length) {
50584
+ try {
50585
+ for (const g of await c.groups()) names[g.id] = g.name;
50586
+ } catch {
50587
+ }
50588
+ }
50589
+ const summary = [
50590
+ `\u{1F514} Notifications: ${p.enabled ? "ON" : "OFF"}`,
50591
+ ` Sound: ${p.sound ? "on" : "off"}`,
50592
+ ` DMs: ${p.dms ? "on" : "off"} \xB7 Groups: ${p.groups ? "on" : "off"}${p.onlyMentions ? " (only @mentions)" : ""}`,
50593
+ ` Quiet hours: ${p.quietFrom && p.quietTo ? `${p.quietFrom}\u2013${p.quietTo}` : "off"}`,
50594
+ ` Muted groups: ${p.mutedGroups.length ? p.mutedGroups.map((id) => names[id] ?? id).join(", ") : "none"}`,
50595
+ ` Muted people: ${p.mutedUsers.length ? p.mutedUsers.map((u) => "@" + u).join(", ") : "none"}`
50596
+ ].join("\n");
50597
+ return ok(summary);
50598
+ })
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
+ );
50638
+ }
50396
50639
  }
50397
50640
  function attachInbox(c, inbox, self, onIncoming) {
50398
50641
  const capture = (e) => {