From 413d2ff3da8e4dd7c2b12845a0f12f710f89c8f4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 18:52:22 -0700 Subject: [PATCH] iMessage: lazy-load setup wizard surface --- extensions/imessage/src/channel.runtime.ts | 1 + extensions/imessage/src/channel.ts | 10 +- extensions/imessage/src/setup-core.ts | 236 +++++++++++++++++++++ extensions/imessage/src/setup-surface.ts | 111 +--------- src/plugin-sdk/imessage.ts | 6 +- src/plugin-sdk/index.ts | 6 +- 6 files changed, 254 insertions(+), 116 deletions(-) create mode 100644 extensions/imessage/src/channel.runtime.ts create mode 100644 extensions/imessage/src/setup-core.ts diff --git a/extensions/imessage/src/channel.runtime.ts b/extensions/imessage/src/channel.runtime.ts new file mode 100644 index 00000000000..81229e49ff9 --- /dev/null +++ b/extensions/imessage/src/channel.runtime.ts @@ -0,0 +1 @@ +export { imessageSetupWizard } from "./setup-surface.js"; diff --git a/extensions/imessage/src/channel.ts b/extensions/imessage/src/channel.ts index 5760d1c2fb3..f2621dea5c2 100644 --- a/extensions/imessage/src/channel.ts +++ b/extensions/imessage/src/channel.ts @@ -28,10 +28,18 @@ import { import { resolveOutboundSendDep } from "../../../src/infra/outbound/send-deps.js"; import { buildPassiveProbedChannelStatusSummary } from "../../shared/channel-status-summary.js"; import { getIMessageRuntime } from "./runtime.js"; -import { imessageSetupAdapter, imessageSetupWizard } from "./setup-surface.js"; +import { createIMessageSetupWizardProxy, imessageSetupAdapter } from "./setup-core.js"; const meta = getChatChannelMeta("imessage"); +async function loadIMessageChannelRuntime() { + return await import("./channel.runtime.js"); +} + +const imessageSetupWizard = createIMessageSetupWizardProxy(async () => ({ + imessageSetupWizard: (await loadIMessageChannelRuntime()).imessageSetupWizard, +})); + type IMessageSendFn = ReturnType< typeof getIMessageRuntime >["channel"]["imessage"]["sendMessageIMessage"]; diff --git a/extensions/imessage/src/setup-core.ts b/extensions/imessage/src/setup-core.ts new file mode 100644 index 00000000000..69a8072bd59 --- /dev/null +++ b/extensions/imessage/src/setup-core.ts @@ -0,0 +1,236 @@ +import type { ChannelOnboardingDmPolicy } from "../../../src/channels/plugins/onboarding-types.js"; +import { + parseOnboardingEntriesAllowingWildcard, + promptParsedAllowFromForScopedChannel, + setChannelDmPolicyWithAllowFrom, + setOnboardingChannelEnabled, +} from "../../../src/channels/plugins/onboarding/helpers.js"; +import { + applyAccountNameToChannelSection, + migrateBaseNameToDefaultAccount, +} from "../../../src/channels/plugins/setup-helpers.js"; +import { type ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; +import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js"; +import { formatDocsLink } from "../../../src/terminal/links.js"; +import type { WizardPrompter } from "../../../src/wizard/prompts.js"; +import { + listIMessageAccountIds, + resolveDefaultIMessageAccountId, + resolveIMessageAccount, +} from "./accounts.js"; +import { normalizeIMessageHandle } from "./targets.js"; + +const channel = "imessage" as const; + +export function parseIMessageAllowFromEntries(raw: string): { entries: string[]; error?: string } { + return parseOnboardingEntriesAllowingWildcard(raw, (entry) => { + const lower = entry.toLowerCase(); + if (lower.startsWith("chat_id:")) { + const id = entry.slice("chat_id:".length).trim(); + if (!/^\d+$/.test(id)) { + return { error: `Invalid chat_id: ${entry}` }; + } + return { value: entry }; + } + if (lower.startsWith("chat_guid:")) { + if (!entry.slice("chat_guid:".length).trim()) { + return { error: "Invalid chat_guid entry" }; + } + return { value: entry }; + } + if (lower.startsWith("chat_identifier:")) { + if (!entry.slice("chat_identifier:".length).trim()) { + return { error: "Invalid chat_identifier entry" }; + } + return { value: entry }; + } + if (!normalizeIMessageHandle(entry)) { + return { error: `Invalid handle: ${entry}` }; + } + return { value: entry }; + }); +} + +function buildIMessageSetupPatch(input: { + cliPath?: string; + dbPath?: string; + service?: "imessage" | "sms" | "auto"; + region?: string; +}) { + return { + ...(input.cliPath ? { cliPath: input.cliPath } : {}), + ...(input.dbPath ? { dbPath: input.dbPath } : {}), + ...(input.service ? { service: input.service } : {}), + ...(input.region ? { region: input.region } : {}), + }; +} + +async function promptIMessageAllowFrom(params: { + cfg: OpenClawConfig; + prompter: WizardPrompter; + accountId?: string; +}): Promise { + return promptParsedAllowFromForScopedChannel({ + cfg: params.cfg, + channel, + accountId: params.accountId, + defaultAccountId: resolveDefaultIMessageAccountId(params.cfg), + prompter: params.prompter, + noteTitle: "iMessage allowlist", + noteLines: [ + "Allowlist iMessage DMs by handle or chat target.", + "Examples:", + "- +15555550123", + "- user@example.com", + "- chat_id:123", + "- chat_guid:... or chat_identifier:...", + "Multiple entries: comma-separated.", + `Docs: ${formatDocsLink("/imessage", "imessage")}`, + ], + message: "iMessage allowFrom (handle or chat_id)", + placeholder: "+15555550123, user@example.com, chat_id:123", + parseEntries: parseIMessageAllowFromEntries, + getExistingAllowFrom: ({ cfg, accountId }) => + resolveIMessageAccount({ cfg, accountId }).config.allowFrom ?? [], + }); +} + +export const imessageSetupAdapter: ChannelSetupAdapter = { + resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), + applyAccountName: ({ cfg, accountId, name }) => + applyAccountNameToChannelSection({ + cfg, + channelKey: channel, + accountId, + name, + }), + applyAccountConfig: ({ cfg, accountId, input }) => { + const namedConfig = applyAccountNameToChannelSection({ + cfg, + channelKey: channel, + accountId, + name: input.name, + }); + const next = + accountId !== DEFAULT_ACCOUNT_ID + ? migrateBaseNameToDefaultAccount({ + cfg: namedConfig, + channelKey: channel, + }) + : namedConfig; + if (accountId === DEFAULT_ACCOUNT_ID) { + return { + ...next, + channels: { + ...next.channels, + imessage: { + ...next.channels?.imessage, + enabled: true, + ...buildIMessageSetupPatch(input), + }, + }, + }; + } + return { + ...next, + channels: { + ...next.channels, + imessage: { + ...next.channels?.imessage, + enabled: true, + accounts: { + ...next.channels?.imessage?.accounts, + [accountId]: { + ...next.channels?.imessage?.accounts?.[accountId], + enabled: true, + ...buildIMessageSetupPatch(input), + }, + }, + }, + }, + }; + }, +}; + +export function createIMessageSetupWizardProxy( + loadWizard: () => Promise<{ imessageSetupWizard: ChannelSetupWizard }>, +) { + const imessageDmPolicy: ChannelOnboardingDmPolicy = { + label: "iMessage", + channel, + policyKey: "channels.imessage.dmPolicy", + allowFromKey: "channels.imessage.allowFrom", + getCurrent: (cfg: OpenClawConfig) => cfg.channels?.imessage?.dmPolicy ?? "pairing", + setPolicy: (cfg: OpenClawConfig, policy) => + setChannelDmPolicyWithAllowFrom({ + cfg, + channel, + dmPolicy: policy, + }), + promptAllowFrom: promptIMessageAllowFrom, + }; + + return { + channel, + status: { + configuredLabel: "configured", + unconfiguredLabel: "needs setup", + configuredHint: "imsg found", + unconfiguredHint: "imsg missing", + configuredScore: 1, + unconfiguredScore: 0, + resolveConfigured: ({ cfg }) => + listIMessageAccountIds(cfg).some((accountId) => { + const account = resolveIMessageAccount({ cfg, accountId }); + return Boolean( + account.config.cliPath || + account.config.dbPath || + account.config.allowFrom || + account.config.service || + account.config.region, + ); + }), + resolveStatusLines: async (params) => + (await loadWizard()).imessageSetupWizard.status.resolveStatusLines?.(params) ?? [], + resolveSelectionHint: async (params) => + await (await loadWizard()).imessageSetupWizard.status.resolveSelectionHint?.(params), + resolveQuickstartScore: async (params) => + await (await loadWizard()).imessageSetupWizard.status.resolveQuickstartScore?.(params), + }, + credentials: [], + textInputs: [ + { + inputKey: "cliPath", + message: "imsg CLI path", + initialValue: ({ cfg, accountId }) => + resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg", + currentValue: ({ cfg, accountId }) => + resolveIMessageAccount({ cfg, accountId }).config.cliPath ?? "imsg", + shouldPrompt: async (params) => { + const input = (await loadWizard()).imessageSetupWizard.textInputs?.find( + (entry) => entry.inputKey === "cliPath", + ); + return (await input?.shouldPrompt?.(params)) ?? false; + }, + confirmCurrentValue: false, + applyCurrentValue: true, + helpTitle: "iMessage", + helpLines: ["imsg CLI path required to enable iMessage."], + }, + ], + completionNote: { + title: "iMessage next steps", + lines: [ + "This is still a work in progress.", + "Ensure OpenClaw has Full Disk Access to Messages DB.", + "Grant Automation permission for Messages when prompted.", + "List chats with: imsg chats --limit 20", + `Docs: ${formatDocsLink("/imessage", "imessage")}`, + ], + }, + dmPolicy: imessageDmPolicy, + disable: (cfg: OpenClawConfig) => setOnboardingChannelEnabled(cfg, channel, false), + } satisfies ChannelSetupWizard; +} diff --git a/extensions/imessage/src/setup-surface.ts b/extensions/imessage/src/setup-surface.ts index 69382ff4014..90fcf648e60 100644 --- a/extensions/imessage/src/setup-surface.ts +++ b/extensions/imessage/src/setup-surface.ts @@ -5,15 +5,10 @@ import { setChannelDmPolicyWithAllowFrom, setOnboardingChannelEnabled, } from "../../../src/channels/plugins/onboarding/helpers.js"; -import { - applyAccountNameToChannelSection, - migrateBaseNameToDefaultAccount, -} from "../../../src/channels/plugins/setup-helpers.js"; import { type ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; -import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js"; import { detectBinary } from "../../../src/commands/onboard-helpers.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; -import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js"; +import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js"; import { formatDocsLink } from "../../../src/terminal/links.js"; import type { WizardPrompter } from "../../../src/wizard/prompts.js"; import { @@ -21,53 +16,10 @@ import { resolveDefaultIMessageAccountId, resolveIMessageAccount, } from "./accounts.js"; -import { normalizeIMessageHandle } from "./targets.js"; +import { imessageSetupAdapter, parseIMessageAllowFromEntries } from "./setup-core.js"; const channel = "imessage" as const; -export function parseIMessageAllowFromEntries(raw: string): { entries: string[]; error?: string } { - return parseOnboardingEntriesAllowingWildcard(raw, (entry) => { - const lower = entry.toLowerCase(); - if (lower.startsWith("chat_id:")) { - const id = entry.slice("chat_id:".length).trim(); - if (!/^\d+$/.test(id)) { - return { error: `Invalid chat_id: ${entry}` }; - } - return { value: entry }; - } - if (lower.startsWith("chat_guid:")) { - if (!entry.slice("chat_guid:".length).trim()) { - return { error: "Invalid chat_guid entry" }; - } - return { value: entry }; - } - if (lower.startsWith("chat_identifier:")) { - if (!entry.slice("chat_identifier:".length).trim()) { - return { error: "Invalid chat_identifier entry" }; - } - return { value: entry }; - } - if (!normalizeIMessageHandle(entry)) { - return { error: `Invalid handle: ${entry}` }; - } - return { value: entry }; - }); -} - -function buildIMessageSetupPatch(input: { - cliPath?: string; - dbPath?: string; - service?: "imessage" | "sms" | "auto"; - region?: string; -}) { - return { - ...(input.cliPath ? { cliPath: input.cliPath } : {}), - ...(input.dbPath ? { dbPath: input.dbPath } : {}), - ...(input.service ? { service: input.service } : {}), - ...(input.region ? { region: input.region } : {}), - }; -} - async function promptIMessageAllowFrom(params: { cfg: OpenClawConfig; prompter: WizardPrompter; @@ -113,63 +65,6 @@ const imessageDmPolicy: ChannelOnboardingDmPolicy = { promptAllowFrom: promptIMessageAllowFrom, }; -export const imessageSetupAdapter: ChannelSetupAdapter = { - resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), - applyAccountName: ({ cfg, accountId, name }) => - applyAccountNameToChannelSection({ - cfg, - channelKey: channel, - accountId, - name, - }), - applyAccountConfig: ({ cfg, accountId, input }) => { - const namedConfig = applyAccountNameToChannelSection({ - cfg, - channelKey: channel, - accountId, - name: input.name, - }); - const next = - accountId !== DEFAULT_ACCOUNT_ID - ? migrateBaseNameToDefaultAccount({ - cfg: namedConfig, - channelKey: channel, - }) - : namedConfig; - if (accountId === DEFAULT_ACCOUNT_ID) { - return { - ...next, - channels: { - ...next.channels, - imessage: { - ...next.channels?.imessage, - enabled: true, - ...buildIMessageSetupPatch(input), - }, - }, - }; - } - return { - ...next, - channels: { - ...next.channels, - imessage: { - ...next.channels?.imessage, - enabled: true, - accounts: { - ...next.channels?.imessage?.accounts, - [accountId]: { - ...next.channels?.imessage?.accounts?.[accountId], - enabled: true, - ...buildIMessageSetupPatch(input), - }, - }, - }, - }, - }; - }, -}; - export const imessageSetupWizard: ChannelSetupWizard = { channel, status: { @@ -236,3 +131,5 @@ export const imessageSetupWizard: ChannelSetupWizard = { dmPolicy: imessageDmPolicy, disable: (cfg) => setOnboardingChannelEnabled(cfg, channel, false), }; + +export { imessageSetupAdapter, parseIMessageAllowFromEntries }; diff --git a/src/plugin-sdk/imessage.ts b/src/plugin-sdk/imessage.ts index 8c8727ef5d9..1d767798873 100644 --- a/src/plugin-sdk/imessage.ts +++ b/src/plugin-sdk/imessage.ts @@ -24,10 +24,8 @@ export { resolveIMessageGroupRequireMention, resolveIMessageGroupToolPolicy, } from "../channels/plugins/group-mentions.js"; -export { - imessageSetupAdapter, - imessageSetupWizard, -} from "../../extensions/imessage/src/setup-surface.js"; +export { imessageSetupWizard } from "../../extensions/imessage/src/setup-surface.js"; +export { imessageSetupAdapter } from "../../extensions/imessage/src/setup-core.js"; export { IMessageConfigSchema } from "../config/zod-schema.providers-core.js"; export { resolveChannelMediaMaxBytes } from "../channels/plugins/media-limits.js"; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 04d03c56f8e..2880a60ee58 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -706,10 +706,8 @@ export { resolveIMessageAccount, type ResolvedIMessageAccount, } from "../../extensions/imessage/src/accounts.js"; -export { - imessageSetupAdapter, - imessageSetupWizard, -} from "../../extensions/imessage/src/setup-surface.js"; +export { imessageSetupWizard } from "../../extensions/imessage/src/setup-surface.js"; +export { imessageSetupAdapter } from "../../extensions/imessage/src/setup-core.js"; export { looksLikeIMessageTargetId, normalizeIMessageMessagingTarget,