UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

219 lines (218 loc) 7.34 kB
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js"; import { i as formatErrorMessage } from "./errors-BXgSefBE.js"; import { p as resolveUserPath } from "./utils-CCC-BEJH.js"; import "./error-runtime-C8vbtAJt.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import "./text-utility-runtime-BZ3jGr-e.js"; import { spawn } from "node:child_process"; import { StringDecoder } from "node:string_decoder"; //#region extensions/imessage/src/constants.ts /** Default timeout for iMessage probe/RPC operations (10 seconds). */ const DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS = 1e4; const DEFAULT_IMESSAGE_SEND_TIMEOUT_MS = 15e4; //#endregion //#region extensions/imessage/src/client.ts const PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR = "imsg cannot access ~/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway."; function isTestEnv() { const vitest = normalizeLowercaseStringOrEmpty(process.env.VITEST); return Boolean(vitest); } function normalizeIMessageFullDiskAccessError(message) { const normalized = normalizeLowercaseStringOrEmpty(message); if (!normalized.includes("full disk access") || !normalized.includes("chat.db")) return; return PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR; } var IMessageRpcClient = class { constructor(opts = {}) { this.pending = /* @__PURE__ */ new Map(); this.closedResolve = null; this.child = null; this.stdoutBuffer = ""; this.stdoutDecoder = new StringDecoder("utf8"); this.nextId = 1; this.publicProcessError = null; this.cliPath = opts.cliPath?.trim() || "imsg"; this.dbPath = opts.dbPath?.trim() ? resolveUserPath(opts.dbPath) : void 0; this.runtime = opts.runtime; this.onNotification = opts.onNotification; this.closed = new Promise((resolve) => { this.closedResolve = resolve; }); } async start() { if (this.child) return; if (isTestEnv()) throw new Error("Refusing to start imsg rpc in test environment; mock iMessage RPC client"); const args = ["rpc", "--json"]; if (this.dbPath) args.push("--db", this.dbPath); const child = spawn(this.cliPath, args, { stdio: [ "pipe", "pipe", "pipe" ] }); this.child = child; child.stdout.on("data", (chunk) => { if (this.child !== child) return; this.handleStdoutChunk(chunk); }); child.stderr?.on("data", (chunk) => { const lines = chunk.toString().split(/\r?\n/); for (const line of lines) { if (!line.trim()) continue; const trimmed = line.trim(); this.recordProcessDiagnostic(trimmed); this.runtime?.error?.(`imsg rpc: ${trimmed}`); } }); child.on("error", (err) => { this.failAll(err instanceof Error ? err : new Error(String(err))); this.closedResolve?.(); }); child.stdin.on("error", (err) => { this.failAll(err instanceof Error ? err : new Error(String(err))); }); child.on("close", (code, signal) => { if (this.child === child) this.flushStdoutBuffer(); this.failAll(this.buildCloseError(code, signal)); this.closedResolve?.(); }); } async stop() { if (!this.child) return; this.stdoutBuffer = ""; this.stdoutDecoder.end(); this.child.stdin?.end(); const child = this.child; this.child = null; await Promise.race([this.closed, new Promise((resolve) => { setTimeout(() => { if (!child.killed) child.kill("SIGTERM"); resolve(); }, 500); })]); } async waitForClose() { await this.closed; } async request(method, params, opts) { if (!this.child || !this.child.stdin) throw new Error("imsg rpc not running"); const id = this.nextId++; const line = `${JSON.stringify({ jsonrpc: "2.0", id, method, params: params ?? {} })}\n`; const timeoutMs = opts?.timeoutMs ?? 1e4; const response = new Promise((resolve, reject) => { const key = String(id); const timer = timeoutMs > 0 ? setTimeout(() => { this.pending.delete(key); reject(/* @__PURE__ */ new Error(`imsg rpc timeout (${method})`)); }, timeoutMs) : void 0; this.pending.set(key, { resolve: (value) => resolve(value), reject, timer }); }); this.child.stdin.write(line, (err) => { if (err) { const key = String(id); const pending = this.pending.get(key); if (pending) { if (pending.timer) clearTimeout(pending.timer); this.pending.delete(key); pending.reject(err instanceof Error ? err : new Error(String(err))); } } }); return await response; } handleStdoutChunk(chunk) { const text = typeof chunk === "string" ? chunk : this.stdoutDecoder.write(chunk); this.stdoutBuffer += text; let newlineIndex = this.stdoutBuffer.indexOf("\n"); while (newlineIndex !== -1) { const line = this.stdoutBuffer.slice(0, newlineIndex); this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1); this.handleStdoutLine(line); newlineIndex = this.stdoutBuffer.indexOf("\n"); } } flushStdoutBuffer() { const tail = this.stdoutDecoder.end(); if (tail) this.stdoutBuffer += tail; if (!this.stdoutBuffer) return; const line = this.stdoutBuffer; this.stdoutBuffer = ""; this.handleStdoutLine(line); } handleStdoutLine(line) { const trimmed = line.trim(); if (!trimmed) return; this.handleLine(trimmed); } handleLine(line) { let parsed; try { parsed = JSON.parse(line); } catch (err) { this.recordProcessDiagnostic(line); const detail = formatErrorMessage(err); this.runtime?.error?.(`imsg rpc: failed to parse ${line}: ${detail}`); return; } if (parsed.id !== void 0 && parsed.id !== null) { const key = String(parsed.id); const pending = this.pending.get(key); if (!pending) return; if (pending.timer) clearTimeout(pending.timer); this.pending.delete(key); if (parsed.error) { const baseMessage = parsed.error.message ?? "imsg rpc error"; const details = parsed.error.data; const code = parsed.error.code; const suffixes = []; if (typeof code === "number") suffixes.push(`code=${code}`); if (details !== void 0) { const detailText = typeof details === "string" ? details : JSON.stringify(details, null, 2); if (detailText) suffixes.push(detailText); } const msg = suffixes.length > 0 ? `${baseMessage}: ${suffixes.join(" ")}` : baseMessage; pending.reject(new Error(msg)); return; } pending.resolve(parsed.result); return; } if (parsed.method) this.onNotification?.({ method: parsed.method, params: parsed.params }); } recordProcessDiagnostic(line) { this.publicProcessError ??= normalizeIMessageFullDiskAccessError(line) ?? null; } buildCloseError(code, signal) { if (this.publicProcessError) return new Error(this.publicProcessError); if (code !== 0 && code !== null) { const reason = signal ? `signal ${signal}` : `code ${code}`; return /* @__PURE__ */ new Error(`imsg rpc exited (${reason})`); } return /* @__PURE__ */ new Error("imsg rpc closed"); } failAll(err) { for (const [key, pending] of this.pending.entries()) { if (pending.timer) clearTimeout(pending.timer); pending.reject(err); this.pending.delete(key); } } }; async function createIMessageRpcClient(opts = {}) { const client = new IMessageRpcClient(opts); await client.start(); return client; } //#endregion export { DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS as n, DEFAULT_IMESSAGE_SEND_TIMEOUT_MS as r, createIMessageRpcClient as t };