jefrichat-mcp 0.4.0 → 0.6.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 +84 -16
  2. package/dist/index.js +125 -17
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -49397,6 +49397,61 @@ var MIME = {
49397
49397
  ".zip": "application/zip"
49398
49398
  };
49399
49399
  var mimeOf = (name) => MIME[np.extname(name).toLowerCase()] ?? "application/octet-stream";
49400
+ function screenshotDir() {
49401
+ 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;
49404
+ } catch {
49405
+ }
49406
+ const desktop = np.join(os2.homedir(), "Desktop");
49407
+ return fs2.existsSync(desktop) ? desktop : null;
49408
+ }
49409
+ function resolveLocalFile(rawPath) {
49410
+ const expand = (p) => p.startsWith("~") ? np.join(os2.homedir(), p.slice(1)) : p;
49411
+ const exists = (p) => {
49412
+ try {
49413
+ return fs2.existsSync(p) ? p : null;
49414
+ } catch {
49415
+ return null;
49416
+ }
49417
+ };
49418
+ const abs = expand(rawPath);
49419
+ let hit = exists(abs);
49420
+ if (hit) return hit;
49421
+ const UNI = /[    ⁠]/g;
49422
+ for (const v of [abs.replace(/ /g, "\u202F"), abs.replace(UNI, " "), abs.replace(/ (AM|PM)(\.[a-z0-9]+)?$/i, "\u202F$1$2")]) {
49423
+ hit = exists(v);
49424
+ if (hit) return hit;
49425
+ }
49426
+ const base = np.basename(abs);
49427
+ const looksLikeShot = /screencaptureui/i.test(abs) || /^Screenshot[\s  ]/i.test(base);
49428
+ if (looksLikeShot) {
49429
+ const dir = screenshotDir();
49430
+ if (dir) {
49431
+ let files = [];
49432
+ try {
49433
+ files = fs2.readdirSync(dir);
49434
+ } catch {
49435
+ }
49436
+ const norm = (s) => s.replace(/[    ⁠\s]+/g, " ").trim().toLowerCase();
49437
+ const want = norm(base);
49438
+ for (const f of files) if (norm(f) === want) {
49439
+ hit = exists(np.join(dir, f));
49440
+ if (hit) return hit;
49441
+ }
49442
+ const shots = files.filter((f) => /^Screenshot[\s  ].*\.(png|jpe?g)$/i.test(f)).map((f) => {
49443
+ const p = np.join(dir, f);
49444
+ try {
49445
+ return { p, m: fs2.statSync(p).mtimeMs };
49446
+ } catch {
49447
+ return { p, m: 0 };
49448
+ }
49449
+ }).filter((x) => x.m > 0).sort((a, b) => b.m - a.m);
49450
+ if (shots.length && Date.now() - shots[0].m < 5 * 60 * 1e3) return shots[0].p;
49451
+ }
49452
+ }
49453
+ return null;
49454
+ }
49400
49455
  var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
49401
49456
  var MB = (n) => (n / 1024 / 1024).toFixed(1);
49402
49457
  var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
@@ -49541,8 +49596,8 @@ ${fp}`);
49541
49596
  `I can't read local files from the cloud connector. Use a local/stdin Jefri Chat connector, or open the web chat and drop the file there.`
49542
49597
  );
49543
49598
  }
49544
- const abs = path2.startsWith("~") ? np.join(os2.homedir(), path2.slice(1)) : path2;
49545
- if (!fs2.existsSync(abs)) {
49599
+ const abs = resolveLocalFile(path2);
49600
+ if (!abs) {
49546
49601
  return fail(
49547
49602
  `I can't find a file at ${path2} on this machine.`
49548
49603
  );
@@ -49602,8 +49657,8 @@ ${fp}`);
49602
49657
  `I can't read local files from the cloud connector. Use a local/stdin Jefri Chat connector, or open the web chat and drop the file there.`
49603
49658
  );
49604
49659
  }
49605
- const abs = path2.startsWith("~") ? np.join(os2.homedir(), path2.slice(1)) : path2;
49606
- if (!fs2.existsSync(abs)) {
49660
+ const abs = resolveLocalFile(path2);
49661
+ if (!abs) {
49607
49662
  return fail(
49608
49663
  `I can't find a file at ${path2} on this machine.`
49609
49664
  );
@@ -49662,8 +49717,8 @@ ${psCmd}
49662
49717
  (Alternatively the user can drag "${fname}" straight into the chat: ${dropLink})`
49663
49718
  );
49664
49719
  }
49665
- const abs = path2.startsWith("~") ? np.join(os2.homedir(), path2.slice(1)) : path2;
49666
- if (!fs2.existsSync(abs)) {
49720
+ const abs = resolveLocalFile(path2);
49721
+ if (!abs) {
49667
49722
  return fail(`I can't find a file at ${path2} on this machine.`);
49668
49723
  }
49669
49724
  const buf = fs2.readFileSync(abs);
@@ -50207,8 +50262,8 @@ ${cmd}`);
50207
50262
  ${cmd}`
50208
50263
  );
50209
50264
  }
50210
- const abs = path2.startsWith("~") ? np.join(os2.homedir(), path2.slice(1)) : path2;
50211
- if (!fs2.existsSync(abs)) return fail(`I can't find a file at ${path2} on this machine.`);
50265
+ const abs = resolveLocalFile(path2);
50266
+ if (!abs) return fail(`I can't find a file at ${path2} on this machine.`);
50212
50267
  const buf = fs2.readFileSync(abs);
50213
50268
  name = np.basename(abs);
50214
50269
  mime = mimeOf(name);
@@ -50243,19 +50298,22 @@ ${cmd}`
50243
50298
  "jefri_history",
50244
50299
  {
50245
50300
  title: "Read conversation history",
50246
- description: "Read the recent message history of your DM thread with a given user.",
50301
+ description: "Read the recent message history of a conversation \u2014 either a DM (pass `with` = a username) or a GROUP (pass `groupId`, from jefri_groups). Shows past messages AND any files/images (with how to download them).",
50247
50302
  inputSchema: {
50248
- with: external_exports.string().describe("the other person's username"),
50303
+ with: external_exports.string().optional().describe("the other person's username (for a DM)"),
50304
+ groupId: external_exports.string().optional().describe("a group's id (for group history) \u2014 from jefri_groups"),
50249
50305
  limit: external_exports.number().optional().describe("max messages to return (default 20)")
50250
50306
  }
50251
50307
  },
50252
- async ({ with: other, limit }) => withClient(async (c) => {
50253
- const convId = dmConversationId(c.identity.username, other);
50308
+ async ({ with: other, groupId, limit }) => withClient(async (c) => {
50309
+ if (!other && !groupId) return fail("Give `with` (a username, for a DM) or `groupId` (for a group).");
50310
+ const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, other);
50311
+ const label = groupId ? "this group" : `@${other}`;
50254
50312
  const p = waitFor(c, "history", (e) => e.conversationId === convId);
50255
50313
  c.history(convId);
50256
50314
  const res = await p;
50257
50315
  const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
50258
- if (!msgs.length) return ok(`No messages yet with @${other}.`);
50316
+ if (!msgs.length) return ok(`No messages yet in ${label}.`);
50259
50317
  const lines = await Promise.all(msgs.map(async (m) => {
50260
50318
  if (m.encryptionMode === "private_e2e" && m.encryptedPayload) {
50261
50319
  try {
@@ -50265,7 +50323,11 @@ ${cmd}`
50265
50323
  return `${m.senderUsername}: \u{1F512} Private message unavailable on this device`;
50266
50324
  }
50267
50325
  }
50268
- return `${m.senderUsername}: ${m.kind === "file" ? `\u{1F4CE} ${m.fileName}` : m.content}`;
50326
+ if (m.kind === "file") {
50327
+ const dl = groupId ? `jefri_download_file(groupId: "${groupId}", fileName: "${m.fileName}")` : `jefri_download_file(from: "${m.senderUsername}", fileName: "${m.fileName}")`;
50328
+ return `${m.senderUsername}: \u{1F4CE} ${m.fileName} \u2014 to save it call ${dl}`;
50329
+ }
50330
+ return `${m.senderUsername}: ${m.content}`;
50269
50331
  }));
50270
50332
  return ok(lines.join("\n"));
50271
50333
  })
@@ -50332,10 +50394,16 @@ ${cmd}`
50332
50394
  })
50333
50395
  );
50334
50396
  }
50335
- function attachInbox(c, inbox, self) {
50397
+ function attachInbox(c, inbox, self, onIncoming) {
50336
50398
  const capture = (e) => {
50337
50399
  const m = e.message;
50338
- if (m.senderUsername !== self) inbox.push(m);
50400
+ if (m.senderUsername !== self) {
50401
+ inbox.push(m);
50402
+ try {
50403
+ onIncoming?.(m);
50404
+ } catch {
50405
+ }
50406
+ }
50339
50407
  };
50340
50408
  c.on("message_received", capture);
50341
50409
  c.on("file_received", capture);
package/dist/index.js CHANGED
@@ -25305,6 +25305,61 @@ var MIME = {
25305
25305
  ".zip": "application/zip"
25306
25306
  };
25307
25307
  var mimeOf = (name) => MIME[np.extname(name).toLowerCase()] ?? "application/octet-stream";
25308
+ function screenshotDir() {
25309
+ 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;
25312
+ } catch {
25313
+ }
25314
+ const desktop = np.join(os2.homedir(), "Desktop");
25315
+ return fs2.existsSync(desktop) ? desktop : null;
25316
+ }
25317
+ function resolveLocalFile(rawPath) {
25318
+ const expand = (p) => p.startsWith("~") ? np.join(os2.homedir(), p.slice(1)) : p;
25319
+ const exists = (p) => {
25320
+ try {
25321
+ return fs2.existsSync(p) ? p : null;
25322
+ } catch {
25323
+ return null;
25324
+ }
25325
+ };
25326
+ const abs = expand(rawPath);
25327
+ let hit = exists(abs);
25328
+ if (hit) return hit;
25329
+ const UNI = /[    ⁠]/g;
25330
+ for (const v of [abs.replace(/ /g, "\u202F"), abs.replace(UNI, " "), abs.replace(/ (AM|PM)(\.[a-z0-9]+)?$/i, "\u202F$1$2")]) {
25331
+ hit = exists(v);
25332
+ if (hit) return hit;
25333
+ }
25334
+ const base = np.basename(abs);
25335
+ const looksLikeShot = /screencaptureui/i.test(abs) || /^Screenshot[\s  ]/i.test(base);
25336
+ if (looksLikeShot) {
25337
+ const dir = screenshotDir();
25338
+ if (dir) {
25339
+ let files = [];
25340
+ try {
25341
+ files = fs2.readdirSync(dir);
25342
+ } catch {
25343
+ }
25344
+ const norm = (s) => s.replace(/[    ⁠\s]+/g, " ").trim().toLowerCase();
25345
+ const want = norm(base);
25346
+ for (const f of files) if (norm(f) === want) {
25347
+ hit = exists(np.join(dir, f));
25348
+ if (hit) return hit;
25349
+ }
25350
+ const shots = files.filter((f) => /^Screenshot[\s  ].*\.(png|jpe?g)$/i.test(f)).map((f) => {
25351
+ const p = np.join(dir, f);
25352
+ try {
25353
+ return { p, m: fs2.statSync(p).mtimeMs };
25354
+ } catch {
25355
+ return { p, m: 0 };
25356
+ }
25357
+ }).filter((x) => x.m > 0).sort((a, b) => b.m - a.m);
25358
+ if (shots.length && Date.now() - shots[0].m < 5 * 60 * 1e3) return shots[0].p;
25359
+ }
25360
+ }
25361
+ return null;
25362
+ }
25308
25363
  var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
25309
25364
  var MB = (n) => (n / 1024 / 1024).toFixed(1);
25310
25365
  var WS_SAFE_FILE_BYTES = 45 * 1024 * 1024;
@@ -25449,8 +25504,8 @@ ${fp}`);
25449
25504
  `I can't read local files from the cloud connector. Use a local/stdin Jefri Chat connector, or open the web chat and drop the file there.`
25450
25505
  );
25451
25506
  }
25452
- const abs = path3.startsWith("~") ? np.join(os2.homedir(), path3.slice(1)) : path3;
25453
- if (!fs2.existsSync(abs)) {
25507
+ const abs = resolveLocalFile(path3);
25508
+ if (!abs) {
25454
25509
  return fail(
25455
25510
  `I can't find a file at ${path3} on this machine.`
25456
25511
  );
@@ -25510,8 +25565,8 @@ ${fp}`);
25510
25565
  `I can't read local files from the cloud connector. Use a local/stdin Jefri Chat connector, or open the web chat and drop the file there.`
25511
25566
  );
25512
25567
  }
25513
- const abs = path3.startsWith("~") ? np.join(os2.homedir(), path3.slice(1)) : path3;
25514
- if (!fs2.existsSync(abs)) {
25568
+ const abs = resolveLocalFile(path3);
25569
+ if (!abs) {
25515
25570
  return fail(
25516
25571
  `I can't find a file at ${path3} on this machine.`
25517
25572
  );
@@ -25570,8 +25625,8 @@ ${psCmd}
25570
25625
  (Alternatively the user can drag "${fname}" straight into the chat: ${dropLink})`
25571
25626
  );
25572
25627
  }
25573
- const abs = path3.startsWith("~") ? np.join(os2.homedir(), path3.slice(1)) : path3;
25574
- if (!fs2.existsSync(abs)) {
25628
+ const abs = resolveLocalFile(path3);
25629
+ if (!abs) {
25575
25630
  return fail(`I can't find a file at ${path3} on this machine.`);
25576
25631
  }
25577
25632
  const buf = fs2.readFileSync(abs);
@@ -26115,8 +26170,8 @@ ${cmd}`);
26115
26170
  ${cmd}`
26116
26171
  );
26117
26172
  }
26118
- const abs = path3.startsWith("~") ? np.join(os2.homedir(), path3.slice(1)) : path3;
26119
- if (!fs2.existsSync(abs)) return fail(`I can't find a file at ${path3} on this machine.`);
26173
+ const abs = resolveLocalFile(path3);
26174
+ if (!abs) return fail(`I can't find a file at ${path3} on this machine.`);
26120
26175
  const buf = fs2.readFileSync(abs);
26121
26176
  name = np.basename(abs);
26122
26177
  mime = mimeOf(name);
@@ -26151,19 +26206,22 @@ ${cmd}`
26151
26206
  "jefri_history",
26152
26207
  {
26153
26208
  title: "Read conversation history",
26154
- description: "Read the recent message history of your DM thread with a given user.",
26209
+ description: "Read the recent message history of a conversation \u2014 either a DM (pass `with` = a username) or a GROUP (pass `groupId`, from jefri_groups). Shows past messages AND any files/images (with how to download them).",
26155
26210
  inputSchema: {
26156
- with: external_exports.string().describe("the other person's username"),
26211
+ with: external_exports.string().optional().describe("the other person's username (for a DM)"),
26212
+ groupId: external_exports.string().optional().describe("a group's id (for group history) \u2014 from jefri_groups"),
26157
26213
  limit: external_exports.number().optional().describe("max messages to return (default 20)")
26158
26214
  }
26159
26215
  },
26160
- async ({ with: other, limit }) => withClient(async (c) => {
26161
- const convId = dmConversationId(c.identity.username, other);
26216
+ async ({ with: other, groupId, limit }) => withClient(async (c) => {
26217
+ if (!other && !groupId) return fail("Give `with` (a username, for a DM) or `groupId` (for a group).");
26218
+ const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, other);
26219
+ const label = groupId ? "this group" : `@${other}`;
26162
26220
  const p = waitFor(c, "history", (e) => e.conversationId === convId);
26163
26221
  c.history(convId);
26164
26222
  const res = await p;
26165
26223
  const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
26166
- if (!msgs.length) return ok(`No messages yet with @${other}.`);
26224
+ if (!msgs.length) return ok(`No messages yet in ${label}.`);
26167
26225
  const lines = await Promise.all(msgs.map(async (m) => {
26168
26226
  if (m.encryptionMode === "private_e2e" && m.encryptedPayload) {
26169
26227
  try {
@@ -26173,7 +26231,11 @@ ${cmd}`
26173
26231
  return `${m.senderUsername}: \u{1F512} Private message unavailable on this device`;
26174
26232
  }
26175
26233
  }
26176
- return `${m.senderUsername}: ${m.kind === "file" ? `\u{1F4CE} ${m.fileName}` : m.content}`;
26234
+ if (m.kind === "file") {
26235
+ const dl = groupId ? `jefri_download_file(groupId: "${groupId}", fileName: "${m.fileName}")` : `jefri_download_file(from: "${m.senderUsername}", fileName: "${m.fileName}")`;
26236
+ return `${m.senderUsername}: \u{1F4CE} ${m.fileName} \u2014 to save it call ${dl}`;
26237
+ }
26238
+ return `${m.senderUsername}: ${m.content}`;
26177
26239
  }));
26178
26240
  return ok(lines.join("\n"));
26179
26241
  })
@@ -26240,15 +26302,49 @@ ${cmd}`
26240
26302
  })
26241
26303
  );
26242
26304
  }
26243
- function attachInbox(c, inbox2, self) {
26305
+ function attachInbox(c, inbox2, self, onIncoming) {
26244
26306
  const capture = (e) => {
26245
26307
  const m = e.message;
26246
- if (m.senderUsername !== self) inbox2.push(m);
26308
+ if (m.senderUsername !== self) {
26309
+ inbox2.push(m);
26310
+ try {
26311
+ onIncoming?.(m);
26312
+ } catch {
26313
+ }
26314
+ }
26247
26315
  };
26248
26316
  c.on("message_received", capture);
26249
26317
  c.on("file_received", capture);
26250
26318
  }
26251
26319
 
26320
+ // src/notify.ts
26321
+ import cp2 from "node:child_process";
26322
+ var OFF = process.env.JEFRI_NOTIFY === "0" || process.env.JEFRI_NOTIFY === "false";
26323
+ var clean = (s) => (s || "").replace(/[\r\n\t]+/g, " ").replace(/[\x00-\x1F]/g, "").slice(0, 140).trim();
26324
+ function notifyIncoming(title, body) {
26325
+ if (OFF) return;
26326
+ const t = clean(title) || "Jefri Chat";
26327
+ const b = clean(body);
26328
+ try {
26329
+ process.stderr.write("\x07");
26330
+ } catch {
26331
+ }
26332
+ try {
26333
+ if (process.platform === "darwin") {
26334
+ const esc2 = (x) => x.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
26335
+ const script = `display notification "${esc2(b)}" with title "${esc2(t)}" sound name "Ping"`;
26336
+ cp2.spawn("osascript", ["-e", script], { stdio: "ignore", detached: true }).unref();
26337
+ } else if (process.platform === "win32") {
26338
+ const esc2 = (x) => x.replace(/'/g, "''");
26339
+ 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()`;
26340
+ cp2.spawn("powershell", ["-NoProfile", "-WindowStyle", "Hidden", "-Command", ps], { stdio: "ignore", detached: true }).unref();
26341
+ } else {
26342
+ cp2.spawn("notify-send", ["-a", "Jefri Chat", t, b], { stdio: "ignore", detached: true }).unref();
26343
+ }
26344
+ } catch {
26345
+ }
26346
+ }
26347
+
26252
26348
  // src/index.ts
26253
26349
  var SERVER = process.env.ACP_SERVER ?? "http://localhost:4000";
26254
26350
  var TOKEN = process.env.ACP_TOKEN;
@@ -26302,7 +26398,19 @@ function ensureClient() {
26302
26398
  if (c.identity) cacheToken(c.token);
26303
26399
  }
26304
26400
  const self = c.identity?.username ?? USERNAME;
26305
- attachInbox(c, inbox, self);
26401
+ const groupNames = /* @__PURE__ */ new Map();
26402
+ const refreshGroups = () => c.groups().then((gs) => {
26403
+ groupNames.clear();
26404
+ for (const g of gs) groupNames.set(g.id, g.name);
26405
+ }).catch(() => {
26406
+ });
26407
+ void refreshGroups();
26408
+ setInterval(refreshGroups, 6e4).unref?.();
26409
+ attachInbox(c, inbox, self, (m) => {
26410
+ const preview = m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : m.content ?? "";
26411
+ const where = m.groupId ? ` \xB7 ${groupNames.get(m.groupId) ?? "group"}` : "";
26412
+ notifyIncoming("Jefri Chat", `${m.senderUsername}${where}: ${preview}`);
26413
+ });
26306
26414
  log(`connected to ${SERVER} as ${self}`);
26307
26415
  return c;
26308
26416
  })().catch((e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.6.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": {