fix(imessage): report non-mac default imsg hosts

This commit is contained in:
Vincent Koc
2026-05-07 04:52:01 -07:00
parent 4ad4be9aff
commit 84638bfbb0
4 changed files with 44 additions and 3 deletions

View File

@@ -2,7 +2,7 @@
"name": "@openclaw/imessage", "name": "@openclaw/imessage",
"version": "2026.5.6", "version": "2026.5.6",
"private": true, "private": true,
"description": "OpenClaw iMessage channel plugin", "description": "OpenClaw iMessage channel plugin using imsg on a signed-in Mac",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@openclaw/plugin-sdk": "workspace:*" "@openclaw/plugin-sdk": "workspace:*"
@@ -19,7 +19,7 @@
"detailLabel": "iMessage", "detailLabel": "iMessage",
"docsPath": "/channels/imessage", "docsPath": "/channels/imessage",
"docsLabel": "imessage", "docsLabel": "imessage",
"blurb": "this is still a work in progress.", "blurb": "iMessage via the imsg CLI on a signed-in Mac or SSH wrapper.",
"aliases": [ "aliases": [
"imsg" "imsg"
], ],

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract"; import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract";
import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime"; import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot"; import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
@@ -17,6 +18,7 @@ export type IMessageProbe = BaseProbeResult & {
export type IMessageProbeOptions = { export type IMessageProbeOptions = {
cliPath?: string; cliPath?: string;
dbPath?: string; dbPath?: string;
platform?: NodeJS.Platform;
runtime?: RuntimeEnv; runtime?: RuntimeEnv;
}; };
@@ -28,6 +30,21 @@ type RpcSupportResult = {
const rpcSupportCache = new Map<string, RpcSupportResult>(); const rpcSupportCache = new Map<string, RpcSupportResult>();
function isDefaultLocalIMessageCliPath(cliPath: string): boolean {
const trimmed = cliPath.trim();
return trimmed === "imsg" || (!trimmed.includes("/") && path.basename(trimmed) === "imsg");
}
export function resolveIMessageNonMacHostError(
cliPath: string,
platform: NodeJS.Platform = process.platform,
): string | undefined {
if (platform === "darwin" || !isDefaultLocalIMessageCliPath(cliPath)) {
return undefined;
}
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: string, timeoutMs: number): Promise<RpcSupportResult> { async function probeRpcSupport(cliPath: string, timeoutMs: number): Promise<RpcSupportResult> {
const cached = rpcSupportCache.get(cliPath); const cached = rpcSupportCache.get(cliPath);
if (cached) { if (cached) {
@@ -76,6 +93,11 @@ export async function probeIMessage(
const effectiveTimeout = const effectiveTimeout =
timeoutMs ?? cfg?.channels?.imessage?.probeTimeoutMs ?? DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS; timeoutMs ?? cfg?.channels?.imessage?.probeTimeoutMs ?? DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS;
const nonMacHostError = resolveIMessageNonMacHostError(cliPath, opts.platform);
if (nonMacHostError) {
return { ok: false, fatal: true, error: nonMacHostError };
}
const detected = await detectBinary(cliPath); const detected = await detectBinary(cliPath);
if (!detected) { if (!detected) {
return { ok: false, error: `imsg not found (${cliPath})` }; return { ok: false, error: `imsg not found (${cliPath})` };

View File

@@ -169,7 +169,8 @@ export function createIMessageCliPathTextInput(
export const imessageCompletionNote = { export const imessageCompletionNote = {
title: "iMessage next steps", title: "iMessage next steps",
lines: [ lines: [
"This is still a work in progress.", "Run OpenClaw on the Mac signed into Messages, or set cliPath to an SSH wrapper that runs imsg on that Mac.",
"Linux/Windows hosts cannot run the default local imsg path directly.",
"Ensure OpenClaw has Full Disk Access to Messages DB.", "Ensure OpenClaw has Full Disk Access to Messages DB.",
"Grant Automation permission for Messages when prompted.", "Grant Automation permission for Messages when prompted.",
"List chats with: imsg chats --limit 20", "List chats with: imsg chats --limit 20",

View File

@@ -169,6 +169,24 @@ describe("probeIMessage", () => {
expect(createIMessageRpcClientMock).not.toHaveBeenCalled(); expect(createIMessageRpcClientMock).not.toHaveBeenCalled();
}); });
it("fails fast for default local imsg probes on non-mac hosts", async () => {
const createIMessageRpcClientMock = vi
.spyOn(clientModule, "createIMessageRpcClient")
.mockResolvedValue({
request: vi.fn(),
stop: vi.fn(),
} as unknown as Awaited<ReturnType<typeof clientModule.createIMessageRpcClient>>);
const result = await probeIMessage(1000, { cliPath: "imsg", platform: "linux" });
expect(result.ok).toBe(false);
expect(result.fatal).toBe(true);
expect(result.error).toMatch(/macOS/i);
expect(result.error).toMatch(/SSH wrapper/i);
expect(setupRuntime.detectBinary).not.toHaveBeenCalled();
expect(createIMessageRpcClientMock).not.toHaveBeenCalled();
});
it("status probe uses account-scoped cliPath and dbPath", async () => { it("status probe uses account-scoped cliPath and dbPath", async () => {
const probeSpy = vi.spyOn(channelRuntimeModule, "probeIMessageAccount").mockResolvedValue({ const probeSpy = vi.spyOn(channelRuntimeModule, "probeIMessageAccount").mockResolvedValue({
ok: true, ok: true,