@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
492 lines (485 loc) • 17.1 kB
JavaScript
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js";
import { A as normalizeE164, P as resolveUserPath } from "./registry-dD2_jBuv.js";
import { x as mediaKindFromMime } from "./fs-safe-GrTh3Ydq.js";
import { n as loadConfig } from "./config-42hNXHap.js";
import { t as resolveIMessageAccount } from "./accounts-Bfw4rqq8.js";
import { n as resolveMarkdownTableMode } from "./markdown-tables-CNKgA0te.js";
import { t as resolveOutboundAttachmentFromUrl } from "./outbound-attachment-BYcoEf4w.js";
import { t as convertMarkdownTables } from "./tables-CUmmBJsI.js";
import { spawn } from "node:child_process";
import { createInterface } from "node:readline";
//#region src/plugin-sdk/allow-from.ts
function isAllowedParsedChatSender(params) {
const allowFrom = params.allowFrom.map((entry) => String(entry).trim());
if (allowFrom.length === 0) return true;
if (allowFrom.includes("*")) return true;
const senderNormalized = params.normalizeSender(params.sender);
const chatId = params.chatId ?? void 0;
const chatGuid = params.chatGuid?.trim();
const chatIdentifier = params.chatIdentifier?.trim();
for (const entry of allowFrom) {
if (!entry) continue;
const parsed = params.parseAllowTarget(entry);
if (parsed.kind === "chat_id" && chatId !== void 0) {
if (parsed.chatId === chatId) return true;
} else if (parsed.kind === "chat_guid" && chatGuid) {
if (parsed.chatGuid === chatGuid) return true;
} else if (parsed.kind === "chat_identifier" && chatIdentifier) {
if (parsed.chatIdentifier === chatIdentifier) return true;
} else if (parsed.kind === "handle" && senderNormalized) {
if (parsed.handle === senderNormalized) return true;
}
}
return false;
}
//#endregion
//#region src/imessage/target-parsing-helpers.ts
function stripPrefix(value, prefix) {
return value.slice(prefix.length).trim();
}
function resolveServicePrefixedTarget(params) {
for (const { prefix, service } of params.servicePrefixes) {
if (!params.lower.startsWith(prefix)) continue;
const remainder = stripPrefix(params.trimmed, prefix);
if (!remainder) throw new Error(`${prefix} target is required`);
const remainderLower = remainder.toLowerCase();
if (params.isChatTarget(remainderLower)) return params.parseTarget(remainder);
return {
kind: "handle",
to: remainder,
service
};
}
return null;
}
function parseChatTargetPrefixesOrThrow(params) {
for (const prefix of params.chatIdPrefixes) if (params.lower.startsWith(prefix)) {
const value = stripPrefix(params.trimmed, prefix);
const chatId = Number.parseInt(value, 10);
if (!Number.isFinite(chatId)) throw new Error(`Invalid chat_id: ${value}`);
return {
kind: "chat_id",
chatId
};
}
for (const prefix of params.chatGuidPrefixes) if (params.lower.startsWith(prefix)) {
const value = stripPrefix(params.trimmed, prefix);
if (!value) throw new Error("chat_guid is required");
return {
kind: "chat_guid",
chatGuid: value
};
}
for (const prefix of params.chatIdentifierPrefixes) if (params.lower.startsWith(prefix)) {
const value = stripPrefix(params.trimmed, prefix);
if (!value) throw new Error("chat_identifier is required");
return {
kind: "chat_identifier",
chatIdentifier: value
};
}
return null;
}
function resolveServicePrefixedAllowTarget(params) {
for (const { prefix } of params.servicePrefixes) {
if (!params.lower.startsWith(prefix)) continue;
const remainder = stripPrefix(params.trimmed, prefix);
if (!remainder) return {
kind: "handle",
handle: ""
};
return params.parseAllowTarget(remainder);
}
return null;
}
function parseChatAllowTargetPrefixes(params) {
for (const prefix of params.chatIdPrefixes) if (params.lower.startsWith(prefix)) {
const value = stripPrefix(params.trimmed, prefix);
const chatId = Number.parseInt(value, 10);
if (Number.isFinite(chatId)) return {
kind: "chat_id",
chatId
};
}
for (const prefix of params.chatGuidPrefixes) if (params.lower.startsWith(prefix)) {
const value = stripPrefix(params.trimmed, prefix);
if (value) return {
kind: "chat_guid",
chatGuid: value
};
}
for (const prefix of params.chatIdentifierPrefixes) if (params.lower.startsWith(prefix)) {
const value = stripPrefix(params.trimmed, prefix);
if (value) return {
kind: "chat_identifier",
chatIdentifier: value
};
}
return null;
}
//#endregion
//#region src/imessage/targets.ts
const CHAT_ID_PREFIXES = [
"chat_id:",
"chatid:",
"chat:"
];
const CHAT_GUID_PREFIXES = [
"chat_guid:",
"chatguid:",
"guid:"
];
const CHAT_IDENTIFIER_PREFIXES = [
"chat_identifier:",
"chatidentifier:",
"chatident:"
];
const SERVICE_PREFIXES = [
{
prefix: "imessage:",
service: "imessage"
},
{
prefix: "sms:",
service: "sms"
},
{
prefix: "auto:",
service: "auto"
}
];
function normalizeIMessageHandle(raw) {
const trimmed = raw.trim();
if (!trimmed) return "";
const lowered = trimmed.toLowerCase();
if (lowered.startsWith("imessage:")) return normalizeIMessageHandle(trimmed.slice(9));
if (lowered.startsWith("sms:")) return normalizeIMessageHandle(trimmed.slice(4));
if (lowered.startsWith("auto:")) return normalizeIMessageHandle(trimmed.slice(5));
for (const prefix of CHAT_ID_PREFIXES) if (lowered.startsWith(prefix)) return `chat_id:${trimmed.slice(prefix.length).trim()}`;
for (const prefix of CHAT_GUID_PREFIXES) if (lowered.startsWith(prefix)) return `chat_guid:${trimmed.slice(prefix.length).trim()}`;
for (const prefix of CHAT_IDENTIFIER_PREFIXES) if (lowered.startsWith(prefix)) return `chat_identifier:${trimmed.slice(prefix.length).trim()}`;
if (trimmed.includes("@")) return trimmed.toLowerCase();
const normalized = normalizeE164(trimmed);
if (normalized) return normalized;
return trimmed.replace(/\s+/g, "");
}
function parseIMessageTarget(raw) {
const trimmed = raw.trim();
if (!trimmed) throw new Error("iMessage target is required");
const lower = trimmed.toLowerCase();
const servicePrefixed = resolveServicePrefixedTarget({
trimmed,
lower,
servicePrefixes: SERVICE_PREFIXES,
isChatTarget: (remainderLower) => CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) || CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) || CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p)),
parseTarget: parseIMessageTarget
});
if (servicePrefixed) return servicePrefixed;
const chatTarget = parseChatTargetPrefixesOrThrow({
trimmed,
lower,
chatIdPrefixes: CHAT_ID_PREFIXES,
chatGuidPrefixes: CHAT_GUID_PREFIXES,
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES
});
if (chatTarget) return chatTarget;
return {
kind: "handle",
to: trimmed,
service: "auto"
};
}
function parseIMessageAllowTarget(raw) {
const trimmed = raw.trim();
if (!trimmed) return {
kind: "handle",
handle: ""
};
const lower = trimmed.toLowerCase();
const servicePrefixed = resolveServicePrefixedAllowTarget({
trimmed,
lower,
servicePrefixes: SERVICE_PREFIXES,
parseAllowTarget: parseIMessageAllowTarget
});
if (servicePrefixed) return servicePrefixed;
const chatTarget = parseChatAllowTargetPrefixes({
trimmed,
lower,
chatIdPrefixes: CHAT_ID_PREFIXES,
chatGuidPrefixes: CHAT_GUID_PREFIXES,
chatIdentifierPrefixes: CHAT_IDENTIFIER_PREFIXES
});
if (chatTarget) return chatTarget;
return {
kind: "handle",
handle: normalizeIMessageHandle(trimmed)
};
}
function isAllowedIMessageSender(params) {
return isAllowedParsedChatSender({
allowFrom: params.allowFrom,
sender: params.sender,
chatId: params.chatId,
chatGuid: params.chatGuid,
chatIdentifier: params.chatIdentifier,
normalizeSender: normalizeIMessageHandle,
parseAllowTarget: parseIMessageAllowTarget
});
}
function formatIMessageChatTarget(chatId) {
if (!chatId || !Number.isFinite(chatId)) return "";
return `chat_id:${chatId}`;
}
//#endregion
//#region src/imessage/constants.ts
/** Default timeout for iMessage probe/RPC operations (10 seconds). */
const DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS = 1e4;
//#endregion
//#region src/imessage/client.ts
function isTestEnv() {
const vitest = process.env.VITEST?.trim().toLowerCase();
return Boolean(vitest);
}
var IMessageRpcClient = class {
constructor(opts = {}) {
this.pending = /* @__PURE__ */ new Map();
this.closedResolve = null;
this.child = null;
this.reader = null;
this.nextId = 1;
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"];
if (this.dbPath) args.push("--db", this.dbPath);
const child = spawn(this.cliPath, args, { stdio: [
"pipe",
"pipe",
"pipe"
] });
this.child = child;
this.reader = createInterface({ input: child.stdout });
this.reader.on("line", (line) => {
const trimmed = line.trim();
if (!trimmed) return;
this.handleLine(trimmed);
});
child.stderr?.on("data", (chunk) => {
const lines = chunk.toString().split(/\r?\n/);
for (const line of lines) {
if (!line.trim()) continue;
this.runtime?.error?.(`imsg rpc: ${line.trim()}`);
}
});
child.on("error", (err) => {
this.failAll(err instanceof Error ? err : new Error(String(err)));
this.closedResolve?.();
});
child.on("close", (code, signal) => {
if (code !== 0 && code !== null) {
const reason = signal ? `signal ${signal}` : `code ${code}`;
this.failAll(/* @__PURE__ */ new Error(`imsg rpc exited (${reason})`));
} else this.failAll(/* @__PURE__ */ new Error("imsg rpc closed"));
this.closedResolve?.();
});
}
async stop() {
if (!this.child) return;
this.reader?.close();
this.reader = null;
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 payload = {
jsonrpc: "2.0",
id,
method,
params: params ?? {}
};
const line = `${JSON.stringify(payload)}\n`;
const timeoutMs = opts?.timeoutMs ?? DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS;
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);
return await response;
}
handleLine(line) {
let parsed;
try {
parsed = JSON.parse(line);
} catch (err) {
const detail = err instanceof Error ? err.message : String(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
});
}
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
//#region src/imessage/send.ts
var send_exports = /* @__PURE__ */ __exportAll({ sendMessageIMessage: () => sendMessageIMessage });
const LEADING_REPLY_TAG_RE = /^\s*\[\[\s*reply_to\s*:\s*([^\]\n]+)\s*\]\]\s*/i;
const MAX_REPLY_TO_ID_LENGTH = 256;
function stripUnsafeReplyTagChars(value) {
let next = "";
for (const ch of value) {
const code = ch.charCodeAt(0);
if (code >= 0 && code <= 31 || code === 127 || ch === "[" || ch === "]") continue;
next += ch;
}
return next;
}
function sanitizeReplyToId(rawReplyToId) {
const trimmed = rawReplyToId?.trim();
if (!trimmed) return;
const sanitized = stripUnsafeReplyTagChars(trimmed).trim();
if (!sanitized) return;
if (sanitized.length > MAX_REPLY_TO_ID_LENGTH) return sanitized.slice(0, MAX_REPLY_TO_ID_LENGTH);
return sanitized;
}
function prependReplyTagIfNeeded(message, replyToId) {
const resolvedReplyToId = sanitizeReplyToId(replyToId);
if (!resolvedReplyToId) return message;
const replyTag = `[[reply_to:${resolvedReplyToId}]]`;
const existingLeadingTag = message.match(LEADING_REPLY_TAG_RE);
if (existingLeadingTag) {
const remainder = message.slice(existingLeadingTag[0].length).trimStart();
return remainder ? `${replyTag} ${remainder}` : replyTag;
}
const trimmedMessage = message.trimStart();
return trimmedMessage ? `${replyTag} ${trimmedMessage}` : replyTag;
}
function resolveMessageId(result) {
if (!result) return null;
const raw = typeof result.messageId === "string" && result.messageId.trim() || typeof result.message_id === "string" && result.message_id.trim() || typeof result.id === "string" && result.id.trim() || typeof result.guid === "string" && result.guid.trim() || (typeof result.message_id === "number" ? String(result.message_id) : null) || (typeof result.id === "number" ? String(result.id) : null);
return raw ? String(raw).trim() : null;
}
async function sendMessageIMessage(to, text, opts = {}) {
const cfg = opts.config ?? loadConfig();
const account = opts.account ?? resolveIMessageAccount({
cfg,
accountId: opts.accountId
});
const cliPath = opts.cliPath?.trim() || account.config.cliPath?.trim() || "imsg";
const dbPath = opts.dbPath?.trim() || account.config.dbPath?.trim();
const target = parseIMessageTarget(opts.chatId ? formatIMessageChatTarget(opts.chatId) : to);
const service = opts.service ?? (target.kind === "handle" ? target.service : void 0) ?? account.config.service;
const region = opts.region?.trim() || account.config.region?.trim() || "US";
const maxBytes = typeof opts.maxBytes === "number" ? opts.maxBytes : typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb * 1024 * 1024 : 16 * 1024 * 1024;
let message = text ?? "";
let filePath;
if (opts.mediaUrl?.trim()) {
const resolved = await (opts.resolveAttachmentImpl ?? resolveOutboundAttachmentFromUrl)(opts.mediaUrl.trim(), maxBytes, { localRoots: opts.mediaLocalRoots });
filePath = resolved.path;
if (!message.trim()) {
const kind = mediaKindFromMime(resolved.contentType ?? void 0);
if (kind) message = kind === "image" ? "<media:image>" : `<media:${kind}>`;
}
}
if (!message.trim() && !filePath) throw new Error("iMessage send requires text or media");
if (message.trim()) {
const tableMode = resolveMarkdownTableMode({
cfg,
channel: "imessage",
accountId: account.accountId
});
message = convertMarkdownTables(message, tableMode);
}
message = prependReplyTagIfNeeded(message, opts.replyToId);
const params = {
text: message,
service: service || "auto",
region
};
if (filePath) params.file = filePath;
if (target.kind === "chat_id") params.chat_id = target.chatId;
else if (target.kind === "chat_guid") params.chat_guid = target.chatGuid;
else if (target.kind === "chat_identifier") params.chat_identifier = target.chatIdentifier;
else params.to = target.to;
const client = opts.client ?? (opts.createClient ? await opts.createClient({
cliPath,
dbPath
}) : await createIMessageRpcClient({
cliPath,
dbPath
}));
const shouldClose = !opts.client;
try {
const result = await client.request("send", params, { timeoutMs: opts.timeoutMs });
return { messageId: resolveMessageId(result) ?? (result?.ok ? "ok" : "unknown") };
} finally {
if (shouldClose) await client.stop();
}
}
//#endregion
export { formatIMessageChatTarget as a, parseIMessageTarget as c, DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS as i, send_exports as n, isAllowedIMessageSender as o, createIMessageRpcClient as r, normalizeIMessageHandle as s, sendMessageIMessage as t };