jefrichat-mcp 0.9.0 → 0.13.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 +56 -16
  2. package/dist/index.js +362 -126
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -49471,16 +49471,21 @@ function setPrefs(patch) {
49471
49471
  import fs3 from "node:fs";
49472
49472
  import np2 from "node:path";
49473
49473
  import os3 from "node:os";
49474
+ var DEFAULT_TEMPLATE = '{persona}You are @{me}, an autonomous agent on Jefri Chat. A message came in{where}. It may address several agents by @name (e.g. "@alice: do X @bob: do Y") \u2014 do ONLY the part addressed to you (@{me}); ignore parts meant for other agents. If nothing is addressed to you, reply with nothing to do. You can do ANYTHING (research, writing, analysis, code, running tools) \u2014 not just code. If a CLAUDE.md or relevant files are in this folder, read them first to get oriented. If the request is unclear, reply with ONE short clarifying question and stop (only your owner will answer). Otherwise do your part and reply concisely with the result.\n\n{context}Latest message from @{sender}: {message}';
49474
49475
  var DEFAULTS2 = {
49475
49476
  enabled: false,
49476
49477
  brain: "claude",
49477
49478
  workdir: process.cwd(),
49478
49479
  replyMode: "mentions",
49479
49480
  persona: "",
49480
- replyToBots: false
49481
+ replyToBots: false,
49482
+ ownerOnly: false,
49483
+ contextMessages: 12,
49484
+ promptTemplate: DEFAULT_TEMPLATE
49481
49485
  };
49482
49486
  var DIR = np2.join(os3.homedir(), ".jefri");
49483
49487
  var FILE = np2.join(DIR, "autonomous.json");
49488
+ var LOG_FILE = np2.join(DIR, "autonomous.log");
49484
49489
  function loadCfg() {
49485
49490
  try {
49486
49491
  return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(FILE, "utf8")) };
@@ -49521,6 +49526,18 @@ var MIME = {
49521
49526
  ".zip": "application/zip"
49522
49527
  };
49523
49528
  var mimeOf = (name) => MIME[np3.extname(name).toLowerCase()] ?? "application/octet-stream";
49529
+ function fmtTime(iso) {
49530
+ if (!iso) return "";
49531
+ const d = new Date(iso);
49532
+ if (isNaN(d.getTime())) return "";
49533
+ return d.toLocaleString(void 0, {
49534
+ month: "short",
49535
+ day: "numeric",
49536
+ hour: "2-digit",
49537
+ minute: "2-digit",
49538
+ hour12: false
49539
+ });
49540
+ }
49524
49541
  function screenshotDir() {
49525
49542
  try {
49526
49543
  const out = cp2.execFileSync("defaults", ["read", "com.apple.screencapture", "location"], { encoding: "utf8", timeout: 2e3 }).trim();
@@ -49639,22 +49656,30 @@ ${e?.message ?? e}`);
49639
49656
  "jefri_agents",
49640
49657
  {
49641
49658
  title: "List your connections",
49642
- description: "List the people and agents you're connected to (plus your own agents) \u2014 these are who you can message. To reach someone new, use jefri_search to find them, then jefri_connect to request a connection (their owner approves).",
49659
+ description: "List the people and agents you're connected to (plus your own agents) \u2014 these are who you can message, with a live \u{1F7E2} online / \u26AA offline indicator for each. NOTE: you can message someone even when they're \u26AA offline \u2014 the message is stored and delivered the moment they reconnect, so never refuse to send just because a recipient is offline. To reach someone new, use jefri_search to find them, then jefri_connect.",
49643
49660
  inputSchema: {}
49644
49661
  },
49645
49662
  async () => withClient(async (c) => {
49646
49663
  const all = await c.identities();
49647
- const lines = all.filter((i) => i.username !== c.identity?.username).map(
49648
- (i) => `\u2022 @${i.username} \u2014 ${i.displayName} [${i.type}] (${i.status})${i.owner ? ` \xB7 by @${i.owner}` : ""}${i.tags.length ? " \xB7 " + i.tags.join(", ") : ""}`
49649
- );
49650
- return ok(lines.length ? lines.join("\n") : "No other identities on the network yet.");
49664
+ const others = all.filter((i) => i.username !== c.identity?.username);
49665
+ const isOn = (i) => i.status !== "offline" && !!i.status;
49666
+ others.sort((a, b) => isOn(a) === isOn(b) ? a.username.localeCompare(b.username) : isOn(a) ? -1 : 1);
49667
+ const lines = others.map((i) => {
49668
+ const dot = isOn(i) ? "\u{1F7E2}" : "\u26AA";
49669
+ const live = isOn(i) ? i.status : "offline";
49670
+ return `${dot} @${i.username} \u2014 ${i.displayName} [${i.type}] (${live})${i.owner ? ` \xB7 by @${i.owner}` : ""}${i.tags.length ? " \xB7 " + i.tags.join(", ") : ""}`;
49671
+ });
49672
+ const onCount = others.filter(isOn).length;
49673
+ const header = others.length ? `${onCount} online / ${others.length} total:
49674
+ ` : "";
49675
+ return ok(lines.length ? header + lines.join("\n") : "No other identities on the network yet.");
49651
49676
  })
49652
49677
  );
49653
49678
  server.registerTool(
49654
49679
  "jefri_send",
49655
49680
  {
49656
49681
  title: "Send a message",
49657
- description: "Send a direct message to another Jefri Chat user or agent by username. Use jefri_agents first if you don't know the username.",
49682
+ description: "Send a direct message to another Jefri Chat user or agent by username. Works whether or not they're online \u2014 an offline recipient gets the message the moment they reconnect, so send it regardless of their status. Use jefri_agents first if you don't know the username.",
49658
49683
  inputSchema: {
49659
49684
  to: external_exports.string().describe("recipient username, e.g. claude_backend"),
49660
49685
  text: external_exports.string().describe("the message body")
@@ -50308,17 +50333,19 @@ ${cmd}`);
50308
50333
  return name ? ` (in group "${name}" \xB7 reply with jefri_send_group groupId="${m.groupId}")` : ` (in group id ${m.groupId})`;
50309
50334
  };
50310
50335
  const lines = await Promise.all(msgs.map(async (m) => {
50336
+ const ts = fmtTime(m.createdAt);
50337
+ const at = ts ? `[${ts}] ` : "";
50311
50338
  if (m.encryptionMode === "private_e2e" && m.encryptedPayload) {
50312
50339
  try {
50313
50340
  const plain = await c.decryptPrivatePayload(m.encryptedPayload);
50314
50341
  const body2 = plain.kind === "file" ? `\u{1F512}\u{1F4CE} private file: ${plain.fileName ?? m.fileName}` : `\u{1F512} ${plain.content ?? ""}`;
50315
- return `@${m.senderUsername}${where(m)}: ${body2}`;
50342
+ return `${at}@${m.senderUsername}${where(m)}: ${body2}`;
50316
50343
  } catch {
50317
- return `@${m.senderUsername}${where(m)}: \u{1F512} Private message unavailable on this device`;
50344
+ return `${at}@${m.senderUsername}${where(m)}: \u{1F512} Private message unavailable on this device`;
50318
50345
  }
50319
50346
  }
50320
50347
  const body = m.kind === "file" ? `\u{1F4CE} sent a file: ${m.fileName} \u2014 to save it call jefri_download_file(from: "${m.senderUsername}"${m.fileName ? `, fileName: "${m.fileName}"` : ""})` : m.content;
50321
- return `@${m.senderUsername}${where(m)}: ${body}`;
50348
+ return `${at}@${m.senderUsername}${where(m)}: ${body}`;
50322
50349
  }));
50323
50350
  return ok(`\u{1F4E8} ${lines.length} message(s):
50324
50351
  ` + lines.join("\n"));
@@ -50439,19 +50466,21 @@ ${cmd}`
50439
50466
  const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
50440
50467
  if (!msgs.length) return ok(`No messages yet in ${label}.`);
50441
50468
  const lines = await Promise.all(msgs.map(async (m) => {
50469
+ const ts = fmtTime(m.createdAt);
50470
+ const at = ts ? `[${ts}] ` : "";
50442
50471
  if (m.encryptionMode === "private_e2e" && m.encryptedPayload) {
50443
50472
  try {
50444
50473
  const plain = await c.decryptPrivatePayload(m.encryptedPayload);
50445
- return `${m.senderUsername}: ${plain.kind === "file" ? `\u{1F512}\u{1F4CE} ${plain.fileName ?? m.fileName}` : `\u{1F512} ${plain.content ?? ""}`}`;
50474
+ return `${at}${m.senderUsername}: ${plain.kind === "file" ? `\u{1F512}\u{1F4CE} ${plain.fileName ?? m.fileName}` : `\u{1F512} ${plain.content ?? ""}`}`;
50446
50475
  } catch {
50447
- return `${m.senderUsername}: \u{1F512} Private message unavailable on this device`;
50476
+ return `${at}${m.senderUsername}: \u{1F512} Private message unavailable on this device`;
50448
50477
  }
50449
50478
  }
50450
50479
  if (m.kind === "file") {
50451
50480
  const dl = groupId ? `jefri_download_file(groupId: "${groupId}", fileName: "${m.fileName}")` : `jefri_download_file(from: "${m.senderUsername}", fileName: "${m.fileName}")`;
50452
- return `${m.senderUsername}: \u{1F4CE} ${m.fileName} \u2014 to save it call ${dl}`;
50481
+ return `${at}${m.senderUsername}: \u{1F4CE} ${m.fileName} \u2014 to save it call ${dl}`;
50453
50482
  }
50454
- return `${m.senderUsername}: ${m.content}`;
50483
+ return `${at}${m.senderUsername}: ${m.content}`;
50455
50484
  }));
50456
50485
  return ok(lines.join("\n"));
50457
50486
  })
@@ -50607,7 +50636,10 @@ ${cmd}`
50607
50636
  brain: external_exports.string().optional().describe("'claude', 'codex', or a full custom command (e.g. for OpenClaw)"),
50608
50637
  workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
50609
50638
  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"),
50639
+ persona: external_exports.string().optional().describe("role/instructions for the agent (injected as {persona})"),
50640
+ promptTemplate: external_exports.string().optional().describe("advanced: fully customize how the message is framed to the brain. Placeholders: {persona} {me} {sender} {where} {context} {message}. Pass 'default' to reset."),
50641
+ ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER \u2014 ignore other people (great for a private agent team). Default off."),
50642
+ contextMessages: external_exports.number().optional().describe("how many recent messages of the conversation to give the brain for memory (0 = stateless, default 12)"),
50611
50643
  replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)")
50612
50644
  }
50613
50645
  },
@@ -50615,9 +50647,13 @@ ${cmd}`
50615
50647
  const patch = {};
50616
50648
  if (typeof args.enabled === "boolean") patch.enabled = args.enabled;
50617
50649
  if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
50650
+ if (typeof args.ownerOnly === "boolean") patch.ownerOnly = args.ownerOnly;
50651
+ if (typeof args.contextMessages === "number") patch.contextMessages = Math.max(0, Math.min(40, Math.floor(args.contextMessages)));
50618
50652
  if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim();
50619
50653
  if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
50620
50654
  if (typeof args.persona === "string") patch.persona = args.persona;
50655
+ if (typeof args.promptTemplate === "string" && args.promptTemplate.trim())
50656
+ patch.promptTemplate = args.promptTemplate.trim().toLowerCase() === "default" ? DEFAULT_TEMPLATE : args.promptTemplate;
50621
50657
  if (args.replyMode) patch.replyMode = args.replyMode;
50622
50658
  const c = setAuto(patch);
50623
50659
  const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
@@ -50626,8 +50662,12 @@ ${cmd}`
50626
50662
  ` Brain: ${c.brain}`,
50627
50663
  ` Work folder: ${c.workdir}`,
50628
50664
  ` Replies to: ${modeLabel}`,
50665
+ ` Owner-only: ${c.ownerOnly ? "yes (only you direct me)" : "no (anyone who @mentions me)"}`,
50629
50666
  ` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
50630
- ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`
50667
+ ` Conversation memory: ${c.contextMessages > 0 ? `last ${c.contextMessages} messages` : "off"}`,
50668
+ ` Prompt: ${c.promptTemplate === DEFAULT_TEMPLATE ? "default (@name routing + ask-if-unclear)" : "custom"}`,
50669
+ ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`,
50670
+ ` Activity log: ~/.jefri/autonomous.log (watch it: tail -f ~/.jefri/autonomous.log)`
50631
50671
  ];
50632
50672
  if (c.enabled)
50633
50673
  lines.push(`
package/dist/index.js CHANGED
@@ -6883,12 +6883,12 @@ var require_dist = __commonJS({
6883
6883
  throw new Error(`Unknown format "${name}"`);
6884
6884
  return f;
6885
6885
  };
6886
- function addFormats(ajv, list, fs6, exportName) {
6886
+ function addFormats(ajv, list, fs7, exportName) {
6887
6887
  var _a;
6888
6888
  var _b;
6889
6889
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6890
6890
  for (const f of list)
6891
- ajv.addFormat(f, fs6[f]);
6891
+ ajv.addFormat(f, fs7[f]);
6892
6892
  }
6893
6893
  module.exports = exports = formatsPlugin;
6894
6894
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -6940,14 +6940,14 @@ var require_buffer_util = __commonJS({
6940
6940
  }
6941
6941
  return target;
6942
6942
  }
6943
- function _mask(source, mask, output, offset, length) {
6943
+ function _mask(source, mask2, output, offset, length) {
6944
6944
  for (let i = 0; i < length; i++) {
6945
- output[offset + i] = source[i] ^ mask[i & 3];
6945
+ output[offset + i] = source[i] ^ mask2[i & 3];
6946
6946
  }
6947
6947
  }
6948
- function _unmask(buffer, mask) {
6948
+ function _unmask(buffer, mask2) {
6949
6949
  for (let i = 0; i < buffer.length; i++) {
6950
- buffer[i] ^= mask[i & 3];
6950
+ buffer[i] ^= mask2[i & 3];
6951
6951
  }
6952
6952
  }
6953
6953
  function toArrayBuffer(buf) {
@@ -6980,13 +6980,13 @@ var require_buffer_util = __commonJS({
6980
6980
  if (!process.env.WS_NO_BUFFER_UTIL) {
6981
6981
  try {
6982
6982
  const bufferUtil = __require("bufferutil");
6983
- module.exports.mask = function(source, mask, output, offset, length) {
6984
- if (length < 48) _mask(source, mask, output, offset, length);
6985
- else bufferUtil.mask(source, mask, output, offset, length);
6983
+ module.exports.mask = function(source, mask2, output, offset, length) {
6984
+ if (length < 48) _mask(source, mask2, output, offset, length);
6985
+ else bufferUtil.mask(source, mask2, output, offset, length);
6986
6986
  };
6987
- module.exports.unmask = function(buffer, mask) {
6988
- if (buffer.length < 32) _unmask(buffer, mask);
6989
- else bufferUtil.unmask(buffer, mask);
6987
+ module.exports.unmask = function(buffer, mask2) {
6988
+ if (buffer.length < 32) _unmask(buffer, mask2);
6989
+ else bufferUtil.unmask(buffer, mask2);
6990
6990
  };
6991
6991
  } catch (e) {
6992
6992
  }
@@ -8327,14 +8327,14 @@ var require_sender = __commonJS({
8327
8327
  * @public
8328
8328
  */
8329
8329
  static frame(data, options) {
8330
- let mask;
8330
+ let mask2;
8331
8331
  let merge2 = false;
8332
8332
  let offset = 2;
8333
8333
  let skipMasking = false;
8334
8334
  if (options.mask) {
8335
- mask = options.maskBuffer || maskBuffer;
8335
+ mask2 = options.maskBuffer || maskBuffer;
8336
8336
  if (options.generateMask) {
8337
- options.generateMask(mask);
8337
+ options.generateMask(mask2);
8338
8338
  } else {
8339
8339
  if (randomPoolPointer === RANDOM_POOL_SIZE) {
8340
8340
  if (randomPool === void 0) {
@@ -8343,12 +8343,12 @@ var require_sender = __commonJS({
8343
8343
  randomFillSync(randomPool, 0, RANDOM_POOL_SIZE);
8344
8344
  randomPoolPointer = 0;
8345
8345
  }
8346
- mask[0] = randomPool[randomPoolPointer++];
8347
- mask[1] = randomPool[randomPoolPointer++];
8348
- mask[2] = randomPool[randomPoolPointer++];
8349
- mask[3] = randomPool[randomPoolPointer++];
8346
+ mask2[0] = randomPool[randomPoolPointer++];
8347
+ mask2[1] = randomPool[randomPoolPointer++];
8348
+ mask2[2] = randomPool[randomPoolPointer++];
8349
+ mask2[3] = randomPool[randomPoolPointer++];
8350
8350
  }
8351
- skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0;
8351
+ skipMasking = (mask2[0] | mask2[1] | mask2[2] | mask2[3]) === 0;
8352
8352
  offset = 6;
8353
8353
  }
8354
8354
  let dataLength;
@@ -8383,16 +8383,16 @@ var require_sender = __commonJS({
8383
8383
  }
8384
8384
  if (!options.mask) return [target, data];
8385
8385
  target[1] |= 128;
8386
- target[offset - 4] = mask[0];
8387
- target[offset - 3] = mask[1];
8388
- target[offset - 2] = mask[2];
8389
- target[offset - 1] = mask[3];
8386
+ target[offset - 4] = mask2[0];
8387
+ target[offset - 3] = mask2[1];
8388
+ target[offset - 2] = mask2[2];
8389
+ target[offset - 1] = mask2[3];
8390
8390
  if (skipMasking) return [target, data];
8391
8391
  if (merge2) {
8392
- applyMask(data, mask, target, offset, dataLength);
8392
+ applyMask(data, mask2, target, offset, dataLength);
8393
8393
  return [target];
8394
8394
  }
8395
- applyMask(data, mask, data, 0, dataLength);
8395
+ applyMask(data, mask2, data, 0, dataLength);
8396
8396
  return [target, data];
8397
8397
  }
8398
8398
  /**
@@ -8404,7 +8404,7 @@ var require_sender = __commonJS({
8404
8404
  * @param {Function} [cb] Callback
8405
8405
  * @public
8406
8406
  */
8407
- close(code, data, mask, cb) {
8407
+ close(code, data, mask2, cb) {
8408
8408
  let buf;
8409
8409
  if (code === void 0) {
8410
8410
  buf = EMPTY_BUFFER;
@@ -8432,7 +8432,7 @@ var require_sender = __commonJS({
8432
8432
  [kByteLength]: buf.length,
8433
8433
  fin: true,
8434
8434
  generateMask: this._generateMask,
8435
- mask,
8435
+ mask: mask2,
8436
8436
  maskBuffer: this._maskBuffer,
8437
8437
  opcode: 8,
8438
8438
  readOnly: false,
@@ -8452,7 +8452,7 @@ var require_sender = __commonJS({
8452
8452
  * @param {Function} [cb] Callback
8453
8453
  * @public
8454
8454
  */
8455
- ping(data, mask, cb) {
8455
+ ping(data, mask2, cb) {
8456
8456
  let byteLength;
8457
8457
  let readOnly;
8458
8458
  if (typeof data === "string") {
@@ -8473,7 +8473,7 @@ var require_sender = __commonJS({
8473
8473
  [kByteLength]: byteLength,
8474
8474
  fin: true,
8475
8475
  generateMask: this._generateMask,
8476
- mask,
8476
+ mask: mask2,
8477
8477
  maskBuffer: this._maskBuffer,
8478
8478
  opcode: 9,
8479
8479
  readOnly,
@@ -8499,7 +8499,7 @@ var require_sender = __commonJS({
8499
8499
  * @param {Function} [cb] Callback
8500
8500
  * @public
8501
8501
  */
8502
- pong(data, mask, cb) {
8502
+ pong(data, mask2, cb) {
8503
8503
  let byteLength;
8504
8504
  let readOnly;
8505
8505
  if (typeof data === "string") {
@@ -8520,7 +8520,7 @@ var require_sender = __commonJS({
8520
8520
  [kByteLength]: byteLength,
8521
8521
  fin: true,
8522
8522
  generateMask: this._generateMask,
8523
- mask,
8523
+ mask: mask2,
8524
8524
  maskBuffer: this._maskBuffer,
8525
8525
  opcode: 10,
8526
8526
  readOnly,
@@ -9431,24 +9431,24 @@ var require_websocket = __commonJS({
9431
9431
  * @param {Function} [cb] Callback which is executed when the ping is sent
9432
9432
  * @public
9433
9433
  */
9434
- ping(data, mask, cb) {
9434
+ ping(data, mask2, cb) {
9435
9435
  if (this.readyState === _WebSocket.CONNECTING) {
9436
9436
  throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");
9437
9437
  }
9438
9438
  if (typeof data === "function") {
9439
9439
  cb = data;
9440
- data = mask = void 0;
9441
- } else if (typeof mask === "function") {
9442
- cb = mask;
9443
- mask = void 0;
9440
+ data = mask2 = void 0;
9441
+ } else if (typeof mask2 === "function") {
9442
+ cb = mask2;
9443
+ mask2 = void 0;
9444
9444
  }
9445
9445
  if (typeof data === "number") data = data.toString();
9446
9446
  if (this.readyState !== _WebSocket.OPEN) {
9447
9447
  sendAfterClose(this, data, cb);
9448
9448
  return;
9449
9449
  }
9450
- if (mask === void 0) mask = !this._isServer;
9451
- this._sender.ping(data || EMPTY_BUFFER, mask, cb);
9450
+ if (mask2 === void 0) mask2 = !this._isServer;
9451
+ this._sender.ping(data || EMPTY_BUFFER, mask2, cb);
9452
9452
  }
9453
9453
  /**
9454
9454
  * Send a pong.
@@ -9458,24 +9458,24 @@ var require_websocket = __commonJS({
9458
9458
  * @param {Function} [cb] Callback which is executed when the pong is sent
9459
9459
  * @public
9460
9460
  */
9461
- pong(data, mask, cb) {
9461
+ pong(data, mask2, cb) {
9462
9462
  if (this.readyState === _WebSocket.CONNECTING) {
9463
9463
  throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");
9464
9464
  }
9465
9465
  if (typeof data === "function") {
9466
9466
  cb = data;
9467
- data = mask = void 0;
9468
- } else if (typeof mask === "function") {
9469
- cb = mask;
9470
- mask = void 0;
9467
+ data = mask2 = void 0;
9468
+ } else if (typeof mask2 === "function") {
9469
+ cb = mask2;
9470
+ mask2 = void 0;
9471
9471
  }
9472
9472
  if (typeof data === "number") data = data.toString();
9473
9473
  if (this.readyState !== _WebSocket.OPEN) {
9474
9474
  sendAfterClose(this, data, cb);
9475
9475
  return;
9476
9476
  }
9477
- if (mask === void 0) mask = !this._isServer;
9478
- this._sender.pong(data || EMPTY_BUFFER, mask, cb);
9477
+ if (mask2 === void 0) mask2 = !this._isServer;
9478
+ this._sender.pong(data || EMPTY_BUFFER, mask2, cb);
9479
9479
  }
9480
9480
  /**
9481
9481
  * Resume the socket.
@@ -10441,13 +10441,13 @@ var require_websocket_server = __commonJS({
10441
10441
  }
10442
10442
  }
10443
10443
  if (this.options.verifyClient) {
10444
- const info = {
10444
+ const info2 = {
10445
10445
  origin: req.headers[`${version2 === 8 ? "sec-websocket-origin" : "origin"}`],
10446
10446
  secure: !!(req.socket.authorized || req.socket.encrypted),
10447
10447
  req
10448
10448
  };
10449
10449
  if (this.options.verifyClient.length === 2) {
10450
- this.options.verifyClient(info, (verified, code, message, headers) => {
10450
+ this.options.verifyClient(info2, (verified, code, message, headers) => {
10451
10451
  if (!verified) {
10452
10452
  return abortHandshake(socket, code || 401, message, headers);
10453
10453
  }
@@ -10463,7 +10463,7 @@ var require_websocket_server = __commonJS({
10463
10463
  });
10464
10464
  return;
10465
10465
  }
10466
- if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
10466
+ if (!this.options.verifyClient(info2)) return abortHandshake(socket, 401);
10467
10467
  }
10468
10468
  this.completeUpgrade(extensions, key, protocols, req, socket, head, cb);
10469
10469
  }
@@ -13186,10 +13186,10 @@ var ZodObject = class _ZodObject extends ZodType {
13186
13186
  catchall: index
13187
13187
  });
13188
13188
  }
13189
- pick(mask) {
13189
+ pick(mask2) {
13190
13190
  const shape = {};
13191
- for (const key of util.objectKeys(mask)) {
13192
- if (mask[key] && this.shape[key]) {
13191
+ for (const key of util.objectKeys(mask2)) {
13192
+ if (mask2[key] && this.shape[key]) {
13193
13193
  shape[key] = this.shape[key];
13194
13194
  }
13195
13195
  }
@@ -13198,10 +13198,10 @@ var ZodObject = class _ZodObject extends ZodType {
13198
13198
  shape: () => shape
13199
13199
  });
13200
13200
  }
13201
- omit(mask) {
13201
+ omit(mask2) {
13202
13202
  const shape = {};
13203
13203
  for (const key of util.objectKeys(this.shape)) {
13204
- if (!mask[key]) {
13204
+ if (!mask2[key]) {
13205
13205
  shape[key] = this.shape[key];
13206
13206
  }
13207
13207
  }
@@ -13216,11 +13216,11 @@ var ZodObject = class _ZodObject extends ZodType {
13216
13216
  deepPartial() {
13217
13217
  return deepPartialify(this);
13218
13218
  }
13219
- partial(mask) {
13219
+ partial(mask2) {
13220
13220
  const newShape = {};
13221
13221
  for (const key of util.objectKeys(this.shape)) {
13222
13222
  const fieldSchema = this.shape[key];
13223
- if (mask && !mask[key]) {
13223
+ if (mask2 && !mask2[key]) {
13224
13224
  newShape[key] = fieldSchema;
13225
13225
  } else {
13226
13226
  newShape[key] = fieldSchema.optional();
@@ -13231,10 +13231,10 @@ var ZodObject = class _ZodObject extends ZodType {
13231
13231
  shape: () => newShape
13232
13232
  });
13233
13233
  }
13234
- required(mask) {
13234
+ required(mask2) {
13235
13235
  const newShape = {};
13236
13236
  for (const key of util.objectKeys(this.shape)) {
13237
- if (mask && !mask[key]) {
13237
+ if (mask2 && !mask2[key]) {
13238
13238
  newShape[key] = this.shape[key];
13239
13239
  } else {
13240
13240
  const fieldSchema = this.shape[key];
@@ -15005,14 +15005,14 @@ var BIGINT_FORMAT_RANGES = {
15005
15005
  int64: [/* @__PURE__ */ BigInt("-9223372036854775808"), /* @__PURE__ */ BigInt("9223372036854775807")],
15006
15006
  uint64: [/* @__PURE__ */ BigInt(0), /* @__PURE__ */ BigInt("18446744073709551615")]
15007
15007
  };
15008
- function pick(schema, mask) {
15008
+ function pick(schema, mask2) {
15009
15009
  const newShape = {};
15010
15010
  const currDef = schema._zod.def;
15011
- for (const key in mask) {
15011
+ for (const key in mask2) {
15012
15012
  if (!(key in currDef.shape)) {
15013
15013
  throw new Error(`Unrecognized key: "${key}"`);
15014
15014
  }
15015
- if (!mask[key])
15015
+ if (!mask2[key])
15016
15016
  continue;
15017
15017
  newShape[key] = currDef.shape[key];
15018
15018
  }
@@ -15022,14 +15022,14 @@ function pick(schema, mask) {
15022
15022
  checks: []
15023
15023
  });
15024
15024
  }
15025
- function omit(schema, mask) {
15025
+ function omit(schema, mask2) {
15026
15026
  const newShape = { ...schema._zod.def.shape };
15027
15027
  const currDef = schema._zod.def;
15028
- for (const key in mask) {
15028
+ for (const key in mask2) {
15029
15029
  if (!(key in currDef.shape)) {
15030
15030
  throw new Error(`Unrecognized key: "${key}"`);
15031
15031
  }
15032
- if (!mask[key])
15032
+ if (!mask2[key])
15033
15033
  continue;
15034
15034
  delete newShape[key];
15035
15035
  }
@@ -15068,15 +15068,15 @@ function merge(a, b) {
15068
15068
  // delete existing checks
15069
15069
  });
15070
15070
  }
15071
- function partial(Class2, schema, mask) {
15071
+ function partial(Class2, schema, mask2) {
15072
15072
  const oldShape = schema._zod.def.shape;
15073
15073
  const shape = { ...oldShape };
15074
- if (mask) {
15075
- for (const key in mask) {
15074
+ if (mask2) {
15075
+ for (const key in mask2) {
15076
15076
  if (!(key in oldShape)) {
15077
15077
  throw new Error(`Unrecognized key: "${key}"`);
15078
15078
  }
15079
- if (!mask[key])
15079
+ if (!mask2[key])
15080
15080
  continue;
15081
15081
  shape[key] = Class2 ? new Class2({
15082
15082
  type: "optional",
@@ -15097,15 +15097,15 @@ function partial(Class2, schema, mask) {
15097
15097
  checks: []
15098
15098
  });
15099
15099
  }
15100
- function required(Class2, schema, mask) {
15100
+ function required(Class2, schema, mask2) {
15101
15101
  const oldShape = schema._zod.def.shape;
15102
15102
  const shape = { ...oldShape };
15103
- if (mask) {
15104
- for (const key in mask) {
15103
+ if (mask2) {
15104
+ for (const key in mask2) {
15105
15105
  if (!(key in shape)) {
15106
15106
  throw new Error(`Unrecognized key: "${key}"`);
15107
15107
  }
15108
- if (!mask[key])
15108
+ if (!mask2[key])
15109
15109
  continue;
15110
15110
  shape[key] = new Class2({
15111
15111
  type: "nonoptional",
@@ -18983,8 +18983,8 @@ var ZodObject2 = /* @__PURE__ */ $constructor("ZodObject", (inst, def) => {
18983
18983
  return util_exports.extend(inst, incoming);
18984
18984
  };
18985
18985
  inst.merge = (other) => util_exports.merge(inst, other);
18986
- inst.pick = (mask) => util_exports.pick(inst, mask);
18987
- inst.omit = (mask) => util_exports.omit(inst, mask);
18986
+ inst.pick = (mask2) => util_exports.pick(inst, mask2);
18987
+ inst.omit = (mask2) => util_exports.omit(inst, mask2);
18988
18988
  inst.partial = (...args) => util_exports.partial(ZodOptional2, inst, args[0]);
18989
18989
  inst.required = (...args) => util_exports.required(ZodNonOptional, inst, args[0]);
18990
18990
  });
@@ -22315,25 +22315,25 @@ var Protocol = class {
22315
22315
  });
22316
22316
  }
22317
22317
  _resetTimeout(messageId) {
22318
- const info = this._timeoutInfo.get(messageId);
22319
- if (!info)
22318
+ const info2 = this._timeoutInfo.get(messageId);
22319
+ if (!info2)
22320
22320
  return false;
22321
- const totalElapsed = Date.now() - info.startTime;
22322
- if (info.maxTotalTimeout && totalElapsed >= info.maxTotalTimeout) {
22321
+ const totalElapsed = Date.now() - info2.startTime;
22322
+ if (info2.maxTotalTimeout && totalElapsed >= info2.maxTotalTimeout) {
22323
22323
  this._timeoutInfo.delete(messageId);
22324
22324
  throw McpError.fromError(ErrorCode.RequestTimeout, "Maximum total timeout exceeded", {
22325
- maxTotalTimeout: info.maxTotalTimeout,
22325
+ maxTotalTimeout: info2.maxTotalTimeout,
22326
22326
  totalElapsed
22327
22327
  });
22328
22328
  }
22329
- clearTimeout(info.timeoutId);
22330
- info.timeoutId = setTimeout(info.onTimeout, info.timeout);
22329
+ clearTimeout(info2.timeoutId);
22330
+ info2.timeoutId = setTimeout(info2.onTimeout, info2.timeout);
22331
22331
  return true;
22332
22332
  }
22333
22333
  _cleanupTimeout(messageId) {
22334
- const info = this._timeoutInfo.get(messageId);
22335
- if (info) {
22336
- clearTimeout(info.timeoutId);
22334
+ const info2 = this._timeoutInfo.get(messageId);
22335
+ if (info2) {
22336
+ clearTimeout(info2.timeoutId);
22337
22337
  this._timeoutInfo.delete(messageId);
22338
22338
  }
22339
22339
  }
@@ -22378,8 +22378,8 @@ var Protocol = class {
22378
22378
  this._progressHandlers.clear();
22379
22379
  this._taskProgressTokens.clear();
22380
22380
  this._pendingDebouncedNotifications.clear();
22381
- for (const info of this._timeoutInfo.values()) {
22382
- clearTimeout(info.timeoutId);
22381
+ for (const info2 of this._timeoutInfo.values()) {
22382
+ clearTimeout(info2.timeoutId);
22383
22383
  }
22384
22384
  this._timeoutInfo.clear();
22385
22385
  for (const controller of this._requestHandlerAbortControllers.values()) {
@@ -23833,7 +23833,7 @@ var McpZodTypeKind;
23833
23833
  // ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@3.25.76/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/toolNameValidation.js
23834
23834
  var TOOL_NAME_REGEX = /^[A-Za-z0-9._-]{1,128}$/;
23835
23835
  function validateToolName(name) {
23836
- const warnings = [];
23836
+ const warnings2 = [];
23837
23837
  if (name.length === 0) {
23838
23838
  return {
23839
23839
  isValid: false,
@@ -23847,34 +23847,34 @@ function validateToolName(name) {
23847
23847
  };
23848
23848
  }
23849
23849
  if (name.includes(" ")) {
23850
- warnings.push("Tool name contains spaces, which may cause parsing issues");
23850
+ warnings2.push("Tool name contains spaces, which may cause parsing issues");
23851
23851
  }
23852
23852
  if (name.includes(",")) {
23853
- warnings.push("Tool name contains commas, which may cause parsing issues");
23853
+ warnings2.push("Tool name contains commas, which may cause parsing issues");
23854
23854
  }
23855
23855
  if (name.startsWith("-") || name.endsWith("-")) {
23856
- warnings.push("Tool name starts or ends with a dash, which may cause parsing issues in some contexts");
23856
+ warnings2.push("Tool name starts or ends with a dash, which may cause parsing issues in some contexts");
23857
23857
  }
23858
23858
  if (name.startsWith(".") || name.endsWith(".")) {
23859
- warnings.push("Tool name starts or ends with a dot, which may cause parsing issues in some contexts");
23859
+ warnings2.push("Tool name starts or ends with a dot, which may cause parsing issues in some contexts");
23860
23860
  }
23861
23861
  if (!TOOL_NAME_REGEX.test(name)) {
23862
23862
  const invalidChars = name.split("").filter((char) => !/[A-Za-z0-9._-]/.test(char)).filter((char, index, arr) => arr.indexOf(char) === index);
23863
- warnings.push(`Tool name contains invalid characters: ${invalidChars.map((c) => `"${c}"`).join(", ")}`, "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)");
23863
+ warnings2.push(`Tool name contains invalid characters: ${invalidChars.map((c) => `"${c}"`).join(", ")}`, "Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)");
23864
23864
  return {
23865
23865
  isValid: false,
23866
- warnings
23866
+ warnings: warnings2
23867
23867
  };
23868
23868
  }
23869
23869
  return {
23870
23870
  isValid: true,
23871
- warnings
23871
+ warnings: warnings2
23872
23872
  };
23873
23873
  }
23874
- function issueToolNameWarning(name, warnings) {
23875
- if (warnings.length > 0) {
23874
+ function issueToolNameWarning(name, warnings2) {
23875
+ if (warnings2.length > 0) {
23876
23876
  console.warn(`Tool name validation warning for "${name}":`);
23877
- for (const warning of warnings) {
23877
+ for (const warning of warnings2) {
23878
23878
  console.warn(` - ${warning}`);
23879
23879
  }
23880
23880
  console.warn("Tool registration will proceed, but this may cause compatibility issues.");
@@ -24788,7 +24788,7 @@ var StdioServerTransport = class {
24788
24788
  };
24789
24789
 
24790
24790
  // src/index.ts
24791
- import fs5 from "node:fs";
24791
+ import fs6 from "node:fs";
24792
24792
  import os5 from "node:os";
24793
24793
  import path2 from "node:path";
24794
24794
 
@@ -25417,7 +25417,8 @@ function notifyIncoming(m) {
25417
25417
  }
25418
25418
  } else if (process.platform === "win32") {
25419
25419
  const esc2 = (x) => x.replace(/'/g, "''");
25420
- 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()`;
25420
+ const silent = sound ? "" : `$a=$tpl.CreateElement('audio');$a.SetAttribute('silent','true');$tpl.DocumentElement.AppendChild($a)|Out-Null;`;
25421
+ const ps = `$t='${esc2(title)}';$b='${esc2(body)}';try{$null=[Windows.UI.Notifications.ToastNotificationManager,Windows.UI.Notifications,ContentType=WindowsRuntime];$tpl=[Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02);$x=$tpl.GetElementsByTagName('text');$x.Item(0).AppendChild($tpl.CreateTextNode($t))|Out-Null;$x.Item(1).AppendChild($tpl.CreateTextNode($b))|Out-Null;` + silent + `$toast=[Windows.UI.Notifications.ToastNotification]::new($tpl);[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe').Show($toast);}catch{Add-Type -AssemblyName System.Windows.Forms;Add-Type -AssemblyName System.Drawing;$n=New-Object System.Windows.Forms.NotifyIcon;$n.Icon=[System.Drawing.SystemIcons]::Information;$n.BalloonTipTitle=$t;$n.BalloonTipText=$b;$n.Visible=$true;$n.ShowBalloonTip(6000);Start-Sleep -Milliseconds 6500;$n.Dispose();}`;
25421
25422
  cp.spawn("powershell", ["-NoProfile", "-WindowStyle", "Hidden", "-Command", ps], { stdio: "ignore", detached: true }).unref();
25422
25423
  } else {
25423
25424
  cp.spawn("notify-send", ["-a", "Jefri Chat", title, body], { stdio: "ignore", detached: true }).unref();
@@ -25431,16 +25432,30 @@ import cp2 from "node:child_process";
25431
25432
  import fs3 from "node:fs";
25432
25433
  import np2 from "node:path";
25433
25434
  import os3 from "node:os";
25435
+ var DEFAULT_TEMPLATE = '{persona}You are @{me}, an autonomous agent on Jefri Chat. A message came in{where}. It may address several agents by @name (e.g. "@alice: do X @bob: do Y") \u2014 do ONLY the part addressed to you (@{me}); ignore parts meant for other agents. If nothing is addressed to you, reply with nothing to do. You can do ANYTHING (research, writing, analysis, code, running tools) \u2014 not just code. If a CLAUDE.md or relevant files are in this folder, read them first to get oriented. If the request is unclear, reply with ONE short clarifying question and stop (only your owner will answer). Otherwise do your part and reply concisely with the result.\n\n{context}Latest message from @{sender}: {message}';
25434
25436
  var DEFAULTS2 = {
25435
25437
  enabled: false,
25436
25438
  brain: "claude",
25437
25439
  workdir: process.cwd(),
25438
25440
  replyMode: "mentions",
25439
25441
  persona: "",
25440
- replyToBots: false
25442
+ replyToBots: false,
25443
+ ownerOnly: false,
25444
+ contextMessages: 12,
25445
+ promptTemplate: DEFAULT_TEMPLATE
25441
25446
  };
25442
25447
  var DIR = np2.join(os3.homedir(), ".jefri");
25443
25448
  var FILE = np2.join(DIR, "autonomous.json");
25449
+ var LOG_FILE = np2.join(DIR, "autonomous.log");
25450
+ function logLine(line) {
25451
+ try {
25452
+ fs3.mkdirSync(DIR, { recursive: true });
25453
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
25454
+ fs3.appendFileSync(LOG_FILE, `[${ts}] ${line}
25455
+ `);
25456
+ } catch {
25457
+ }
25458
+ }
25444
25459
  function loadCfg() {
25445
25460
  try {
25446
25461
  return { ...DEFAULTS2, ...JSON.parse(fs3.readFileSync(FILE, "utf8")) };
@@ -25461,6 +25476,14 @@ function setAuto(patch) {
25461
25476
  }
25462
25477
  return { ...cfg };
25463
25478
  }
25479
+ var selfName = "";
25480
+ var ownerName = "";
25481
+ var fetchHistory = null;
25482
+ function configureAuto(opts) {
25483
+ selfName = opts.self;
25484
+ ownerName = opts.owner ?? "";
25485
+ fetchHistory = opts.getHistory;
25486
+ }
25464
25487
  var expand = (p) => p.startsWith("~") ? np2.join(os3.homedir(), p.slice(1)) : p;
25465
25488
  function brainArgv(brain, prompt) {
25466
25489
  if (brain === "claude") return ["claude", ["-p", "--permission-mode", "acceptEdits", prompt]];
@@ -25468,11 +25491,13 @@ function brainArgv(brain, prompt) {
25468
25491
  const parts = brain.trim().split(/\s+/);
25469
25492
  return [parts[0], [...parts.slice(1), prompt]];
25470
25493
  }
25471
- var MAX_PROMPT = 6e3;
25494
+ var MAX_PROMPT = 12e3;
25472
25495
  var MAX_REPLY = 7900;
25473
25496
  var TIMEOUT_MS = 5 * 60 * 1e3;
25474
25497
  function shouldHandle(m) {
25475
25498
  if (!cfg.enabled) return false;
25499
+ const fromOwner = !!ownerName && m.sender === ownerName;
25500
+ if (cfg.ownerOnly && !fromOwner) return false;
25476
25501
  if (m.senderIsBot && !cfg.replyToBots) return false;
25477
25502
  if (!m.isGroup) return true;
25478
25503
  if (cfg.replyMode === "all") return true;
@@ -25522,23 +25547,47 @@ function runBrain(cmd, args, cwd) {
25522
25547
  });
25523
25548
  });
25524
25549
  }
25550
+ async function buildContext(conversationId) {
25551
+ if (cfg.contextMessages <= 0 || !fetchHistory) return "";
25552
+ let msgs = [];
25553
+ try {
25554
+ msgs = await fetchHistory(conversationId);
25555
+ } catch {
25556
+ return "";
25557
+ }
25558
+ const recent = msgs.slice(-cfg.contextMessages);
25559
+ if (!recent.length) return "";
25560
+ const lines = recent.map((h) => {
25561
+ const body = h.kind === "file" ? `[sent a file: ${h.fileName ?? "file"}]` : h.content ?? "";
25562
+ return `@${h.sender}: ${body.replace(/\s+/g, " ").slice(0, 400)}`;
25563
+ });
25564
+ return `Recent conversation (for context \u2014 reply only to the latest message):
25565
+ ${lines.join("\n")}
25566
+
25567
+ `;
25568
+ }
25525
25569
  function handleAutonomous(m, reply, log2) {
25526
25570
  if (!shouldHandle(m)) return;
25571
+ const where = m.isGroup ? " in a group" : "";
25572
+ logLine(`\u2190 @${m.sender}${where}: ${m.content.replace(/\s+/g, " ").slice(0, 300)}`);
25527
25573
  queue.push(async () => {
25528
25574
  const persona = cfg.persona ? cfg.persona.trim() + "\n\n" : "";
25529
- const where = m.isGroup ? " in a group" : "";
25530
- const prompt = `${persona}You are an autonomous agent on Jefri Chat. @${m.sender} sent you a message${where}. Do what it asks (you can use your tools/files) and then give a concise reply to send back.
25531
-
25532
- Message from @${m.sender}: ${m.content}`.slice(0, MAX_PROMPT);
25575
+ const context = await buildContext(m.conversationId);
25576
+ const prompt = (cfg.promptTemplate || DEFAULT_TEMPLATE).replace(/\{persona\}/g, persona).replace(/\{me\}/g, selfName).replace(/\{sender\}/g, m.sender).replace(/\{where\}/g, where).replace(/\{context\}/g, context).replace(/\{message\}/g, m.content).slice(0, MAX_PROMPT);
25533
25577
  const [cmd, args] = brainArgv(cfg.brain, prompt);
25534
- log2(`autonomous: @${m.sender} \u2192 running "${cmd}"\u2026`);
25578
+ log2(`autonomous: @${m.sender} \u2192 running "${cfg.brain}"\u2026`);
25579
+ logLine(` running: ${cfg.brain} in ${cfg.workdir}`);
25535
25580
  try {
25536
25581
  const out = await runBrain(cmd, args, cfg.workdir);
25537
- reply((out.trim() || "(done)").slice(0, MAX_REPLY));
25582
+ const text = (out.trim() || "(done)").slice(0, MAX_REPLY);
25583
+ reply(text);
25538
25584
  log2(`autonomous: replied to @${m.sender}`);
25585
+ logLine(` \u2192 replied: ${text.replace(/\s+/g, " ").slice(0, 300)}`);
25539
25586
  } catch (e) {
25540
- reply(`(couldn't finish autonomously: ${e?.message ?? e})`.slice(0, MAX_REPLY));
25587
+ const msg = `(couldn't finish autonomously: ${e?.message ?? e})`.slice(0, MAX_REPLY);
25588
+ reply(msg);
25541
25589
  log2(`autonomous: failed for @${m.sender}: ${e?.message ?? e}`);
25590
+ logLine(` \u2717 failed: ${e?.message ?? e}`);
25542
25591
  }
25543
25592
  });
25544
25593
  void drain();
@@ -25565,6 +25614,18 @@ var MIME = {
25565
25614
  ".zip": "application/zip"
25566
25615
  };
25567
25616
  var mimeOf = (name) => MIME[np3.extname(name).toLowerCase()] ?? "application/octet-stream";
25617
+ function fmtTime(iso) {
25618
+ if (!iso) return "";
25619
+ const d = new Date(iso);
25620
+ if (isNaN(d.getTime())) return "";
25621
+ return d.toLocaleString(void 0, {
25622
+ month: "short",
25623
+ day: "numeric",
25624
+ hour: "2-digit",
25625
+ minute: "2-digit",
25626
+ hour12: false
25627
+ });
25628
+ }
25568
25629
  function screenshotDir() {
25569
25630
  try {
25570
25631
  const out = cp3.execFileSync("defaults", ["read", "com.apple.screencapture", "location"], { encoding: "utf8", timeout: 2e3 }).trim();
@@ -25683,22 +25744,30 @@ ${e?.message ?? e}`);
25683
25744
  "jefri_agents",
25684
25745
  {
25685
25746
  title: "List your connections",
25686
- description: "List the people and agents you're connected to (plus your own agents) \u2014 these are who you can message. To reach someone new, use jefri_search to find them, then jefri_connect to request a connection (their owner approves).",
25747
+ description: "List the people and agents you're connected to (plus your own agents) \u2014 these are who you can message, with a live \u{1F7E2} online / \u26AA offline indicator for each. NOTE: you can message someone even when they're \u26AA offline \u2014 the message is stored and delivered the moment they reconnect, so never refuse to send just because a recipient is offline. To reach someone new, use jefri_search to find them, then jefri_connect.",
25687
25748
  inputSchema: {}
25688
25749
  },
25689
25750
  async () => withClient(async (c) => {
25690
25751
  const all = await c.identities();
25691
- const lines = all.filter((i) => i.username !== c.identity?.username).map(
25692
- (i) => `\u2022 @${i.username} \u2014 ${i.displayName} [${i.type}] (${i.status})${i.owner ? ` \xB7 by @${i.owner}` : ""}${i.tags.length ? " \xB7 " + i.tags.join(", ") : ""}`
25693
- );
25694
- return ok(lines.length ? lines.join("\n") : "No other identities on the network yet.");
25752
+ const others = all.filter((i) => i.username !== c.identity?.username);
25753
+ const isOn = (i) => i.status !== "offline" && !!i.status;
25754
+ others.sort((a, b) => isOn(a) === isOn(b) ? a.username.localeCompare(b.username) : isOn(a) ? -1 : 1);
25755
+ const lines = others.map((i) => {
25756
+ const dot = isOn(i) ? "\u{1F7E2}" : "\u26AA";
25757
+ const live = isOn(i) ? i.status : "offline";
25758
+ return `${dot} @${i.username} \u2014 ${i.displayName} [${i.type}] (${live})${i.owner ? ` \xB7 by @${i.owner}` : ""}${i.tags.length ? " \xB7 " + i.tags.join(", ") : ""}`;
25759
+ });
25760
+ const onCount = others.filter(isOn).length;
25761
+ const header = others.length ? `${onCount} online / ${others.length} total:
25762
+ ` : "";
25763
+ return ok(lines.length ? header + lines.join("\n") : "No other identities on the network yet.");
25695
25764
  })
25696
25765
  );
25697
25766
  server2.registerTool(
25698
25767
  "jefri_send",
25699
25768
  {
25700
25769
  title: "Send a message",
25701
- description: "Send a direct message to another Jefri Chat user or agent by username. Use jefri_agents first if you don't know the username.",
25770
+ description: "Send a direct message to another Jefri Chat user or agent by username. Works whether or not they're online \u2014 an offline recipient gets the message the moment they reconnect, so send it regardless of their status. Use jefri_agents first if you don't know the username.",
25702
25771
  inputSchema: {
25703
25772
  to: external_exports.string().describe("recipient username, e.g. claude_backend"),
25704
25773
  text: external_exports.string().describe("the message body")
@@ -26352,17 +26421,19 @@ ${cmd}`);
26352
26421
  return name ? ` (in group "${name}" \xB7 reply with jefri_send_group groupId="${m.groupId}")` : ` (in group id ${m.groupId})`;
26353
26422
  };
26354
26423
  const lines = await Promise.all(msgs.map(async (m) => {
26424
+ const ts = fmtTime(m.createdAt);
26425
+ const at = ts ? `[${ts}] ` : "";
26355
26426
  if (m.encryptionMode === "private_e2e" && m.encryptedPayload) {
26356
26427
  try {
26357
26428
  const plain = await c.decryptPrivatePayload(m.encryptedPayload);
26358
26429
  const body2 = plain.kind === "file" ? `\u{1F512}\u{1F4CE} private file: ${plain.fileName ?? m.fileName}` : `\u{1F512} ${plain.content ?? ""}`;
26359
- return `@${m.senderUsername}${where(m)}: ${body2}`;
26430
+ return `${at}@${m.senderUsername}${where(m)}: ${body2}`;
26360
26431
  } catch {
26361
- return `@${m.senderUsername}${where(m)}: \u{1F512} Private message unavailable on this device`;
26432
+ return `${at}@${m.senderUsername}${where(m)}: \u{1F512} Private message unavailable on this device`;
26362
26433
  }
26363
26434
  }
26364
26435
  const body = m.kind === "file" ? `\u{1F4CE} sent a file: ${m.fileName} \u2014 to save it call jefri_download_file(from: "${m.senderUsername}"${m.fileName ? `, fileName: "${m.fileName}"` : ""})` : m.content;
26365
- return `@${m.senderUsername}${where(m)}: ${body}`;
26436
+ return `${at}@${m.senderUsername}${where(m)}: ${body}`;
26366
26437
  }));
26367
26438
  return ok(`\u{1F4E8} ${lines.length} message(s):
26368
26439
  ` + lines.join("\n"));
@@ -26483,19 +26554,21 @@ ${cmd}`
26483
26554
  const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
26484
26555
  if (!msgs.length) return ok(`No messages yet in ${label}.`);
26485
26556
  const lines = await Promise.all(msgs.map(async (m) => {
26557
+ const ts = fmtTime(m.createdAt);
26558
+ const at = ts ? `[${ts}] ` : "";
26486
26559
  if (m.encryptionMode === "private_e2e" && m.encryptedPayload) {
26487
26560
  try {
26488
26561
  const plain = await c.decryptPrivatePayload(m.encryptedPayload);
26489
- return `${m.senderUsername}: ${plain.kind === "file" ? `\u{1F512}\u{1F4CE} ${plain.fileName ?? m.fileName}` : `\u{1F512} ${plain.content ?? ""}`}`;
26562
+ return `${at}${m.senderUsername}: ${plain.kind === "file" ? `\u{1F512}\u{1F4CE} ${plain.fileName ?? m.fileName}` : `\u{1F512} ${plain.content ?? ""}`}`;
26490
26563
  } catch {
26491
- return `${m.senderUsername}: \u{1F512} Private message unavailable on this device`;
26564
+ return `${at}${m.senderUsername}: \u{1F512} Private message unavailable on this device`;
26492
26565
  }
26493
26566
  }
26494
26567
  if (m.kind === "file") {
26495
26568
  const dl = groupId ? `jefri_download_file(groupId: "${groupId}", fileName: "${m.fileName}")` : `jefri_download_file(from: "${m.senderUsername}", fileName: "${m.fileName}")`;
26496
- return `${m.senderUsername}: \u{1F4CE} ${m.fileName} \u2014 to save it call ${dl}`;
26569
+ return `${at}${m.senderUsername}: \u{1F4CE} ${m.fileName} \u2014 to save it call ${dl}`;
26497
26570
  }
26498
- return `${m.senderUsername}: ${m.content}`;
26571
+ return `${at}${m.senderUsername}: ${m.content}`;
26499
26572
  }));
26500
26573
  return ok(lines.join("\n"));
26501
26574
  })
@@ -26651,7 +26724,10 @@ ${cmd}`
26651
26724
  brain: external_exports.string().optional().describe("'claude', 'codex', or a full custom command (e.g. for OpenClaw)"),
26652
26725
  workdir: external_exports.string().optional().describe("folder the brain works in (scope it!)"),
26653
26726
  replyMode: external_exports.enum(["mentions", "dms", "all"]).optional().describe("groups: mentions=only when @-mentioned, all=every message, dms=DMs only"),
26654
- persona: external_exports.string().optional().describe("role/instructions for the agent"),
26727
+ persona: external_exports.string().optional().describe("role/instructions for the agent (injected as {persona})"),
26728
+ promptTemplate: external_exports.string().optional().describe("advanced: fully customize how the message is framed to the brain. Placeholders: {persona} {me} {sender} {where} {context} {message}. Pass 'default' to reset."),
26729
+ ownerOnly: external_exports.boolean().optional().describe("only act on messages from your OWNER \u2014 ignore other people (great for a private agent team). Default off."),
26730
+ contextMessages: external_exports.number().optional().describe("how many recent messages of the conversation to give the brain for memory (0 = stateless, default 12)"),
26655
26731
  replyToBots: external_exports.boolean().optional().describe("also auto-reply to other agents (default off \u2014 prevents bot loops)")
26656
26732
  }
26657
26733
  },
@@ -26659,9 +26735,13 @@ ${cmd}`
26659
26735
  const patch = {};
26660
26736
  if (typeof args.enabled === "boolean") patch.enabled = args.enabled;
26661
26737
  if (typeof args.replyToBots === "boolean") patch.replyToBots = args.replyToBots;
26738
+ if (typeof args.ownerOnly === "boolean") patch.ownerOnly = args.ownerOnly;
26739
+ if (typeof args.contextMessages === "number") patch.contextMessages = Math.max(0, Math.min(40, Math.floor(args.contextMessages)));
26662
26740
  if (typeof args.brain === "string" && args.brain.trim()) patch.brain = args.brain.trim();
26663
26741
  if (typeof args.workdir === "string" && args.workdir.trim()) patch.workdir = args.workdir.trim();
26664
26742
  if (typeof args.persona === "string") patch.persona = args.persona;
26743
+ if (typeof args.promptTemplate === "string" && args.promptTemplate.trim())
26744
+ patch.promptTemplate = args.promptTemplate.trim().toLowerCase() === "default" ? DEFAULT_TEMPLATE : args.promptTemplate;
26665
26745
  if (args.replyMode) patch.replyMode = args.replyMode;
26666
26746
  const c = setAuto(patch);
26667
26747
  const modeLabel = c.replyMode === "all" ? "every message" : c.replyMode === "dms" ? "DMs only" : "DMs + group @mentions";
@@ -26670,8 +26750,12 @@ ${cmd}`
26670
26750
  ` Brain: ${c.brain}`,
26671
26751
  ` Work folder: ${c.workdir}`,
26672
26752
  ` Replies to: ${modeLabel}`,
26753
+ ` Owner-only: ${c.ownerOnly ? "yes (only you direct me)" : "no (anyone who @mentions me)"}`,
26673
26754
  ` Persona: ${c.persona ? c.persona.slice(0, 80) : "(none)"}`,
26674
- ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`
26755
+ ` Conversation memory: ${c.contextMessages > 0 ? `last ${c.contextMessages} messages` : "off"}`,
26756
+ ` Prompt: ${c.promptTemplate === DEFAULT_TEMPLATE ? "default (@name routing + ask-if-unclear)" : "custom"}`,
26757
+ ` Reply to other bots: ${c.replyToBots ? "yes" : "no"}`,
26758
+ ` Activity log: ~/.jefri/autonomous.log (watch it: tail -f ~/.jefri/autonomous.log)`
26675
26759
  ];
26676
26760
  if (c.enabled)
26677
26761
  lines.push(`
@@ -26696,6 +26780,134 @@ function attachInbox(c, inbox2, self, onIncoming) {
26696
26780
  c.on("file_received", capture);
26697
26781
  }
26698
26782
 
26783
+ // src/doctor.ts
26784
+ import cp4 from "node:child_process";
26785
+ import fs5 from "node:fs";
26786
+ import np4 from "node:path";
26787
+ import { fileURLToPath } from "node:url";
26788
+ var T = !!process.stdout.isTTY;
26789
+ var C = {
26790
+ g: T ? "\x1B[32m" : "",
26791
+ r: T ? "\x1B[31m" : "",
26792
+ y: T ? "\x1B[33m" : "",
26793
+ d: T ? "\x1B[2m" : "",
26794
+ b: T ? "\x1B[1m" : "",
26795
+ x: T ? "\x1B[0m" : ""
26796
+ };
26797
+ var problems = 0;
26798
+ var warnings = 0;
26799
+ var pass = (s, extra = "") => console.log(` ${C.g}\u2713${C.x} ${s}${extra ? ` ${C.d}${extra}${C.x}` : ""}`);
26800
+ var fail2 = (s, fix = "") => {
26801
+ problems++;
26802
+ console.log(` ${C.r}\u2717${C.x} ${s}${fix ? `
26803
+ ${C.y}\u2192 ${fix}${C.x}` : ""}`);
26804
+ };
26805
+ var warn = (s, fix = "") => {
26806
+ warnings++;
26807
+ console.log(` ${C.y}!${C.x} ${s}${fix ? `
26808
+ ${C.d}${fix}${C.x}` : ""}`);
26809
+ };
26810
+ var info = (s) => console.log(` ${C.d}i ${s}${C.x}`);
26811
+ function mask(t) {
26812
+ if (t.length <= 12) return t.slice(0, 4) + "\u2026";
26813
+ return t.slice(0, 8) + "\u2026" + t.slice(-4);
26814
+ }
26815
+ function connectorVersion() {
26816
+ try {
26817
+ const here = np4.dirname(fileURLToPath(import.meta.url));
26818
+ const pkg = JSON.parse(fs5.readFileSync(np4.join(here, "..", "package.json"), "utf8"));
26819
+ return pkg.version ?? "?";
26820
+ } catch {
26821
+ return "?";
26822
+ }
26823
+ }
26824
+ function hasCli(cmd) {
26825
+ try {
26826
+ const r = cp4.spawnSync(cmd, ["--version"], {
26827
+ encoding: "utf8",
26828
+ timeout: 5e3,
26829
+ shell: process.platform === "win32"
26830
+ });
26831
+ return r.status === 0 || !!(r.stdout && r.stdout.trim());
26832
+ } catch {
26833
+ return false;
26834
+ }
26835
+ }
26836
+ async function fetchWithTimeout(url, opts, ms) {
26837
+ const ac = new AbortController();
26838
+ const t = setTimeout(() => ac.abort(), ms);
26839
+ try {
26840
+ return await fetch(url, { ...opts, signal: ac.signal });
26841
+ } finally {
26842
+ clearTimeout(t);
26843
+ }
26844
+ }
26845
+ async function runDoctor() {
26846
+ const SERVER2 = process.env.ACP_SERVER ?? "http://localhost:4000";
26847
+ const TOKEN2 = process.env.ACP_TOKEN ?? "";
26848
+ console.log(`
26849
+ ${C.b}\u{1FA7A} Jefri Chat connector \u2014 setup check${C.x}
26850
+ `);
26851
+ const major = Number(process.versions.node.split(".")[0]);
26852
+ if (major >= 18) pass(`Node ${process.version}`);
26853
+ else fail2(`Node ${process.version} is too old`, "Install Node 18+ from https://nodejs.org");
26854
+ pass(`Connector jefrichat-mcp v${connectorVersion()}`, "npm i -g jefrichat-mcp@latest to update");
26855
+ pass(`Hub URL ${SERVER2}`);
26856
+ if (!TOKEN2)
26857
+ fail2("No ACP_TOKEN set", "Get your token from jefrichat.com \u2192 Connect, then re-add the connector");
26858
+ else if (!TOKEN2.startsWith("acp_"))
26859
+ warn(`Token doesn't look right (${mask(TOKEN2)})`, "Tokens start with acp_ \u2014 check you copied the whole thing");
26860
+ else pass(`Token present (${mask(TOKEN2)})`);
26861
+ if (TOKEN2) {
26862
+ try {
26863
+ const r = await fetchWithTimeout(`${SERVER2}/api/me`, { headers: { Authorization: `Bearer ${TOKEN2}` } }, 8e3);
26864
+ if (r.status === 200) {
26865
+ const j = await r.json().catch(() => ({}));
26866
+ const id = j.identity ?? {};
26867
+ const owner = id.owner ? `, owned by @${id.owner}` : "";
26868
+ pass("Hub reachable & authenticated", `you are @${id.username ?? "?"} (${id.type ?? "?"}${owner})`);
26869
+ } else if (r.status === 401) {
26870
+ fail2("Token rejected by the hub (401)", "The agent may have been deleted or the token is wrong \u2014 get a fresh one from Connect");
26871
+ } else {
26872
+ fail2(`Hub returned HTTP ${r.status}`, "The hub is up but something's off \u2014 try again in a moment");
26873
+ }
26874
+ } catch (e) {
26875
+ const err = e;
26876
+ const reason = err?.name === "AbortError" ? "timed out" : err?.message ?? String(e);
26877
+ fail2(`Can't reach the hub (${reason})`, "Check internet / VPN / firewall \u2014 corporate networks sometimes block it");
26878
+ }
26879
+ } else {
26880
+ info("Skipping hub auth check (no token)");
26881
+ }
26882
+ const auto = getAuto();
26883
+ const brainCmd = auto.brain === "claude" ? "claude" : auto.brain === "codex" ? "codex" : auto.brain.trim().split(/\s+/)[0];
26884
+ if (hasCli(brainCmd)) pass(`Autonomous brain "${brainCmd}" found on PATH`);
26885
+ else info(`Autonomous brain "${brainCmd}" not on PATH (only needed if you enable autonomous mode)`);
26886
+ const prefs2 = getPrefs();
26887
+ if (!prefs2.enabled) info("Desktop notifications are OFF (use the jefri_notifications tool to turn on)");
26888
+ else if (macNeedsTerminalNotifier)
26889
+ warn("macOS notifications need terminal-notifier to show reliably", "run: brew install terminal-notifier");
26890
+ else if (process.platform === "win32")
26891
+ pass("Desktop notifications on (Windows toast)", "if they don't appear, turn off Focus Assist in Settings");
26892
+ else pass("Desktop notifications on");
26893
+ if (auto.enabled)
26894
+ info(`Autonomous mode is ON \u2014 brain "${auto.brain}" in ${auto.workdir} (jefri_autonomous to change)`);
26895
+ console.log("");
26896
+ if (problems === 0 && warnings === 0) {
26897
+ console.log(`${C.g}${C.b}All good \u2014 you're ready to chat.${C.x}
26898
+ `);
26899
+ } else if (problems === 0) {
26900
+ console.log(`${C.y}Ready, with ${warnings} thing${warnings === 1 ? "" : "s"} to look at above.${C.x}
26901
+ `);
26902
+ } else {
26903
+ console.log(
26904
+ `${C.r}${C.b}${problems} problem${problems === 1 ? "" : "s"} to fix${C.x}${warnings ? ` (+${warnings} warning${warnings === 1 ? "" : "s"})` : ""} before this will work. See the \u2192 hints above.
26905
+ `
26906
+ );
26907
+ }
26908
+ process.exitCode = problems ? 1 : 0;
26909
+ }
26910
+
26699
26911
  // src/index.ts
26700
26912
  var SERVER = process.env.ACP_SERVER ?? "http://localhost:4000";
26701
26913
  var TOKEN = process.env.ACP_TOKEN;
@@ -26708,7 +26920,7 @@ var TOKEN_CACHE = path2.join(os5.homedir(), ".acp", "tokens.json");
26708
26920
  var cacheKey = `${SERVER}::${USERNAME}`;
26709
26921
  function readTokenCache() {
26710
26922
  try {
26711
- return JSON.parse(fs5.readFileSync(TOKEN_CACHE, "utf8"));
26923
+ return JSON.parse(fs6.readFileSync(TOKEN_CACHE, "utf8"));
26712
26924
  } catch {
26713
26925
  return {};
26714
26926
  }
@@ -26716,8 +26928,8 @@ function readTokenCache() {
26716
26928
  function cacheToken(tok) {
26717
26929
  const c = readTokenCache();
26718
26930
  c[cacheKey] = tok;
26719
- fs5.mkdirSync(path2.dirname(TOKEN_CACHE), { recursive: true });
26720
- fs5.writeFileSync(TOKEN_CACHE, JSON.stringify(c, null, 2));
26931
+ fs6.mkdirSync(path2.dirname(TOKEN_CACHE), { recursive: true });
26932
+ fs6.writeFileSync(TOKEN_CACHE, JSON.stringify(c, null, 2));
26721
26933
  }
26722
26934
  var clientPromise = null;
26723
26935
  var inbox = [];
@@ -26765,6 +26977,25 @@ function ensureClient() {
26765
26977
  });
26766
26978
  void refreshAgents();
26767
26979
  setInterval(refreshAgents, 6e4).unref?.();
26980
+ const getHistory = (conversationId) => new Promise((resolve) => {
26981
+ const handler = (e) => {
26982
+ if (e?.conversationId !== conversationId) return;
26983
+ c.off("history", handler);
26984
+ resolve((e.messages ?? []).map((mm) => ({ sender: mm.senderUsername, content: mm.content, kind: mm.kind, fileName: mm.fileName })));
26985
+ };
26986
+ c.on("history", handler);
26987
+ try {
26988
+ c.history(conversationId);
26989
+ } catch {
26990
+ c.off("history", handler);
26991
+ resolve([]);
26992
+ }
26993
+ setTimeout(() => {
26994
+ c.off("history", handler);
26995
+ resolve([]);
26996
+ }, 3e3);
26997
+ });
26998
+ configureAuto({ self, owner: c.identity?.owner, getHistory });
26768
26999
  if (macNeedsTerminalNotifier)
26769
27000
  log("tip: for reliable desktop notifications on macOS, run: brew install terminal-notifier");
26770
27001
  if (getAuto().enabled)
@@ -26788,6 +27019,7 @@ function ensureClient() {
26788
27019
  sender: m.senderUsername,
26789
27020
  isGroup,
26790
27021
  groupId: m.groupId,
27022
+ conversationId: m.conversationId,
26791
27023
  isMention,
26792
27024
  senderIsBot: agentUsers.has(m.senderUsername)
26793
27025
  },
@@ -26811,6 +27043,10 @@ function ensureClient() {
26811
27043
  var server = new McpServer({ name: "jefrichat", version: "0.1.0" });
26812
27044
  registerAcpTools(server, { ensureClient, inbox, serverUrl: SERVER, local: true });
26813
27045
  async function main() {
27046
+ if (process.argv[2] === "doctor") {
27047
+ await runDoctor();
27048
+ return;
27049
+ }
26814
27050
  ensureClient().catch((e) => log("initial connect failed (will retry on first tool):", e?.message ?? e));
26815
27051
  const transport = new StdioServerTransport();
26816
27052
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.9.0",
3
+ "version": "0.13.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": {