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.
- package/dist/http.js +274 -46
- package/dist/index.js +139 -31
- 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
|
|
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
|
-
//
|
|
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
|
-
|
|
60717
|
-
|
|
60718
|
-
|
|
60719
|
-
|
|
60720
|
-
|
|
60721
|
-
|
|
60722
|
-
|
|
60723
|
-
|
|
60724
|
-
|
|
60725
|
-
|
|
60726
|
-
|
|
60727
|
-
|
|
60728
|
-
|
|
60729
|
-
|
|
60730
|
-
|
|
60731
|
-
|
|
60732
|
-
|
|
60733
|
-
)
|
|
60734
|
-
|
|
60735
|
-
|
|
60736
|
-
|
|
60737
|
-
|
|
60738
|
-
|
|
60739
|
-
|
|
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")
|
|
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.
|
|
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
|
-
|
|
63012
|
+
pooled.acp.close();
|
|
62849
63013
|
} catch {
|
|
62850
63014
|
}
|
|
62851
|
-
log(`
|
|
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
|
-
|
|
63167
|
+
const poolKey = sha256(token).toString("hex");
|
|
63168
|
+
let pooled;
|
|
62971
63169
|
try {
|
|
62972
|
-
|
|
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
|
-
|
|
63021
|
-
|
|
63022
|
-
|
|
63023
|
-
|
|
63024
|
-
|
|
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
|
-
|
|
63033
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
36704
|
-
|
|
36705
|
-
|
|
36706
|
-
|
|
36707
|
-
|
|
36708
|
-
|
|
36709
|
-
|
|
36710
|
-
|
|
36711
|
-
|
|
36712
|
-
|
|
36713
|
-
|
|
36714
|
-
|
|
36715
|
-
|
|
36716
|
-
|
|
36717
|
-
|
|
36718
|
-
|
|
36719
|
-
|
|
36720
|
-
)
|
|
36721
|
-
|
|
36722
|
-
|
|
36723
|
-
|
|
36724
|
-
|
|
36725
|
-
|
|
36726
|
-
|
|
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")
|
|
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
|
-
|
|
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