jefrichat-mcp 0.31.0 → 0.33.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 +274 -46
  2. package/dist/index.js +139 -31
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -60681,11 +60681,11 @@ var JefriClient = class _JefriClient {
60681
60681
  isAlive = true;
60682
60682
  // heartbeat: did we get a pong since the last ping?
60683
60683
  hasConnected = false;
60684
- // armed for auto-reconnect only after the first open
60684
+ // armed for auto-reconnect only after the hub `registered` us
60685
60685
  lastPresence = "online";
60686
60686
  // re-announce this on reconnect
60687
60687
  clientInfo;
60688
- // version handshake, re-sent on reconnect
60688
+ // handshake, re-sent on reconnect
60689
60689
  constructor(server) {
60690
60690
  this.server = server.replace(/\/$/, "");
60691
60691
  }
@@ -60713,31 +60713,46 @@ var JefriClient = class _JefriClient {
60713
60713
  }
60714
60714
  client.token = token;
60715
60715
  client.clientInfo = opts.clientInfo;
60716
- await client.openSocket();
60717
- if (!client.identity) {
60718
- await new Promise((resolve, reject) => {
60719
- let done = false;
60720
- const finish = (fn) => {
60721
- if (done) return;
60722
- done = true;
60723
- client.off("registered", onReg);
60724
- client.off("error", onErr);
60725
- clearTimeout(timer);
60726
- fn();
60727
- };
60728
- const onReg = () => finish(resolve);
60729
- const onErr = (ev) => finish(
60730
- () => reject(
60731
- /auth/i.test(ev?.message ?? "") ? new JefriAuthError("invalid or expired Jefri Chat token \u2014 get a fresh connect command") : new Error(ev?.message ?? "hub rejected the connection")
60732
- )
60733
- );
60734
- const timer = setTimeout(
60735
- () => finish(() => reject(new Error("timed out waiting for the hub to register the connection"))),
60736
- 8e3
60737
- );
60738
- client.on("registered", onReg);
60739
- client.on("error", onErr);
60740
- });
60716
+ let onReg = () => {
60717
+ };
60718
+ let onErr = () => {
60719
+ };
60720
+ let timer;
60721
+ const registered = new Promise((resolve, reject) => {
60722
+ let done = false;
60723
+ const finish = (fn) => {
60724
+ if (done) return;
60725
+ done = true;
60726
+ client.off("registered", onReg);
60727
+ client.off("error", onErr);
60728
+ clearTimeout(timer);
60729
+ fn();
60730
+ };
60731
+ onReg = () => finish(resolve);
60732
+ onErr = (ev) => finish(
60733
+ () => reject(
60734
+ /auth/i.test(ev?.message ?? "") ? new JefriAuthError("invalid or expired Jefri Chat token \u2014 get a fresh connect command") : new Error(ev?.message ?? "hub rejected the connection")
60735
+ )
60736
+ );
60737
+ const regTimeoutMs = typeof process !== "undefined" && Number(process.env?.JEFRI_REGISTER_TIMEOUT_MS) || 8e3;
60738
+ timer = setTimeout(
60739
+ () => finish(() => reject(new Error("timed out waiting for the hub to register the connection"))),
60740
+ regTimeoutMs
60741
+ );
60742
+ client.on("registered", onReg);
60743
+ client.on("error", onErr);
60744
+ });
60745
+ try {
60746
+ await Promise.all([client.openSocket(), registered]);
60747
+ } catch (e) {
60748
+ client.off("registered", onReg);
60749
+ client.off("error", onErr);
60750
+ clearTimeout(timer);
60751
+ try {
60752
+ client.close();
60753
+ } catch {
60754
+ }
60755
+ throw e;
60741
60756
  }
60742
60757
  if (opts.status) client.presence(opts.status);
60743
60758
  await client.ensureE2EKey().catch(() => {
@@ -60750,8 +60765,6 @@ var JefriClient = class _JefriClient {
60750
60765
  return new Promise((resolve, reject) => {
60751
60766
  let settled = false;
60752
60767
  this.ws.on("open", () => {
60753
- this.reconnectAttempts = 0;
60754
- this.hasConnected = true;
60755
60768
  this.startPing();
60756
60769
  try {
60757
60770
  this.ws.send(JSON.stringify({ type: "presence", status: this.lastPresence }));
@@ -60781,7 +60794,11 @@ var JefriClient = class _JefriClient {
60781
60794
  } catch {
60782
60795
  return;
60783
60796
  }
60784
- if (ev.event === "registered") this.identity = ev.identity;
60797
+ if (ev.event === "registered") {
60798
+ this.identity = ev.identity;
60799
+ this.hasConnected = true;
60800
+ this.reconnectAttempts = 0;
60801
+ }
60785
60802
  if (ev.event === "error" && /auth/i.test(ev.message ?? "")) {
60786
60803
  this.authFailed = true;
60787
60804
  }
@@ -60953,6 +60970,19 @@ var JefriClient = class _JefriClient {
60953
60970
  history(conversationId, before) {
60954
60971
  this.send({ type: "history", conversationId, ...before ? { before } : {} });
60955
60972
  }
60973
+ /** Announce (or update) the HOST APP this connector is embedded in — Claude
60974
+ * Code, Codex, ChatGPT, … — typically detected from the MCP `initialize`
60975
+ * handshake AFTER the hub socket is already open. Merges into clientInfo so
60976
+ * reconnects re-send it, and pushes an immediate client_info frame if live. */
60977
+ announceHost(host) {
60978
+ const h = (host ?? "").trim();
60979
+ this.clientInfo = { ...this.clientInfo ?? { name: "jefri-sdk", version: "0" }, host: h };
60980
+ try {
60981
+ if (this.ws?.readyState === this.ws?.OPEN)
60982
+ this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
60983
+ } catch {
60984
+ }
60985
+ }
60956
60986
  close() {
60957
60987
  this.closing = true;
60958
60988
  this.stopPing();
@@ -61181,6 +61211,65 @@ var JefriClient = class _JefriClient {
61181
61211
  };
61182
61212
  var serveDir = path2.join(os.homedir(), ".acp", "serve");
61183
61213
 
61214
+ // src/conn-pool.ts
61215
+ function attachRef(c, id) {
61216
+ if (c.closed) return false;
61217
+ c.refs.add(id);
61218
+ return true;
61219
+ }
61220
+ function detachRef(c, id) {
61221
+ c.refs.delete(id);
61222
+ return c.refs.size === 0;
61223
+ }
61224
+ function createKeyedPool() {
61225
+ const ready = /* @__PURE__ */ new Map();
61226
+ const pending = /* @__PURE__ */ new Map();
61227
+ return {
61228
+ async acquire(key, create) {
61229
+ const existing = ready.get(key);
61230
+ if (existing !== void 0) return existing;
61231
+ const inflight = pending.get(key);
61232
+ if (inflight) return inflight;
61233
+ const p = (async () => {
61234
+ const value = await create();
61235
+ ready.set(key, value);
61236
+ return value;
61237
+ })();
61238
+ pending.set(key, p);
61239
+ try {
61240
+ return await p;
61241
+ } finally {
61242
+ pending.delete(key);
61243
+ }
61244
+ },
61245
+ get: (key) => ready.get(key),
61246
+ drop: (key) => {
61247
+ const v = ready.get(key);
61248
+ ready.delete(key);
61249
+ return v;
61250
+ },
61251
+ has: (key) => ready.has(key),
61252
+ size: () => ready.size
61253
+ };
61254
+ }
61255
+
61256
+ // src/hosts.ts
61257
+ function aggregateFreshHosts(sessions2, poolKey, now, freshMs) {
61258
+ const hosts = /* @__PURE__ */ new Set();
61259
+ for (const s of sessions2) if (s.poolKey === poolKey && s.host && now - s.lastActive < freshMs) hosts.add(s.host);
61260
+ return [...hosts].slice(0, 5).join(",").slice(0, 200);
61261
+ }
61262
+ function formatAgentStatus(agent, body) {
61263
+ const dot = body.online ? "\u{1F7E2} online" : "\u26AA offline";
61264
+ const state = body.active ? "active" : "\u{1F6D1} deactivated";
61265
+ const app2 = body.online && body.app ? ` \xB7 app: ${body.app}` : "";
61266
+ let out = `@${agent} \u2014 ${dot}${app2} \xB7 ${state}`;
61267
+ if (body.online && body.multiHost && Array.isArray(body.hosts) && body.hosts.length > 1)
61268
+ out += `
61269
+ \u26A0\uFE0F ${body.hosts.length} apps recently active on this credential \u2014 ${body.hosts.join(", ")}. If you didn't connect all of them, revoke this key (Connected apps) and reconnect.`;
61270
+ return out;
61271
+ }
61272
+
61184
61273
  // src/tools.ts
61185
61274
  import fs4 from "node:fs";
61186
61275
  import np3 from "node:path";
@@ -61756,6 +61845,69 @@ ${e?.message ?? e}`);
61756
61845
  return ok3(lines.length ? header + lines.join("\n") : "No other identities on the network yet.");
61757
61846
  })
61758
61847
  );
61848
+ const NOT_OWNER = "This only works when you run the Jefri connector as your OWN (human) account and own this agent. An agent session can't manage agents \u2014 use the web app or an owner session.";
61849
+ const ownerApi = async (c, path3, method) => {
61850
+ const res = await fetch(`${ctx.serverUrl}${path3}`, {
61851
+ method,
61852
+ headers: { Authorization: `Bearer ${c.token}` }
61853
+ });
61854
+ let body = {};
61855
+ try {
61856
+ body = await res.json();
61857
+ } catch {
61858
+ }
61859
+ return { status: res.status, body };
61860
+ };
61861
+ server.registerTool(
61862
+ "jefri_agent_apps",
61863
+ {
61864
+ title: "See where your agents are running",
61865
+ description: `For an agent you OWN, show which app has been RECENTLY ACTIVE on its connection (auto-detected, e.g. "Claude Code" or "Codex", or the label you gave that key), whether it's online, and whether it's active or deactivated \u2014 plus a warning if more than one app is active on the same credential (a shared/copied token). Omit \`agent\` to list all your agents. Requires running this connector as the agent's human OWNER.`,
61866
+ inputSchema: {
61867
+ agent: external_exports.string().optional().describe("username of one of YOUR agents; omit to list all of them")
61868
+ }
61869
+ },
61870
+ async ({ agent }) => withClient(async (c) => {
61871
+ if (agent) {
61872
+ const { status, body } = await ownerApi(c, `/api/agents/${encodeURIComponent(agent)}/status`, "GET");
61873
+ if (status !== 200) return fail(body?.error === "not allowed" ? NOT_OWNER : `Could not read @${agent} (HTTP ${status}).`);
61874
+ return ok3(formatAgentStatus(agent, body));
61875
+ }
61876
+ const mine = (await c.identities()).filter((i) => i.type === "agent" && i.owner === c.identity?.username);
61877
+ if (!mine.length) return ok3("You don't own any agents (or you're not connected as their owner).");
61878
+ const lines = mine.map((i) => `${i.status && i.status !== "offline" ? "\u{1F7E2}" : "\u26AA"} @${i.username} \u2014 ${i.displayName}`);
61879
+ return ok3(`Your agents:
61880
+ ${lines.join("\n")}
61881
+
61882
+ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
61883
+ })
61884
+ );
61885
+ server.registerTool(
61886
+ "jefri_deactivate",
61887
+ {
61888
+ title: "Deactivate an agent",
61889
+ description: "Deactivate an agent you OWN: it is immediately disconnected and CANNOT reconnect from any app (Claude, Codex, SDK, \u2026) until you reactivate it. Reversible via jefri_activate. Requires running this connector as the agent's human OWNER \u2014 an agent session cannot deactivate agents.",
61890
+ inputSchema: { agent: external_exports.string().describe("username of YOUR agent to deactivate") }
61891
+ },
61892
+ async ({ agent }) => withClient(async (c) => {
61893
+ const { status, body } = await ownerApi(c, `/api/agents/${encodeURIComponent(agent)}/deactivate`, "POST");
61894
+ if (status !== 200) return fail(body?.error === "not allowed" ? NOT_OWNER : `Could not deactivate @${agent} (HTTP ${status}).`);
61895
+ return ok3(`\u{1F6D1} Deactivated @${agent}. It's disconnected and can't reconnect from any app until you reactivate it.`);
61896
+ })
61897
+ );
61898
+ server.registerTool(
61899
+ "jefri_activate",
61900
+ {
61901
+ title: "Reactivate an agent",
61902
+ description: "Reactivate an agent you OWN that was previously deactivated, so it can connect again. Requires running this connector as the agent's human OWNER.",
61903
+ inputSchema: { agent: external_exports.string().describe("username of YOUR agent to reactivate") }
61904
+ },
61905
+ async ({ agent }) => withClient(async (c) => {
61906
+ const { status, body } = await ownerApi(c, `/api/agents/${encodeURIComponent(agent)}/activate`, "POST");
61907
+ if (status !== 200) return fail(body?.error === "not allowed" ? NOT_OWNER : `Could not reactivate @${agent} (HTTP ${status}).`);
61908
+ return ok3(`\u2705 Reactivated @${agent}. It can connect again now.`);
61909
+ })
61910
+ );
61759
61911
  server.registerTool(
61760
61912
  "jefri_send",
61761
61913
  {
@@ -62818,6 +62970,8 @@ var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
62818
62970
  var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
62819
62971
  var log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
62820
62972
  var sessions = /* @__PURE__ */ new Map();
62973
+ var HOST_FRESH_MS = Number(process.env.MCP_HOST_FRESH_MS ?? 10 * 60 * 1e3);
62974
+ var hubPool = createKeyedPool();
62821
62975
  var SESSION_TTL_MS = Number(process.env.MCP_SESSION_TTL_MS ?? 4 * 60 * 60 * 1e3);
62822
62976
  var MAX_SESSIONS = Number(process.env.MCP_MAX_SESSIONS ?? 1500);
62823
62977
  var MAX_SESSIONS_PER_IDENTITY = Number(process.env.MCP_MAX_SESSIONS_PER_IDENTITY ?? 20);
@@ -62841,14 +62995,50 @@ function closeSession(sid, reason) {
62841
62995
  if (!s) return;
62842
62996
  sessions.delete(sid);
62843
62997
  try {
62844
- s.acp.close();
62998
+ s.transport.close?.();
62845
62999
  } catch {
62846
63000
  }
63001
+ const pooled = hubPool.get(s.poolKey);
63002
+ if (pooled) {
63003
+ if (detachRef(pooled, sid)) teardownPool(s.poolKey, pooled, "last session gone");
63004
+ else if (s.host) announcePoolHosts(s.poolKey, pooled);
63005
+ }
63006
+ log(`session ${sid.slice(0, 8)}\u2026 closed (${reason}) (${sessions.size} active)`);
63007
+ }
63008
+ function teardownPool(poolKey, pooled, reason) {
63009
+ pooled.closed = true;
63010
+ if (hubPool.get(poolKey) === pooled) hubPool.drop(poolKey);
62847
63011
  try {
62848
- s.transport.close?.();
63012
+ pooled.acp.close();
62849
63013
  } catch {
62850
63014
  }
62851
- log(`session ${sid.slice(0, 8)}\u2026 closed (${reason}) (${sessions.size} active)`);
63015
+ log(`hub connection ${poolKey.slice(0, 8)}\u2026 closed (${reason})`);
63016
+ }
63017
+ function announcePoolHosts(poolKey, pooled) {
63018
+ const joined = aggregateFreshHosts(sessions.values(), poolKey, Date.now(), HOST_FRESH_MS);
63019
+ if (joined === pooled.lastHosts) return;
63020
+ pooled.lastHosts = joined;
63021
+ pooled.acp.announceHost(joined);
63022
+ }
63023
+ function closePool(poolKey, reason) {
63024
+ const pooled = hubPool.get(poolKey);
63025
+ if (!pooled) return;
63026
+ pooled.closed = true;
63027
+ for (const sid of [...pooled.refs]) closeSession(sid, reason);
63028
+ if (hubPool.get(poolKey) === pooled) teardownPool(poolKey, pooled, reason);
63029
+ }
63030
+ async function acquirePooledConn(token, poolKey) {
63031
+ return hubPool.acquire(poolKey, async () => {
63032
+ const acp = await JefriClient.connect({ server: HUB, token, status: "online" });
63033
+ const inbox = [];
63034
+ if (acp.identity) attachInbox(acp, inbox, acp.identity.username);
63035
+ const pooled = { acp, inbox, refs: /* @__PURE__ */ new Set(), closed: false };
63036
+ acp.on("force_disconnect", () => closePool(poolKey, "credential revoked"));
63037
+ acp.on("error", (ev) => {
63038
+ if (/auth|revoked|unauthor|disabled/i.test(String(ev?.message ?? ""))) closePool(poolKey, "auth revoked");
63039
+ });
63040
+ return pooled;
63041
+ });
62852
63042
  }
62853
63043
  function touch(sid) {
62854
63044
  if (!sid) return;
@@ -62860,6 +63050,13 @@ setInterval(() => {
62860
63050
  for (const [sid, s] of sessions) {
62861
63051
  if (now - s.lastActive > SESSION_TTL_MS) closeSession(sid, "idle");
62862
63052
  }
63053
+ const seenPools = /* @__PURE__ */ new Set();
63054
+ for (const s of sessions.values()) {
63055
+ if (seenPools.has(s.poolKey)) continue;
63056
+ seenPools.add(s.poolKey);
63057
+ const pooled = hubPool.get(s.poolKey);
63058
+ if (pooled) announcePoolHosts(s.poolKey, pooled);
63059
+ }
62863
63060
  for (const [ip, e] of initHits) {
62864
63061
  if (now > e.resetAt) initHits.delete(ip);
62865
63062
  }
@@ -62967,9 +63164,10 @@ app.post("/mcp", async (req, res) => {
62967
63164
  });
62968
63165
  return;
62969
63166
  }
62970
- let acp;
63167
+ const poolKey = sha256(token).toString("hex");
63168
+ let pooled;
62971
63169
  try {
62972
- acp = await JefriClient.connect({ server: HUB, token, status: "online" });
63170
+ pooled = await acquirePooledConn(token, poolKey);
62973
63171
  } catch (e) {
62974
63172
  if (e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED") {
62975
63173
  res.status(401).json({
@@ -62985,7 +63183,9 @@ app.post("/mcp", async (req, res) => {
62985
63183
  res.status(502).json({ jsonrpc: "2.0", error: { code: -32002, message: `Hub unreachable: ${e?.message ?? e}` }, id: null });
62986
63184
  return;
62987
63185
  }
63186
+ const acp = pooled.acp;
62988
63187
  if (!acp.identity) {
63188
+ closePool(poolKey, "no identity");
62989
63189
  res.status(401).json({
62990
63190
  jsonrpc: "2.0",
62991
63191
  error: { code: -32001, message: "Invalid or expired Jefri Chat token \u2014 rotate it in the Jefri Chat web app and reconnect." },
@@ -62993,13 +63193,29 @@ app.post("/mcp", async (req, res) => {
62993
63193
  });
62994
63194
  return;
62995
63195
  }
62996
- const inbox = [];
62997
- attachInbox(acp, inbox, acp.identity.username);
62998
63196
  const mcp = new McpServer({ name: "jefrichat", version: "0.1.0" });
62999
- registerAcpTools(mcp, { ensureClient: async () => acp, inbox, serverUrl: HUB, local: false });
63197
+ registerAcpTools(mcp, { ensureClient: async () => acp, inbox: pooled.inbox, serverUrl: HUB, local: false });
63198
+ let mySid;
63199
+ mcp.server.oninitialized = () => {
63200
+ try {
63201
+ const raw = mcp.server.getClientVersion()?.name || void 0;
63202
+ const s = mySid ? sessions.get(mySid) : void 0;
63203
+ if (s) s.host = raw;
63204
+ announcePoolHosts(poolKey, pooled);
63205
+ } catch {
63206
+ }
63207
+ };
63000
63208
  const transport = new StreamableHTTPServerTransport({
63001
63209
  sessionIdGenerator: () => randomUUID(),
63002
63210
  onsessioninitialized: (sid) => {
63211
+ if (pooled.closed) {
63212
+ try {
63213
+ transport.close?.();
63214
+ } catch {
63215
+ }
63216
+ log(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
63217
+ return;
63218
+ }
63003
63219
  const meName = acp.identity.username;
63004
63220
  const mine = [...sessions.entries()].filter(([, s]) => s.acp.identity?.username === meName);
63005
63221
  if (mine.length >= MAX_SESSIONS_PER_IDENTITY) {
@@ -63017,20 +63233,32 @@ app.post("/mcp", async (req, res) => {
63017
63233
  }
63018
63234
  if (oldest) closeSession(oldest, "evicted (cap)");
63019
63235
  }
63020
- sessions.set(sid, { transport, acp, lastActive: Date.now(), tokenHash: sha256(token), inflight: 0 });
63021
- log(`session ${sid.slice(0, 8)}\u2026 open as @${acp.identity.username} (${sessions.size} active)`);
63022
- acp.on("force_disconnect", () => closeSession(sid, "credential revoked"));
63023
- acp.on("error", (ev) => {
63024
- if (/auth|revoked|unauthor|disabled/i.test(String(ev?.message ?? ""))) closeSession(sid, "auth revoked");
63025
- });
63236
+ if (!attachRef(pooled, sid)) {
63237
+ try {
63238
+ transport.close?.();
63239
+ } catch {
63240
+ }
63241
+ log(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
63242
+ return;
63243
+ }
63244
+ sessions.set(sid, { transport, acp, lastActive: Date.now(), tokenHash: sha256(token), inflight: 0, poolKey });
63245
+ mySid = sid;
63246
+ log(`session ${sid.slice(0, 8)}\u2026 open as @${acp.identity.username} (${sessions.size} active, ${pooled.refs.size} on this connection)`);
63026
63247
  }
63027
63248
  });
63028
63249
  transport.onclose = () => {
63029
63250
  const sid = transport.sessionId;
63030
63251
  if (sid) closeSession(sid, "client closed");
63031
63252
  };
63032
- await mcp.connect(transport);
63033
- await transport.handleRequest(req, res, req.body);
63253
+ const reservation = "init:" + randomUUID();
63254
+ pooled.refs.add(reservation);
63255
+ try {
63256
+ await mcp.connect(transport);
63257
+ await transport.handleRequest(req, res, req.body);
63258
+ } finally {
63259
+ pooled.refs.delete(reservation);
63260
+ if (pooled.refs.size === 0 && hubPool.get(poolKey) === pooled) teardownPool(poolKey, pooled, "init failed, no session");
63261
+ }
63034
63262
  return;
63035
63263
  }
63036
63264
  res.status(404).json({
package/dist/index.js CHANGED
@@ -36668,11 +36668,11 @@ var JefriClient = class _JefriClient {
36668
36668
  isAlive = true;
36669
36669
  // heartbeat: did we get a pong since the last ping?
36670
36670
  hasConnected = false;
36671
- // armed for auto-reconnect only after the first open
36671
+ // armed for auto-reconnect only after the hub `registered` us
36672
36672
  lastPresence = "online";
36673
36673
  // re-announce this on reconnect
36674
36674
  clientInfo;
36675
- // version handshake, re-sent on reconnect
36675
+ // handshake, re-sent on reconnect
36676
36676
  constructor(server2) {
36677
36677
  this.server = server2.replace(/\/$/, "");
36678
36678
  }
@@ -36700,31 +36700,46 @@ var JefriClient = class _JefriClient {
36700
36700
  }
36701
36701
  client.token = token;
36702
36702
  client.clientInfo = opts.clientInfo;
36703
- await client.openSocket();
36704
- if (!client.identity) {
36705
- await new Promise((resolve, reject) => {
36706
- let done = false;
36707
- const finish = (fn) => {
36708
- if (done) return;
36709
- done = true;
36710
- client.off("registered", onReg);
36711
- client.off("error", onErr);
36712
- clearTimeout(timer);
36713
- fn();
36714
- };
36715
- const onReg = () => finish(resolve);
36716
- const onErr = (ev) => finish(
36717
- () => reject(
36718
- /auth/i.test(ev?.message ?? "") ? new JefriAuthError("invalid or expired Jefri Chat token \u2014 get a fresh connect command") : new Error(ev?.message ?? "hub rejected the connection")
36719
- )
36720
- );
36721
- const timer = setTimeout(
36722
- () => finish(() => reject(new Error("timed out waiting for the hub to register the connection"))),
36723
- 8e3
36724
- );
36725
- client.on("registered", onReg);
36726
- client.on("error", onErr);
36727
- });
36703
+ let onReg = () => {
36704
+ };
36705
+ let onErr = () => {
36706
+ };
36707
+ let timer;
36708
+ const registered = new Promise((resolve, reject) => {
36709
+ let done = false;
36710
+ const finish = (fn) => {
36711
+ if (done) return;
36712
+ done = true;
36713
+ client.off("registered", onReg);
36714
+ client.off("error", onErr);
36715
+ clearTimeout(timer);
36716
+ fn();
36717
+ };
36718
+ onReg = () => finish(resolve);
36719
+ onErr = (ev) => finish(
36720
+ () => reject(
36721
+ /auth/i.test(ev?.message ?? "") ? new JefriAuthError("invalid or expired Jefri Chat token \u2014 get a fresh connect command") : new Error(ev?.message ?? "hub rejected the connection")
36722
+ )
36723
+ );
36724
+ const regTimeoutMs = typeof process !== "undefined" && Number(process.env?.JEFRI_REGISTER_TIMEOUT_MS) || 8e3;
36725
+ timer = setTimeout(
36726
+ () => finish(() => reject(new Error("timed out waiting for the hub to register the connection"))),
36727
+ regTimeoutMs
36728
+ );
36729
+ client.on("registered", onReg);
36730
+ client.on("error", onErr);
36731
+ });
36732
+ try {
36733
+ await Promise.all([client.openSocket(), registered]);
36734
+ } catch (e) {
36735
+ client.off("registered", onReg);
36736
+ client.off("error", onErr);
36737
+ clearTimeout(timer);
36738
+ try {
36739
+ client.close();
36740
+ } catch {
36741
+ }
36742
+ throw e;
36728
36743
  }
36729
36744
  if (opts.status) client.presence(opts.status);
36730
36745
  await client.ensureE2EKey().catch(() => {
@@ -36737,8 +36752,6 @@ var JefriClient = class _JefriClient {
36737
36752
  return new Promise((resolve, reject) => {
36738
36753
  let settled = false;
36739
36754
  this.ws.on("open", () => {
36740
- this.reconnectAttempts = 0;
36741
- this.hasConnected = true;
36742
36755
  this.startPing();
36743
36756
  try {
36744
36757
  this.ws.send(JSON.stringify({ type: "presence", status: this.lastPresence }));
@@ -36768,7 +36781,11 @@ var JefriClient = class _JefriClient {
36768
36781
  } catch {
36769
36782
  return;
36770
36783
  }
36771
- if (ev.event === "registered") this.identity = ev.identity;
36784
+ if (ev.event === "registered") {
36785
+ this.identity = ev.identity;
36786
+ this.hasConnected = true;
36787
+ this.reconnectAttempts = 0;
36788
+ }
36772
36789
  if (ev.event === "error" && /auth/i.test(ev.message ?? "")) {
36773
36790
  this.authFailed = true;
36774
36791
  }
@@ -36940,6 +36957,19 @@ var JefriClient = class _JefriClient {
36940
36957
  history(conversationId, before) {
36941
36958
  this.send({ type: "history", conversationId, ...before ? { before } : {} });
36942
36959
  }
36960
+ /** Announce (or update) the HOST APP this connector is embedded in — Claude
36961
+ * Code, Codex, ChatGPT, … — typically detected from the MCP `initialize`
36962
+ * handshake AFTER the hub socket is already open. Merges into clientInfo so
36963
+ * reconnects re-send it, and pushes an immediate client_info frame if live. */
36964
+ announceHost(host) {
36965
+ const h = (host ?? "").trim();
36966
+ this.clientInfo = { ...this.clientInfo ?? { name: "jefri-sdk", version: "0" }, host: h };
36967
+ try {
36968
+ if (this.ws?.readyState === this.ws?.OPEN)
36969
+ this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
36970
+ } catch {
36971
+ }
36972
+ }
36943
36973
  close() {
36944
36974
  this.closing = true;
36945
36975
  this.stopPing();
@@ -37324,6 +37354,18 @@ async function readCapped(stream, maxBytes, deadline) {
37324
37354
  return Buffer.concat(chunks);
37325
37355
  }
37326
37356
 
37357
+ // src/hosts.ts
37358
+ function formatAgentStatus(agent, body) {
37359
+ const dot = body.online ? "\u{1F7E2} online" : "\u26AA offline";
37360
+ const state = body.active ? "active" : "\u{1F6D1} deactivated";
37361
+ const app = body.online && body.app ? ` \xB7 app: ${body.app}` : "";
37362
+ let out = `@${agent} \u2014 ${dot}${app} \xB7 ${state}`;
37363
+ if (body.online && body.multiHost && Array.isArray(body.hosts) && body.hosts.length > 1)
37364
+ out += `
37365
+ \u26A0\uFE0F ${body.hosts.length} apps recently active on this credential \u2014 ${body.hosts.join(", ")}. If you didn't connect all of them, revoke this key (Connected apps) and reconnect.`;
37366
+ return out;
37367
+ }
37368
+
37327
37369
  // src/notify.ts
37328
37370
  import cp from "node:child_process";
37329
37371
  import fs2 from "node:fs";
@@ -38420,6 +38462,69 @@ ${e?.message ?? e}`);
38420
38462
  return ok3(lines.length ? header + lines.join("\n") : "No other identities on the network yet.");
38421
38463
  })
38422
38464
  );
38465
+ const NOT_OWNER = "This only works when you run the Jefri connector as your OWN (human) account and own this agent. An agent session can't manage agents \u2014 use the web app or an owner session.";
38466
+ const ownerApi = async (c, path4, method) => {
38467
+ const res = await fetch(`${ctx.serverUrl}${path4}`, {
38468
+ method,
38469
+ headers: { Authorization: `Bearer ${c.token}` }
38470
+ });
38471
+ let body = {};
38472
+ try {
38473
+ body = await res.json();
38474
+ } catch {
38475
+ }
38476
+ return { status: res.status, body };
38477
+ };
38478
+ server2.registerTool(
38479
+ "jefri_agent_apps",
38480
+ {
38481
+ title: "See where your agents are running",
38482
+ description: `For an agent you OWN, show which app has been RECENTLY ACTIVE on its connection (auto-detected, e.g. "Claude Code" or "Codex", or the label you gave that key), whether it's online, and whether it's active or deactivated \u2014 plus a warning if more than one app is active on the same credential (a shared/copied token). Omit \`agent\` to list all your agents. Requires running this connector as the agent's human OWNER.`,
38483
+ inputSchema: {
38484
+ agent: external_exports.string().optional().describe("username of one of YOUR agents; omit to list all of them")
38485
+ }
38486
+ },
38487
+ async ({ agent }) => withClient(async (c) => {
38488
+ if (agent) {
38489
+ const { status, body } = await ownerApi(c, `/api/agents/${encodeURIComponent(agent)}/status`, "GET");
38490
+ if (status !== 200) return fail(body?.error === "not allowed" ? NOT_OWNER : `Could not read @${agent} (HTTP ${status}).`);
38491
+ return ok3(formatAgentStatus(agent, body));
38492
+ }
38493
+ const mine = (await c.identities()).filter((i) => i.type === "agent" && i.owner === c.identity?.username);
38494
+ if (!mine.length) return ok3("You don't own any agents (or you're not connected as their owner).");
38495
+ const lines = mine.map((i) => `${i.status && i.status !== "offline" ? "\u{1F7E2}" : "\u26AA"} @${i.username} \u2014 ${i.displayName}`);
38496
+ return ok3(`Your agents:
38497
+ ${lines.join("\n")}
38498
+
38499
+ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
38500
+ })
38501
+ );
38502
+ server2.registerTool(
38503
+ "jefri_deactivate",
38504
+ {
38505
+ title: "Deactivate an agent",
38506
+ description: "Deactivate an agent you OWN: it is immediately disconnected and CANNOT reconnect from any app (Claude, Codex, SDK, \u2026) until you reactivate it. Reversible via jefri_activate. Requires running this connector as the agent's human OWNER \u2014 an agent session cannot deactivate agents.",
38507
+ inputSchema: { agent: external_exports.string().describe("username of YOUR agent to deactivate") }
38508
+ },
38509
+ async ({ agent }) => withClient(async (c) => {
38510
+ const { status, body } = await ownerApi(c, `/api/agents/${encodeURIComponent(agent)}/deactivate`, "POST");
38511
+ if (status !== 200) return fail(body?.error === "not allowed" ? NOT_OWNER : `Could not deactivate @${agent} (HTTP ${status}).`);
38512
+ return ok3(`\u{1F6D1} Deactivated @${agent}. It's disconnected and can't reconnect from any app until you reactivate it.`);
38513
+ })
38514
+ );
38515
+ server2.registerTool(
38516
+ "jefri_activate",
38517
+ {
38518
+ title: "Reactivate an agent",
38519
+ description: "Reactivate an agent you OWN that was previously deactivated, so it can connect again. Requires running this connector as the agent's human OWNER.",
38520
+ inputSchema: { agent: external_exports.string().describe("username of YOUR agent to reactivate") }
38521
+ },
38522
+ async ({ agent }) => withClient(async (c) => {
38523
+ const { status, body } = await ownerApi(c, `/api/agents/${encodeURIComponent(agent)}/activate`, "POST");
38524
+ if (status !== 200) return fail(body?.error === "not allowed" ? NOT_OWNER : `Could not reactivate @${agent} (HTTP ${status}).`);
38525
+ return ok3(`\u2705 Reactivated @${agent}. It can connect again now.`);
38526
+ })
38527
+ );
38423
38528
  server2.registerTool(
38424
38529
  "jefri_send",
38425
38530
  {
@@ -39839,7 +39944,10 @@ async function main() {
39839
39944
  const transport = new StdioServerTransport();
39840
39945
  server.server.oninitialized = () => {
39841
39946
  try {
39842
- setAutoHost(server.server.getClientVersion()?.name);
39947
+ const rawHost = server.server.getClientVersion()?.name;
39948
+ setAutoHost(rawHost);
39949
+ void ensureClient().then((cl) => cl.announceHost(rawHost)).catch(() => {
39950
+ });
39843
39951
  } catch {
39844
39952
  }
39845
39953
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jefrichat-mcp",
3
- "version": "0.31.0",
3
+ "version": "0.33.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": {