openclaw-imessage-photon 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,10 @@ agent processes).
16
16
  dispatch straight into the embedded agent runtime.
17
17
  - **No Mac required** — Photon Spectrum Cloud relays iMessage.
18
18
  - **Text, images, voice, polls, effects, contacts** — both directions.
19
+ - **Rich link previews & Mini App cards** — send a URL as an iMessage link
20
+ card (`url`), a sheet-style Mini App card (`appUrl`, opens inside the
21
+ Spectrum App — no Safari jump), or a live in-transcript card (`appUrl` +
22
+ `appLive`).
19
23
  - **Voice transcription** — plug any `tools.media.audio` STT provider
20
24
  (Deepgram, ElevenLabs, Groq, ...) via ordinary OpenClaw config.
21
25
  - **Green-friendly defaults** — DM allowlist, seen-ack reaction, tapbacks,
@@ -85,6 +89,21 @@ iMessage provider, and copy the Project ID / Secret.
85
89
  | `enableTyping` | `false` | Typing indicator while processing |
86
90
  | `enableReadReceipts` | `false` | Mark inbound as read |
87
91
 
92
+ ### Rich links & Mini App cards
93
+
94
+ Plain `send` accepts three URL flavors:
95
+
96
+ | send param | iMessage rendering |
97
+ |---|---|
98
+ | `url` / `link` | Rich link preview card (tap opens in Safari) |
99
+ | `appUrl` | Mini App card: compact title card; tap opens the full page in the Spectrum iMessage App — no Safari jump |
100
+ | `appUrl` + `appLive: true` | Live card: interactive page rendered inline in the transcript (swipe/click, no tap-to-open) |
101
+
102
+ App cards use Photon's **Spectrum App** host (Apple-approved iMessage
103
+ launcher) — no business plan needed for `app()` cards; recipients install the
104
+ Spectrum App once. Custom `customizedMiniApp()` (bring-your-own extension) is
105
+ business-plan only.
106
+
88
107
  ### Voice transcription
89
108
 
90
109
  Configure any OpenClaw media-audio STT provider, e.g. Deepgram:
@@ -116,7 +135,7 @@ core media pipeline; the agent then sees the transcript.
116
135
  | Typing indicators | ✅ (`enableTyping`) | ✅ |
117
136
  | Read receipts | ✅ (`enableReadReceipts`) | ✅ |
118
137
  | Stickers / text animations | ❌ | ✅ (needs `@photon-ai/advanced-imessage`) |
119
- | Mini-app / status cards | (business-account only) | ✅ |
138
+ | Mini-app / status cards | `appUrl`/`appLive` via send (Spectrum App host); custom `customizedMiniApp()` business-only | ✅ (needs `@photon-ai/advanced-imessage`) |
120
139
  | photonDoctor diagnostic tool | ❌ (use `openclaw channels status`) | ✅ |
121
140
  | Onboarding | manual credentials / env | device-code auto-provisioning |
122
141
  | License | **MIT** | UNLICENSED |
@@ -158,8 +177,10 @@ and `cp -r dist/*` there, or re-run `plugins install --force`.
158
177
  project via Photon's public API (no manual credential copying)
159
178
  - [ ] **Group mention gating**: `requireMention` config (only reply in groups
160
179
  when mentioned)
161
- - [ ] **Edit/unsend notifications**: `enableEditUnsend` switch (agent reacts
162
- to inbound edit/unsend events)
180
+ - [ ] **Plugin config schema**: declare plugin-owned `channels.imessage-photon.*`
181
+ keys via `configSchema` (blocked-ish today: openclaw's validator rejects
182
+ unknown keys, which is why extra switches like edit/unsend can't be added
183
+ without it)
163
184
  - [ ] Multi-account / remote iMessage line refinement
164
185
 
165
186
  ## License
package/README.zh-CN.md CHANGED
@@ -9,6 +9,7 @@
9
9
  - **原生渠道插件** —— 运行于 OpenClaw gateway 进程内;消息直接进入内嵌 agent 运行时。
10
10
  - **无需 Mac** —— Photon Spectrum Cloud 中继 iMessage。
11
11
  - **文本、图片、语音、投票、特效、名片** —— 双向。
12
+ - **富链接预览 & Mini App 卡片** —— 用 `url` 发链接卡片;用 `appUrl` 发 sheet 式 Mini App 卡片(在 Spectrum App 内打开,不跳 Safari);用 `appUrl` + `appLive` 发 live 卡片(对话内直接交互)。
12
13
  - **语音转写** —— 通过普通 OpenClaw 配置接入任意 `tools.media.audio` 转录 provider(Deepgram、ElevenLabs、Groq……)。
13
14
  - **对用户友好的默认行为** —— DM 白名单、👀 已读反应、tapback、自动重连、已读回执、输入中指示。
14
15
  - **引导式配置向导** —— `openclaw onboard` 引导完成 Photon 项目配置(凭据通过 `SPECTRUM_PROJECT_ID` / `SPECTRUM_PROJECT_SECRET` 环境变量,或从控制台粘贴)。
@@ -74,6 +75,18 @@ openclaw gateway restart
74
75
  | `enableTyping` | `false` | 处理中显示输入中指示 |
75
76
  | `enableReadReceipts` | `false` | 入站消息标记已读 |
76
77
 
78
+ ### 富链接 & Mini App 卡片
79
+
80
+ `send` 支持三种 URL 形态:
81
+
82
+ | send 参数 | iMessage 渲染 |
83
+ |---|---|
84
+ | `url` / `link` | 富链接预览卡片(点击打开 Safari) |
85
+ | `appUrl` | Mini App 卡片:紧凑标题卡片;点击在 Spectrum App 内打开完整页面——不跳 Safari |
86
+ | `appUrl` + `appLive: true` | Live 卡片:页面内嵌在对话中直接交互(滑动/点击,无需点开) |
87
+
88
+ App 卡片使用 Photon 的 **Spectrum App** 宿主(Apple 已批准的 iMessage 启动器)——`app()` 卡片**无需 business 套餐**;接收方装一次 Spectrum App 即可。自定义 `customizedMiniApp()`(自带扩展)才需要 business。
89
+
77
90
  ### 语音转写
78
91
 
79
92
  配置任一 OpenClaw 媒体音频转录 provider,例如 Deepgram:
@@ -103,7 +116,7 @@ openclaw gateway restart
103
116
  | 输入中指示 | ✅(`enableTyping`) | ✅ |
104
117
  | 已读回执 | ✅(`enableReadReceipts`) | ✅ |
105
118
  | 贴纸 / 文字动画 | ❌ | ✅(需 `@photon-ai/advanced-imessage`) |
106
- | mini-app / 状态卡片 | ❌(仅 business 账号) | |
119
+ | mini-app / 状态卡片 | `appUrl`/`appLive`(通过 send,Spectrum App 宿主);自定义 `customizedMiniApp()` 仅 business | ✅(需 `@photon-ai/advanced-imessage`) |
107
120
  | photonDoctor 诊断工具 | ❌(用 `openclaw channels status`) | ✅ |
108
121
  | 引导配置 | 手动凭据 / 环境变量 | 设备码自动开通 |
109
122
  | 许可证 | **MIT** | UNLICENSED |
@@ -133,7 +146,7 @@ systemctl --user restart openclaw-gateway
133
146
  - [ ] **测试文件**:config 解析 / inbound 分类 / 去重逻辑(防回归)
134
147
  - [ ] **设备码 onboarding**:调用 Photon 公开 API(app.photon.codes)自动登录 + 自动开通项目,免手动复制凭据
135
148
  - [ ] **群组 @ 门控**:`requireMention` 配置(群聊中只有被 @ 才回复)
136
- - [ ] **edit/unsend 通知 agent**:`enableEditUnsend` 开关(收到用户的编辑/撤回事件并回应)
149
+ - [ ] **插件 config schema**:通过 `configSchema` 声明插件自有 `channels.imessage-photon.*` 配置键(当前 openclaw 校验器拒绝未知键,这正是 edit/unsend 等附加开关无法加入的原因)
137
150
  - [ ] multi-account / 远程 iMessage 线路支持细化
138
151
 
139
152
  ## 许可证
@@ -1,7 +1,8 @@
1
1
  import { readFile } from "node:fs/promises";
2
+ import { Type } from "typebox";
2
3
  import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
3
4
  import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
4
- import { text as textContent, markdown as markdownContent, richlink as richlinkContent, attachment as attachmentContent, poll as pollContent, read as readContent, reply as replyContent, voice as voiceContent, contact as contactContent, } from "spectrum-ts";
5
+ import { text as textContent, markdown as markdownContent, richlink as richlinkContent, app as appContent, attachment as attachmentContent, poll as pollContent, read as readContent, reply as replyContent, voice as voiceContent, contact as contactContent, } from "spectrum-ts";
5
6
  import { effect as effectContent } from "@spectrum-ts/imessage";
6
7
  import { resolveAccount } from "./channel.js";
7
8
  import { resolveSpace, resolveMessage } from "./spectrum.js";
@@ -199,6 +200,27 @@ export function createMessageActions() {
199
200
  mediaSourceParams: {
200
201
  sendAttachment: ["media", "mediaUrl", "filePath", "path", "file"],
201
202
  },
203
+ // Expose rich-link and App-card capabilities on the shared send action.
204
+ schema: [
205
+ {
206
+ properties: {
207
+ url: Type.String({
208
+ description: "HTTPS URL to send as a rich link-preview message (iMessage renders it as a link card). Mutually exclusive with text/markdown/appUrl.",
209
+ }),
210
+ link: Type.String({
211
+ description: "Alias for url: HTTPS URL to send as a rich link-preview message.",
212
+ }),
213
+ appUrl: Type.String({
214
+ description: "HTTPS URL to send as a native iMessage App card (Spectrum App host). Recipient opens it in the Spectrum iMessage App without leaving Messages; degrades to a link preview on unsupported platforms. Mutually exclusive with text/markdown/url.",
215
+ }),
216
+ appLive: Type.Optional(Type.Boolean({
217
+ description: "When sending an App card (appUrl set), request live in-transcript rendering (MSMessageLiveLayout). Requires the recipient to have the Spectrum App extension installed.",
218
+ })),
219
+ },
220
+ actions: ["send"],
221
+ visibility: "current-channel",
222
+ },
223
+ ],
202
224
  };
203
225
  },
204
226
  supportsAction: ({ action }) => SUPPORTED.has(action),
@@ -216,17 +238,23 @@ export function createMessageActions() {
216
238
  case "send": {
217
239
  const text = readString(params, "text", "message", "content");
218
240
  const url = readString(params, "url", "link");
219
- const md = readString(params, "markdown", "md");
241
+ const appUrl = readString(params, "appUrl", "app_url", "cardUrl");
242
+ if (appUrl) {
243
+ const live = params.appLive === true || params.appLive === "true" || params.live === true || params.live === "true";
244
+ await space.send(appContent(appUrl, live ? { live: true } : undefined));
245
+ return actionOk({ to, type: "app", url: appUrl, live });
246
+ }
220
247
  if (url) {
221
248
  await space.send(richlinkContent(url));
222
249
  return actionOk({ to, type: "richlink", url });
223
250
  }
251
+ const md = readString(params, "markdown", "md");
224
252
  if (md) {
225
253
  await space.send(markdownContent(md));
226
254
  return actionOk({ to, type: "markdown" });
227
255
  }
228
256
  if (!text)
229
- throw new Error("send requires text/message/content (or url/link, or markdown/md)");
257
+ throw new Error("send requires text/message/content (or url/link, or appUrl)");
230
258
  await space.send(textContent(text));
231
259
  return actionOk({ to, type: "text" });
232
260
  }
@@ -1,5 +1,5 @@
1
1
  import { createChatChannelPlugin, } from "openclaw/plugin-sdk/channel-core";
2
- import { connectSpectrum, resetSpectrum, rememberSpace, rememberMessage } from "./spectrum.js";
2
+ import { connectSpectrum, resetSpectrum, rememberSpace, rememberMessage, resolveOrOpenSpace, sendText } from "./spectrum.js";
3
3
  import { handleInbound } from "./inbound.js";
4
4
  import { createMessageActions } from "./actions.js";
5
5
  function channelSection(cfg) {
@@ -36,6 +36,7 @@ export function resolveAccount(cfg, accountId) {
36
36
  enableGroups: Boolean(section?.enableGroups),
37
37
  enableTyping: Boolean(section?.enableTyping),
38
38
  enableReadReceipts: Boolean(section?.enableReadReceipts),
39
+ contactNames: section?.contactNames ?? {},
39
40
  };
40
41
  }
41
42
  const setupWizard = {
@@ -160,6 +161,29 @@ export const imessagePhotonPlugin = createChatChannelPlugin({
160
161
  reply: true,
161
162
  },
162
163
  actions: createMessageActions(),
164
+ agentPrompt: {
165
+ // Text guidance layered on top of the fixed openclaw message-tool
166
+ // schema: keep replies as plain text by default, and only use
167
+ // reply-quoting when the user themselves quoted a message.
168
+ messageToolHints: ({ cfg }) => {
169
+ const account = resolveAccount(cfg);
170
+ const hints = [
171
+ "Prefer plain text (text) over markdown (markdown) unless the recipient explicitly asked for rich formatting. Plain text renders best in iMessage.",
172
+ "For everyday replies use the plain send action without quotes.",
173
+ ];
174
+ if (account.enableMedia) {
175
+ hints.push("Images/audio/video/files are sent with sendAttachment (media/buffer/filePath).");
176
+ }
177
+ return hints;
178
+ },
179
+ inboundFormattingHints: () => ({
180
+ text_markup: "plain text",
181
+ rules: [
182
+ "When the user's message is a reply-to (quoted) message, reply by quoting that message back using the reply action with its messageId — this keeps the quoted conversation thread visible in iMessage.",
183
+ "When the user's message is NOT a reply, reply with a plain send (no quote).",
184
+ ],
185
+ }),
186
+ },
163
187
  setupWizard,
164
188
  config: {
165
189
  listAccountIds: () => ["default"],
@@ -176,6 +200,21 @@ export const imessagePhotonPlugin = createChatChannelPlugin({
176
200
  };
177
201
  },
178
202
  },
203
+ // Directory: give iMessage contacts a friendly display name so sessions
204
+ // show "iMessage <last4>" instead of the raw E.164 number.
205
+ directory: {
206
+ listPeers: async ({ cfg }) => {
207
+ const account = resolveAccount(cfg);
208
+ const peers = account.allowFrom ?? [];
209
+ return peers.map((raw) => {
210
+ const id = String(raw).trim();
211
+ if (!id)
212
+ return null;
213
+ const tail = id.slice(-4);
214
+ return { kind: "user", id, name: `iMessage ${tail}` };
215
+ }).filter((e) => e !== null);
216
+ },
217
+ },
179
218
  setup: {
180
219
  applyAccountConfig: ({ cfg, input }) => ({
181
220
  ...cfg,
@@ -239,9 +278,21 @@ export const imessagePhotonPlugin = createChatChannelPlugin({
239
278
  },
240
279
  threading: { topLevelReplyToMode: "reply" },
241
280
  outbound: {
242
- // Outbound delivery runs inside the gateway via our message actions
243
- // (handleAction). Declaring deliveryMode "gateway" makes the shared
244
- // message tool treat this channel as send-capable.
245
- deliveryMode: "gateway",
281
+ // Real active-outbound surface: lets the core deliver proactive sends
282
+ // (cron announcements, heartbeat, control-UI new message) to iMessage via
283
+ // Spectrum. sendText resolves/creates the 1:1 Space by phone number.
284
+ deliveryMode: "direct",
285
+ sendText: async ({ to, text }) => {
286
+ const space = await resolveOrOpenSpace(to);
287
+ if (!space)
288
+ throw new Error(`imessage-photon: no reachable space for ${to}`);
289
+ await sendText(space, text);
290
+ return {
291
+ channel: "imessage-photon",
292
+ messageId: "",
293
+ target: { kind: "conversation", id: to },
294
+ timestamp: Date.now(),
295
+ };
296
+ },
246
297
  },
247
298
  });
@@ -36,8 +36,7 @@ export async function handleInbound(runtime, cfg, space, message, log) {
36
36
  await dispatchPollEvent(runtime, cfg, space, message, account, log);
37
37
  }
38
38
  else if (message.content.type === "edit" || message.content.type === "unsend") {
39
- // Agent-facing edit/unsend handling is opt-in (enableEditUnsend);
40
- // until then, these are system events: log only.
39
+ // edit/unsend from the user are system events: log only (no agent turn).
41
40
  log?.(`[imessage-photon] system event type=${message.content.type} from ${space.id}`);
42
41
  }
43
42
  else {
@@ -49,7 +48,16 @@ export async function handleInbound(runtime, cfg, space, message, log) {
49
48
  /** Shared turn dispatch: build session route + context, then run the agent. */
50
49
  async function runAgentTurn(runtime, cfg, space, message, account, log, opts) {
51
50
  const phone = senderPhone(space, message) ?? space.id;
52
- const senderName = message.sender?.name;
51
+ // Sender display name, separate from the conversation label:
52
+ // 1. explicit per-number contact name from config (contactNames) — bubbles show
53
+ // this instead of a phone-derived label, e.g. "Ethan".
54
+ // 2. any real name the platform delivers (spectrum currently never sends one).
55
+ // 3. stable fallback "iMessage <last4 digits>" so the label never fluctuates.
56
+ const platformName = message.sender?.name;
57
+ const senderName = account.contactNames[phone] ?? platformName?.trim() ?? `iMessage ${phone.slice(-4)}`;
58
+ // Conversation title stays a stable, human-meaningful label independent of the
59
+ // per-message sender display name.
60
+ const conversationLabel = `iMessage ${phone.slice(-4)}`;
53
61
  // Read receipt: mark the conversation read (iMessage marks the whole chat).
54
62
  if (account.enableReadReceipts) {
55
63
  message.read().catch((err) => log?.(`[imessage-photon] read receipt failed: ${String(err)}`));
@@ -92,6 +100,7 @@ async function runAgentTurn(runtime, cfg, space, message, account, log, opts) {
92
100
  });
93
101
  await runtime.inbound.run({
94
102
  channel: CHANNEL,
103
+ accountId: "default",
95
104
  raw: message,
96
105
  adapter: {
97
106
  ingest: (raw) => ({
@@ -102,9 +111,17 @@ async function runAgentTurn(runtime, cfg, space, message, account, log, opts) {
102
111
  raw,
103
112
  }),
104
113
  resolveTurn: async (input) => {
114
+ // Pass the configured DM scope through so iMessage DMs get their own
115
+ // session (agent:main:imessage-photon:direct:<phone>) instead of being
116
+ // merged into the main session (agent:main:main). The default
117
+ // dmScope in core is "main"; without this, every channel merges.
118
+ const s = cfg.session;
105
119
  const routeSessionKey = runtime.routing.buildAgentSessionKey({
106
120
  agentId: AGENT_ID,
107
121
  channel: CHANNEL,
122
+ mainKey: s?.mainKey ?? "main",
123
+ dmScope: s?.dmScope ?? "main",
124
+ identityLinks: s?.identityLinks,
108
125
  peer: { kind: "direct", id: phone },
109
126
  });
110
127
  const storePath = runtime.session.resolveStorePath(cfg.session?.store, {
@@ -112,9 +129,10 @@ async function runAgentTurn(runtime, cfg, space, message, account, log, opts) {
112
129
  });
113
130
  const ctxPayload = await runtime.inbound.buildContext({
114
131
  channel: CHANNEL,
132
+ accountId: "default",
115
133
  from: phone,
116
134
  sender: { id: phone, name: senderName },
117
- conversation: { kind: "direct", id: space.id },
135
+ conversation: { kind: "direct", id: space.id, label: conversationLabel },
118
136
  route: { agentId: AGENT_ID, routeSessionKey },
119
137
  reply: { to: phone, sourceReplyDeliveryMode: "reply" },
120
138
  message: { rawBody: opts.rawText, bodyForAgent: opts.textForAgent },
@@ -128,6 +146,21 @@ async function runAgentTurn(runtime, cfg, space, message, account, log, opts) {
128
146
  storePath,
129
147
  ctxPayload,
130
148
  recordInboundSession: runtime.session.recordInboundSession,
149
+ // openclaw ≥ 2026.8.1 requires prepared turns to declare a dispatch
150
+ // lifecycle: turnAdoptionLifecycle is undefined for this non-durable
151
+ // direct-DM turn; onDispatchSkipped releases what runDispatch would
152
+ // have settled (typing indicator + seen-ack) when dispatch is
153
+ // skipped (outboundEcho / botLoopProtection / observeOnly).
154
+ runDispatchLifecycle: {
155
+ turnAdoptionLifecycle: undefined,
156
+ onDispatchSkipped: async () => {
157
+ await stopTyping();
158
+ if (ackHandle) {
159
+ const h = ackHandle;
160
+ await h.remove().catch((err) => log?.(`[imessage-photon] ack remove failed (skipped): ${String(err)}`));
161
+ }
162
+ },
163
+ },
131
164
  runDispatch: () => runtime.reply.dispatchReplyWithBufferedBlockDispatcher({
132
165
  ctx: ctxPayload,
133
166
  cfg,
@@ -39,6 +39,33 @@ export function resolveSpace(target) {
39
39
  return undefined;
40
40
  return spacesByPhone.get(phone);
41
41
  }
42
+ /**
43
+ * Resolve a Space for an outbound target, falling back to resolving/creating
44
+ * the 1:1 conversation via the platform when the target was never observed on
45
+ * the inbound stream (active outbound sends).
46
+ */
47
+ export async function resolveOrOpenSpace(target) {
48
+ const cached = resolveSpace(target);
49
+ if (cached)
50
+ return cached;
51
+ const explicitPhone = normalizePhone(target);
52
+ if (!explicitPhone || !app)
53
+ return undefined;
54
+ try {
55
+ const namespace = app.space;
56
+ const create = namespace?.["imessage"]
57
+ ?.create;
58
+ if (!create)
59
+ return undefined;
60
+ const space = await create(explicitPhone);
61
+ if (space)
62
+ rememberSpace(space);
63
+ return space;
64
+ }
65
+ catch {
66
+ return undefined;
67
+ }
68
+ }
42
69
  /** Number of known spaces (used in diagnostics). */
43
70
  export function knownSpaceCount() {
44
71
  return spacesByPhone.size;
@@ -3,7 +3,9 @@
3
3
  "name": "iMessage (Photon)",
4
4
  "description": "iMessage channel via Photon Spectrum Cloud — no Mac required. Register at https://photon.codes, create a project, and paste the Project ID and Secret.",
5
5
  "icon": "https://cdn.simpleicons.org/imessage",
6
- "channels": ["imessage-photon"],
6
+ "channels": [
7
+ "imessage-photon"
8
+ ],
7
9
  "channelEnvVars": {
8
10
  "imessage-photon": [
9
11
  "SPECTRUM_PROJECT_ID",
@@ -22,20 +24,38 @@
22
24
  "$schema": "http://json-schema.org/draft-07/schema#",
23
25
  "type": "object",
24
26
  "properties": {
25
- "enabled": { "type": "boolean" },
27
+ "enabled": {
28
+ "type": "boolean"
29
+ },
26
30
  "dmPolicy": {
27
31
  "type": "string",
28
- "enum": ["allowlist", "pairing", "open", "disabled"]
32
+ "enum": [
33
+ "allowlist",
34
+ "pairing",
35
+ "open",
36
+ "disabled"
37
+ ]
29
38
  },
30
39
  "dmSecurity": {
31
40
  "type": "string",
32
- "enum": ["allowlist", "pairing", "open", "disabled"]
41
+ "enum": [
42
+ "allowlist",
43
+ "pairing",
44
+ "open",
45
+ "disabled"
46
+ ]
47
+ },
48
+ "projectId": {
49
+ "type": "string"
50
+ },
51
+ "projectSecret": {
52
+ "type": "string"
33
53
  },
34
- "projectId": { "type": "string" },
35
- "projectSecret": { "type": "string" },
36
54
  "allowFrom": {
37
55
  "type": "array",
38
- "items": { "type": "string" },
56
+ "items": {
57
+ "type": "string"
58
+ },
39
59
  "description": "Allowed sender phone numbers in E.164 form, e.g. +8613800138000. Empty = allow everyone."
40
60
  },
41
61
  "ackReaction": {
@@ -45,7 +65,10 @@
45
65
  },
46
66
  "tapbackNotifications": {
47
67
  "type": "string",
48
- "enum": ["off", "all"],
68
+ "enum": [
69
+ "off",
70
+ "all"
71
+ ],
49
72
  "default": "all",
50
73
  "description": "off = ignore inbound tapbacks; all = forward them to the agent as system events."
51
74
  },
@@ -88,15 +111,29 @@
88
111
  "type": "boolean",
89
112
  "default": false,
90
113
  "description": "Mark inbound messages as read (read receipts)."
114
+ },
115
+ "contactNames": {
116
+ "type": "object",
117
+ "additionalProperties": {
118
+ "type": "string"
119
+ },
120
+ "description": "Map of E.164 phone numbers to human-readable display names shown as the message sender in conversation bubbles, e.g. {\"+8613800138000\": \"Ethan\"}. Falls back to \"iMessage <last4 digits>\" and then to any name provided by the platform."
91
121
  }
92
122
  },
93
123
  "additionalProperties": false
94
124
  },
95
125
  "uiHints": {
96
- "projectId": { "label": "Photon project ID" },
97
- "projectSecret": { "label": "Photon project secret", "sensitive": true },
98
- "allowFrom": { "label": "Allowed phone numbers" }
126
+ "projectId": {
127
+ "label": "Photon project ID"
128
+ },
129
+ "projectSecret": {
130
+ "label": "Photon project secret",
131
+ "sensitive": true
132
+ },
133
+ "allowFrom": {
134
+ "label": "Allowed phone numbers"
135
+ }
99
136
  }
100
137
  }
101
138
  }
102
- }
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-imessage-photon",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "iMessage channel for OpenClaw via Photon Spectrum Cloud (no Mac required).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,7 +41,8 @@
41
41
  }
42
42
  },
43
43
  "dependencies": {
44
- "spectrum-ts": "^12.8.0"
44
+ "spectrum-ts": "^12.8.0",
45
+ "typebox": "1.3.3"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@types/node": "^22",