agentschat-mcp 0.34.0 → 0.35.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.
@@ -0,0 +1,85 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join, resolve } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { validateIdentityProfile } from "../src/identity.ts";
6
+ import { parse as parseToml } from "smol-toml";
7
+
8
+ export interface BridgeConfig {
9
+ cwd: string; profileFile: string; source: string; agentId: string; token: string;
10
+ apiUrl: string; wsUrl: string; channels: string[]; senders: string[];
11
+ codexBin: string; stateDir: string;
12
+ }
13
+ export interface IdentitySettings { profile?: string; agent_id?: string; channels?: string[]; senders?: string[]; api_url?: string; ws_url?: string }
14
+ function readJson(file: string): any {
15
+ try { return JSON.parse(readFileSync(file, "utf8")); }
16
+ catch { throw new Error(`Cannot read valid JSON: ${file}`); }
17
+ }
18
+ function strings(value: unknown, name: string): string[] {
19
+ if (value === undefined) return [];
20
+ if (!Array.isArray(value) || value.some(x => typeof x !== "string" || !x.trim()))
21
+ throw new Error(`${name} must be an array of nonempty strings`);
22
+ return value;
23
+ }
24
+ export function resolveConfig(opts: { cwd?: string; profile?: string; codexBin?: string; settings?: IdentitySettings },
25
+ env: NodeJS.ProcessEnv = process.env, home = homedir()): BridgeConfig {
26
+ const cwd = realpathSync(opts.cwd ?? process.cwd());
27
+ const configFile = join(cwd, ".agentschat/config.json");
28
+ const project = opts.settings ?? (existsSync(configFile) ? readJson(configFile) : {});
29
+ if (!project || typeof project !== "object" || Array.isArray(project)) throw new Error("Invalid project config");
30
+ const allowed = new Set(["profile", "agent_id", "channels", "senders", "api_url", "ws_url"]);
31
+ if (Object.keys(project).some(k => !allowed.has(k))) throw new Error("Unknown project config field (credentials belong in a private profile)");
32
+ for (const k of ["profile", "agent_id", "api_url", "ws_url"])
33
+ if (project[k] !== undefined && (typeof project[k] !== "string" || !project[k].trim())) throw new Error(`Invalid project ${k}`);
34
+ const localProfile = join(cwd, ".agentschat/profile.json");
35
+ // Read only the project file, not Codex's merged global config. Identity lookup
36
+ // must not execute the configured MCP command or inherit its token overrides.
37
+ let codexProfile: string | undefined;
38
+ const codexFile = join(cwd, ".codex/config.toml");
39
+ if (opts.settings === undefined && existsSync(codexFile)) {
40
+ let doc: any;
41
+ try { doc = parseToml(readFileSync(codexFile, "utf8")); }
42
+ catch { throw new Error("Invalid project .codex/config.toml"); }
43
+ const mcp = doc.mcp_servers?.agentschat;
44
+ if (mcp && mcp.enabled !== false) {
45
+ const args = Array.isArray(mcp.args) ? mcp.args : [];
46
+ const index = args.indexOf("--profile");
47
+ codexProfile = mcp.env?.AGENTSCHAT_PROFILE ?? mcp.env?.AGENTCHAT_PROFILE ?? (index >= 0 ? args[index + 1] : undefined);
48
+ if ((index >= 0 || codexProfile !== undefined) && (typeof codexProfile !== "string" || !codexProfile.trim() || codexProfile.startsWith("--"))) throw new Error("Invalid project MCP profile selector");
49
+ }
50
+ }
51
+ const choices = [
52
+ [opts.profile, "flag"], [project.profile, "project-config"],
53
+ [opts.settings === undefined && existsSync(localProfile) ? localProfile : undefined, "project-profile"],
54
+ [codexProfile, "project-codex"],
55
+ [env.AGENTSCHAT_PROFILE, "env"], [env.AGENTCHAT_PROFILE, "legacy-env"], ["profile", "default"],
56
+ ];
57
+ const [selector, source] = choices.find(([v]) => v !== undefined && v !== "") as [string, string];
58
+ let profileFile: string;
59
+ if (selector.startsWith("~/")) profileFile = join(home, selector.slice(2));
60
+ else if (isAbsolute(selector) || selector.includes("/")) profileFile = resolve(cwd, selector);
61
+ else {
62
+ const name = selector.endsWith(".json") ? selector : `${selector}.json`;
63
+ profileFile = join(home, ".agentschat/profiles", name);
64
+ if (!existsSync(profileFile)) profileFile = join(home, ".agentschat", name);
65
+ if (!existsSync(profileFile)) profileFile = join(home, ".agentchat", name);
66
+ }
67
+ if (!existsSync(profileFile)) throw new Error(`Selected profile missing: ${profileFile}; no identity fallback or registration`);
68
+ if (process.platform !== "win32" && (statSync(profileFile).mode & 0o077)) throw new Error(`Profile must be private (chmod 600): ${profileFile}`);
69
+ const profile = readJson(profileFile);
70
+ validateIdentityProfile(profile, profileFile);
71
+ if (project.agent_id !== undefined && project.agent_id !== profile.agent_id) throw new Error("Project agent_id does not match selected profile");
72
+ const apiUrl = project.api_url ?? "https://agents-chat.com";
73
+ const wsUrl = project.ws_url ?? `${apiUrl.replace(/^http/, "ws").replace(/\/$/, "")}/ws`;
74
+ for (const [raw, protocols] of [[apiUrl, ["https:", "http:"]], [wsUrl, ["wss:", "ws:"]]] as const) {
75
+ const url = new URL(raw);
76
+ if (!(protocols as readonly string[]).includes(url.protocol) || url.username || url.password || url.search || url.hash ||
77
+ (!url.protocol.endsWith("s:") && !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))) throw new Error("Server URLs must use TLS (except loopback) and contain no credentials/query");
78
+ }
79
+ const canonicalApi = new URL(apiUrl).href.replace(/\/$/, "");
80
+ const key = createHash("sha256").update(JSON.stringify([cwd, canonicalApi, profile.agent_id])).digest("hex").slice(0, 24);
81
+ return { cwd, profileFile, source, agentId: profile.agent_id, token: profile.token,
82
+ apiUrl: canonicalApi, wsUrl, channels: strings(project.channels, "channels"),
83
+ senders: strings(project.senders, "senders"), codexBin: opts.codexBin ?? "codex",
84
+ stateDir: join(home, ".agentschat/codex-bridge", key) };
85
+ }
@@ -0,0 +1,81 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { mkdirSync, openSync, closeSync, readFileSync, writeFileSync, unlinkSync, existsSync, renameSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { createHash } from "node:crypto";
6
+ import { parseArgs } from "node:util";
7
+ import { loadBots, defaultRegistry, type BotConfig } from "./bots-config.ts";
8
+ import { codexPresent } from "./processes.ts";
9
+ const { values } = parseArgs({ options: { "codex-bots": { type: "boolean" }, config: { type: "string" }, "watch-codex": { type: "boolean" }, check: { type: "boolean" }, status: { type: "boolean" }, help: { type: "boolean" } } });
10
+ const root = join(homedir(), ".agentschat/codex-bots"), registry = resolve(values.config ?? defaultRegistry());
11
+ const statusFile = join(root, "status.json"), lock = join(root, "manager.lock");
12
+ interface Worker { child?: ChildProcess; config: BotConfig; fingerprint: string; failures: number; next: number; status: string }
13
+ const workers = new Map<string, Worker>();
14
+ let stopping = false, missing = 0, timer: ReturnType<typeof setTimeout> | undefined;
15
+ function removeDeadLock(file: string) {
16
+ if (!existsSync(file)) return;
17
+ const pid = Number(readFileSync(file, "utf8"));
18
+ if (!Number.isInteger(pid) || pid <= 0) return;
19
+ try { process.kill(pid, 0); } catch (e: any) { if (e.code === "ESRCH") unlinkSync(file); }
20
+ }
21
+ function save() {
22
+ writeFileSync(statusFile + ".tmp", JSON.stringify({ pid: process.pid, updated_at: new Date().toISOString(), bots: [...workers.values()].map(w => ({ name: w.config.name, agent_id: w.config.agentId, workdir: w.config.cwd, pid: w.child?.pid, status: w.status })) }, null, 2), { mode: 0o600 });
23
+ renameSync(statusFile + ".tmp", statusFile);
24
+ }
25
+ async function stop(w: Worker) {
26
+ const child = w.child;
27
+ if (child && child.exitCode === null && child.signalCode === null) await new Promise<void>(done => {
28
+ const timeout = setTimeout(() => killGroup(child), 20_000);
29
+ child.once("exit", () => { clearTimeout(timeout); killGroup(child); done(); }); child.kill("SIGTERM");
30
+ });
31
+ w.child = undefined; w.status = "stopped";
32
+ }
33
+ function killGroup(child: ChildProcess) { if (child.pid) try { process.kill(-child.pid, "SIGKILL"); } catch {} }
34
+ function start(w: Worker) {
35
+ removeDeadLock(join(w.config.stateDir, "bridge.lock")); w.status = "starting";
36
+ const child = spawn(process.execPath, [resolve(process.argv[1]!), "--codex-bridge", "--managed-worker"], { cwd: w.config.cwd, detached: true, stdio: ["ignore", "pipe", "pipe", "ipc"] });
37
+ w.child = child;
38
+ child.once("spawn", () => child.send(w.config, () => {}));
39
+ let buffer = "";
40
+ child.stderr?.on("data", data => {
41
+ buffer = (buffer + String(data)).slice(-8192);
42
+ if (buffer.includes(`AgentsChat connected as ${w.config.agentId}`)) { w.status = "connected"; w.failures = 0; buffer = ""; save(); }
43
+ else if (buffer.includes("disconnected")) { w.status = "reconnecting"; buffer = ""; save(); }
44
+ });
45
+ child.stdout?.resume();
46
+ const failed = () => { if (w.child !== child) return; killGroup(child); w.child = undefined; w.status = "retrying"; w.next = Date.now() + Math.min(60_000, 1000 * 2 ** Math.min(++w.failures, 6)); save(); };
47
+ child.once("error", failed); child.once("exit", failed);
48
+ }
49
+ async function tick() {
50
+ try {
51
+ const configs = loadBots(registry);
52
+ for (const [name, worker] of workers) {
53
+ const c = configs.find(c => c.name === name);
54
+ if (!c || createHash("sha256").update(JSON.stringify(c)).digest("hex") !== worker.fingerprint) { await stop(worker); workers.delete(name); }
55
+ }
56
+ for (const c of configs) if (!workers.has(c.name)) workers.set(c.name, { config: c, fingerprint: createHash("sha256").update(JSON.stringify(c)).digest("hex"), failures: 0, next: 0, status: "waiting" });
57
+ } catch { console.error("Invalid registry; keeping last valid configuration"); }
58
+ if (stopping) return;
59
+ let active: boolean;
60
+ try { active = !values["watch-codex"] || await codexPresent(); } catch { console.error("Process detection unavailable"); return; }
61
+ missing = active ? 0 : missing + 1;
62
+ for (const w of workers.values()) {
63
+ if (stopping) break;
64
+ if (active && !w.child && Date.now() >= w.next) start(w);
65
+ else if (!active && missing >= 2 && w.child) await stop(w);
66
+ }
67
+ save();
68
+ }
69
+ async function main() {
70
+ if (values.help) { console.log("agentschat-mcp --codex-bots [--config FILE] [--watch-codex] [--check|--status]\nCentral registry: ~/.agentschat/codex-bots.json; profiles: ~/.agentschat/profiles\nWatch polls external Codex processes every 5 seconds."); return; }
71
+ if (values.status) { console.log(readFileSync(statusFile, "utf8")); return; }
72
+ const configs = loadBots(registry);
73
+ if (values.check) { console.log(JSON.stringify(configs.map(c => ({ name: c.name, agent_id: c.agentId, workdir: c.cwd, profile: c.profileFile })))); return; }
74
+ mkdirSync(root, { recursive: true, mode: 0o700 }); removeDeadLock(lock);
75
+ const fd = openSync(lock, "wx", 0o600); writeFileSync(fd, String(process.pid)); closeSync(fd);
76
+ let running: Promise<void> = Promise.resolve();
77
+ const loop = () => { running = tick().catch(() => console.error("Supervisor tick failed")).finally(() => { if (!stopping) timer = setTimeout(loop, 5000); }); };
78
+ const shutdown = async () => { if (stopping) return; stopping = true; clearTimeout(timer); await running; await Promise.all([...workers.values()].map(stop)); save(); if (readFileSync(lock, "utf8") === String(process.pid)) unlinkSync(lock); };
79
+ process.once("SIGINT", () => void shutdown()); process.once("SIGTERM", () => void shutdown()); loop();
80
+ }
81
+ main().catch(() => { console.error("Bot manager failed; check registry, profile permissions, and existing manager"); process.exitCode = 1; });
@@ -0,0 +1,21 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { basename } from "node:path";
4
+
5
+ export interface ProcessRow { pid: number; ppid: number; command: string }
6
+ export function parseProcesses(text: string): ProcessRow[] {
7
+ return text.split("\n").flatMap(line => {
8
+ const m = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
9
+ return m ? [{ pid: Number(m[1]), ppid: Number(m[2]), command: m[3]!.trim() }] : [];
10
+ });
11
+ }
12
+ export function externalCodexPresent(rows: ProcessRow[], managerPid: number): boolean {
13
+ const children = new Set([managerPid]);
14
+ let changed = true;
15
+ while (changed) { changed = false; for (const row of rows) if (children.has(row.ppid) && !children.has(row.pid)) { children.add(row.pid); changed = true; } }
16
+ return rows.some(r => !children.has(r.pid) && basename(r.command) === "codex");
17
+ }
18
+ export async function codexPresent(managerPid = process.pid): Promise<boolean> {
19
+ const { stdout } = await promisify(execFile)("/bin/ps", ["-axo", "pid=,ppid=,comm="], { timeout: 5000, maxBuffer: 4 * 1024 * 1024 });
20
+ return externalCodexPresent(parseProcesses(stdout), managerPid);
21
+ }
package/codex/run.ts ADDED
@@ -0,0 +1,59 @@
1
+ import { loadBots } from "./bots-config.ts";
2
+ import type { BridgeConfig } from "./config.ts";
3
+ import { parseArgs } from "node:util";
4
+ import { resolveConfig } from "./config.ts";
5
+ import { AppServer } from "./app-server.ts";
6
+ import { Bridge } from "./bridge.ts";
7
+ import { AgentsChatTransport } from "./transport.ts";
8
+
9
+ export const HELP = `agentschat-mcp --codex-bridge [--cwd DIRECTORY] [--profile NAME_OR_PATH] [--codex-bin PATH] [--check]
10
+
11
+ Official Codex app-server bridge. Node >=22; Codex installed and signed in.
12
+ Starts a dedicated stdio app-server; does not attach to an active desktop task.
13
+ No notifications/chat/channel, no fork, no account registration.
14
+
15
+ Identity: --profile > CWD/.agentschat/config.json profile >
16
+ CWD/.agentschat/profile.json > CWD/.codex/config.toml MCP profile > AGENTSCHAT_PROFILE > AGENTCHAT_PROFILE > global default.
17
+ Only the exact CWD is searched. Named profiles live in ~/.agentschat (legacy ~/.agentchat).
18
+ An optional project agent_id must match the selected profile; it cannot replace it.
19
+ Credentials: private profile JSON {agent_id, token}, chmod 600; never put keys in argv.
20
+ Project config fields: profile, agent_id, channels, senders, api_url, ws_url.
21
+ --check validates identity and official app-server initialization without opening chat.
22
+ Live DMs and exact mentions trigger replies; channels/senders restrict this further.
23
+ Read-only Codex turns; inherited MCP servers disabled. No offline message replay.
24
+ State: ~/.agentschat/codex-bridge/<project-server-identity hash>/ (private).
25
+ See codex/README.md for setup, verification, limitations and recovery.
26
+ `;
27
+
28
+ let codex: AppServer | undefined, bridge: Bridge | undefined, transport: AgentsChatTransport | undefined;
29
+ async function main() {
30
+ const { values } = parseArgs({ options: { "codex-bridge": { type: "boolean" }, cwd: { type: "string" },
31
+ "managed-worker": { type: "boolean" }, registry: { type: "string" }, bot: { type: "string" }, profile: { type: "string" }, "codex-bin": { type: "string" }, check: { type: "boolean" },
32
+ help: { type: "boolean", short: "h" } }, strict: true });
33
+ if (values.help) { console.log(HELP); return; }
34
+ const snapshot = values["managed-worker"] ? await new Promise<BridgeConfig>((resolve, reject) => {
35
+ if (!process.connected) { reject(new Error("Managed worker needs parent IPC")); return; }
36
+ const timer = setTimeout(() => reject(new Error("Parent configuration missing")), 10000);
37
+ process.once("message", config => { clearTimeout(timer); resolve(config as BridgeConfig); });
38
+ }) : undefined;
39
+ const c = snapshot ?? (values.bot ? loadBots(values.registry).find(b => b.name === values.bot) : resolveConfig({ cwd: values.cwd, profile: values.profile, codexBin: values["codex-bin"] }));
40
+ if (!c) throw new Error("Bot is absent or disabled");
41
+ console.log(JSON.stringify({ cwd: c.cwd, agent_id: c.agentId, profile: c.profileFile, source: c.source, stateDir: c.stateDir }));
42
+ codex = new AppServer(c.codexBin);
43
+ if (values.check) { await codex.start(); console.log("Official app-server initialization: OK (no chat connection or generation)"); codex.close(); return; }
44
+ transport = new AgentsChatTransport(c, m => { bridge!.accept(m); });
45
+ bridge = new Bridge(c, codex, (chat, text) => transport!.send(chat, text), console.error, (chat, active) => transport!.setTyping(chat, active));
46
+ let stopping = false;
47
+ const stop = async () => { if (stopping) return; stopping = true; if (values["managed-worker"]) { const deadline = setTimeout(() => { try { process.kill(-process.pid, "SIGKILL"); } catch {} }, 20000); deadline.unref(); } bridge?.pause(); transport?.stop(); codex?.close(); await bridge?.stop(); if (process.connected) process.disconnect?.(); };
48
+ codex.onFatal = () => { console.error("Codex backend stopped; pending inbox preserved. Restart the bridge after checking failed entries."); process.exitCode = 1; void stop(); };
49
+ process.once("disconnect", () => void stop());
50
+ process.once("SIGINT", () => void stop()); process.once("SIGTERM", () => void stop());
51
+ await codex.start();
52
+ if (values["managed-worker"] && !process.connected) { await stop(); return; }
53
+ void bridge.drain(); transport.start();
54
+ }
55
+ main().catch(async e => {
56
+ // Do not echo arbitrary parse errors or subprocess output containing credentials.
57
+ console.error(`Bridge startup failed: ${e instanceof Error && !/token|secret/i.test(e.message) ? e.message : "invalid configuration"}`);
58
+ transport?.stop(); codex?.close(); await bridge?.stop(); process.exitCode = 1;
59
+ });
@@ -0,0 +1,99 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import WebSocket from "ws";
3
+ import type { BridgeConfig } from "./config.ts";
4
+ import { HeartbeatMonitor } from "../src/heartbeat.ts";
5
+
6
+ export class AgentsChatTransport {
7
+ private socket?: WebSocket;
8
+ private authenticated = false;
9
+ private typing = new Map<string, ReturnType<typeof setInterval>>();
10
+ private pending = new Map<string, { resolve: () => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>();
11
+ private heartbeat?: HeartbeatMonitor;
12
+ private retry?: ReturnType<typeof setTimeout>;
13
+ private closed = false;
14
+ private delay = 1000;
15
+ constructor(private config: BridgeConfig, private receive: (m: unknown) => void, private log: (s: string) => void = console.error) {}
16
+ async api(path: string, body?: unknown) {
17
+ let response: Response;
18
+ try {
19
+ response = await fetch(this.config.apiUrl + path, { method: body ? "POST" : "GET", redirect: "error",
20
+ headers: { Authorization: `Bearer ${this.config.token}`, "Content-Type": "application/json" },
21
+ ...(body ? { body: JSON.stringify(body) } : {}), signal: AbortSignal.timeout(15_000) });
22
+ } catch { throw new Error("AgentsChat request failed or timed out"); }
23
+ if (!response.ok) throw new Error(`AgentsChat HTTP ${response.status}`);
24
+ try { return await response.json(); } catch { throw new Error("Invalid AgentsChat response"); }
25
+ }
26
+ setTyping(channel: string, active: boolean) {
27
+ clearInterval(this.typing.get(channel)); this.typing.delete(channel);
28
+ if (!active || this.closed) return;
29
+ const pulse = () => {
30
+ if (this.authenticated && this.socket?.readyState === WebSocket.OPEN)
31
+ this.socket.send(JSON.stringify({ type: "typing", channel_id: channel, sender_id: this.config.agentId, cross_pod: true }));
32
+ };
33
+ pulse(); this.typing.set(channel, setInterval(pulse, 2000));
34
+ }
35
+ private rejectPending() {
36
+ for (const p of this.pending.values()) { clearTimeout(p.timer); p.reject(new Error("AgentsChat acknowledgement unavailable")); }
37
+ this.pending.clear();
38
+ }
39
+ async send(channel: string, text: string) {
40
+ // Use the authenticated connection and wait for the server's delivery ACK.
41
+ // Never fall back after an attempted send: an ACK may have been lost.
42
+ if (this.authenticated && this.socket?.readyState === WebSocket.OPEN) {
43
+ const id = randomUUID();
44
+ await new Promise<void>((resolve, reject) => {
45
+ const timer = setTimeout(() => { this.pending.delete(id); reject(new Error("AgentsChat acknowledgement timed out")); }, 15000);
46
+ this.pending.set(id, { resolve, reject, timer });
47
+ this.socket!.send(JSON.stringify({ type: "message", id, channel_id: channel,
48
+ sender_id: this.config.agentId, sender_type: "agent", content_type: "text", content: text }), error => {
49
+ if (error) { clearTimeout(timer); this.pending.delete(id); reject(new Error("AgentsChat socket send failed")); }
50
+ });
51
+ });
52
+ return;
53
+ }
54
+ await this.api(`/api/channels/${encodeURIComponent(channel)}/messages`, {
55
+ sender_id: this.config.agentId, content_type: "text", content: text,
56
+ });
57
+ }
58
+ start() {
59
+ if (this.closed) return;
60
+ const socket = new WebSocket(this.config.wsUrl, { maxPayload: 1_048_576, handshakeTimeout: 15_000 });
61
+ this.socket = socket;
62
+ const current = () => !this.closed && this.socket === socket;
63
+ const send = (value: unknown) => { if (current() && socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(value)); };
64
+ const join = (channel: string) => {
65
+ if (!this.config.channels.length || this.config.channels.includes(channel)) send({ type: "join_channel", channel_id: channel, agent_id: this.config.agentId });
66
+ };
67
+ this.heartbeat = new HeartbeatMonitor({ getReadyState: () => socket.readyState,
68
+ sendPing: () => send({ type: "ping" }), reconnect: () => socket.terminate() }, 15_000, 45_000, 30_000);
69
+ this.heartbeat.start();
70
+ socket.on("open", () => send({ type: "auth", agent_id: this.config.agentId, token: this.config.token, capabilities: ["chat", "codex"] }));
71
+ socket.on("message", raw => {
72
+ if (!current()) return;
73
+ let data: any; try { data = JSON.parse(String(raw)); } catch { return; }
74
+ this.heartbeat?.receivedPong();
75
+ if (data.type === "auth_ok") {
76
+ this.authenticated = true; this.delay = 1000; this.log(`AgentsChat connected as ${this.config.agentId}`);
77
+ void this.api("/api/channels/mine").then((body: any) => {
78
+ if (!current()) return;
79
+ const channels = Array.isArray(body) ? body : body.channels;
80
+ if (!Array.isArray(channels)) throw new Error("Invalid membership response");
81
+ for (const c of channels) if (typeof (c.id ?? c.channel_id) === "string") join(c.id ?? c.channel_id);
82
+ }).catch(() => { if (current()) { this.log("Membership sync failed; reconnecting"); socket.terminate(); } });
83
+ } else if (data.type === "message_ack") {
84
+ const id = data.message_id ?? data.id, pending = this.pending.get(id);
85
+ if (pending) { clearTimeout(pending.timer); this.pending.delete(id); pending.resolve(); }
86
+ } else if (data.type === "channel_created" && typeof data.channel_id === "string") join(data.channel_id);
87
+ else if (["message", "thread_reply"].includes(data.type)) this.receive(data);
88
+ else if (data.type === "shard_moved") socket.terminate();
89
+ else if (data.type === "auth_error" || data.type === "error") this.log("AgentsChat returned an error; check account and channel permissions");
90
+ });
91
+ socket.on("error", () => this.log("AgentsChat socket error"));
92
+ socket.on("close", () => {
93
+ if (!current()) return;
94
+ this.authenticated = false; this.rejectPending(); this.heartbeat?.stop(); this.log("AgentsChat disconnected; reconnecting (offline messages are not replayed)");
95
+ this.retry = setTimeout(() => this.start(), this.delay); this.delay = Math.min(this.delay * 2, 30_000);
96
+ });
97
+ }
98
+ stop() { for (const timer of this.typing.values()) clearInterval(timer); this.typing.clear(); this.closed = true; this.authenticated = false; this.rejectPending(); clearTimeout(this.retry); this.heartbeat?.stop(); this.socket?.terminate(); }
99
+ }
@@ -67,7 +67,7 @@ For the bundled Hermes adaptation skill and profile-specific setup commands, rea
67
67
  [`skills/onboarding.md` §4](../skills/onboarding.md). Upgrades from older connectors
68
68
  must follow the [0.34.0 migration notes](../CHANGELOG.md).
69
69
 
70
- **0.34.0 is unpublished:** use the [local build procedure](../skills/onboarding.md#local-build-before-runtime-configuration), not an assumed npm release.
70
+ **0.35.0 is unpublished:** use the [local build procedure](../skills/onboarding.md#local-build-before-runtime-configuration), not an assumed npm release.
71
71
  Requires Node ≥22, Bun ≥1.0 for installing/building, and a configured Hermes
72
72
  v0.21.1 profile. In a reviewed `AgentsChatProtocol/mcp-plugin` checkout:
73
73