jefrichat-mcp 0.7.0 → 0.8.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 +195 -55
  2. package/dist/index.js +245 -96
  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 fs4 = __require("fs");
1304
+ stream2 = new fs4.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 fs4 = __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 fs4.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 fs4 = __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 = fs4.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 fs4 = __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
+ fs4.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
+ fs4.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
+ fs4.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 = fs4.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, fs4, 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, fs4[f]);
29748
29748
  }
29749
29749
  module.exports = exports = formatsPlugin;
29750
29750
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -49373,10 +49373,69 @@ var AcpClient = class _AcpClient {
49373
49373
  };
49374
49374
 
49375
49375
  // src/tools.ts
49376
+ import fs3 from "node:fs";
49377
+ import np2 from "node:path";
49378
+ import cp2 from "node:child_process";
49379
+ import os3 from "node:os";
49380
+
49381
+ // src/notify.ts
49382
+ import cp from "node:child_process";
49376
49383
  import fs2 from "node:fs";
49377
49384
  import np from "node:path";
49378
- import cp from "node:child_process";
49379
49385
  import os2 from "node:os";
49386
+ var OFF = process.env.JEFRI_NOTIFY === "0" || process.env.JEFRI_NOTIFY === "false";
49387
+ function findTerminalNotifier() {
49388
+ if (process.platform !== "darwin") return null;
49389
+ for (const p of ["/opt/homebrew/bin/terminal-notifier", "/usr/local/bin/terminal-notifier"]) {
49390
+ try {
49391
+ if (fs2.existsSync(p)) return p;
49392
+ } catch {
49393
+ }
49394
+ }
49395
+ try {
49396
+ const out = (cp.spawnSync("which", ["terminal-notifier"], { encoding: "utf8" }).stdout || "").trim();
49397
+ if (out && fs2.existsSync(out)) return out;
49398
+ } catch {
49399
+ }
49400
+ return null;
49401
+ }
49402
+ var TERMINAL_NOTIFIER = findTerminalNotifier();
49403
+ var macNeedsTerminalNotifier = process.platform === "darwin" && !TERMINAL_NOTIFIER && !OFF;
49404
+ var DEFAULTS = {
49405
+ enabled: true,
49406
+ sound: true,
49407
+ dms: true,
49408
+ groups: true,
49409
+ onlyMentions: false,
49410
+ quietFrom: null,
49411
+ quietTo: null,
49412
+ mutedGroups: [],
49413
+ mutedUsers: []
49414
+ };
49415
+ var PREFS_DIR = np.join(os2.homedir(), ".jefri");
49416
+ var PREFS_FILE = np.join(PREFS_DIR, "notify.json");
49417
+ function loadPrefs() {
49418
+ try {
49419
+ return { ...DEFAULTS, ...JSON.parse(fs2.readFileSync(PREFS_FILE, "utf8")) };
49420
+ } catch {
49421
+ return { ...DEFAULTS };
49422
+ }
49423
+ }
49424
+ var prefs = loadPrefs();
49425
+ function getPrefs() {
49426
+ return { ...prefs };
49427
+ }
49428
+ function setPrefs(patch) {
49429
+ prefs = { ...prefs, ...patch };
49430
+ try {
49431
+ fs2.mkdirSync(PREFS_DIR, { recursive: true });
49432
+ fs2.writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
49433
+ } catch {
49434
+ }
49435
+ return { ...prefs };
49436
+ }
49437
+
49438
+ // src/tools.ts
49380
49439
  var MIME = {
49381
49440
  ".jpg": "image/jpeg",
49382
49441
  ".jpeg": "image/jpeg",
@@ -49396,21 +49455,21 @@ var MIME = {
49396
49455
  ".json": "application/json",
49397
49456
  ".zip": "application/zip"
49398
49457
  };
49399
- var mimeOf = (name) => MIME[np.extname(name).toLowerCase()] ?? "application/octet-stream";
49458
+ var mimeOf = (name) => MIME[np2.extname(name).toLowerCase()] ?? "application/octet-stream";
49400
49459
  function screenshotDir() {
49401
49460
  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;
49461
+ 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;
49404
49463
  } catch {
49405
49464
  }
49406
- const desktop = np.join(os2.homedir(), "Desktop");
49407
- return fs2.existsSync(desktop) ? desktop : null;
49465
+ const desktop = np2.join(os3.homedir(), "Desktop");
49466
+ return fs3.existsSync(desktop) ? desktop : null;
49408
49467
  }
49409
49468
  function resolveLocalFile(rawPath) {
49410
- const expand = (p) => p.startsWith("~") ? np.join(os2.homedir(), p.slice(1)) : p;
49469
+ const expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
49411
49470
  const exists = (p) => {
49412
49471
  try {
49413
- return fs2.existsSync(p) ? p : null;
49472
+ return fs3.existsSync(p) ? p : null;
49414
49473
  } catch {
49415
49474
  return null;
49416
49475
  }
@@ -49423,26 +49482,26 @@ function resolveLocalFile(rawPath) {
49423
49482
  hit = exists(v);
49424
49483
  if (hit) return hit;
49425
49484
  }
49426
- const base = np.basename(abs);
49485
+ const base = np2.basename(abs);
49427
49486
  const looksLikeShot = /screencaptureui/i.test(abs) || /^Screenshot[\s  ]/i.test(base);
49428
49487
  if (looksLikeShot) {
49429
49488
  const dir = screenshotDir();
49430
49489
  if (dir) {
49431
49490
  let files = [];
49432
49491
  try {
49433
- files = fs2.readdirSync(dir);
49492
+ files = fs3.readdirSync(dir);
49434
49493
  } catch {
49435
49494
  }
49436
49495
  const norm = (s) => s.replace(/[    ⁠\s]+/g, " ").trim().toLowerCase();
49437
49496
  const want = norm(base);
49438
49497
  for (const f of files) if (norm(f) === want) {
49439
- hit = exists(np.join(dir, f));
49498
+ hit = exists(np2.join(dir, f));
49440
49499
  if (hit) return hit;
49441
49500
  }
49442
49501
  const shots = files.filter((f) => /^Screenshot[\s  ].*\.(png|jpe?g)$/i.test(f)).map((f) => {
49443
- const p = np.join(dir, f);
49502
+ const p = np2.join(dir, f);
49444
49503
  try {
49445
- return { p, m: fs2.statSync(p).mtimeMs };
49504
+ return { p, m: fs3.statSync(p).mtimeMs };
49446
49505
  } catch {
49447
49506
  return { p, m: 0 };
49448
49507
  }
@@ -49602,8 +49661,8 @@ ${fp}`);
49602
49661
  `I can't find a file at ${path2} on this machine.`
49603
49662
  );
49604
49663
  }
49605
- const buf = fs2.readFileSync(abs);
49606
- name = np.basename(abs);
49664
+ const buf = fs3.readFileSync(abs);
49665
+ name = np2.basename(abs);
49607
49666
  mime = mimeOf(name);
49608
49667
  url = `data:${mime};base64,${buf.toString("base64")}`;
49609
49668
  } else if (dataUrl && fileName) {
@@ -49663,8 +49722,8 @@ ${fp}`);
49663
49722
  `I can't find a file at ${path2} on this machine.`
49664
49723
  );
49665
49724
  }
49666
- const buf = fs2.readFileSync(abs);
49667
- name = np.basename(abs);
49725
+ const buf = fs3.readFileSync(abs);
49726
+ name = np2.basename(abs);
49668
49727
  mime = mimeOf(name);
49669
49728
  url = `data:${mime};base64,${buf.toString("base64")}`;
49670
49729
  } else if (dataUrl && fileName) {
@@ -49696,7 +49755,7 @@ ${fp}`);
49696
49755
  let name, mime, url;
49697
49756
  if (path2) {
49698
49757
  if (!ctx.local) {
49699
- const fname = np.basename(path2);
49758
+ const fname = np2.basename(path2);
49700
49759
  const fmime = mimeOf(fname);
49701
49760
  const hub = ctx.serverUrl.replace(/\/$/, "");
49702
49761
  const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
@@ -49721,8 +49780,8 @@ ${psCmd}
49721
49780
  if (!abs) {
49722
49781
  return fail(`I can't find a file at ${path2} on this machine.`);
49723
49782
  }
49724
- const buf = fs2.readFileSync(abs);
49725
- name = np.basename(abs);
49783
+ const buf = fs3.readFileSync(abs);
49784
+ name = np2.basename(abs);
49726
49785
  mime = mimeOf(name);
49727
49786
  url = `data:${mime};base64,${buf.toString("base64")}`;
49728
49787
  } else if (dataUrl && fileName) {
@@ -49803,57 +49862,57 @@ ${psCmd}
49803
49862
  `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
49863
  );
49805
49864
  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))
49865
+ const dir = rawDir.startsWith("~") ? np2.join(os3.homedir(), rawDir.slice(1)) : rawDir;
49866
+ if (!fs3.existsSync(dir))
49808
49867
  return fail(
49809
49868
  `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
49869
  );
49811
49870
  let stat;
49812
49871
  try {
49813
- stat = fs2.statSync(dir);
49872
+ stat = fs3.statSync(dir);
49814
49873
  } catch {
49815
49874
  return fail(`Cannot read "${rawDir}".`);
49816
49875
  }
49817
49876
  if (!stat.isDirectory())
49818
49877
  return fail(`"${rawDir}" is a file, not a folder \u2014 use jefri_send_file for a single file.`);
49819
49878
  try {
49820
- cp.execFileSync("zip", ["-v"], { stdio: "ignore" });
49879
+ cp2.execFileSync("zip", ["-v"], { stdio: "ignore" });
49821
49880
  } catch {
49822
49881
  return fail(`The 'zip' command isn't available on this machine, and I need it to package the folder.`);
49823
49882
  }
49824
- const base = np.basename(np.resolve(dir)) || "project";
49825
- const tmpZip = np.join(os2.tmpdir(), `acp-${base}-${Date.now()}.zip`);
49883
+ const base = np2.basename(np2.resolve(dir)) || "project";
49884
+ const tmpZip = np2.join(os3.tmpdir(), `acp-${base}-${Date.now()}.zip`);
49826
49885
  let isRepo = false;
49827
49886
  try {
49828
- cp.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
49887
+ cp2.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
49829
49888
  isRepo = true;
49830
49889
  } catch {
49831
49890
  isRepo = false;
49832
49891
  }
49833
49892
  try {
49834
49893
  if (isRepo) {
49835
- cp.execSync(
49894
+ cp2.execSync(
49836
49895
  `cd ${shq(dir)} && git ls-files --cached --others --exclude-standard -z | xargs -0 zip -q ${shq(tmpZip)} --`,
49837
49896
  { stdio: "ignore" }
49838
49897
  );
49839
49898
  } else {
49840
- cp.execSync(
49899
+ cp2.execSync(
49841
49900
  `cd ${shq(dir)} && zip -qr ${shq(tmpZip)} . -x '*/node_modules/*' '*/.git/*' '*/dist/*' '*/.next/*' '*/build/*' '*/__pycache__/*' '*/.venv/*' '*/venv/*' '*.log' '*/.DS_Store'`,
49842
49901
  { stdio: "ignore" }
49843
49902
  );
49844
49903
  }
49845
49904
  } catch (e) {
49846
49905
  try {
49847
- fs2.rmSync(tmpZip, { force: true });
49906
+ fs3.rmSync(tmpZip, { force: true });
49848
49907
  } catch {
49849
49908
  }
49850
49909
  return fail(`Could not zip "${rawDir}": ${e?.message ?? e}`);
49851
49910
  }
49852
- if (!fs2.existsSync(tmpZip))
49911
+ if (!fs3.existsSync(tmpZip))
49853
49912
  return fail(`Nothing to send \u2014 "${rawDir}" produced an empty archive (all files ignored or folder empty).`);
49854
- const size = fs2.statSync(tmpZip).size;
49913
+ const size = fs3.statSync(tmpZip).size;
49855
49914
  if (size > MAX_UPLOAD_BYTES) {
49856
- fs2.rmSync(tmpZip, { force: true });
49915
+ fs3.rmSync(tmpZip, { force: true });
49857
49916
  return fail(
49858
49917
  `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
49918
  );
@@ -49865,7 +49924,7 @@ ${psCmd}
49865
49924
  const res = await fetch(`${hub}/api/files?${q}`, {
49866
49925
  method: "POST",
49867
49926
  headers: { Authorization: `Bearer ${c.token}`, "Content-Type": "application/zip" },
49868
- body: fs2.readFileSync(tmpZip)
49927
+ body: fs3.readFileSync(tmpZip)
49869
49928
  });
49870
49929
  if (!res.ok) {
49871
49930
  const t = await res.text().catch(() => "");
@@ -49875,7 +49934,7 @@ ${psCmd}
49875
49934
  return fail(`Upload failed: ${e?.message ?? e}`);
49876
49935
  } finally {
49877
49936
  try {
49878
- fs2.rmSync(tmpZip, { force: true });
49937
+ fs3.rmSync(tmpZip, { force: true });
49879
49938
  } catch {
49880
49939
  }
49881
49940
  }
@@ -49910,12 +49969,12 @@ ${psCmd}
49910
49969
  const hub = ctx.serverUrl.replace(/\/$/, "");
49911
49970
  const out = savePath || match.fileName || "downloaded.file";
49912
49971
  if (ctx.local) {
49913
- const outPath = out.startsWith("~") ? np.join(os2.homedir(), out.slice(1)) : out;
49972
+ const outPath = out.startsWith("~") ? np2.join(os3.homedir(), out.slice(1)) : out;
49914
49973
  try {
49915
49974
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
49916
49975
  if (!r.ok) throw new Error(`download failed (${r.status})`);
49917
49976
  const buf = Buffer.from(await r.arrayBuffer());
49918
- fs2.writeFileSync(outPath, buf);
49977
+ fs3.writeFileSync(outPath, buf);
49919
49978
  return ok(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
49920
49979
  } catch (e) {
49921
49980
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
@@ -50016,7 +50075,7 @@ ${cmd}`);
50016
50075
  }
50017
50076
  },
50018
50077
  async ({ departmentId, path: path2, kind }) => withClient(async (c) => {
50019
- const fname = np.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50078
+ const fname = np2.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50020
50079
  const mime = mimeOf(fname);
50021
50080
  const q = `name=${encodeURIComponent(fname)}&kind=${kind ?? "file"}`;
50022
50081
  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 +50159,7 @@ ${cmd}`);
50100
50159
  inputSchema: { path: external_exports.string().describe("local file path to upload") }
50101
50160
  },
50102
50161
  async ({ path: path2 }) => withClient(async (c) => {
50103
- const fname = np.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50162
+ const fname = np2.basename(path2.startsWith("~") ? path2.slice(1) : path2);
50104
50163
  const mime = mimeOf(fname);
50105
50164
  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
50165
  return ok(`To add ${fname} to your document memory, run in the terminal:
@@ -50252,7 +50311,7 @@ ${cmd}`);
50252
50311
  if (path2) {
50253
50312
  if (!ctx.local) {
50254
50313
  const hub = ctx.serverUrl.replace(/\/$/, "");
50255
- const fname = np.basename(path2);
50314
+ const fname = np2.basename(path2);
50256
50315
  const fmime = mimeOf(fname);
50257
50316
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
50258
50317
  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 +50323,8 @@ ${cmd}`
50264
50323
  }
50265
50324
  const abs = resolveLocalFile(path2);
50266
50325
  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);
50326
+ const buf = fs3.readFileSync(abs);
50327
+ name = np2.basename(abs);
50269
50328
  mime = mimeOf(name);
50270
50329
  url = `data:${mime};base64,${buf.toString("base64")}`;
50271
50330
  } else if (dataUrl && fileName) {
@@ -50393,6 +50452,87 @@ ${cmd}`
50393
50452
  return ok(`Status set to ${status}.`);
50394
50453
  })
50395
50454
  );
50455
+ if (ctx.local) {
50456
+ server.registerTool(
50457
+ "jefri_notifications",
50458
+ {
50459
+ title: "Notification settings",
50460
+ 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.",
50461
+ inputSchema: {
50462
+ enabled: external_exports.boolean().optional().describe("master switch for all desktop notifications"),
50463
+ sound: external_exports.boolean().optional().describe("play a sound with notifications"),
50464
+ dms: external_exports.boolean().optional().describe("notify for direct messages"),
50465
+ groups: external_exports.boolean().optional().describe("notify for group messages"),
50466
+ onlyMentions: external_exports.boolean().optional().describe("for groups: only notify when someone @-mentions you"),
50467
+ quietFrom: external_exports.string().optional().describe("start of quiet hours, HH:MM 24h (e.g. 22:00), or 'off' to clear"),
50468
+ quietTo: external_exports.string().optional().describe("end of quiet hours, HH:MM 24h (e.g. 08:00)"),
50469
+ muteGroup: external_exports.string().optional().describe("a group's name or id to silence"),
50470
+ unmuteGroup: external_exports.string().optional().describe("a group's name or id to unmute"),
50471
+ muteUser: external_exports.string().optional().describe("a username to silence"),
50472
+ unmuteUser: external_exports.string().optional().describe("a username to unmute")
50473
+ }
50474
+ },
50475
+ async (args) => withClient(async (c) => {
50476
+ const patch = {};
50477
+ for (const k of ["enabled", "sound", "dms", "groups", "onlyMentions"])
50478
+ if (typeof args[k] === "boolean") patch[k] = args[k];
50479
+ if (args.quietFrom === "off" || args.quietTo === "off") {
50480
+ patch.quietFrom = null;
50481
+ patch.quietTo = null;
50482
+ } else {
50483
+ if (args.quietFrom) patch.quietFrom = args.quietFrom;
50484
+ if (args.quietTo) patch.quietTo = args.quietTo;
50485
+ }
50486
+ const cur = getPrefs();
50487
+ const mutedGroups = new Set(cur.mutedGroups);
50488
+ const mutedUsers = new Set(cur.mutedUsers);
50489
+ let groupList = [];
50490
+ const needGroups = args.muteGroup || args.unmuteGroup;
50491
+ if (needGroups) {
50492
+ try {
50493
+ groupList = await c.groups();
50494
+ } catch {
50495
+ }
50496
+ }
50497
+ const resolveGroup = (nameOrId) => {
50498
+ const byId = groupList.find((g) => g.id === nameOrId);
50499
+ if (byId) return byId.id;
50500
+ const byName = groupList.find((g) => g.name.toLowerCase() === nameOrId.toLowerCase());
50501
+ return byName ? byName.id : null;
50502
+ };
50503
+ if (args.muteGroup) {
50504
+ const id = resolveGroup(args.muteGroup);
50505
+ if (!id) return fail(`No group named "${args.muteGroup}".`);
50506
+ mutedGroups.add(id);
50507
+ }
50508
+ if (args.unmuteGroup) {
50509
+ const id = resolveGroup(args.unmuteGroup);
50510
+ if (id) mutedGroups.delete(id);
50511
+ }
50512
+ if (args.muteUser) mutedUsers.add(args.muteUser.replace(/^@/, ""));
50513
+ if (args.unmuteUser) mutedUsers.delete(args.unmuteUser.replace(/^@/, ""));
50514
+ patch.mutedGroups = [...mutedGroups];
50515
+ patch.mutedUsers = [...mutedUsers];
50516
+ const p = setPrefs(patch);
50517
+ const names = {};
50518
+ if (p.mutedGroups.length) {
50519
+ try {
50520
+ for (const g of await c.groups()) names[g.id] = g.name;
50521
+ } catch {
50522
+ }
50523
+ }
50524
+ const summary = [
50525
+ `\u{1F514} Notifications: ${p.enabled ? "ON" : "OFF"}`,
50526
+ ` Sound: ${p.sound ? "on" : "off"}`,
50527
+ ` DMs: ${p.dms ? "on" : "off"} \xB7 Groups: ${p.groups ? "on" : "off"}${p.onlyMentions ? " (only @mentions)" : ""}`,
50528
+ ` Quiet hours: ${p.quietFrom && p.quietTo ? `${p.quietFrom}\u2013${p.quietTo}` : "off"}`,
50529
+ ` Muted groups: ${p.mutedGroups.length ? p.mutedGroups.map((id) => names[id] ?? id).join(", ") : "none"}`,
50530
+ ` Muted people: ${p.mutedUsers.length ? p.mutedUsers.map((u) => "@" + u).join(", ") : "none"}`
50531
+ ].join("\n");
50532
+ return ok(summary);
50533
+ })
50534
+ );
50535
+ }
50396
50536
  }
50397
50537
  function attachInbox(c, inbox, self, onIncoming) {
50398
50538
  const capture = (e) => {
package/dist/index.js CHANGED
@@ -24789,7 +24789,7 @@ var StdioServerTransport = class {
24789
24789
 
24790
24790
  // src/index.ts
24791
24791
  import fs4 from "node:fs";
24792
- import os3 from "node:os";
24792
+ import os4 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
@@ -25281,10 +25281,120 @@ var AcpClient = class _AcpClient {
25281
25281
  };
25282
25282
 
25283
25283
  // 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";
25288
+
25289
+ // src/notify.ts
25290
+ import cp from "node:child_process";
25284
25291
  import fs2 from "node:fs";
25285
25292
  import np from "node:path";
25286
- import cp from "node:child_process";
25287
25293
  import os2 from "node:os";
25294
+ var OFF = process.env.JEFRI_NOTIFY === "0" || process.env.JEFRI_NOTIFY === "false";
25295
+ function findTerminalNotifier() {
25296
+ if (process.platform !== "darwin") return null;
25297
+ for (const p of ["/opt/homebrew/bin/terminal-notifier", "/usr/local/bin/terminal-notifier"]) {
25298
+ try {
25299
+ if (fs2.existsSync(p)) return p;
25300
+ } catch {
25301
+ }
25302
+ }
25303
+ try {
25304
+ const out = (cp.spawnSync("which", ["terminal-notifier"], { encoding: "utf8" }).stdout || "").trim();
25305
+ if (out && fs2.existsSync(out)) return out;
25306
+ } catch {
25307
+ }
25308
+ return null;
25309
+ }
25310
+ var TERMINAL_NOTIFIER = findTerminalNotifier();
25311
+ var macNeedsTerminalNotifier = process.platform === "darwin" && !TERMINAL_NOTIFIER && !OFF;
25312
+ var DEFAULTS = {
25313
+ enabled: true,
25314
+ sound: true,
25315
+ dms: true,
25316
+ groups: true,
25317
+ onlyMentions: false,
25318
+ quietFrom: null,
25319
+ quietTo: null,
25320
+ mutedGroups: [],
25321
+ mutedUsers: []
25322
+ };
25323
+ var PREFS_DIR = np.join(os2.homedir(), ".jefri");
25324
+ var PREFS_FILE = np.join(PREFS_DIR, "notify.json");
25325
+ function loadPrefs() {
25326
+ try {
25327
+ return { ...DEFAULTS, ...JSON.parse(fs2.readFileSync(PREFS_FILE, "utf8")) };
25328
+ } catch {
25329
+ return { ...DEFAULTS };
25330
+ }
25331
+ }
25332
+ var prefs = loadPrefs();
25333
+ function getPrefs() {
25334
+ return { ...prefs };
25335
+ }
25336
+ function setPrefs(patch) {
25337
+ prefs = { ...prefs, ...patch };
25338
+ try {
25339
+ fs2.mkdirSync(PREFS_DIR, { recursive: true });
25340
+ fs2.writeFileSync(PREFS_FILE, JSON.stringify(prefs, null, 2));
25341
+ } catch {
25342
+ }
25343
+ return { ...prefs };
25344
+ }
25345
+ function inQuietHours() {
25346
+ if (!prefs.quietFrom || !prefs.quietTo) return false;
25347
+ const parse3 = (s) => {
25348
+ const [h, m] = s.split(":").map(Number);
25349
+ return (h || 0) * 60 + (m || 0);
25350
+ };
25351
+ const now = /* @__PURE__ */ new Date();
25352
+ const cur = now.getHours() * 60 + now.getMinutes();
25353
+ const from = parse3(prefs.quietFrom), to = parse3(prefs.quietTo);
25354
+ if (from === to) return false;
25355
+ return from < to ? cur >= from && cur < to : cur >= from || cur < to;
25356
+ }
25357
+ var clean = (s) => (s || "").replace(/[\r\n\t]+/g, " ").replace(/[\x00-\x1F]/g, "").slice(0, 140).trim();
25358
+ function notifyIncoming(m) {
25359
+ if (OFF || !prefs.enabled) return;
25360
+ if (m.isGroup ? !prefs.groups : !prefs.dms) return;
25361
+ if (m.isGroup && prefs.onlyMentions && !m.isMention) return;
25362
+ if (m.isGroup && m.groupId && prefs.mutedGroups.includes(m.groupId)) return;
25363
+ if (prefs.mutedUsers.includes(m.sender)) return;
25364
+ if (inQuietHours()) return;
25365
+ const sound = prefs.sound;
25366
+ const title = "Jefri Chat";
25367
+ const body = clean(m.body);
25368
+ if (sound) {
25369
+ try {
25370
+ process.stderr.write("\x07");
25371
+ } catch {
25372
+ }
25373
+ }
25374
+ try {
25375
+ if (process.platform === "darwin") {
25376
+ if (TERMINAL_NOTIFIER) {
25377
+ const args = ["-title", title, "-message", body];
25378
+ if (sound) args.push("-sound", "Ping");
25379
+ cp.spawn(TERMINAL_NOTIFIER, args, { stdio: "ignore", detached: true }).unref();
25380
+ } else {
25381
+ const esc2 = (x) => x.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
25382
+ const snd = sound ? ` sound name "Ping"` : "";
25383
+ const script = `display notification "${esc2(body)}" with title "${esc2(title)}"${snd}`;
25384
+ cp.spawn("osascript", ["-e", script], { stdio: "ignore", detached: true }).unref();
25385
+ }
25386
+ } else if (process.platform === "win32") {
25387
+ const esc2 = (x) => x.replace(/'/g, "''");
25388
+ const ps = `Add-Type -AssemblyName System.Windows.Forms;$n=New-Object System.Windows.Forms.NotifyIcon;$n.Icon=[System.Drawing.SystemIcons]::Information;$n.BalloonTipTitle='${esc2(title)}';$n.BalloonTipText='${esc2(body)}';$n.Visible=$true;$n.ShowBalloonTip(6000);Start-Sleep -Milliseconds 6500;$n.Dispose()`;
25389
+ cp.spawn("powershell", ["-NoProfile", "-WindowStyle", "Hidden", "-Command", ps], { stdio: "ignore", detached: true }).unref();
25390
+ } else {
25391
+ cp.spawn("notify-send", ["-a", "Jefri Chat", title, body], { stdio: "ignore", detached: true }).unref();
25392
+ }
25393
+ } catch {
25394
+ }
25395
+ }
25396
+
25397
+ // src/tools.ts
25288
25398
  var MIME = {
25289
25399
  ".jpg": "image/jpeg",
25290
25400
  ".jpeg": "image/jpeg",
@@ -25304,21 +25414,21 @@ var MIME = {
25304
25414
  ".json": "application/json",
25305
25415
  ".zip": "application/zip"
25306
25416
  };
25307
- var mimeOf = (name) => MIME[np.extname(name).toLowerCase()] ?? "application/octet-stream";
25417
+ var mimeOf = (name) => MIME[np2.extname(name).toLowerCase()] ?? "application/octet-stream";
25308
25418
  function screenshotDir() {
25309
25419
  try {
25310
- const out = cp.execFileSync("defaults", ["read", "com.apple.screencapture", "location"], { encoding: "utf8", timeout: 2e3 }).trim();
25311
- if (out) return out.startsWith("~") ? np.join(os2.homedir(), out.slice(1)) : out;
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;
25312
25422
  } catch {
25313
25423
  }
25314
- const desktop = np.join(os2.homedir(), "Desktop");
25315
- return fs2.existsSync(desktop) ? desktop : null;
25424
+ const desktop = np2.join(os3.homedir(), "Desktop");
25425
+ return fs3.existsSync(desktop) ? desktop : null;
25316
25426
  }
25317
25427
  function resolveLocalFile(rawPath) {
25318
- const expand = (p) => p.startsWith("~") ? np.join(os2.homedir(), p.slice(1)) : p;
25428
+ const expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
25319
25429
  const exists = (p) => {
25320
25430
  try {
25321
- return fs2.existsSync(p) ? p : null;
25431
+ return fs3.existsSync(p) ? p : null;
25322
25432
  } catch {
25323
25433
  return null;
25324
25434
  }
@@ -25331,26 +25441,26 @@ function resolveLocalFile(rawPath) {
25331
25441
  hit = exists(v);
25332
25442
  if (hit) return hit;
25333
25443
  }
25334
- const base = np.basename(abs);
25444
+ const base = np2.basename(abs);
25335
25445
  const looksLikeShot = /screencaptureui/i.test(abs) || /^Screenshot[\s  ]/i.test(base);
25336
25446
  if (looksLikeShot) {
25337
25447
  const dir = screenshotDir();
25338
25448
  if (dir) {
25339
25449
  let files = [];
25340
25450
  try {
25341
- files = fs2.readdirSync(dir);
25451
+ files = fs3.readdirSync(dir);
25342
25452
  } catch {
25343
25453
  }
25344
25454
  const norm = (s) => s.replace(/[    ⁠\s]+/g, " ").trim().toLowerCase();
25345
25455
  const want = norm(base);
25346
25456
  for (const f of files) if (norm(f) === want) {
25347
- hit = exists(np.join(dir, f));
25457
+ hit = exists(np2.join(dir, f));
25348
25458
  if (hit) return hit;
25349
25459
  }
25350
25460
  const shots = files.filter((f) => /^Screenshot[\s  ].*\.(png|jpe?g)$/i.test(f)).map((f) => {
25351
- const p = np.join(dir, f);
25461
+ const p = np2.join(dir, f);
25352
25462
  try {
25353
- return { p, m: fs2.statSync(p).mtimeMs };
25463
+ return { p, m: fs3.statSync(p).mtimeMs };
25354
25464
  } catch {
25355
25465
  return { p, m: 0 };
25356
25466
  }
@@ -25510,8 +25620,8 @@ ${fp}`);
25510
25620
  `I can't find a file at ${path3} on this machine.`
25511
25621
  );
25512
25622
  }
25513
- const buf = fs2.readFileSync(abs);
25514
- name = np.basename(abs);
25623
+ const buf = fs3.readFileSync(abs);
25624
+ name = np2.basename(abs);
25515
25625
  mime = mimeOf(name);
25516
25626
  url = `data:${mime};base64,${buf.toString("base64")}`;
25517
25627
  } else if (dataUrl && fileName) {
@@ -25571,8 +25681,8 @@ ${fp}`);
25571
25681
  `I can't find a file at ${path3} on this machine.`
25572
25682
  );
25573
25683
  }
25574
- const buf = fs2.readFileSync(abs);
25575
- name = np.basename(abs);
25684
+ const buf = fs3.readFileSync(abs);
25685
+ name = np2.basename(abs);
25576
25686
  mime = mimeOf(name);
25577
25687
  url = `data:${mime};base64,${buf.toString("base64")}`;
25578
25688
  } else if (dataUrl && fileName) {
@@ -25604,7 +25714,7 @@ ${fp}`);
25604
25714
  let name, mime, url;
25605
25715
  if (path3) {
25606
25716
  if (!ctx.local) {
25607
- const fname = np.basename(path3);
25717
+ const fname = np2.basename(path3);
25608
25718
  const fmime = mimeOf(fname);
25609
25719
  const hub = ctx.serverUrl.replace(/\/$/, "");
25610
25720
  const web = hub.replace("acp-hub.", "acp-web.").replace(":4000", ":4321");
@@ -25629,8 +25739,8 @@ ${psCmd}
25629
25739
  if (!abs) {
25630
25740
  return fail(`I can't find a file at ${path3} on this machine.`);
25631
25741
  }
25632
- const buf = fs2.readFileSync(abs);
25633
- name = np.basename(abs);
25742
+ const buf = fs3.readFileSync(abs);
25743
+ name = np2.basename(abs);
25634
25744
  mime = mimeOf(name);
25635
25745
  url = `data:${mime};base64,${buf.toString("base64")}`;
25636
25746
  } else if (dataUrl && fileName) {
@@ -25711,57 +25821,57 @@ ${psCmd}
25711
25821
  `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.`
25712
25822
  );
25713
25823
  const rawDir = path3 && path3.trim() ? path3 : process.cwd();
25714
- const dir = rawDir.startsWith("~") ? np.join(os2.homedir(), rawDir.slice(1)) : rawDir;
25715
- if (!fs2.existsSync(dir))
25824
+ const dir = rawDir.startsWith("~") ? np2.join(os3.homedir(), rawDir.slice(1)) : rawDir;
25825
+ if (!fs3.existsSync(dir))
25716
25826
  return fail(
25717
25827
  `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.`
25718
25828
  );
25719
25829
  let stat;
25720
25830
  try {
25721
- stat = fs2.statSync(dir);
25831
+ stat = fs3.statSync(dir);
25722
25832
  } catch {
25723
25833
  return fail(`Cannot read "${rawDir}".`);
25724
25834
  }
25725
25835
  if (!stat.isDirectory())
25726
25836
  return fail(`"${rawDir}" is a file, not a folder \u2014 use jefri_send_file for a single file.`);
25727
25837
  try {
25728
- cp.execFileSync("zip", ["-v"], { stdio: "ignore" });
25838
+ cp2.execFileSync("zip", ["-v"], { stdio: "ignore" });
25729
25839
  } catch {
25730
25840
  return fail(`The 'zip' command isn't available on this machine, and I need it to package the folder.`);
25731
25841
  }
25732
- const base = np.basename(np.resolve(dir)) || "project";
25733
- const tmpZip = np.join(os2.tmpdir(), `acp-${base}-${Date.now()}.zip`);
25842
+ const base = np2.basename(np2.resolve(dir)) || "project";
25843
+ const tmpZip = np2.join(os3.tmpdir(), `acp-${base}-${Date.now()}.zip`);
25734
25844
  let isRepo = false;
25735
25845
  try {
25736
- cp.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
25846
+ cp2.execFileSync("git", ["-C", dir, "rev-parse", "--is-inside-work-tree"], { stdio: "ignore" });
25737
25847
  isRepo = true;
25738
25848
  } catch {
25739
25849
  isRepo = false;
25740
25850
  }
25741
25851
  try {
25742
25852
  if (isRepo) {
25743
- cp.execSync(
25853
+ cp2.execSync(
25744
25854
  `cd ${shq(dir)} && git ls-files --cached --others --exclude-standard -z | xargs -0 zip -q ${shq(tmpZip)} --`,
25745
25855
  { stdio: "ignore" }
25746
25856
  );
25747
25857
  } else {
25748
- cp.execSync(
25858
+ cp2.execSync(
25749
25859
  `cd ${shq(dir)} && zip -qr ${shq(tmpZip)} . -x '*/node_modules/*' '*/.git/*' '*/dist/*' '*/.next/*' '*/build/*' '*/__pycache__/*' '*/.venv/*' '*/venv/*' '*.log' '*/.DS_Store'`,
25750
25860
  { stdio: "ignore" }
25751
25861
  );
25752
25862
  }
25753
25863
  } catch (e) {
25754
25864
  try {
25755
- fs2.rmSync(tmpZip, { force: true });
25865
+ fs3.rmSync(tmpZip, { force: true });
25756
25866
  } catch {
25757
25867
  }
25758
25868
  return fail(`Could not zip "${rawDir}": ${e?.message ?? e}`);
25759
25869
  }
25760
- if (!fs2.existsSync(tmpZip))
25870
+ if (!fs3.existsSync(tmpZip))
25761
25871
  return fail(`Nothing to send \u2014 "${rawDir}" produced an empty archive (all files ignored or folder empty).`);
25762
- const size = fs2.statSync(tmpZip).size;
25872
+ const size = fs3.statSync(tmpZip).size;
25763
25873
  if (size > MAX_UPLOAD_BYTES) {
25764
- fs2.rmSync(tmpZip, { force: true });
25874
+ fs3.rmSync(tmpZip, { force: true });
25765
25875
  return fail(
25766
25876
  `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.`
25767
25877
  );
@@ -25773,7 +25883,7 @@ ${psCmd}
25773
25883
  const res = await fetch(`${hub}/api/files?${q}`, {
25774
25884
  method: "POST",
25775
25885
  headers: { Authorization: `Bearer ${c.token}`, "Content-Type": "application/zip" },
25776
- body: fs2.readFileSync(tmpZip)
25886
+ body: fs3.readFileSync(tmpZip)
25777
25887
  });
25778
25888
  if (!res.ok) {
25779
25889
  const t = await res.text().catch(() => "");
@@ -25783,7 +25893,7 @@ ${psCmd}
25783
25893
  return fail(`Upload failed: ${e?.message ?? e}`);
25784
25894
  } finally {
25785
25895
  try {
25786
- fs2.rmSync(tmpZip, { force: true });
25896
+ fs3.rmSync(tmpZip, { force: true });
25787
25897
  } catch {
25788
25898
  }
25789
25899
  }
@@ -25818,12 +25928,12 @@ ${psCmd}
25818
25928
  const hub = ctx.serverUrl.replace(/\/$/, "");
25819
25929
  const out = savePath || match.fileName || "downloaded.file";
25820
25930
  if (ctx.local) {
25821
- const outPath = out.startsWith("~") ? np.join(os2.homedir(), out.slice(1)) : out;
25931
+ const outPath = out.startsWith("~") ? np2.join(os3.homedir(), out.slice(1)) : out;
25822
25932
  try {
25823
25933
  const r = await fetch(`${hub}/api/files/${match.id}`, { headers: { authorization: `Bearer ${c.token}` } });
25824
25934
  if (!r.ok) throw new Error(`download failed (${r.status})`);
25825
25935
  const buf = Buffer.from(await r.arrayBuffer());
25826
- fs2.writeFileSync(outPath, buf);
25936
+ fs3.writeFileSync(outPath, buf);
25827
25937
  return ok(`\u2705 Saved \u{1F4CE} ${match.fileName} from ${where} to ${outPath} (${MB(buf.length)} MB).`);
25828
25938
  } catch (e) {
25829
25939
  return fail(`Couldn't save the file: ${e?.message ?? e}`);
@@ -25924,7 +26034,7 @@ ${cmd}`);
25924
26034
  }
25925
26035
  },
25926
26036
  async ({ departmentId, path: path3, kind }) => withClient(async (c) => {
25927
- const fname = np.basename(path3.startsWith("~") ? path3.slice(1) : path3);
26037
+ const fname = np2.basename(path3.startsWith("~") ? path3.slice(1) : path3);
25928
26038
  const mime = mimeOf(fname);
25929
26039
  const q = `name=${encodeURIComponent(fname)}&kind=${kind ?? "file"}`;
25930
26040
  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)}`;
@@ -26008,7 +26118,7 @@ ${cmd}`);
26008
26118
  inputSchema: { path: external_exports.string().describe("local file path to upload") }
26009
26119
  },
26010
26120
  async ({ path: path3 }) => withClient(async (c) => {
26011
- const fname = np.basename(path3.startsWith("~") ? path3.slice(1) : path3);
26121
+ const fname = np2.basename(path3.startsWith("~") ? path3.slice(1) : path3);
26012
26122
  const mime = mimeOf(fname);
26013
26123
  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)}`;
26014
26124
  return ok(`To add ${fname} to your document memory, run in the terminal:
@@ -26160,7 +26270,7 @@ ${cmd}`);
26160
26270
  if (path3) {
26161
26271
  if (!ctx.local) {
26162
26272
  const hub = ctx.serverUrl.replace(/\/$/, "");
26163
- const fname = np.basename(path3);
26273
+ const fname = np2.basename(path3);
26164
26274
  const fmime = mimeOf(fname);
26165
26275
  const q = `groupId=${encodeURIComponent(groupId)}&fileName=${encodeURIComponent(fname)}` + (caption ? `&caption=${encodeURIComponent(caption)}` : "");
26166
26276
  const cmd = `curl -s -X POST ${shq(`${hub}/api/files?${q}`)} -H "Authorization: Bearer ${c.token}" -H "Content-Type: ${fmime}" --data-binary @${shq(path3)}`;
@@ -26172,8 +26282,8 @@ ${cmd}`
26172
26282
  }
26173
26283
  const abs = resolveLocalFile(path3);
26174
26284
  if (!abs) return fail(`I can't find a file at ${path3} on this machine.`);
26175
- const buf = fs2.readFileSync(abs);
26176
- name = np.basename(abs);
26285
+ const buf = fs3.readFileSync(abs);
26286
+ name = np2.basename(abs);
26177
26287
  mime = mimeOf(name);
26178
26288
  url = `data:${mime};base64,${buf.toString("base64")}`;
26179
26289
  } else if (dataUrl && fileName) {
@@ -26301,6 +26411,87 @@ ${cmd}`
26301
26411
  return ok(`Status set to ${status}.`);
26302
26412
  })
26303
26413
  );
26414
+ if (ctx.local) {
26415
+ server2.registerTool(
26416
+ "jefri_notifications",
26417
+ {
26418
+ title: "Notification settings",
26419
+ 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.",
26420
+ inputSchema: {
26421
+ enabled: external_exports.boolean().optional().describe("master switch for all desktop notifications"),
26422
+ sound: external_exports.boolean().optional().describe("play a sound with notifications"),
26423
+ dms: external_exports.boolean().optional().describe("notify for direct messages"),
26424
+ groups: external_exports.boolean().optional().describe("notify for group messages"),
26425
+ onlyMentions: external_exports.boolean().optional().describe("for groups: only notify when someone @-mentions you"),
26426
+ quietFrom: external_exports.string().optional().describe("start of quiet hours, HH:MM 24h (e.g. 22:00), or 'off' to clear"),
26427
+ quietTo: external_exports.string().optional().describe("end of quiet hours, HH:MM 24h (e.g. 08:00)"),
26428
+ muteGroup: external_exports.string().optional().describe("a group's name or id to silence"),
26429
+ unmuteGroup: external_exports.string().optional().describe("a group's name or id to unmute"),
26430
+ muteUser: external_exports.string().optional().describe("a username to silence"),
26431
+ unmuteUser: external_exports.string().optional().describe("a username to unmute")
26432
+ }
26433
+ },
26434
+ async (args) => withClient(async (c) => {
26435
+ const patch = {};
26436
+ for (const k of ["enabled", "sound", "dms", "groups", "onlyMentions"])
26437
+ if (typeof args[k] === "boolean") patch[k] = args[k];
26438
+ if (args.quietFrom === "off" || args.quietTo === "off") {
26439
+ patch.quietFrom = null;
26440
+ patch.quietTo = null;
26441
+ } else {
26442
+ if (args.quietFrom) patch.quietFrom = args.quietFrom;
26443
+ if (args.quietTo) patch.quietTo = args.quietTo;
26444
+ }
26445
+ const cur = getPrefs();
26446
+ const mutedGroups = new Set(cur.mutedGroups);
26447
+ const mutedUsers = new Set(cur.mutedUsers);
26448
+ let groupList = [];
26449
+ const needGroups = args.muteGroup || args.unmuteGroup;
26450
+ if (needGroups) {
26451
+ try {
26452
+ groupList = await c.groups();
26453
+ } catch {
26454
+ }
26455
+ }
26456
+ const resolveGroup = (nameOrId) => {
26457
+ const byId = groupList.find((g) => g.id === nameOrId);
26458
+ if (byId) return byId.id;
26459
+ const byName = groupList.find((g) => g.name.toLowerCase() === nameOrId.toLowerCase());
26460
+ return byName ? byName.id : null;
26461
+ };
26462
+ if (args.muteGroup) {
26463
+ const id = resolveGroup(args.muteGroup);
26464
+ if (!id) return fail(`No group named "${args.muteGroup}".`);
26465
+ mutedGroups.add(id);
26466
+ }
26467
+ if (args.unmuteGroup) {
26468
+ const id = resolveGroup(args.unmuteGroup);
26469
+ if (id) mutedGroups.delete(id);
26470
+ }
26471
+ if (args.muteUser) mutedUsers.add(args.muteUser.replace(/^@/, ""));
26472
+ if (args.unmuteUser) mutedUsers.delete(args.unmuteUser.replace(/^@/, ""));
26473
+ patch.mutedGroups = [...mutedGroups];
26474
+ patch.mutedUsers = [...mutedUsers];
26475
+ const p = setPrefs(patch);
26476
+ const names = {};
26477
+ if (p.mutedGroups.length) {
26478
+ try {
26479
+ for (const g of await c.groups()) names[g.id] = g.name;
26480
+ } catch {
26481
+ }
26482
+ }
26483
+ const summary = [
26484
+ `\u{1F514} Notifications: ${p.enabled ? "ON" : "OFF"}`,
26485
+ ` Sound: ${p.sound ? "on" : "off"}`,
26486
+ ` DMs: ${p.dms ? "on" : "off"} \xB7 Groups: ${p.groups ? "on" : "off"}${p.onlyMentions ? " (only @mentions)" : ""}`,
26487
+ ` Quiet hours: ${p.quietFrom && p.quietTo ? `${p.quietFrom}\u2013${p.quietTo}` : "off"}`,
26488
+ ` Muted groups: ${p.mutedGroups.length ? p.mutedGroups.map((id) => names[id] ?? id).join(", ") : "none"}`,
26489
+ ` Muted people: ${p.mutedUsers.length ? p.mutedUsers.map((u) => "@" + u).join(", ") : "none"}`
26490
+ ].join("\n");
26491
+ return ok(summary);
26492
+ })
26493
+ );
26494
+ }
26304
26495
  }
26305
26496
  function attachInbox(c, inbox2, self, onIncoming) {
26306
26497
  const capture = (e) => {
@@ -26317,56 +26508,6 @@ function attachInbox(c, inbox2, self, onIncoming) {
26317
26508
  c.on("file_received", capture);
26318
26509
  }
26319
26510
 
26320
- // src/notify.ts
26321
- import cp2 from "node:child_process";
26322
- import fs3 from "node:fs";
26323
- var OFF = process.env.JEFRI_NOTIFY === "0" || process.env.JEFRI_NOTIFY === "false";
26324
- function findTerminalNotifier() {
26325
- if (process.platform !== "darwin") return null;
26326
- for (const p of ["/opt/homebrew/bin/terminal-notifier", "/usr/local/bin/terminal-notifier"]) {
26327
- try {
26328
- if (fs3.existsSync(p)) return p;
26329
- } catch {
26330
- }
26331
- }
26332
- try {
26333
- const out = (cp2.spawnSync("which", ["terminal-notifier"], { encoding: "utf8" }).stdout || "").trim();
26334
- if (out && fs3.existsSync(out)) return out;
26335
- } catch {
26336
- }
26337
- return null;
26338
- }
26339
- var TERMINAL_NOTIFIER = findTerminalNotifier();
26340
- var macNeedsTerminalNotifier = process.platform === "darwin" && !TERMINAL_NOTIFIER && !OFF;
26341
- var clean = (s) => (s || "").replace(/[\r\n\t]+/g, " ").replace(/[\x00-\x1F]/g, "").slice(0, 140).trim();
26342
- function notifyIncoming(title, body) {
26343
- if (OFF) return;
26344
- const t = clean(title) || "Jefri Chat";
26345
- const b = clean(body);
26346
- try {
26347
- process.stderr.write("\x07");
26348
- } catch {
26349
- }
26350
- try {
26351
- if (process.platform === "darwin") {
26352
- if (TERMINAL_NOTIFIER) {
26353
- cp2.spawn(TERMINAL_NOTIFIER, ["-title", t, "-message", b, "-sound", "Ping"], { stdio: "ignore", detached: true }).unref();
26354
- } else {
26355
- const esc2 = (x) => x.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
26356
- const script = `display notification "${esc2(b)}" with title "${esc2(t)}" sound name "Ping"`;
26357
- cp2.spawn("osascript", ["-e", script], { stdio: "ignore", detached: true }).unref();
26358
- }
26359
- } else if (process.platform === "win32") {
26360
- const esc2 = (x) => x.replace(/'/g, "''");
26361
- const ps = `Add-Type -AssemblyName System.Windows.Forms;$n=New-Object System.Windows.Forms.NotifyIcon;$n.Icon=[System.Drawing.SystemIcons]::Information;$n.BalloonTipTitle='${esc2(t)}';$n.BalloonTipText='${esc2(b)}';$n.Visible=$true;$n.ShowBalloonTip(6000);Start-Sleep -Milliseconds 6500;$n.Dispose()`;
26362
- cp2.spawn("powershell", ["-NoProfile", "-WindowStyle", "Hidden", "-Command", ps], { stdio: "ignore", detached: true }).unref();
26363
- } else {
26364
- cp2.spawn("notify-send", ["-a", "Jefri Chat", t, b], { stdio: "ignore", detached: true }).unref();
26365
- }
26366
- } catch {
26367
- }
26368
- }
26369
-
26370
26511
  // src/index.ts
26371
26512
  var SERVER = process.env.ACP_SERVER ?? "http://localhost:4000";
26372
26513
  var TOKEN = process.env.ACP_TOKEN;
@@ -26375,7 +26516,7 @@ var NAME = process.env.ACP_NAME ?? USERNAME;
26375
26516
  var TAGS = (process.env.ACP_TAGS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
26376
26517
  var CAPS = (process.env.ACP_CAPABILITIES ?? "coding").split(",").map((s) => s.trim()).filter(Boolean);
26377
26518
  var log = (...a) => console.error("[jefrichat-mcp]", ...a);
26378
- var TOKEN_CACHE = path2.join(os3.homedir(), ".acp", "tokens.json");
26519
+ var TOKEN_CACHE = path2.join(os4.homedir(), ".acp", "tokens.json");
26379
26520
  var cacheKey = `${SERVER}::${USERNAME}`;
26380
26521
  function readTokenCache() {
26381
26522
  try {
@@ -26432,8 +26573,16 @@ function ensureClient() {
26432
26573
  log("tip: for reliable desktop notifications on macOS, run: brew install terminal-notifier");
26433
26574
  attachInbox(c, inbox, self, (m) => {
26434
26575
  const preview = m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : m.content ?? "";
26435
- const where = m.groupId ? ` \xB7 ${groupNames.get(m.groupId) ?? "group"}` : "";
26436
- notifyIncoming("Jefri Chat", `${m.senderUsername}${where}: ${preview}`);
26576
+ const isGroup = !!m.groupId;
26577
+ const gname = isGroup ? groupNames.get(m.groupId) ?? "group" : "";
26578
+ const isMention = isGroup && typeof m.content === "string" && new RegExp(`@${self}\\b`, "i").test(m.content);
26579
+ notifyIncoming({
26580
+ body: `${m.senderUsername}${isGroup ? ` \xB7 ${gname}` : ""}: ${preview}`,
26581
+ isGroup,
26582
+ groupId: m.groupId,
26583
+ sender: m.senderUsername,
26584
+ isMention
26585
+ });
26437
26586
  });
26438
26587
  log(`connected to ${SERVER} as ${self}`);
26439
26588
  return c;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.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": {