jefrichat-mcp 0.48.6 → 0.48.8

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 +163 -53
  2. package/dist/index.js +402 -47
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -20888,8 +20888,8 @@ var require_application = __commonJS({
20888
20888
  tryRender(view, renderOptions, done);
20889
20889
  };
20890
20890
  app2.listen = function listen() {
20891
- var server = http2.createServer(this);
20892
- return server.listen.apply(server, arguments);
20891
+ var server2 = http2.createServer(this);
20892
+ return server2.listen.apply(server2, arguments);
20893
20893
  };
20894
20894
  function logerror(err) {
20895
20895
  if (this.get("env") !== "test") console.error(err.stack || err.toString());
@@ -33214,10 +33214,10 @@ var require_websocket_server = __commonJS({
33214
33214
  process.nextTick(emitClose, this);
33215
33215
  }
33216
33216
  } else {
33217
- const server = this._server;
33217
+ const server2 = this._server;
33218
33218
  this._removeListeners();
33219
33219
  this._removeListeners = this._server = null;
33220
- server.close(() => {
33220
+ server2.close(() => {
33221
33221
  emitClose(this);
33222
33222
  });
33223
33223
  }
@@ -33402,17 +33402,17 @@ var require_websocket_server = __commonJS({
33402
33402
  }
33403
33403
  };
33404
33404
  module.exports = WebSocketServer2;
33405
- function addListeners(server, map3) {
33406
- for (const event of Object.keys(map3)) server.on(event, map3[event]);
33405
+ function addListeners(server2, map3) {
33406
+ for (const event of Object.keys(map3)) server2.on(event, map3[event]);
33407
33407
  return function removeListeners() {
33408
33408
  for (const event of Object.keys(map3)) {
33409
- server.removeListener(event, map3[event]);
33409
+ server2.removeListener(event, map3[event]);
33410
33410
  }
33411
33411
  };
33412
33412
  }
33413
- function emitClose(server) {
33414
- server._state = CLOSED;
33415
- server.emit("close");
33413
+ function emitClose(server2) {
33414
+ server2._state = CLOSED;
33415
+ server2.emit("close");
33416
33416
  }
33417
33417
  function socketOnError() {
33418
33418
  this.destroy();
@@ -33431,11 +33431,11 @@ var require_websocket_server = __commonJS({
33431
33431
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
33432
33432
  );
33433
33433
  }
33434
- function abortHandshakeOrEmitwsClientError(server, req, socket, code3, message, headers) {
33435
- if (server.listenerCount("wsClientError")) {
33434
+ function abortHandshakeOrEmitwsClientError(server2, req, socket, code3, message, headers) {
33435
+ if (server2.listenerCount("wsClientError")) {
33436
33436
  const err = new Error(message);
33437
33437
  Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
33438
- server.emit("wsClientError", err, socket, req);
33438
+ server2.emit("wsClientError", err, socket, req);
33439
33439
  } else {
33440
33440
  abortHandshake(socket, code3, message, headers);
33441
33441
  }
@@ -60697,15 +60697,15 @@ var JefriClient = class _JefriClient {
60697
60697
  get online() {
60698
60698
  return this.hasConnected && !this.closing && this.ws?.readyState === this.ws?.OPEN;
60699
60699
  }
60700
- constructor(server) {
60701
- this.server = server.replace(/\/$/, "");
60700
+ constructor(server2) {
60701
+ this.server = server2.replace(/\/$/, "");
60702
60702
  }
60703
60703
  static async connect(opts = {}) {
60704
- const server = (opts.server ?? "https://jefrichat.com").replace(/\/$/, "");
60705
- const client = new _JefriClient(server);
60704
+ const server2 = (opts.server ?? "https://jefrichat.com").replace(/\/$/, "");
60705
+ const client = new _JefriClient(server2);
60706
60706
  let token = opts.token;
60707
60707
  if (!token) {
60708
- const res = await fetch(`${server}/api/agents`, {
60708
+ const res = await fetch(`${server2}/api/agents`, {
60709
60709
  method: "POST",
60710
60710
  headers: { "content-type": "application/json" },
60711
60711
  body: JSON.stringify({
@@ -60976,6 +60976,28 @@ var JefriClient = class _JefriClient {
60976
60976
  announceHost(host) {
60977
60977
  const h = (host ?? "").trim();
60978
60978
  this.clientInfo = { ...this.clientInfo ?? { name: "jefri-sdk", version: "0" }, host: h };
60979
+ this.sendClientInfo();
60980
+ }
60981
+ /** Announce WHERE this connector runs — the folder and the machine.
60982
+ *
60983
+ * Separate from announceHost because the two are learned at different times
60984
+ * and by different means: the host app arrives from the MCP handshake, the
60985
+ * location is simply observed. Callers pass values they READ NOW
60986
+ * (process.cwd(), os.hostname()) — never a configured preference, which is
60987
+ * what made the old reported folder wrong. Omitted fields are left untouched
60988
+ * by the hub rather than cleared, so a caller that knows only one may send
60989
+ * only that one. */
60990
+ announceWhere(where) {
60991
+ const base = this.clientInfo ?? { name: "jefri-sdk", version: "0" };
60992
+ const next = { ...base };
60993
+ if (where.cwd !== void 0) next.cwd = (where.cwd ?? "").trim();
60994
+ if (where.machine !== void 0) next.machine = (where.machine ?? "").trim();
60995
+ this.clientInfo = next;
60996
+ this.sendClientInfo();
60997
+ }
60998
+ /** Push the current clientInfo if the socket is live. Reconnects re-send it
60999
+ * from the stored copy, so a failure here is not worth reporting. */
61000
+ sendClientInfo() {
60979
61001
  try {
60980
61002
  if (this.ws?.readyState === this.ws?.OPEN)
60981
61003
  this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
@@ -61983,7 +62005,21 @@ function waitFor(c, event, match, timeoutMs = 4e3) {
61983
62005
  }, timeoutMs);
61984
62006
  });
61985
62007
  }
61986
- var ok3 = (text5) => ({ content: [{ type: "text", text: text5 }] });
62008
+ var upgradeNotice = null;
62009
+ var upgradeShownAt = 0;
62010
+ var UPGRADE_REPEAT_MS = 24 * 60 * 60 * 1e3;
62011
+ function takeUpgradeNotice(now = Date.now()) {
62012
+ if (!upgradeNotice) return null;
62013
+ if (upgradeShownAt && now - upgradeShownAt < UPGRADE_REPEAT_MS) return null;
62014
+ upgradeShownAt = now;
62015
+ return upgradeNotice;
62016
+ }
62017
+ var ok3 = (text5, now = Date.now()) => {
62018
+ const notice = takeUpgradeNotice(now);
62019
+ return { content: [{ type: "text", text: notice ? `${text5}
62020
+
62021
+ ${notice}` : text5 }] };
62022
+ };
61987
62023
  var fail = (text5) => ({ content: [{ type: "text", text: text5 }], isError: true });
61988
62024
  var WATERMARK_FILE = np3.join(os4.homedir(), ".jefri", "inbox.json");
61989
62025
  var inboxWatermark = new Map(
@@ -62002,9 +62038,9 @@ function saveWatermark() {
62002
62038
  } catch {
62003
62039
  }
62004
62040
  }
62005
- function registerAcpTools(server, ctx) {
62041
+ function registerAcpTools(server2, ctx) {
62006
62042
  const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
62007
- const e2eServer = ENABLE_E2E ? server : { registerTool: () => void 0 };
62043
+ const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
62008
62044
  const withClient = async (fn) => {
62009
62045
  try {
62010
62046
  const c = await ctx.ensureClient();
@@ -62014,7 +62050,7 @@ function registerAcpTools(server, ctx) {
62014
62050
  ${e?.message ?? e}`);
62015
62051
  }
62016
62052
  };
62017
- server.registerTool(
62053
+ server2.registerTool(
62018
62054
  "jefri_whoami",
62019
62055
  {
62020
62056
  title: "Who am I on Jefri Chat",
@@ -62025,7 +62061,7 @@ ${e?.message ?? e}`);
62025
62061
  async (c) => ok3(`You are "${c.identity.displayName}" (@${c.identity.username}) on Jefri Chat at ${ctx.serverUrl}, status: online.`)
62026
62062
  )
62027
62063
  );
62028
- server.registerTool(
62064
+ server2.registerTool(
62029
62065
  "jefri_agents",
62030
62066
  {
62031
62067
  title: "List your connections",
@@ -62061,7 +62097,7 @@ ${e?.message ?? e}`);
62061
62097
  }
62062
62098
  return { status: res.status, body };
62063
62099
  };
62064
- server.registerTool(
62100
+ server2.registerTool(
62065
62101
  "jefri_agent_apps",
62066
62102
  {
62067
62103
  title: "See where your agents are running",
@@ -62085,7 +62121,7 @@ ${lines.join("\n")}
62085
62121
  Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62086
62122
  })
62087
62123
  );
62088
- server.registerTool(
62124
+ server2.registerTool(
62089
62125
  "jefri_deactivate",
62090
62126
  {
62091
62127
  title: "Deactivate an agent",
@@ -62098,7 +62134,7 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62098
62134
  return ok3(`\u{1F6D1} Deactivated @${agent}. It's disconnected and can't reconnect from any app until you reactivate it.`);
62099
62135
  })
62100
62136
  );
62101
- server.registerTool(
62137
+ server2.registerTool(
62102
62138
  "jefri_activate",
62103
62139
  {
62104
62140
  title: "Reactivate an agent",
@@ -62111,7 +62147,7 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62111
62147
  return ok3(`\u2705 Reactivated @${agent}. It can connect again now.`);
62112
62148
  })
62113
62149
  );
62114
- server.registerTool(
62150
+ server2.registerTool(
62115
62151
  "jefri_send",
62116
62152
  {
62117
62153
  title: "Send a message",
@@ -62264,7 +62300,7 @@ ${fp}`);
62264
62300
  return ok3(`Sent private E2E group file.`);
62265
62301
  })
62266
62302
  );
62267
- server.registerTool(
62303
+ server2.registerTool(
62268
62304
  "jefri_send_file",
62269
62305
  {
62270
62306
  title: "Send a file, PDF, or image",
@@ -62348,7 +62384,7 @@ ${psCmd}
62348
62384
  return ok3(`Sent \u{1F4CE} ${name} to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
62349
62385
  })
62350
62386
  );
62351
- server.registerTool(
62387
+ server2.registerTool(
62352
62388
  "jefri_send_code",
62353
62389
  {
62354
62390
  title: "Send code / text as a file",
@@ -62383,7 +62419,7 @@ ${psCmd}
62383
62419
  return ok3(`Sent \u{1F4C4} ${fileName} (${content3.length} chars) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
62384
62420
  })
62385
62421
  );
62386
- server.registerTool(
62422
+ server2.registerTool(
62387
62423
  "jefri_send_folder",
62388
62424
  {
62389
62425
  title: "Send a whole folder / project (zipped)",
@@ -62481,7 +62517,7 @@ ${psCmd}
62481
62517
  );
62482
62518
  })
62483
62519
  );
62484
- server.registerTool(
62520
+ server2.registerTool(
62485
62521
  "jefri_download_file",
62486
62522
  {
62487
62523
  title: "Download / save a file someone sent you",
@@ -62528,7 +62564,7 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62528
62564
  );
62529
62565
  })
62530
62566
  );
62531
- server.registerTool(
62567
+ server2.registerTool(
62532
62568
  "jefri_connect",
62533
62569
  {
62534
62570
  title: "Request a connection",
@@ -62544,7 +62580,7 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62544
62580
  })
62545
62581
  );
62546
62582
  const hubBase = () => ctx.serverUrl.replace(/\/$/, "");
62547
- server.registerTool(
62583
+ server2.registerTool(
62548
62584
  "jefri_departments",
62549
62585
  {
62550
62586
  title: "List your departments",
@@ -62558,7 +62594,7 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62558
62594
  return ok3(d.map((x) => `\u2022 ${x.name} (${x.org}) \u2014 access: ${x.myLevel} [id: ${x.id}]`).join("\n"));
62559
62595
  })
62560
62596
  );
62561
- server.registerTool(
62597
+ server2.registerTool(
62562
62598
  "jefri_dept_files",
62563
62599
  {
62564
62600
  title: "List a department's files",
@@ -62573,7 +62609,7 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62573
62609
  return ok3(files.map((f) => `\u2022 ${f.name} [${f.kind}] \u2014 by ${f.createdBy}, ${new Date(f.updatedAt).toLocaleDateString()} (id: ${f.id})`).join("\n"));
62574
62610
  })
62575
62611
  );
62576
- server.registerTool(
62612
+ server2.registerTool(
62577
62613
  "jefri_dept_read",
62578
62614
  {
62579
62615
  title: "Download a department file",
@@ -62603,7 +62639,7 @@ ${cmd}
62603
62639
  (Set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`);
62604
62640
  })
62605
62641
  );
62606
- server.registerTool(
62642
+ server2.registerTool(
62607
62643
  "jefri_dept_add_file",
62608
62644
  {
62609
62645
  title: "Add a file to a department",
@@ -62626,7 +62662,7 @@ ${cmd}
62626
62662
  (With 'write' access it's added directly; with 'propose' access it's submitted for an admin to approve; read-only is rejected.)`);
62627
62663
  })
62628
62664
  );
62629
- server.registerTool(
62665
+ server2.registerTool(
62630
62666
  "jefri_my_docs",
62631
62667
  {
62632
62668
  title: "List my documents (memory)",
@@ -62645,7 +62681,7 @@ ${cmd}
62645
62681
  );
62646
62682
  })
62647
62683
  );
62648
- server.registerTool(
62684
+ server2.registerTool(
62649
62685
  "jefri_search_docs",
62650
62686
  {
62651
62687
  title: "Search my document memory",
@@ -62668,7 +62704,7 @@ ${h.text}`).join("\n\n---\n\n")
62668
62704
  );
62669
62705
  })
62670
62706
  );
62671
- server.registerTool(
62707
+ server2.registerTool(
62672
62708
  "jefri_read_doc",
62673
62709
  {
62674
62710
  title: "Read/get one of my documents",
@@ -62693,7 +62729,7 @@ ${cmd}
62693
62729
  (Set your token first: export JEFRI_TOKEN=\u2026 from the app's Connect dialog \u2014 it isn't printed here on purpose.)`);
62694
62730
  })
62695
62731
  );
62696
- server.registerTool(
62732
+ server2.registerTool(
62697
62733
  "jefri_add_doc",
62698
62734
  {
62699
62735
  title: "Add a document to my memory",
@@ -62713,7 +62749,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62713
62749
  );
62714
62750
  })
62715
62751
  );
62716
- server.registerTool(
62752
+ server2.registerTool(
62717
62753
  "jefri_my_tasks",
62718
62754
  {
62719
62755
  title: "My department tasks",
@@ -62730,7 +62766,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62730
62766
  );
62731
62767
  })
62732
62768
  );
62733
- server.registerTool(
62769
+ server2.registerTool(
62734
62770
  "jefri_task_status",
62735
62771
  {
62736
62772
  title: "Update a task's status",
@@ -62752,7 +62788,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62752
62788
  return ok3(`Task marked ${status === "done" ? "\u2705 done" : status}.`);
62753
62789
  })
62754
62790
  );
62755
- server.registerTool(
62791
+ server2.registerTool(
62756
62792
  "jefri_inbox",
62757
62793
  {
62758
62794
  title: "Read unread messages",
@@ -62808,7 +62844,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62808
62844
  ` + lines.join("\n"));
62809
62845
  })
62810
62846
  );
62811
- server.registerTool(
62847
+ server2.registerTool(
62812
62848
  "jefri_groups",
62813
62849
  {
62814
62850
  title: "List your groups",
@@ -62824,7 +62860,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62824
62860
  );
62825
62861
  })
62826
62862
  );
62827
- server.registerTool(
62863
+ server2.registerTool(
62828
62864
  "jefri_send_group",
62829
62865
  {
62830
62866
  title: "Send a group message",
@@ -62842,7 +62878,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62842
62878
  return ok3(`Posted to the group.`);
62843
62879
  })
62844
62880
  );
62845
- server.registerTool(
62881
+ server2.registerTool(
62846
62882
  "jefri_send_group_file",
62847
62883
  {
62848
62884
  title: "Send a file to a group",
@@ -62916,7 +62952,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62916
62952
  return ok3(`Sent \u{1F4CE} ${name} to the group.`);
62917
62953
  })
62918
62954
  );
62919
- server.registerTool(
62955
+ server2.registerTool(
62920
62956
  "jefri_history",
62921
62957
  {
62922
62958
  title: "Read conversation history",
@@ -62956,7 +62992,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62956
62992
  return ok3(lines.join("\n"));
62957
62993
  })
62958
62994
  );
62959
- server.registerTool(
62995
+ server2.registerTool(
62960
62996
  "jefri_search",
62961
62997
  {
62962
62998
  title: "Discover agents",
@@ -62974,7 +63010,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62974
63010
  );
62975
63011
  })
62976
63012
  );
62977
- server.registerTool(
63013
+ server2.registerTool(
62978
63014
  "jefri_set_status",
62979
63015
  {
62980
63016
  title: "Set presence",
@@ -62989,7 +63025,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62989
63025
  })
62990
63026
  );
62991
63027
  if (ctx.local) {
62992
- server.registerTool(
63028
+ server2.registerTool(
62993
63029
  "jefri_notifications",
62994
63030
  {
62995
63031
  title: "Notification settings",
@@ -63068,7 +63104,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63068
63104
  return ok3(summary);
63069
63105
  })
63070
63106
  );
63071
- server.registerTool(
63107
+ server2.registerTool(
63072
63108
  "jefri_autonomous",
63073
63109
  {
63074
63110
  title: "Autonomous mode",
@@ -63145,6 +63181,34 @@ function attachInbox(c, inbox, self, onIncoming) {
63145
63181
  c.on("file_received", capture);
63146
63182
  }
63147
63183
 
63184
+ // src/drain.ts
63185
+ function durationEnv(name, fallback, env = process.env) {
63186
+ const raw = env[name];
63187
+ if (raw === void 0 || raw === "") return fallback;
63188
+ const n = Number(raw);
63189
+ if (!Number.isFinite(n) || n <= 0) {
63190
+ console.error(`[mcp] ignoring ${name}="${raw}" \u2014 expected a positive number; using ${fallback}ms`);
63191
+ return fallback;
63192
+ }
63193
+ return n;
63194
+ }
63195
+ function sweepIdleSessions(map3) {
63196
+ let closed = 0;
63197
+ let busy = 0;
63198
+ for (const s of map3) {
63199
+ if (s.inflight > 0) {
63200
+ busy++;
63201
+ continue;
63202
+ }
63203
+ try {
63204
+ s.transport.close?.();
63205
+ closed++;
63206
+ } catch {
63207
+ }
63208
+ }
63209
+ return { closed, busy };
63210
+ }
63211
+
63148
63212
  // src/http.ts
63149
63213
  var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
63150
63214
  var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
@@ -63288,7 +63352,14 @@ app.use((req, res, next) => {
63288
63352
  next();
63289
63353
  });
63290
63354
  app.use(import_express.default.json({ limit: process.env.MCP_MAX_BODY ?? "4mb" }));
63291
- var health = (_req, res) => res.json({ ok: true, service: "jefrichat-mcp-http", hub: HUB, sessions: sessions.size });
63355
+ var draining = false;
63356
+ var health = (_req, res) => res.status(draining ? 503 : 200).json({
63357
+ ok: !draining,
63358
+ draining,
63359
+ service: "jefrichat-mcp-http",
63360
+ hub: HUB,
63361
+ sessions: sessions.size
63362
+ });
63292
63363
  app.get("/health", health);
63293
63364
  app.get("/mcp/health", health);
63294
63365
  app.post("/mcp", async (req, res) => {
@@ -63466,13 +63537,52 @@ var bySession = async (req, res) => {
63466
63537
  };
63467
63538
  app.get("/mcp", bySession);
63468
63539
  app.delete("/mcp", bySession);
63469
- http.createServer(app).listen(PORT, () => {
63540
+ var server = http.createServer(app).listen(PORT, () => {
63470
63541
  console.error(`
63471
63542
  \u{1F310} Jefri Chat remote MCP (HTTP) on http://localhost:${PORT}/mcp`);
63472
63543
  console.error(` hub: ${HUB}`);
63473
63544
  console.error(` auth: Authorization: Bearer <jefri_token>
63474
63545
  `);
63475
63546
  });
63547
+ var DRAIN_MS = durationEnv("MCP_DRAIN_MS", 15e3);
63548
+ var UNHEALTHY_MS = durationEnv("MCP_UNHEALTHY_MS", 5e3);
63549
+ var shuttingDown = false;
63550
+ function drain2(signal) {
63551
+ if (shuttingDown) return;
63552
+ shuttingDown = true;
63553
+ draining = true;
63554
+ const deadline = Date.now() + UNHEALTHY_MS + DRAIN_MS;
63555
+ console.error(
63556
+ `[mcp] ${signal} \u2014 unhealthy for ${UNHEALTHY_MS}ms, then draining ${sessions.size} session(s) for up to ${DRAIN_MS}ms`
63557
+ );
63558
+ const closeAndFinish = () => {
63559
+ server.close(() => {
63560
+ console.error("[mcp] drained cleanly");
63561
+ process.exit(0);
63562
+ });
63563
+ const sweep = setInterval(() => {
63564
+ const { busy } = sweepIdleSessions(sessions.values());
63565
+ if (busy === 0) {
63566
+ clearInterval(sweep);
63567
+ console.error("[mcp] all sessions idle \u2014 closing");
63568
+ return;
63569
+ }
63570
+ if (Date.now() >= deadline) {
63571
+ clearInterval(sweep);
63572
+ console.error(`[mcp] drain deadline reached with ${busy} session(s) still busy \u2014 exiting`);
63573
+ process.exit(0);
63574
+ }
63575
+ }, 250);
63576
+ sweep.unref();
63577
+ };
63578
+ setTimeout(closeAndFinish, UNHEALTHY_MS).unref();
63579
+ setTimeout(() => {
63580
+ console.error("[mcp] drain deadline reached \u2014 exiting");
63581
+ process.exit(0);
63582
+ }, UNHEALTHY_MS + DRAIN_MS + 500).unref();
63583
+ }
63584
+ process.on("SIGTERM", () => drain2("SIGTERM"));
63585
+ process.on("SIGINT", () => drain2("SIGINT"));
63476
63586
  /*! Bundled license information:
63477
63587
 
63478
63588
  depd/index.js:
package/dist/index.js CHANGED
@@ -10847,8 +10847,10 @@ var init_control_proto = __esm({
10847
10847
  // {agent?, ids?} → {unread: UnreadTally}
10848
10848
  "unread",
10849
10849
  // {} → UnreadTally
10850
- "subscribe"
10850
+ "subscribe",
10851
10851
  // {} → {subscribed: true}; server then pushes EventFrames
10852
+ "history"
10853
+ // {agent, with?|groupId?, limit?} → {messages: HistoryMessage[]}
10852
10854
  ];
10853
10855
  METHOD_SET = new Set(METHODS);
10854
10856
  FrameDecoder = class {
@@ -11441,6 +11443,17 @@ async function openSession() {
11441
11443
  return {
11442
11444
  conns,
11443
11445
  agents: () => conns.flatMap((c2) => c2.agents),
11446
+ async history(agent, q) {
11447
+ const conn = conns.find((c2) => c2.agents.some((a) => a.username === agent)) ?? conns[0];
11448
+ if (!conn) return [];
11449
+ const res = await conn.request("history", {
11450
+ agent,
11451
+ with: q.with,
11452
+ groupId: q.groupId,
11453
+ limit: q.limit ?? 50
11454
+ });
11455
+ return res.messages ?? [];
11456
+ },
11444
11457
  async inbox(limit = 50) {
11445
11458
  const per = await Promise.allSettled(
11446
11459
  conns.map((c2) => c2.request("inbox", { limit }))
@@ -12104,9 +12117,9 @@ __export(run_exports, {
12104
12117
  tmuxPanelSetupArgv: () => tmuxPanelSetupArgv,
12105
12118
  tmuxRunArgv: () => tmuxRunArgv
12106
12119
  });
12107
- import cp7 from "node:child_process";
12120
+ import cp9 from "node:child_process";
12108
12121
  import { createHash as createHash2 } from "node:crypto";
12109
- import readline from "node:readline";
12122
+ import readline2 from "node:readline";
12110
12123
  function tmuxRunArgv(agentCmd, sessionName, cwd, envPairs = []) {
12111
12124
  const env = envPairs.flatMap((e) => ["-e", e]);
12112
12125
  return { cmd: "tmux", args: ["new-session", "-d", "-s", sessionName, "-c", cwd, ...env, "--", ...agentCmd] };
@@ -12157,7 +12170,7 @@ function tmuxPanelSetupArgv(session, launcher, badgeScript) {
12157
12170
  }
12158
12171
  function hasTmux() {
12159
12172
  try {
12160
- cp7.execFileSync("tmux", ["-V"], { stdio: "ignore" });
12173
+ cp9.execFileSync("tmux", ["-V"], { stdio: "ignore" });
12161
12174
  return true;
12162
12175
  } catch {
12163
12176
  return false;
@@ -12175,9 +12188,9 @@ function resolveTmuxInstall(platform, has = hasCmd2) {
12175
12188
  const command = mgr === "pacman" ? "sudo pacman -S tmux" : mgr === "apk" ? "sudo apk add tmux" : mgr ? `sudo ${mgr} install tmux` : "install tmux with your distro's package manager";
12176
12189
  return { kind: "linux-manual", command };
12177
12190
  }
12178
- async function promptYesNoDefaultYes(question) {
12191
+ async function promptYesNoDefaultYes2(question) {
12179
12192
  if (!process.stdin.isTTY) return false;
12180
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
12193
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
12181
12194
  try {
12182
12195
  const ans = (await new Promise((res) => rl.question(question, res))).trim().toLowerCase();
12183
12196
  return ans === "" || ans === "y" || ans === "yes";
@@ -12196,7 +12209,7 @@ async function ensureTmuxInteractive() {
12196
12209
  `
12197
12210
  );
12198
12211
  if (plan.kind === "brew") {
12199
- const yes = await promptYesNoDefaultYes(` Install tmux now with Homebrew? (no password / no sudo) [Y/n] `);
12212
+ const yes = await promptYesNoDefaultYes2(` Install tmux now with Homebrew? (no password / no sudo) [Y/n] `);
12200
12213
  if (!yes) {
12201
12214
  process.stderr.write(` No problem \u2014 install it any time with: ${plan.command}
12202
12215
  `);
@@ -12205,7 +12218,7 @@ async function ensureTmuxInteractive() {
12205
12218
  process.stderr.write(` Installing tmux via Homebrew\u2026
12206
12219
  `);
12207
12220
  try {
12208
- cp7.execFileSync("brew", ["install", "tmux"], { stdio: "inherit" });
12221
+ cp9.execFileSync("brew", ["install", "tmux"], { stdio: "inherit" });
12209
12222
  } catch {
12210
12223
  process.stderr.write(` \u26A0\uFE0F 'brew install tmux' didn't finish \u2014 install it manually (${plan.command}), then re-run this command.
12211
12224
  `);
@@ -12236,7 +12249,7 @@ async function runWrapped(argv) {
12236
12249
  return 2;
12237
12250
  }
12238
12251
  const spawnInherit = (cmd2, args2) => new Promise((res) => {
12239
- const child = cp7.spawn(cmd2, args2, { stdio: "inherit" });
12252
+ const child = cp9.spawn(cmd2, args2, { stdio: "inherit" });
12240
12253
  child.on("error", (e) => {
12241
12254
  process.stderr.write(`jefri run: could not start ${cmd2}: ${e?.message ?? e}
12242
12255
  `);
@@ -12287,21 +12300,21 @@ async function runWrapped(argv) {
12287
12300
  const cwd = process.cwd();
12288
12301
  const name = sessionNameFor(agentCmd, cwd, sessionOptedIn);
12289
12302
  const { cmd, args } = tmuxRunArgv(agentCmd, name, cwd, sessionOptedIn ? ["JEFRI_EXPERIMENTAL_SESSION=1"] : []);
12290
- cp7.spawnSync(cmd, args, { stdio: "ignore" });
12291
- cp7.spawnSync("tmux", ["set-option", "-t", name, "mouse", "on"], { stdio: "ignore" });
12292
- cp7.spawnSync("tmux", ["set-option", "-t", name, "set-clipboard", "on"], { stdio: "ignore" });
12293
- cp7.spawnSync("tmux", ["set-option", "-t", name, "history-limit", "50000"], { stdio: "ignore" });
12303
+ cp9.spawnSync(cmd, args, { stdio: "ignore" });
12304
+ cp9.spawnSync("tmux", ["set-option", "-t", name, "mouse", "on"], { stdio: "ignore" });
12305
+ cp9.spawnSync("tmux", ["set-option", "-t", name, "set-clipboard", "on"], { stdio: "ignore" });
12306
+ cp9.spawnSync("tmux", ["set-option", "-t", name, "history-limit", "50000"], { stdio: "ignore" });
12294
12307
  for (const table of ["copy-mode", "copy-mode-vi"]) {
12295
- cp7.spawnSync("tmux", ["bind-key", "-T", table, "WheelUpPane", "send-keys", "-N2", "-X", "scroll-up"], { stdio: "ignore" });
12296
- cp7.spawnSync("tmux", ["bind-key", "-T", table, "WheelDownPane", "send-keys", "-N2", "-X", "scroll-down"], { stdio: "ignore" });
12308
+ cp9.spawnSync("tmux", ["bind-key", "-T", table, "WheelUpPane", "send-keys", "-N2", "-X", "scroll-up"], { stdio: "ignore" });
12309
+ cp9.spawnSync("tmux", ["bind-key", "-T", table, "WheelDownPane", "send-keys", "-N2", "-X", "scroll-down"], { stdio: "ignore" });
12297
12310
  if (process.platform === "darwin")
12298
- cp7.spawnSync("tmux", ["bind-key", "-T", table, "MouseDragEnd1Pane", "send-keys", "-X", "copy-pipe-and-cancel", "pbcopy"], { stdio: "ignore" });
12311
+ cp9.spawnSync("tmux", ["bind-key", "-T", table, "MouseDragEnd1Pane", "send-keys", "-X", "copy-pipe-and-cancel", "pbcopy"], { stdio: "ignore" });
12299
12312
  }
12300
12313
  try {
12301
12314
  const launcher = writePanelLauncher(`${selfCommand()} panel`);
12302
12315
  const badge = writeBadgeScript(UNREAD_DIR, AGENTS_DIR);
12303
12316
  for (const { cmd: c2, args: a2 } of tmuxPanelSetupArgv(name, launcher, badge))
12304
- cp7.spawnSync(c2, a2, { stdio: "ignore" });
12317
+ cp9.spawnSync(c2, a2, { stdio: "ignore" });
12305
12318
  process.stderr.write(` \u{1F4EC} Jefri panel: press Ctrl-b then j (or click the \u{1F4EC} badge) to read + reply.
12306
12319
 
12307
12320
  `);
@@ -12333,7 +12346,7 @@ __export(panel_exports, {
12333
12346
  runPanel: () => runPanel,
12334
12347
  truncateToWidth: () => truncateToWidth
12335
12348
  });
12336
- import readline2 from "node:readline/promises";
12349
+ import readline3 from "node:readline/promises";
12337
12350
  import { stdin as input, stdout as output } from "node:process";
12338
12351
  function ago(at, now = Date.now()) {
12339
12352
  const s = Math.max(0, Math.round((now - at) / 1e3));
@@ -12342,15 +12355,15 @@ function ago(at, now = Date.now()) {
12342
12355
  if (s < 86400) return `${Math.round(s / 3600)}h`;
12343
12356
  return `${Math.round(s / 86400)}d`;
12344
12357
  }
12345
- function charWidth(cp8) {
12346
- if (cp8 >= 768 && cp8 <= 879 || cp8 >= 6832 && cp8 <= 6911 || cp8 >= 7616 && cp8 <= 7679 || cp8 >= 8400 && cp8 <= 8432 || cp8 >= 65056 && cp8 <= 65071 || cp8 >= 8203 && cp8 <= 8205 || cp8 >= 65024 && cp8 <= 65039 || cp8 >= 917536 && cp8 <= 917631 || cp8 >= 917760 && cp8 <= 917999) return 0;
12347
- const wide = cp8 >= 4352 && cp8 <= 4447 || // Hangul Jamo
12348
- cp8 >= 11904 && cp8 <= 42191 && cp8 !== 12351 || // CJK radicals … Yi
12349
- cp8 >= 44032 && cp8 <= 55203 || // Hangul syllables
12350
- cp8 >= 63744 && cp8 <= 64255 || // CJK compatibility ideographs
12351
- cp8 >= 65040 && cp8 <= 65049 || cp8 >= 65072 && cp8 <= 65135 || cp8 >= 65280 && cp8 <= 65376 || cp8 >= 65504 && cp8 <= 65510 || cp8 >= 127462 && cp8 <= 127487 || // regional indicators (flags)
12352
- cp8 >= 127744 && cp8 <= 129791 || // every modern emoji block
12353
- cp8 >= 131072 && cp8 <= 262141;
12358
+ function charWidth(cp10) {
12359
+ if (cp10 >= 768 && cp10 <= 879 || cp10 >= 6832 && cp10 <= 6911 || cp10 >= 7616 && cp10 <= 7679 || cp10 >= 8400 && cp10 <= 8432 || cp10 >= 65056 && cp10 <= 65071 || cp10 >= 8203 && cp10 <= 8205 || cp10 >= 65024 && cp10 <= 65039 || cp10 >= 917536 && cp10 <= 917631 || cp10 >= 917760 && cp10 <= 917999) return 0;
12360
+ const wide = cp10 >= 4352 && cp10 <= 4447 || // Hangul Jamo
12361
+ cp10 >= 11904 && cp10 <= 42191 && cp10 !== 12351 || // CJK radicals … Yi
12362
+ cp10 >= 44032 && cp10 <= 55203 || // Hangul syllables
12363
+ cp10 >= 63744 && cp10 <= 64255 || // CJK compatibility ideographs
12364
+ cp10 >= 65040 && cp10 <= 65049 || cp10 >= 65072 && cp10 <= 65135 || cp10 >= 65280 && cp10 <= 65376 || cp10 >= 65504 && cp10 <= 65510 || cp10 >= 127462 && cp10 <= 127487 || // regional indicators (flags)
12365
+ cp10 >= 127744 && cp10 <= 129791 || // every modern emoji block
12366
+ cp10 >= 131072 && cp10 <= 262141;
12354
12367
  return wide ? 2 : 1;
12355
12368
  }
12356
12369
  function clusters(s) {
@@ -12476,7 +12489,7 @@ async function runPanel() {
12476
12489
  dirty = true;
12477
12490
  });
12478
12491
  await session.subscribe();
12479
- const rl = readline2.createInterface({ input, output });
12492
+ const rl = readline3.createInterface({ input, output });
12480
12493
  const prompter = new Prompter(rl);
12481
12494
  let messages = [];
12482
12495
  const refresh = async () => {
@@ -38929,6 +38942,28 @@ var JefriClient = class _JefriClient {
38929
38942
  announceHost(host) {
38930
38943
  const h = (host ?? "").trim();
38931
38944
  this.clientInfo = { ...this.clientInfo ?? { name: "jefri-sdk", version: "0" }, host: h };
38945
+ this.sendClientInfo();
38946
+ }
38947
+ /** Announce WHERE this connector runs — the folder and the machine.
38948
+ *
38949
+ * Separate from announceHost because the two are learned at different times
38950
+ * and by different means: the host app arrives from the MCP handshake, the
38951
+ * location is simply observed. Callers pass values they READ NOW
38952
+ * (process.cwd(), os.hostname()) — never a configured preference, which is
38953
+ * what made the old reported folder wrong. Omitted fields are left untouched
38954
+ * by the hub rather than cleared, so a caller that knows only one may send
38955
+ * only that one. */
38956
+ announceWhere(where) {
38957
+ const base = this.clientInfo ?? { name: "jefri-sdk", version: "0" };
38958
+ const next = { ...base };
38959
+ if (where.cwd !== void 0) next.cwd = (where.cwd ?? "").trim();
38960
+ if (where.machine !== void 0) next.machine = (where.machine ?? "").trim();
38961
+ this.clientInfo = next;
38962
+ this.sendClientInfo();
38963
+ }
38964
+ /** Push the current clientInfo if the socket is live. Reconnects re-send it
38965
+ * from the stored copy, so a failure here is not worth reporting. */
38966
+ sendClientInfo() {
38932
38967
  try {
38933
38968
  if (this.ws?.readyState === this.ws?.OPEN)
38934
38969
  this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
@@ -39590,31 +39625,90 @@ function configureAuto(opts) {
39590
39625
  }
39591
39626
  var watcher = null;
39592
39627
  var watchDebounce = null;
39628
+ var watchRetry = null;
39629
+ var pollTimer = null;
39630
+ var pollSig = "";
39631
+ var watchAttempt = 0;
39632
+ var POLL_MS = 3e3;
39633
+ function applyConfigChange() {
39634
+ const wasEnabled = cfg.enabled;
39635
+ cfg = loadCfg();
39636
+ if (cfg.enabled !== wasEnabled) logLine(`config reloaded: enabled ${wasEnabled} \u2192 ${cfg.enabled}`);
39637
+ if (cfg.enabled) acquireResponderLock();
39638
+ else {
39639
+ releaseResponderLock();
39640
+ stopAllWork();
39641
+ }
39642
+ ensureIntervalTimer();
39643
+ }
39644
+ function configPath() {
39645
+ return np3.join(AUTO_DIR, `${safeUser(selfName)}.json`);
39646
+ }
39647
+ function configSignature() {
39648
+ try {
39649
+ const st = fs4.statSync(configPath());
39650
+ return `${st.mtimeMs}:${st.size}`;
39651
+ } catch {
39652
+ return "";
39653
+ }
39654
+ }
39655
+ function startPolling(why) {
39656
+ if (pollTimer) return;
39657
+ pollSig = configSignature();
39658
+ logLine(`config hot-reload switched to polling every ${POLL_MS / 1e3}s (${why}) \u2014 no descriptors needed`);
39659
+ pollTimer = setInterval(() => {
39660
+ const sig = configSignature();
39661
+ if (sig === pollSig) return;
39662
+ pollSig = sig;
39663
+ applyConfigChange();
39664
+ }, POLL_MS);
39665
+ pollTimer.unref?.();
39666
+ }
39667
+ function stopPolling() {
39668
+ if (!pollTimer) return;
39669
+ clearInterval(pollTimer);
39670
+ pollTimer = null;
39671
+ }
39593
39672
  function watchConfig() {
39594
39673
  try {
39595
39674
  watcher?.close();
39596
39675
  } catch {
39597
39676
  }
39677
+ watcher = null;
39678
+ if (watchRetry) {
39679
+ clearTimeout(watchRetry);
39680
+ watchRetry = null;
39681
+ }
39682
+ const degrade = (why) => {
39683
+ startPolling(why);
39684
+ const delay = Math.min(3e4 * 2 ** watchAttempt++, 3e5);
39685
+ watchRetry = setTimeout(watchConfig, delay);
39686
+ watchRetry.unref?.();
39687
+ };
39598
39688
  try {
39599
39689
  fs4.mkdirSync(AUTO_DIR, { recursive: true });
39600
39690
  const file = `${safeUser(selfName)}.json`;
39601
- watcher = fs4.watch(AUTO_DIR, { persistent: false }, (_evt, name) => {
39691
+ const w = fs4.watch(AUTO_DIR, { persistent: false }, (_evt, name) => {
39602
39692
  if (name && name !== file) return;
39603
39693
  if (watchDebounce) clearTimeout(watchDebounce);
39604
- watchDebounce = setTimeout(() => {
39605
- const wasEnabled = cfg.enabled;
39606
- cfg = loadCfg();
39607
- if (cfg.enabled !== wasEnabled) logLine(`config reloaded: enabled ${wasEnabled} \u2192 ${cfg.enabled}`);
39608
- if (cfg.enabled) acquireResponderLock();
39609
- else {
39610
- releaseResponderLock();
39611
- stopAllWork();
39612
- }
39613
- ensureIntervalTimer();
39614
- }, 200);
39694
+ watchDebounce = setTimeout(applyConfigChange, 200);
39615
39695
  });
39616
- watcher.unref?.();
39617
- } catch {
39696
+ w.on("error", (err) => {
39697
+ logLine(`config watch failed (${err?.code ?? err?.message ?? err}) \u2014 falling back to polling, connector unaffected`);
39698
+ try {
39699
+ w.close();
39700
+ } catch {
39701
+ }
39702
+ if (watcher === w) watcher = null;
39703
+ degrade(String(err?.code ?? "watch error"));
39704
+ });
39705
+ watcher = w;
39706
+ watchAttempt = 0;
39707
+ stopPolling();
39708
+ w.unref?.();
39709
+ } catch (e) {
39710
+ logLine(`config watch unavailable (${e?.code ?? e?.message ?? e}) \u2014 using polling instead`);
39711
+ degrade(String(e?.code ?? "watch unavailable"));
39618
39712
  }
39619
39713
  }
39620
39714
  var LOCK_STALE_MS = 9e4;
@@ -40694,7 +40788,24 @@ function waitFor(c2, event, match, timeoutMs = 4e3) {
40694
40788
  }, timeoutMs);
40695
40789
  });
40696
40790
  }
40697
- var ok3 = (text5) => ({ content: [{ type: "text", text: text5 }] });
40791
+ var upgradeNotice = null;
40792
+ var upgradeShownAt = 0;
40793
+ var UPGRADE_REPEAT_MS = 24 * 60 * 60 * 1e3;
40794
+ function setUpgradeNotice(text5) {
40795
+ upgradeNotice = text5;
40796
+ }
40797
+ function takeUpgradeNotice(now = Date.now()) {
40798
+ if (!upgradeNotice) return null;
40799
+ if (upgradeShownAt && now - upgradeShownAt < UPGRADE_REPEAT_MS) return null;
40800
+ upgradeShownAt = now;
40801
+ return upgradeNotice;
40802
+ }
40803
+ var ok3 = (text5, now = Date.now()) => {
40804
+ const notice = takeUpgradeNotice(now);
40805
+ return { content: [{ type: "text", text: notice ? `${text5}
40806
+
40807
+ ${notice}` : text5 }] };
40808
+ };
40698
40809
  var fail = (text5) => ({ content: [{ type: "text", text: text5 }], isError: true });
40699
40810
  var WATERMARK_FILE = np4.join(os4.homedir(), ".jefri", "inbox.json");
40700
40811
  var inboxWatermark = new Map(
@@ -41856,6 +41967,9 @@ function attachInbox(c2, inbox2, self, onIncoming) {
41856
41967
  c2.on("file_received", capture);
41857
41968
  }
41858
41969
 
41970
+ // src/index.ts
41971
+ init_control_proto();
41972
+
41859
41973
  // src/control.ts
41860
41974
  init_control_proto();
41861
41975
  init_registry();
@@ -41883,6 +41997,8 @@ async function startControlServer(opts) {
41883
41997
  owner: opts.owner ?? null,
41884
41998
  host: opts.getHost(),
41885
41999
  workdir: opts.workdir,
42000
+ cwd: opts.cwd ?? null,
42001
+ machine: opts.machine ?? null,
41886
42002
  server: opts.server,
41887
42003
  online: opts.isOnline(),
41888
42004
  unread: currentTally().total,
@@ -41944,6 +42060,19 @@ async function startControlServer(opts) {
41944
42060
  case "subscribe":
41945
42061
  c2.subscribed = true;
41946
42062
  return reply(c2, okRes(id, { subscribed: true }));
42063
+ case "history": {
42064
+ if (!opts.fetchHistory) return fail3(c2, id, "unknown_method", "history is not available from this endpoint");
42065
+ const withUser = typeof params.with === "string" ? params.with : void 0;
42066
+ const groupId = typeof params.groupId === "string" ? params.groupId : void 0;
42067
+ if (!withUser && !groupId) return fail3(c2, id, "bad_request", "history needs `with` (a username) or `groupId`");
42068
+ const limit = Math.min(Math.max(Number(params.limit) || 50, 1), 200);
42069
+ try {
42070
+ const messages2 = await opts.fetchHistory({ with: withUser, groupId, limit });
42071
+ return reply(c2, okRes(id, { messages: messages2 }));
42072
+ } catch (e) {
42073
+ return fail3(c2, id, "internal", `history failed: ${e?.message ?? e}`);
42074
+ }
42075
+ }
41947
42076
  case "inbox": {
41948
42077
  const limit = Math.min(Math.max(Number(params.limit) || 50, 1), MAX_STORED);
41949
42078
  return reply(c2, okRes(id, { messages: messages.slice(-limit).reverse() }));
@@ -42059,7 +42188,14 @@ async function startControlServer(opts) {
42059
42188
  pid: process.pid,
42060
42189
  server: opts.server,
42061
42190
  host: opts.getHost(),
42191
+ // Both, and clearly distinct. `workdir` is the brain's configured folder;
42192
+ // `cwd` is where this process runs. The descriptor previously carried only
42193
+ // `workdir` under a name every reader took to mean "where it runs" — which
42194
+ // is how a stale setting ended up being handed out as the answer to "where
42195
+ // do I go to restart this?".
42062
42196
  workdir: opts.workdir,
42197
+ cwd: opts.cwd ?? null,
42198
+ machine: opts.machine ?? null,
42063
42199
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
42064
42200
  });
42065
42201
  writeUnreadFor(self, 0);
@@ -42139,9 +42275,150 @@ async function startControlServer(opts) {
42139
42275
  init_popup();
42140
42276
 
42141
42277
  // src/doctor.ts
42142
- import cp6 from "node:child_process";
42278
+ import cp8 from "node:child_process";
42143
42279
  init_version();
42144
42280
  init_banner();
42281
+
42282
+ // src/fdcheck.ts
42283
+ import cp6 from "node:child_process";
42284
+ function labelFor(command) {
42285
+ if (/tsx[/\\]dist[/\\]cli\.mjs\s+watch/.test(command)) return "tsx watch dev server";
42286
+ if (/next(-server)?\b.*\bdev\b/.test(command) || /next dev/.test(command)) return "next dev server";
42287
+ if (/nodemon/.test(command)) return "nodemon";
42288
+ if (/webpack|vite/.test(command)) return "bundler dev server";
42289
+ if (/jefrichat-mcp/.test(command)) return "jefri connector";
42290
+ const bin = command.split(/\s+/)[1] ?? command.split(/\s+/)[0] ?? command;
42291
+ return bin.split("/").pop() || "process";
42292
+ }
42293
+ function fdAdvice(hogs, opts) {
42294
+ if (!hogs.length) return null;
42295
+ const total = hogs.reduce((n, h) => n + h.fds, 0);
42296
+ const byLabel = /* @__PURE__ */ new Map();
42297
+ for (const h of hogs) {
42298
+ const cur = byLabel.get(h.label) ?? { count: 0, fds: 0 };
42299
+ byLabel.set(h.label, { count: cur.count + 1, fds: cur.fds + h.fds });
42300
+ }
42301
+ const worst = [...byLabel.entries()].sort((a, b) => b[1].fds - a[1].fds)[0];
42302
+ const [label, agg] = worst;
42303
+ const manyDuplicates = agg.count >= 4;
42304
+ const heavy = total > 2e4 || agg.fds > opts.softLimit * 4;
42305
+ if (!manyDuplicates && !heavy) return null;
42306
+ const plural = agg.count === 1 ? "" : "es";
42307
+ return `${agg.count} "${label}" process${plural} are holding ~${agg.fds.toLocaleString()} open files. File watching (and eventually new connections) can fail with EMFILE while they run.`;
42308
+ }
42309
+ function probeFdHogs(limit = 40) {
42310
+ if (process.platform === "win32") return [];
42311
+ try {
42312
+ const out = cp6.execSync("lsof -n -P 2>/dev/null | awk 'NR>1 {print $2}' | sort | uniq -c | sort -rn | head -" + limit, {
42313
+ encoding: "utf8",
42314
+ timeout: 8e3,
42315
+ maxBuffer: 8 * 1024 * 1024
42316
+ });
42317
+ const hogs = [];
42318
+ for (const line of out.split("\n")) {
42319
+ const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
42320
+ if (!m) continue;
42321
+ const fds = Number(m[1]);
42322
+ const pid = Number(m[2]);
42323
+ if (fds < 500) continue;
42324
+ let command = "";
42325
+ try {
42326
+ command = cp6.execSync(`ps -p ${pid} -o command=`, { encoding: "utf8", timeout: 2e3 }).trim();
42327
+ } catch {
42328
+ continue;
42329
+ }
42330
+ if (!command) continue;
42331
+ hogs.push({ pid, fds, label: labelFor(command) });
42332
+ }
42333
+ return hogs;
42334
+ } catch {
42335
+ return [];
42336
+ }
42337
+ }
42338
+ function softFdLimit() {
42339
+ try {
42340
+ const n = Number(cp6.execSync("ulimit -n", { encoding: "utf8", shell: "/bin/sh", timeout: 2e3 }).trim());
42341
+ return Number.isFinite(n) && n > 0 ? n : 256;
42342
+ } catch {
42343
+ return 256;
42344
+ }
42345
+ }
42346
+
42347
+ // src/setup.ts
42348
+ init_which();
42349
+ import cp7 from "node:child_process";
42350
+ import readline from "node:readline";
42351
+ var LINUX_PKG = { "terminal-notifier": "" };
42352
+ function resolveInstall(tool, platform, has = isOnPath) {
42353
+ if (has(tool)) return { kind: "present", command: "" };
42354
+ if (platform === "darwin") {
42355
+ return has("brew") ? { kind: "brew", command: `brew install ${tool}` } : { kind: "brew-missing", command: `install Homebrew first (https://brew.sh), then: brew install ${tool}` };
42356
+ }
42357
+ if (LINUX_PKG[tool] === "") {
42358
+ return { kind: "windows", command: `${tool} is macOS-only \u2014 notifications on this platform are informational` };
42359
+ }
42360
+ if (platform === "win32") {
42361
+ return { kind: "windows", command: `${tool} has no native Windows build \u2014 use WSL, or skip it` };
42362
+ }
42363
+ const mgr = ["apt-get", "apt", "dnf", "yum", "pacman", "zypper", "apk"].find(has);
42364
+ const command = mgr === "pacman" ? `sudo pacman -S ${tool}` : mgr === "apk" ? `sudo apk add ${tool}` : mgr ? `sudo ${mgr} install ${tool}` : `install ${tool} with your distro's package manager`;
42365
+ return { kind: "linux-manual", command };
42366
+ }
42367
+ async function promptYesNoDefaultYes(question) {
42368
+ if (!process.stdin.isTTY) return false;
42369
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
42370
+ try {
42371
+ const ans = (await new Promise((res) => rl.question(question, res))).trim().toLowerCase();
42372
+ return ans === "" || ans === "y" || ans === "yes";
42373
+ } finally {
42374
+ rl.close();
42375
+ }
42376
+ }
42377
+ async function ensureToolInteractive(opts) {
42378
+ const out = opts.out ?? process.stderr;
42379
+ const plan = resolveInstall(opts.tool, process.platform);
42380
+ if (plan.kind === "present") return true;
42381
+ out.write(`
42382
+ \u{1F427} ${opts.why}
42383
+ `);
42384
+ if (plan.kind !== "brew") {
42385
+ out.write(` ${plan.command}
42386
+ `);
42387
+ if (opts.thenWhat) out.write(` ${opts.thenWhat}
42388
+ `);
42389
+ return false;
42390
+ }
42391
+ const yes = await promptYesNoDefaultYes(` Install ${opts.tool} now with Homebrew? (no password, no sudo) [Y/n] `);
42392
+ if (!yes) {
42393
+ out.write(` No problem \u2014 any time: ${plan.command}
42394
+ `);
42395
+ return false;
42396
+ }
42397
+ out.write(` Installing ${opts.tool}\u2026
42398
+ `);
42399
+ try {
42400
+ cp7.execFileSync("brew", ["install", opts.tool], { stdio: "inherit" });
42401
+ } catch {
42402
+ out.write(` \u26A0\uFE0F '${plan.command}' didn't finish \u2014 run it yourself, then try again.
42403
+ `);
42404
+ return false;
42405
+ }
42406
+ if (isOnPath(opts.tool)) {
42407
+ out.write(` \u2705 ${opts.tool} installed.
42408
+ `);
42409
+ return true;
42410
+ }
42411
+ out.write(` \u26A0\uFE0F ${opts.tool} isn't on PATH yet \u2014 open a new terminal and re-run.
42412
+ `);
42413
+ return false;
42414
+ }
42415
+ var WHY = {
42416
+ "terminal-notifier": 'Clicking a Jefri notification can open a reply box, so you answer without\n going through your agent. macOS needs "terminal-notifier" for that \u2014 without\n it the banner still appears, but clicking it does nothing.',
42417
+ tmux: 'Autonomous "session" mode types incoming messages straight into your LIVE\n agent window so you can watch it work, and gives the reply popup somewhere\n nicer to open. Both need "tmux", a small standard terminal tool.'
42418
+ };
42419
+
42420
+ // src/doctor.ts
42421
+ init_which();
42145
42422
  init_popup();
42146
42423
  import fs10 from "node:fs";
42147
42424
  var T = !!process.stdout.isTTY;
@@ -42173,7 +42450,7 @@ function mask(t) {
42173
42450
  }
42174
42451
  function hasCli(cmd) {
42175
42452
  try {
42176
- const r = cp6.spawnSync(cmd, ["--version"], {
42453
+ const r = cp8.spawnSync(cmd, ["--version"], {
42177
42454
  encoding: "utf8",
42178
42455
  timeout: 5e3,
42179
42456
  shell: process.platform === "win32"
@@ -42267,6 +42544,18 @@ async function runDoctor() {
42267
42544
  const runMode2 = auto.mode === "session" ? auto.sessionReady ? "session (types into your live jefri-managed tmux session)" : "session (\u26A0\uFE0F not a jefri-managed tmux session \u2014 falls back to headless; launch via `jefrichat-mcp run <agent>`)" : auto.mode === "interval" ? `interval (batches every ${auto.intervalMinutes}m)` : "headless (one-shot per message)";
42268
42545
  info(`Autonomous mode is ON \u2014 ${runMode2}, brain "${auto.brain}" in ${auto.workdir} (jefri_autonomous to change)`);
42269
42546
  }
42547
+ try {
42548
+ const hogs = probeFdHogs();
42549
+ const advice = fdAdvice(hogs, { softLimit: softFdLimit() });
42550
+ if (advice) {
42551
+ const worst = hogs.sort((a, b) => b.fds - a.fds)[0];
42552
+ warn(
42553
+ advice,
42554
+ `biggest: pid ${worst.pid} (${worst.fds.toLocaleString()} files). List them with: lsof -n -P | awk '{print $2}' | sort | uniq -c | sort -rn | head`
42555
+ );
42556
+ }
42557
+ } catch {
42558
+ }
42270
42559
  console.log("");
42271
42560
  console.log(
42272
42561
  ` ${C.b}\u{1F4EC} ${panelInvocation(hasCli)}${C.x} ${C.d}\u2014 read your messages and reply WITHOUT going through your agent${C.x}`
@@ -42280,6 +42569,22 @@ async function runDoctor() {
42280
42569
  if (fs10.existsSync(app))
42281
42570
  console.log(` ${C.d}no terminal? open "Jefri Chat" from Spotlight (or drag ${app} to your Dock)${C.x}`);
42282
42571
  }
42572
+ if (process.stdin.isTTY) {
42573
+ if (macNeedsTerminalNotifier && getPrefs().enabled) {
42574
+ await ensureToolInteractive({
42575
+ tool: "terminal-notifier",
42576
+ why: WHY["terminal-notifier"],
42577
+ thenWhat: `Then re-run: ${hasCli("jefri") ? "jefri" : "jefrichat-mcp"} doctor`
42578
+ });
42579
+ }
42580
+ if (auto.enabled && auto.mode === "session" && !isOnPath("tmux")) {
42581
+ await ensureToolInteractive({
42582
+ tool: "tmux",
42583
+ why: WHY.tmux,
42584
+ thenWhat: `Then launch with: JEFRI_EXPERIMENTAL_SESSION=1 ${hasCli("jefri") ? "jefri" : "jefrichat-mcp"} run <agent>`
42585
+ });
42586
+ }
42587
+ }
42283
42588
  console.log("");
42284
42589
  if (problems === 0 && warnings === 0) {
42285
42590
  console.log(`${C.g}${C.b}All good \u2014 you're ready to chat.${C.x}
@@ -42337,7 +42642,12 @@ function ensureClient() {
42337
42642
  if (!clientPromise) {
42338
42643
  clientPromise = (async () => {
42339
42644
  const known = TOKEN ?? readTokenCache()[cacheKey];
42340
- const clientInfo = { name: "jefrichat-mcp", version: connectorVersion() };
42645
+ const clientInfo = {
42646
+ name: "jefrichat-mcp",
42647
+ version: connectorVersion(),
42648
+ cwd: process.cwd(),
42649
+ machine: os7.hostname()
42650
+ };
42341
42651
  const provision = () => JefriClient.connect({
42342
42652
  server: SERVER,
42343
42653
  username: USERNAME,
@@ -42410,6 +42720,8 @@ function ensureClient() {
42410
42720
  owner: c2.identity?.owner,
42411
42721
  server: SERVER,
42412
42722
  workdir: getAuto().workdir,
42723
+ cwd: process.cwd(),
42724
+ machine: os7.hostname(),
42413
42725
  getHost: () => detectedHost,
42414
42726
  isOnline: () => c2.online,
42415
42727
  isAutonomous: () => getAuto().enabled,
@@ -42418,6 +42730,31 @@ function ensureClient() {
42418
42730
  return { label: b.resolved, ready: b.ready };
42419
42731
  },
42420
42732
  sendText: sendAs,
42733
+ // Read a conversation from the HUB, over the socket this process
42734
+ // already holds. The panel's own inbox only has what arrived while
42735
+ // this connector was running, so a freshly-started UI shows nothing
42736
+ // for a conversation that has been going for weeks — which reads as
42737
+ // broken rather than as empty.
42738
+ fetchHistory: async ({ with: other, groupId, limit }) => {
42739
+ const convId = groupId ? groupConversationId(groupId) : dmConversationId(self, other);
42740
+ const p = waitFor(c2, "history", (e) => e.conversationId === convId);
42741
+ c2.history(convId);
42742
+ const res = await p;
42743
+ const raw = (res?.messages ?? []).slice(-limit);
42744
+ return raw.map((m) => ({
42745
+ id: String(m.id ?? ""),
42746
+ // Raw username for routing, sanitized copy for display — the same
42747
+ // split the panel makes, for the same reason: a name can carry a
42748
+ // bidi override that would reorder the line around it.
42749
+ from: String(m.senderUsername ?? ""),
42750
+ fromDisplay: sanitizeForDisplay(String(m.senderUsername ?? "")),
42751
+ text: m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : String(m.content ?? ""),
42752
+ at: String(m.createdAt ?? ""),
42753
+ mine: String(m.senderUsername ?? "") === self,
42754
+ kind: m.kind === "file" ? "file" : "text",
42755
+ fileName: m.fileName ?? null
42756
+ }));
42757
+ },
42421
42758
  handleWithBrain: async (m) => {
42422
42759
  const text5 = await runOnceForMessage({
42423
42760
  content: m.text,
@@ -42601,11 +42938,29 @@ async function main() {
42601
42938
  };
42602
42939
  process.stdin.once("end", shutdownOnHostClose);
42603
42940
  process.stdin.once("close", shutdownOnHostClose);
42941
+ process.on("uncaughtException", (err) => {
42942
+ const code3 = err?.code;
42943
+ if (code3 === "EMFILE" || code3 === "ENFILE" || code3 === "EAGAIN") {
42944
+ log(
42945
+ `WARNING: ${code3} \u2014 this machine is out of file descriptors. Something else is holding them (check for abandoned \`tsx watch\`/dev-server processes). Continuing in degraded mode; file watching may not work until pressure drops.`
42946
+ );
42947
+ return;
42948
+ }
42949
+ log("fatal (uncaught):", err?.stack ?? err);
42950
+ process.exit(1);
42951
+ });
42952
+ process.on("unhandledRejection", (reason) => {
42953
+ log("unhandled rejection:", reason?.stack ?? reason);
42954
+ });
42604
42955
  void checkForUpdate().then((u) => {
42605
- if (u?.outdated)
42956
+ if (u?.outdated) {
42957
+ setUpgradeNotice(
42958
+ `\u26A0\uFE0F Jefri Chat connector ${u.current} is out of date (latest ${u.latest}). Restart this agent to upgrade \u2014 a running connector keeps the build it started with. If it was started with npx, that is enough; if installed globally, run: npm update -g jefrichat-mcp`
42959
+ );
42606
42960
  note(
42607
42961
  `\u2191 update available: jefrichat-mcp ${u.current} \u2192 ${u.latest}. Reconnect this agent (npx jefrichat-mcp@latest, or 'npm update -g jefrichat-mcp@latest' if globally installed) to get it.`
42608
42962
  );
42963
+ }
42609
42964
  }).catch(() => {
42610
42965
  });
42611
42966
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.6",
3
+ "version": "0.48.8",
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": {