jefrichat-mcp 0.48.6 → 0.48.7

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 +22 -0
  2. package/dist/index.js +379 -45
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -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 }));
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;
@@ -41856,6 +41950,9 @@ function attachInbox(c2, inbox2, self, onIncoming) {
41856
41950
  c2.on("file_received", capture);
41857
41951
  }
41858
41952
 
41953
+ // src/index.ts
41954
+ init_control_proto();
41955
+
41859
41956
  // src/control.ts
41860
41957
  init_control_proto();
41861
41958
  init_registry();
@@ -41883,6 +41980,8 @@ async function startControlServer(opts) {
41883
41980
  owner: opts.owner ?? null,
41884
41981
  host: opts.getHost(),
41885
41982
  workdir: opts.workdir,
41983
+ cwd: opts.cwd ?? null,
41984
+ machine: opts.machine ?? null,
41886
41985
  server: opts.server,
41887
41986
  online: opts.isOnline(),
41888
41987
  unread: currentTally().total,
@@ -41944,6 +42043,19 @@ async function startControlServer(opts) {
41944
42043
  case "subscribe":
41945
42044
  c2.subscribed = true;
41946
42045
  return reply(c2, okRes(id, { subscribed: true }));
42046
+ case "history": {
42047
+ if (!opts.fetchHistory) return fail3(c2, id, "unknown_method", "history is not available from this endpoint");
42048
+ const withUser = typeof params.with === "string" ? params.with : void 0;
42049
+ const groupId = typeof params.groupId === "string" ? params.groupId : void 0;
42050
+ if (!withUser && !groupId) return fail3(c2, id, "bad_request", "history needs `with` (a username) or `groupId`");
42051
+ const limit = Math.min(Math.max(Number(params.limit) || 50, 1), 200);
42052
+ try {
42053
+ const messages2 = await opts.fetchHistory({ with: withUser, groupId, limit });
42054
+ return reply(c2, okRes(id, { messages: messages2 }));
42055
+ } catch (e) {
42056
+ return fail3(c2, id, "internal", `history failed: ${e?.message ?? e}`);
42057
+ }
42058
+ }
41947
42059
  case "inbox": {
41948
42060
  const limit = Math.min(Math.max(Number(params.limit) || 50, 1), MAX_STORED);
41949
42061
  return reply(c2, okRes(id, { messages: messages.slice(-limit).reverse() }));
@@ -42059,7 +42171,14 @@ async function startControlServer(opts) {
42059
42171
  pid: process.pid,
42060
42172
  server: opts.server,
42061
42173
  host: opts.getHost(),
42174
+ // Both, and clearly distinct. `workdir` is the brain's configured folder;
42175
+ // `cwd` is where this process runs. The descriptor previously carried only
42176
+ // `workdir` under a name every reader took to mean "where it runs" — which
42177
+ // is how a stale setting ended up being handed out as the answer to "where
42178
+ // do I go to restart this?".
42062
42179
  workdir: opts.workdir,
42180
+ cwd: opts.cwd ?? null,
42181
+ machine: opts.machine ?? null,
42063
42182
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
42064
42183
  });
42065
42184
  writeUnreadFor(self, 0);
@@ -42139,9 +42258,150 @@ async function startControlServer(opts) {
42139
42258
  init_popup();
42140
42259
 
42141
42260
  // src/doctor.ts
42142
- import cp6 from "node:child_process";
42261
+ import cp8 from "node:child_process";
42143
42262
  init_version();
42144
42263
  init_banner();
42264
+
42265
+ // src/fdcheck.ts
42266
+ import cp6 from "node:child_process";
42267
+ function labelFor(command) {
42268
+ if (/tsx[/\\]dist[/\\]cli\.mjs\s+watch/.test(command)) return "tsx watch dev server";
42269
+ if (/next(-server)?\b.*\bdev\b/.test(command) || /next dev/.test(command)) return "next dev server";
42270
+ if (/nodemon/.test(command)) return "nodemon";
42271
+ if (/webpack|vite/.test(command)) return "bundler dev server";
42272
+ if (/jefrichat-mcp/.test(command)) return "jefri connector";
42273
+ const bin = command.split(/\s+/)[1] ?? command.split(/\s+/)[0] ?? command;
42274
+ return bin.split("/").pop() || "process";
42275
+ }
42276
+ function fdAdvice(hogs, opts) {
42277
+ if (!hogs.length) return null;
42278
+ const total = hogs.reduce((n, h) => n + h.fds, 0);
42279
+ const byLabel = /* @__PURE__ */ new Map();
42280
+ for (const h of hogs) {
42281
+ const cur = byLabel.get(h.label) ?? { count: 0, fds: 0 };
42282
+ byLabel.set(h.label, { count: cur.count + 1, fds: cur.fds + h.fds });
42283
+ }
42284
+ const worst = [...byLabel.entries()].sort((a, b) => b[1].fds - a[1].fds)[0];
42285
+ const [label, agg] = worst;
42286
+ const manyDuplicates = agg.count >= 4;
42287
+ const heavy = total > 2e4 || agg.fds > opts.softLimit * 4;
42288
+ if (!manyDuplicates && !heavy) return null;
42289
+ const plural = agg.count === 1 ? "" : "es";
42290
+ 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.`;
42291
+ }
42292
+ function probeFdHogs(limit = 40) {
42293
+ if (process.platform === "win32") return [];
42294
+ try {
42295
+ const out = cp6.execSync("lsof -n -P 2>/dev/null | awk 'NR>1 {print $2}' | sort | uniq -c | sort -rn | head -" + limit, {
42296
+ encoding: "utf8",
42297
+ timeout: 8e3,
42298
+ maxBuffer: 8 * 1024 * 1024
42299
+ });
42300
+ const hogs = [];
42301
+ for (const line of out.split("\n")) {
42302
+ const m = /^\s*(\d+)\s+(\d+)\s*$/.exec(line);
42303
+ if (!m) continue;
42304
+ const fds = Number(m[1]);
42305
+ const pid = Number(m[2]);
42306
+ if (fds < 500) continue;
42307
+ let command = "";
42308
+ try {
42309
+ command = cp6.execSync(`ps -p ${pid} -o command=`, { encoding: "utf8", timeout: 2e3 }).trim();
42310
+ } catch {
42311
+ continue;
42312
+ }
42313
+ if (!command) continue;
42314
+ hogs.push({ pid, fds, label: labelFor(command) });
42315
+ }
42316
+ return hogs;
42317
+ } catch {
42318
+ return [];
42319
+ }
42320
+ }
42321
+ function softFdLimit() {
42322
+ try {
42323
+ const n = Number(cp6.execSync("ulimit -n", { encoding: "utf8", shell: "/bin/sh", timeout: 2e3 }).trim());
42324
+ return Number.isFinite(n) && n > 0 ? n : 256;
42325
+ } catch {
42326
+ return 256;
42327
+ }
42328
+ }
42329
+
42330
+ // src/setup.ts
42331
+ init_which();
42332
+ import cp7 from "node:child_process";
42333
+ import readline from "node:readline";
42334
+ var LINUX_PKG = { "terminal-notifier": "" };
42335
+ function resolveInstall(tool, platform, has = isOnPath) {
42336
+ if (has(tool)) return { kind: "present", command: "" };
42337
+ if (platform === "darwin") {
42338
+ return has("brew") ? { kind: "brew", command: `brew install ${tool}` } : { kind: "brew-missing", command: `install Homebrew first (https://brew.sh), then: brew install ${tool}` };
42339
+ }
42340
+ if (LINUX_PKG[tool] === "") {
42341
+ return { kind: "windows", command: `${tool} is macOS-only \u2014 notifications on this platform are informational` };
42342
+ }
42343
+ if (platform === "win32") {
42344
+ return { kind: "windows", command: `${tool} has no native Windows build \u2014 use WSL, or skip it` };
42345
+ }
42346
+ const mgr = ["apt-get", "apt", "dnf", "yum", "pacman", "zypper", "apk"].find(has);
42347
+ 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`;
42348
+ return { kind: "linux-manual", command };
42349
+ }
42350
+ async function promptYesNoDefaultYes(question) {
42351
+ if (!process.stdin.isTTY) return false;
42352
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
42353
+ try {
42354
+ const ans = (await new Promise((res) => rl.question(question, res))).trim().toLowerCase();
42355
+ return ans === "" || ans === "y" || ans === "yes";
42356
+ } finally {
42357
+ rl.close();
42358
+ }
42359
+ }
42360
+ async function ensureToolInteractive(opts) {
42361
+ const out = opts.out ?? process.stderr;
42362
+ const plan = resolveInstall(opts.tool, process.platform);
42363
+ if (plan.kind === "present") return true;
42364
+ out.write(`
42365
+ \u{1F427} ${opts.why}
42366
+ `);
42367
+ if (plan.kind !== "brew") {
42368
+ out.write(` ${plan.command}
42369
+ `);
42370
+ if (opts.thenWhat) out.write(` ${opts.thenWhat}
42371
+ `);
42372
+ return false;
42373
+ }
42374
+ const yes = await promptYesNoDefaultYes(` Install ${opts.tool} now with Homebrew? (no password, no sudo) [Y/n] `);
42375
+ if (!yes) {
42376
+ out.write(` No problem \u2014 any time: ${plan.command}
42377
+ `);
42378
+ return false;
42379
+ }
42380
+ out.write(` Installing ${opts.tool}\u2026
42381
+ `);
42382
+ try {
42383
+ cp7.execFileSync("brew", ["install", opts.tool], { stdio: "inherit" });
42384
+ } catch {
42385
+ out.write(` \u26A0\uFE0F '${plan.command}' didn't finish \u2014 run it yourself, then try again.
42386
+ `);
42387
+ return false;
42388
+ }
42389
+ if (isOnPath(opts.tool)) {
42390
+ out.write(` \u2705 ${opts.tool} installed.
42391
+ `);
42392
+ return true;
42393
+ }
42394
+ out.write(` \u26A0\uFE0F ${opts.tool} isn't on PATH yet \u2014 open a new terminal and re-run.
42395
+ `);
42396
+ return false;
42397
+ }
42398
+ var WHY = {
42399
+ "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.',
42400
+ 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.'
42401
+ };
42402
+
42403
+ // src/doctor.ts
42404
+ init_which();
42145
42405
  init_popup();
42146
42406
  import fs10 from "node:fs";
42147
42407
  var T = !!process.stdout.isTTY;
@@ -42173,7 +42433,7 @@ function mask(t) {
42173
42433
  }
42174
42434
  function hasCli(cmd) {
42175
42435
  try {
42176
- const r = cp6.spawnSync(cmd, ["--version"], {
42436
+ const r = cp8.spawnSync(cmd, ["--version"], {
42177
42437
  encoding: "utf8",
42178
42438
  timeout: 5e3,
42179
42439
  shell: process.platform === "win32"
@@ -42267,6 +42527,18 @@ async function runDoctor() {
42267
42527
  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
42528
  info(`Autonomous mode is ON \u2014 ${runMode2}, brain "${auto.brain}" in ${auto.workdir} (jefri_autonomous to change)`);
42269
42529
  }
42530
+ try {
42531
+ const hogs = probeFdHogs();
42532
+ const advice = fdAdvice(hogs, { softLimit: softFdLimit() });
42533
+ if (advice) {
42534
+ const worst = hogs.sort((a, b) => b.fds - a.fds)[0];
42535
+ warn(
42536
+ advice,
42537
+ `biggest: pid ${worst.pid} (${worst.fds.toLocaleString()} files). List them with: lsof -n -P | awk '{print $2}' | sort | uniq -c | sort -rn | head`
42538
+ );
42539
+ }
42540
+ } catch {
42541
+ }
42270
42542
  console.log("");
42271
42543
  console.log(
42272
42544
  ` ${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 +42552,22 @@ async function runDoctor() {
42280
42552
  if (fs10.existsSync(app))
42281
42553
  console.log(` ${C.d}no terminal? open "Jefri Chat" from Spotlight (or drag ${app} to your Dock)${C.x}`);
42282
42554
  }
42555
+ if (process.stdin.isTTY) {
42556
+ if (macNeedsTerminalNotifier && getPrefs().enabled) {
42557
+ await ensureToolInteractive({
42558
+ tool: "terminal-notifier",
42559
+ why: WHY["terminal-notifier"],
42560
+ thenWhat: `Then re-run: ${hasCli("jefri") ? "jefri" : "jefrichat-mcp"} doctor`
42561
+ });
42562
+ }
42563
+ if (auto.enabled && auto.mode === "session" && !isOnPath("tmux")) {
42564
+ await ensureToolInteractive({
42565
+ tool: "tmux",
42566
+ why: WHY.tmux,
42567
+ thenWhat: `Then launch with: JEFRI_EXPERIMENTAL_SESSION=1 ${hasCli("jefri") ? "jefri" : "jefrichat-mcp"} run <agent>`
42568
+ });
42569
+ }
42570
+ }
42283
42571
  console.log("");
42284
42572
  if (problems === 0 && warnings === 0) {
42285
42573
  console.log(`${C.g}${C.b}All good \u2014 you're ready to chat.${C.x}
@@ -42337,7 +42625,12 @@ function ensureClient() {
42337
42625
  if (!clientPromise) {
42338
42626
  clientPromise = (async () => {
42339
42627
  const known = TOKEN ?? readTokenCache()[cacheKey];
42340
- const clientInfo = { name: "jefrichat-mcp", version: connectorVersion() };
42628
+ const clientInfo = {
42629
+ name: "jefrichat-mcp",
42630
+ version: connectorVersion(),
42631
+ cwd: process.cwd(),
42632
+ machine: os7.hostname()
42633
+ };
42341
42634
  const provision = () => JefriClient.connect({
42342
42635
  server: SERVER,
42343
42636
  username: USERNAME,
@@ -42410,6 +42703,8 @@ function ensureClient() {
42410
42703
  owner: c2.identity?.owner,
42411
42704
  server: SERVER,
42412
42705
  workdir: getAuto().workdir,
42706
+ cwd: process.cwd(),
42707
+ machine: os7.hostname(),
42413
42708
  getHost: () => detectedHost,
42414
42709
  isOnline: () => c2.online,
42415
42710
  isAutonomous: () => getAuto().enabled,
@@ -42418,6 +42713,31 @@ function ensureClient() {
42418
42713
  return { label: b.resolved, ready: b.ready };
42419
42714
  },
42420
42715
  sendText: sendAs,
42716
+ // Read a conversation from the HUB, over the socket this process
42717
+ // already holds. The panel's own inbox only has what arrived while
42718
+ // this connector was running, so a freshly-started UI shows nothing
42719
+ // for a conversation that has been going for weeks — which reads as
42720
+ // broken rather than as empty.
42721
+ fetchHistory: async ({ with: other, groupId, limit }) => {
42722
+ const convId = groupId ? groupConversationId(groupId) : dmConversationId(self, other);
42723
+ const p = waitFor(c2, "history", (e) => e.conversationId === convId);
42724
+ c2.history(convId);
42725
+ const res = await p;
42726
+ const raw = (res?.messages ?? []).slice(-limit);
42727
+ return raw.map((m) => ({
42728
+ id: String(m.id ?? ""),
42729
+ // Raw username for routing, sanitized copy for display — the same
42730
+ // split the panel makes, for the same reason: a name can carry a
42731
+ // bidi override that would reorder the line around it.
42732
+ from: String(m.senderUsername ?? ""),
42733
+ fromDisplay: sanitizeForDisplay(String(m.senderUsername ?? "")),
42734
+ text: m.kind === "file" ? `\u{1F4CE} ${m.fileName ?? "file"}` : String(m.content ?? ""),
42735
+ at: String(m.createdAt ?? ""),
42736
+ mine: String(m.senderUsername ?? "") === self,
42737
+ kind: m.kind === "file" ? "file" : "text",
42738
+ fileName: m.fileName ?? null
42739
+ }));
42740
+ },
42421
42741
  handleWithBrain: async (m) => {
42422
42742
  const text5 = await runOnceForMessage({
42423
42743
  content: m.text,
@@ -42601,6 +42921,20 @@ async function main() {
42601
42921
  };
42602
42922
  process.stdin.once("end", shutdownOnHostClose);
42603
42923
  process.stdin.once("close", shutdownOnHostClose);
42924
+ process.on("uncaughtException", (err) => {
42925
+ const code3 = err?.code;
42926
+ if (code3 === "EMFILE" || code3 === "ENFILE" || code3 === "EAGAIN") {
42927
+ log(
42928
+ `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.`
42929
+ );
42930
+ return;
42931
+ }
42932
+ log("fatal (uncaught):", err?.stack ?? err);
42933
+ process.exit(1);
42934
+ });
42935
+ process.on("unhandledRejection", (reason) => {
42936
+ log("unhandled rejection:", reason?.stack ?? reason);
42937
+ });
42604
42938
  void checkForUpdate().then((u) => {
42605
42939
  if (u?.outdated)
42606
42940
  note(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.48.6",
3
+ "version": "0.48.7",
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": {