openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
289 lines (288 loc) • 11 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js";
import { C as resolveExpiresAtMsFromDurationMs, o as asDateTimestampMs } from "./number-coercion-CJQ8TR--.js";
import { l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js";
import { r as runCommandWithTimeout } from "./exec-DubsSJS2.js";
import { i as getRuntimeConfig } from "./io-Gi7-pyU-.js";
import { i as stripAssistantInternalScaffolding } from "./assistant-visible-text-QdHceFYu.js";
import "./media-runtime-fgpmX6eL.js";
import "./number-runtime-DBLVDypr.js";
import "./runtime-config-snapshot-CDoFLjqb.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import "./text-chunking-D45TeeEs.js";
import "./core-DSxVv-v1.js";
import { t as detectBinary } from "./detect-binary-ihWW1rG_.js";
import "./setup-DHfdBmUU.js";
import "./process-runtime-Cl-ggYwo.js";
import "./status-helpers-BevxX6Pu.js";
import "./channel-status-DxlXr0Xh.js";
import { t as createIMessageRpcClient } from "./client-RDkCLFWE.js";
import { r as setCachedIMessagePrivateApiStatus, t as getCachedIMessagePrivateApiStatus } from "./private-api-status-DBgq8x0h.js";
import { c as parseIMessageTarget, o as normalizeIMessageHandle } from "./targets-BhNdlUSi.js";
import "./normalize-BfDcnrra.js";
import path from "node:path";
//#region extensions/imessage/src/probe.ts
const RPC_SUPPORT_CACHE_TTL_MS = 300 * 1e3;
const PRIVATE_API_NEGATIVE_TTL_MS = 10 * 1e3;
const rpcSupportCache = /* @__PURE__ */ new Map();
function cacheIMessagePrivateApiStatus(cliPath, status) {
if (status.available) {
setCachedIMessagePrivateApiStatus(cliPath, status, 0);
return;
}
const expiresAt = resolveExpiresAtMsFromDurationMs(PRIVATE_API_NEGATIVE_TTL_MS);
if (expiresAt !== void 0) setCachedIMessagePrivateApiStatus(cliPath, status, expiresAt);
}
function getCachedRpcSupport(cliPath) {
const cached = rpcSupportCache.get(cliPath);
if (!cached) return;
const now = asDateTimestampMs(Date.now());
if (now === void 0 || cached.expiresAt <= now) {
rpcSupportCache.delete(cliPath);
return;
}
return cached.result;
}
function setCachedRpcSupport(cliPath, result) {
const expiresAt = resolveExpiresAtMsFromDurationMs(RPC_SUPPORT_CACHE_TTL_MS);
if (expiresAt === void 0) return;
rpcSupportCache.set(cliPath, {
result,
expiresAt
});
}
function isDefaultLocalIMessageCliPath(cliPath) {
const trimmed = cliPath.trim();
return trimmed === "imsg" || !trimmed.includes("/") && path.basename(trimmed) === "imsg";
}
function resolveIMessageNonMacHostError(cliPath, platform = process.platform) {
if (platform === "darwin" || !isDefaultLocalIMessageCliPath(cliPath)) return;
return "iMessage via the default imsg CLI must run on macOS. Run OpenClaw on the signed-in Messages Mac, or set channels.imessage.cliPath to an SSH wrapper that runs imsg on that Mac.";
}
async function probeRpcSupport(cliPath, timeoutMs) {
const cached = getCachedRpcSupport(cliPath);
if (cached) return cached;
try {
const result = await runCommandWithTimeout([
cliPath,
"rpc",
"--help"
], { timeoutMs });
const combined = `${result.stdout}\n${result.stderr}`.trim();
const normalized = normalizeLowercaseStringOrEmpty(combined);
if (normalized.includes("unknown command") && normalized.includes("rpc")) {
const fatal = {
supported: false,
fatal: true,
error: "imsg CLI does not support the \"rpc\" subcommand (update imsg)"
};
setCachedRpcSupport(cliPath, fatal);
return fatal;
}
if (result.code === 0) {
const supported = { supported: true };
setCachedRpcSupport(cliPath, supported);
return supported;
}
return {
supported: false,
error: combined || `imsg rpc --help failed (code ${String(result.code ?? "unknown")})`
};
} catch (err) {
return {
supported: false,
error: String(err)
};
}
}
function parseStatusPayload(stdout) {
const lines = normalizeStringEntries(stdout.split(/\r?\n/));
for (const line of lines.toReversed()) try {
const value = JSON.parse(line);
if (value && typeof value === "object" && !Array.isArray(value)) return { payload: value };
} catch {}
return {
payload: null,
firstLineSnippet: lines[0]?.slice(0, 120)
};
}
function selectorsFromPayload(payload) {
const raw = payload.selectors;
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
const selectors = {};
for (const [key, value] of Object.entries(raw)) if (typeof value === "boolean") selectors[key] = value;
return selectors;
}
function rpcMethodsFromPayload(payload) {
const raw = payload.rpc_methods;
if (!Array.isArray(raw)) return [];
return raw.filter((entry) => typeof entry === "string");
}
async function probeSendRichSupportsAttachment(cliPath, timeoutMs) {
try {
const result = await runCommandWithTimeout([
cliPath,
"send-rich",
"--help"
], { timeoutMs });
if (result.code !== 0) return false;
const combined = `${result.stdout}\n${result.stderr}`;
return /(?:^|\s)--file\b/m.test(combined);
} catch {
return false;
}
}
async function probeIMessagePrivateApi(cliPath, timeoutMs, options = {}) {
const key = cliPath.trim() || "imsg";
if (!options.forceRefresh) {
const cached = getCachedIMessagePrivateApiStatus(key);
if (cached) return cached;
}
try {
const result = await runCommandWithTimeout([
key,
"status",
"--json"
], { timeoutMs });
const combined = `${result.stdout}\n${result.stderr}`.trim();
const { payload, firstLineSnippet } = parseStatusPayload(result.stdout);
const selectors = payload ? selectorsFromPayload(payload) : {};
const rpcMethods = payload ? rpcMethodsFromPayload(payload) : [];
const advancedFeatures = payload?.advanced_features === true;
const v2Ready = payload?.v2_ready === true;
const statusMessage = typeof payload?.message === "string" ? payload.message : void 0;
const sendRichSupportsAttachment = await probeSendRichSupportsAttachment(key, timeoutMs);
const status = {
available: result.code === 0 && advancedFeatures && v2Ready,
v2Ready,
selectors,
rpcMethods,
cliCapabilities: { sendRichSupportsAttachment },
...statusMessage ? { statusMessage } : {},
...result.code === 0 ? !payload && firstLineSnippet ? { error: `imsg status --json returned no parseable JSONL (first line: "${firstLineSnippet}") — output schema may have changed` } : {} : { error: combined || `imsg status --json failed (code ${String(result.code)})` }
};
cacheIMessagePrivateApiStatus(key, status);
return status;
} catch (err) {
const status = {
available: false,
v2Ready: false,
selectors: {},
rpcMethods: [],
cliCapabilities: { sendRichSupportsAttachment: false },
error: String(err)
};
cacheIMessagePrivateApiStatus(key, status);
return status;
}
}
/**
* Probe iMessage RPC availability.
* @param timeoutMs - Explicit timeout in ms. If undefined, uses config or default.
* @param opts - Additional options (cliPath, dbPath, runtime).
*/
async function probeIMessage(timeoutMs, opts = {}) {
const cfg = opts.cliPath || opts.dbPath ? void 0 : getRuntimeConfig();
const cliPath = opts.cliPath?.trim() || cfg?.channels?.imessage?.cliPath?.trim() || "imsg";
const dbPath = opts.dbPath?.trim() || cfg?.channels?.imessage?.dbPath?.trim();
const effectiveTimeout = timeoutMs ?? cfg?.channels?.imessage?.probeTimeoutMs ?? 1e4;
const nonMacHostError = resolveIMessageNonMacHostError(cliPath, opts.platform);
if (nonMacHostError) return {
ok: false,
fatal: true,
error: nonMacHostError
};
if (!await detectBinary(cliPath)) return {
ok: false,
error: `imsg not found (${cliPath})`
};
const rpcSupport = await probeRpcSupport(cliPath, effectiveTimeout);
if (!rpcSupport.supported) return {
ok: false,
error: rpcSupport.error ?? "imsg rpc unavailable",
fatal: rpcSupport.fatal
};
const privateApi = await probeIMessagePrivateApi(cliPath, effectiveTimeout);
const client = await createIMessageRpcClient({
cliPath,
dbPath,
runtime: opts.runtime
});
try {
await client.request("chats.list", { limit: 1 }, { timeoutMs: effectiveTimeout });
return {
ok: true,
privateApi
};
} catch (err) {
return {
ok: false,
error: String(err),
privateApi
};
} finally {
await client.stop();
}
}
//#endregion
//#region extensions/imessage/src/conversation-id-core.ts
function normalizeIMessageAcpConversationId(conversationId) {
const trimmed = conversationId.trim();
if (!trimmed) return null;
try {
const parsed = parseIMessageTarget(trimmed);
if (parsed.kind === "handle") {
const handle = normalizeIMessageHandle(parsed.to);
return handle ? { conversationId: handle } : null;
}
if (parsed.kind === "chat_id") return { conversationId: String(parsed.chatId) };
if (parsed.kind === "chat_guid") return { conversationId: parsed.chatGuid };
return { conversationId: parsed.chatIdentifier };
} catch {
const handle = normalizeIMessageHandle(trimmed);
return handle ? { conversationId: handle } : null;
}
}
function matchIMessageAcpConversation(params) {
const binding = normalizeIMessageAcpConversationId(params.bindingConversationId);
const conversation = normalizeIMessageAcpConversationId(params.conversationId);
if (!binding || !conversation) return null;
if (binding.conversationId !== conversation.conversationId) return null;
return {
conversationId: conversation.conversationId,
matchPriority: 2
};
}
function resolveIMessageConversationIdFromTarget(target) {
return normalizeIMessageAcpConversationId(target)?.conversationId;
}
//#endregion
//#region extensions/imessage/src/conversation-id.ts
function resolveIMessageInboundConversationId(params) {
if (params.isGroup) return params.chatId != null && Number.isFinite(params.chatId) ? String(params.chatId) : void 0;
return normalizeIMessageHandle(params.sender) || void 0;
}
//#endregion
//#region extensions/imessage/src/monitor/sanitize-outbound.ts
/**
* Patterns that indicate assistant-internal metadata leaked into text.
* These must never reach a user-facing channel.
*/
const INTERNAL_SEPARATOR_RE = /(?:#\+){2,}#?/g;
const ASSISTANT_ROLE_MARKER_RE = /\bassistant\s+to\s*=\s*\w+/gi;
const ROLE_TURN_MARKER_RE = /\b(?:user|system|assistant)\s*:\s*$/gm;
/**
* Strip all assistant-internal scaffolding from outbound text before delivery.
* Applies reasoning/thinking tag removal, memory tag removal, and
* model-specific internal separator stripping.
*/
function sanitizeOutboundText(text) {
if (!text) return text;
let cleaned = stripAssistantInternalScaffolding(text);
cleaned = cleaned.replace(INTERNAL_SEPARATOR_RE, "");
cleaned = cleaned.replace(ASSISTANT_ROLE_MARKER_RE, "");
cleaned = cleaned.replace(ROLE_TURN_MARKER_RE, "");
cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
return cleaned;
}
//#endregion
export { resolveIMessageConversationIdFromTarget as a, normalizeIMessageAcpConversationId as i, resolveIMessageInboundConversationId as n, probeIMessage as o, matchIMessageAcpConversation as r, probeIMessagePrivateApi as s, sanitizeOutboundText as t };