diff --git a/extensions/imessage/package.json b/extensions/imessage/package.json index 860617e5d42..7b6ca03b4d6 100644 --- a/extensions/imessage/package.json +++ b/extensions/imessage/package.json @@ -2,7 +2,7 @@ "name": "@openclaw/imessage", "version": "2026.5.6", "private": true, - "description": "OpenClaw iMessage channel plugin", + "description": "OpenClaw iMessage channel plugin using imsg on a signed-in Mac", "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -19,7 +19,7 @@ "detailLabel": "iMessage", "docsPath": "/channels/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": [ "imsg" ], diff --git a/extensions/imessage/src/probe.ts b/extensions/imessage/src/probe.ts index 740ea3335fc..055f86768b3 100644 --- a/extensions/imessage/src/probe.ts +++ b/extensions/imessage/src/probe.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import type { BaseProbeResult } from "openclaw/plugin-sdk/channel-contract"; import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime"; import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot"; @@ -17,6 +18,7 @@ export type IMessageProbe = BaseProbeResult & { export type IMessageProbeOptions = { cliPath?: string; dbPath?: string; + platform?: NodeJS.Platform; runtime?: RuntimeEnv; }; @@ -28,6 +30,21 @@ type RpcSupportResult = { const rpcSupportCache = new Map(); +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 { const cached = rpcSupportCache.get(cliPath); if (cached) { @@ -76,6 +93,11 @@ export async function probeIMessage( const effectiveTimeout = 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); if (!detected) { return { ok: false, error: `imsg not found (${cliPath})` }; diff --git a/extensions/imessage/src/setup-core.ts b/extensions/imessage/src/setup-core.ts index 74032d75e98..c8d1ca3488e 100644 --- a/extensions/imessage/src/setup-core.ts +++ b/extensions/imessage/src/setup-core.ts @@ -169,7 +169,8 @@ export function createIMessageCliPathTextInput( export const imessageCompletionNote = { title: "iMessage next steps", 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.", "Grant Automation permission for Messages when prompted.", "List chats with: imsg chats --limit 20", diff --git a/extensions/imessage/src/status.test.ts b/extensions/imessage/src/status.test.ts index cbfa9615ef5..99ec98ff37d 100644 --- a/extensions/imessage/src/status.test.ts +++ b/extensions/imessage/src/status.test.ts @@ -169,6 +169,24 @@ describe("probeIMessage", () => { 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>); + + 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 () => { const probeSpy = vi.spyOn(channelRuntimeModule, "probeIMessageAccount").mockResolvedValue({ ok: true,