Files
openclaw/extensions/imessage/src/cli-output.ts
T
Peter Steinberger 71a33f07a9 fix(imessage): make SSH-backed sends and actions reliable (#121038)
* fix(imessage): harden remote Mac transport

Route SSH-backed iMessage actions through JSON-RPC, preserve remote database paths, and stage outbound files on the Messages Mac with bounded cleanup. Keep local action semantics intact while failing closed on ambiguous wrappers and surfacing the remaining imsg v0.13.4 limits.

* fix(imessage): remove test-only exports
2026-08-09 04:48:38 -07:00

79 lines
2.4 KiB
TypeScript

// Bounded one-shot iMessage CLI execution shared by action and send surfaces.
import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
import { expandIMessageUserPath } from "./cli-path.js";
const IMESSAGE_CLI_STDOUT_MAX_BYTES = 8 * 1024 * 1024;
const IMESSAGE_CLI_STDERR_TAIL_BYTES = 64 * 1024;
function parseLastJsonObject(stdout: string): Record<string, unknown> | null {
const last = stdout
.split(/\r?\n/u)
.findLast((line) => line.trim().length > 0)
?.trim();
if (!last) {
return null;
}
try {
const value = JSON.parse(last) as unknown;
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
} catch {
return null;
}
}
export async function runIMessageCliJsonCommand(params: {
cliPath: string;
dbPath?: string;
args: readonly string[];
timeoutMs?: number;
}): Promise<Record<string, unknown>> {
const dbPath = params.dbPath?.trim();
const argv = [
expandIMessageUserPath(params.cliPath),
...params.args,
...(dbPath ? ["--db", dbPath] : []),
"--json",
];
const result = await runCommandWithTimeout(argv, {
killProcessTree: true,
maxOutputBytes: {
stdout: IMESSAGE_CLI_STDOUT_MAX_BYTES,
stderr: IMESSAGE_CLI_STDERR_TAIL_BYTES,
},
outputCapture: { stdout: "head", stderr: "tail" },
terminateOnOutputLimit: { stdout: true },
timeoutMs: params.timeoutMs,
});
if (result.termination === "timeout") {
throw new Error(`iMessage action timed out after ${params.timeoutMs}ms`);
}
if (result.outputLimitExceeded || result.stdoutTruncatedBytes) {
throw new Error(`imsg stdout exceeded ${IMESSAGE_CLI_STDOUT_MAX_BYTES} bytes`);
}
const parsed = parseLastJsonObject(result.stdout);
if (result.code !== 0) {
const detail =
(typeof parsed?.error === "string" && parsed.error.trim()) ||
result.stderr.trim() ||
result.stdout.trim() ||
`imsg exited with code ${result.code}`;
throw new Error(detail);
}
if (!parsed) {
throw new Error(
`imsg returned non-JSON output: ${result.stdout.trim() || result.stderr.trim()}`,
);
}
if (parsed.success === false) {
const detail =
typeof parsed.error === "string" && parsed.error.trim()
? parsed.error.trim()
: "iMessage action failed";
throw new Error(detail);
}
return parsed;
}