@openclaw/imessage 2026.9.1-beta.1 → 2026.9.2
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/dist/{accounts-DIpGOIiN.js → accounts-CCirBEIm.js} +2 -3
- package/dist/{actions.runtime-BhggncbG.js → actions.runtime-D5MAgeSs.js} +5 -5
- package/dist/api.js +5 -5
- package/dist/{approval-handler.runtime-BC-YsgAf.js → approval-handler.runtime-47rs6THN.js} +14 -9
- package/dist/{approval-reactions-Cvwmdlev.js → approval-reactions-sVpDno7d.js} +8 -16
- package/dist/{channel-CnKQ2MVB.js → channel-BxraMrmK.js} +21 -14
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.runtime-BQLCh7bE.js → channel.runtime-2jwkMPGR.js} +59 -14
- package/dist/{config-schema-DV0KP5nC.js → config-schema-DuGbA8Kp.js} +8 -7
- package/dist/contract-api.js +1 -1
- package/dist/doctor-contract-api.js +8 -281
- package/dist/{group-policy-BipELT2x.js → group-policy-Cp8SDG4G.js} +18 -12
- package/dist/media-contract-api.js +1 -1
- package/dist/{media-contract-BVXuQHG2.js → media-contract-pns3TSS5.js} +3 -2
- package/dist/{message-tool-api-BwIxJDoz.js → message-tool-api-CUoFESHj.js} +11 -6
- package/dist/message-tool-api.js +1 -1
- package/dist/{monitor-DRD_Dnp9.js → monitor-CESwxMQ4.js} +319 -48
- package/dist/{question-reactions-viP7ZcyG.js → question-reactions-9tBYB1_F.js} +4 -7
- package/dist/{reaction-context-BAYI7pz0.js → reaction-context-Br-vAJFp.js} +9 -3
- package/dist/{remote-file-CyTTuRAt.js → remote-file-Biz5PjXi.js} +5 -53
- package/dist/runtime-Cza4CY5T.js +8 -0
- package/dist/runtime-api.js +7 -7
- package/dist/{sanitize-outbound-g651fIhi.js → sanitize-outbound-Bvo4g1Mp.js} +470 -82
- package/dist/{send-DVQY6M5M.js → send-DC981dqK.js} +146 -23
- package/dist/state-contract-DvahEY4X.js +56 -0
- package/dist/state-migrations-D-fyHVcb.js +277 -0
- package/node_modules/typebox/build/format/email.mjs +1 -1
- package/node_modules/typebox/build/format/idna/idn-hostname.mjs +19 -1
- package/node_modules/typebox/build/format/json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/json_pointer.mjs +2 -2
- package/node_modules/typebox/build/format/json_pointer_uri_fragment.d.mts +2 -2
- package/node_modules/typebox/build/format/json_pointer_uri_fragment.mjs +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
- package/node_modules/typebox/build/system/memory/assign.mjs +2 -1
- package/node_modules/typebox/build/system/memory/create.mjs +3 -4
- package/node_modules/typebox/build/system/memory/discard.mjs +2 -2
- package/node_modules/typebox/build/system/memory/freeze.d.mts +4 -0
- package/node_modules/typebox/build/system/memory/freeze.mjs +6 -0
- package/node_modules/typebox/build/system/memory/update.mjs +3 -2
- package/node_modules/typebox/package.json +1 -1
- package/openclaw.plugin.json +2 -7
- package/package.json +5 -5
- package/dist/catchup-Dh-Rv4Ur.js +0 -294
- package/dist/monitor-reply-cache-BdeUQaHO.js +0 -353
- package/dist/persisted-echo-cache-DGxy-J4t.js +0 -143
|
@@ -1,15 +1,425 @@
|
|
|
1
|
-
import { u as expandIMessageUserPath } from "./accounts-
|
|
2
|
-
import {
|
|
1
|
+
import { u as expandIMessageUserPath } from "./accounts-CCirBEIm.js";
|
|
2
|
+
import { m as invalidateCachedIMessagePrivateApiStatus } from "./message-tool-api-CUoFESHj.js";
|
|
3
|
+
import { t as getIMessageRuntime } from "./runtime-Cza4CY5T.js";
|
|
4
|
+
import { a as IMESSAGE_REPLY_CACHE_NAMESPACE, f as resolveIMessageReplyCacheEntryKey, i as IMESSAGE_REPLY_CACHE_MAX_ENTRIES, n as IMESSAGE_REPLY_CACHE_COUNTER_KEY, o as IMESSAGE_REPLY_CACHE_TTL_MS, r as IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE } from "./state-contract-DvahEY4X.js";
|
|
5
|
+
import { normalizeE164 } from "openclaw/plugin-sdk/account-resolution";
|
|
6
|
+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
|
+
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
|
8
|
+
import { runCommandWithTimeout } from "openclaw/plugin-sdk/process-runtime";
|
|
3
9
|
import { resolveUserPath } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
4
10
|
import { spawn } from "node:child_process";
|
|
5
11
|
import { StringDecoder } from "node:string_decoder";
|
|
6
12
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
7
13
|
import { FormatCapabilityProfile, findCodeRegions, isInsideCode, markdownToIR, renderMarkdownWithAttributedRanges, sanitizeAssistantVisibleText, stripMarkdown, tokenizeHtmlTags } from "openclaw/plugin-sdk/text-chunking";
|
|
14
|
+
//#region extensions/imessage/src/chat-context.ts
|
|
15
|
+
const EMAIL_HANDLE_PATTERN = /^[^\s@]+@[^\s@]+$/u;
|
|
16
|
+
function parseDirectChatIdentity(raw) {
|
|
17
|
+
const trimmed = raw.trim();
|
|
18
|
+
const parts = trimmed.split(";");
|
|
19
|
+
if (parts.length === 3 && parts[1] === "-" && parts[2]) {
|
|
20
|
+
const service = parts[0]?.toLowerCase();
|
|
21
|
+
if (service === "imessage" || service === "sms" || service === "any") {
|
|
22
|
+
const identifier = parts[2];
|
|
23
|
+
return {
|
|
24
|
+
service,
|
|
25
|
+
identifier: EMAIL_HANDLE_PATTERN.test(identifier) ? identifier.toLowerCase() : identifier
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (parts.length !== 1) return;
|
|
30
|
+
if (trimmed.startsWith("+") || EMAIL_HANDLE_PATTERN.test(trimmed)) return { identifier: trimmed.toLowerCase() };
|
|
31
|
+
}
|
|
32
|
+
function resolveIMessageDirectChatService(configuredService, chatGuid) {
|
|
33
|
+
if (configuredService === "imessage" || configuredService === "sms") return configuredService;
|
|
34
|
+
const observedService = chatGuid ? parseDirectChatIdentity(chatGuid)?.service : void 0;
|
|
35
|
+
return observedService === "imessage" || observedService === "sms" ? observedService : void 0;
|
|
36
|
+
}
|
|
37
|
+
function isIMessageEmailChatIdentifier(raw) {
|
|
38
|
+
const identity = parseDirectChatIdentity(raw);
|
|
39
|
+
return Boolean(identity && EMAIL_HANDLE_PATTERN.test(identity.identifier));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Strip the `iMessage;-;` / `SMS;-;` / `any;-;` service prefix that Messages
|
|
43
|
+
* uses for direct chats. Different layers report direct DMs in different
|
|
44
|
+
* forms, so raw comparison would falsely treat one DM as different chats.
|
|
45
|
+
*/
|
|
46
|
+
function normalizeDirectChatIdentifier(raw) {
|
|
47
|
+
const trimmed = raw.trim();
|
|
48
|
+
return parseDirectChatIdentity(trimmed)?.identifier ?? trimmed;
|
|
49
|
+
}
|
|
50
|
+
function chatContextFromIMessageTarget(target, effectiveService) {
|
|
51
|
+
if (target.kind === "chat_id") return { chatId: target.chatId };
|
|
52
|
+
if (target.kind === "chat_guid") return { chatGuid: target.chatGuid };
|
|
53
|
+
if (target.kind === "chat_identifier") return { chatIdentifier: target.chatIdentifier };
|
|
54
|
+
const trimmedHandle = target.to.trim();
|
|
55
|
+
const canonicalHandle = trimmedHandle.startsWith("+") ? normalizeE164(trimmedHandle) : /^[^\s@]+@[^\s@]+$/u.test(trimmedHandle) ? trimmedHandle.toLowerCase() : void 0;
|
|
56
|
+
if (!canonicalHandle) return {};
|
|
57
|
+
const service = target.service === "auto" ? effectiveService : target.service;
|
|
58
|
+
if (service !== "imessage" && service !== "sms") return {};
|
|
59
|
+
return { chatIdentifier: `${service === "sms" ? "SMS" : "iMessage"};-;${canonicalHandle}` };
|
|
60
|
+
}
|
|
61
|
+
function compareOptional(left, right) {
|
|
62
|
+
return left === void 0 || right === void 0 ? void 0 : left === right;
|
|
63
|
+
}
|
|
64
|
+
function compareChatSelector(cachedRaw, currentRaw, crossKind = false) {
|
|
65
|
+
const cached = normalizeOptionalString(cachedRaw);
|
|
66
|
+
const current = normalizeOptionalString(currentRaw);
|
|
67
|
+
if (!cached || !current) return;
|
|
68
|
+
if (cached === current) return true;
|
|
69
|
+
const cachedDirect = parseDirectChatIdentity(cached);
|
|
70
|
+
const currentDirect = parseDirectChatIdentity(current);
|
|
71
|
+
if (!cachedDirect || !currentDirect) return crossKind ? void 0 : false;
|
|
72
|
+
if (cachedDirect.identifier !== currentDirect.identifier) return false;
|
|
73
|
+
if (cachedDirect.service === "any") return true;
|
|
74
|
+
if (!cachedDirect.service || !currentDirect.service) return;
|
|
75
|
+
return cachedDirect.service === currentDirect.service;
|
|
76
|
+
}
|
|
77
|
+
function resolveIMessageChatMatch(cached, current) {
|
|
78
|
+
const cachedChatGuid = normalizeOptionalString(cached.chatGuid);
|
|
79
|
+
const currentChatGuid = normalizeOptionalString(current.chatGuid);
|
|
80
|
+
const cachedChatIdentifier = normalizeOptionalString(cached.chatIdentifier);
|
|
81
|
+
const currentChatIdentifier = normalizeOptionalString(current.chatIdentifier);
|
|
82
|
+
const comparisons = [
|
|
83
|
+
compareChatSelector(cachedChatGuid, currentChatGuid),
|
|
84
|
+
compareChatSelector(cachedChatIdentifier, currentChatIdentifier),
|
|
85
|
+
compareOptional(cached.chatId, current.chatId),
|
|
86
|
+
compareChatSelector(cachedChatGuid, currentChatIdentifier, true),
|
|
87
|
+
compareChatSelector(cachedChatIdentifier, currentChatGuid, true)
|
|
88
|
+
].filter((comparison) => comparison !== void 0);
|
|
89
|
+
if (comparisons.length === 0) return "unknown";
|
|
90
|
+
return comparisons.every(Boolean) ? "match" : "mismatch";
|
|
91
|
+
}
|
|
92
|
+
function isPositiveIMessageChatMatch(cached, current) {
|
|
93
|
+
return resolveIMessageChatMatch(cached, current) === "match";
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
8
96
|
//#region extensions/imessage/src/constants.ts
|
|
9
97
|
/** Default timeout for iMessage probe/RPC operations (10 seconds). */
|
|
10
98
|
const DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS = 1e4;
|
|
11
99
|
const DEFAULT_IMESSAGE_SEND_TIMEOUT_MS = 18e4;
|
|
12
100
|
//#endregion
|
|
101
|
+
//#region extensions/imessage/src/monitor-reply-cache.ts
|
|
102
|
+
/** Recency window for the "react to the latest message" fallback. */
|
|
103
|
+
const LATEST_FALLBACK_MS = 6e5;
|
|
104
|
+
let persistenceFailureLogged = false;
|
|
105
|
+
function reportPersistenceFailure(scope, err) {
|
|
106
|
+
if (persistenceFailureLogged) return;
|
|
107
|
+
persistenceFailureLogged = true;
|
|
108
|
+
logVerbose(`imessage reply-cache: ${scope} disabled after first failure: ${String(err)}`);
|
|
109
|
+
}
|
|
110
|
+
const imessageReplyCacheByMessageId = /* @__PURE__ */ new Map();
|
|
111
|
+
const imessageShortIdToUuid = /* @__PURE__ */ new Map();
|
|
112
|
+
const imessageUuidToShortId = /* @__PURE__ */ new Map();
|
|
113
|
+
let imessageShortIdCounter = 0;
|
|
114
|
+
function openReplyCacheStore() {
|
|
115
|
+
return getIMessageRuntime().state.openSyncKeyedStore({
|
|
116
|
+
namespace: IMESSAGE_REPLY_CACHE_NAMESPACE,
|
|
117
|
+
maxEntries: IMESSAGE_REPLY_CACHE_MAX_ENTRIES
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function openReplyCacheCounterStore() {
|
|
121
|
+
return getIMessageRuntime().state.openSyncKeyedStore({
|
|
122
|
+
namespace: IMESSAGE_REPLY_CACHE_COUNTER_NAMESPACE,
|
|
123
|
+
maxEntries: 1
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function remainingTtlMs(timestamp) {
|
|
127
|
+
const remaining = IMESSAGE_REPLY_CACHE_TTL_MS - Math.max(0, Date.now() - timestamp);
|
|
128
|
+
return remaining > 0 ? remaining : void 0;
|
|
129
|
+
}
|
|
130
|
+
let hydrated = false;
|
|
131
|
+
function hydrateFromStoreOnce() {
|
|
132
|
+
if (hydrated) return;
|
|
133
|
+
hydrated = true;
|
|
134
|
+
const cutoff = Date.now() - IMESSAGE_REPLY_CACHE_TTL_MS;
|
|
135
|
+
let entries;
|
|
136
|
+
try {
|
|
137
|
+
const counter = openReplyCacheCounterStore().lookup(IMESSAGE_REPLY_CACHE_COUNTER_KEY);
|
|
138
|
+
if (counter && Number.isSafeInteger(counter.counter) && counter.counter > 0) imessageShortIdCounter = Math.max(imessageShortIdCounter, counter.counter);
|
|
139
|
+
entries = openReplyCacheStore().entries().map(({ value }) => value).filter((entry) => entry.timestamp >= cutoff).toSorted((a, b) => a.timestamp - b.timestamp).slice(-IMESSAGE_REPLY_CACHE_MAX_ENTRIES);
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const numeric = Number.parseInt(entry.shortId, 10);
|
|
142
|
+
if (Number.isFinite(numeric) && numeric > imessageShortIdCounter) imessageShortIdCounter = numeric;
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
reportPersistenceFailure("read", err);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (entries.length === 0) return;
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
imessageReplyCacheByMessageId.set(entry.messageId, entry);
|
|
151
|
+
imessageShortIdToUuid.set(entry.shortId, entry.messageId);
|
|
152
|
+
imessageUuidToShortId.set(entry.messageId, entry.shortId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function persistReplyCacheEntry(entry) {
|
|
156
|
+
const ttlMs = remainingTtlMs(entry.timestamp);
|
|
157
|
+
if (!ttlMs) return;
|
|
158
|
+
try {
|
|
159
|
+
openReplyCacheStore().register(resolveIMessageReplyCacheEntryKey(entry.messageId), entry, { ttlMs });
|
|
160
|
+
} catch (err) {
|
|
161
|
+
reportPersistenceFailure("write", err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function deleteReplyCacheEntry(messageId) {
|
|
165
|
+
try {
|
|
166
|
+
openReplyCacheStore().delete(resolveIMessageReplyCacheEntryKey(messageId));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
reportPersistenceFailure("delete", err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function persistReplyCacheCounter() {
|
|
172
|
+
try {
|
|
173
|
+
openReplyCacheCounterStore().register(IMESSAGE_REPLY_CACHE_COUNTER_KEY, { counter: imessageShortIdCounter });
|
|
174
|
+
} catch (err) {
|
|
175
|
+
reportPersistenceFailure("counter", err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function buildReplyCacheEntry(entry, messageId, shortId) {
|
|
179
|
+
return {
|
|
180
|
+
accountId: entry.accountId,
|
|
181
|
+
messageId,
|
|
182
|
+
shortId,
|
|
183
|
+
timestamp: entry.timestamp,
|
|
184
|
+
...typeof entry.chatGuid === "string" ? { chatGuid: entry.chatGuid } : {},
|
|
185
|
+
...typeof entry.chatIdentifier === "string" ? { chatIdentifier: entry.chatIdentifier } : {},
|
|
186
|
+
...typeof entry.chatId === "number" ? { chatId: entry.chatId } : {},
|
|
187
|
+
...typeof entry.isFromMe === "boolean" ? { isFromMe: entry.isFromMe } : {}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function generateShortId() {
|
|
191
|
+
imessageShortIdCounter += 1;
|
|
192
|
+
persistReplyCacheCounter();
|
|
193
|
+
return String(imessageShortIdCounter);
|
|
194
|
+
}
|
|
195
|
+
function rememberIMessageReplyCache(entry) {
|
|
196
|
+
hydrateFromStoreOnce();
|
|
197
|
+
const messageId = entry.messageId.trim();
|
|
198
|
+
if (!messageId) return {
|
|
199
|
+
...entry,
|
|
200
|
+
shortId: ""
|
|
201
|
+
};
|
|
202
|
+
let shortId = imessageUuidToShortId.get(messageId);
|
|
203
|
+
if (!shortId) {
|
|
204
|
+
shortId = generateShortId();
|
|
205
|
+
imessageShortIdToUuid.set(shortId, messageId);
|
|
206
|
+
imessageUuidToShortId.set(messageId, shortId);
|
|
207
|
+
}
|
|
208
|
+
const fullEntry = buildReplyCacheEntry(entry, messageId, shortId);
|
|
209
|
+
imessageReplyCacheByMessageId.delete(messageId);
|
|
210
|
+
imessageReplyCacheByMessageId.set(messageId, fullEntry);
|
|
211
|
+
const cutoff = Date.now() - IMESSAGE_REPLY_CACHE_TTL_MS;
|
|
212
|
+
let evicted = false;
|
|
213
|
+
const deletedMessageIds = [];
|
|
214
|
+
for (const [key, value] of imessageReplyCacheByMessageId) {
|
|
215
|
+
if (value.timestamp >= cutoff) break;
|
|
216
|
+
imessageReplyCacheByMessageId.delete(key);
|
|
217
|
+
deletedMessageIds.push(key);
|
|
218
|
+
if (value.shortId) {
|
|
219
|
+
imessageShortIdToUuid.delete(value.shortId);
|
|
220
|
+
imessageUuidToShortId.delete(key);
|
|
221
|
+
}
|
|
222
|
+
evicted = true;
|
|
223
|
+
}
|
|
224
|
+
while (imessageReplyCacheByMessageId.size > IMESSAGE_REPLY_CACHE_MAX_ENTRIES) {
|
|
225
|
+
const oldest = imessageReplyCacheByMessageId.keys().next().value;
|
|
226
|
+
if (!oldest) break;
|
|
227
|
+
const oldEntry = imessageReplyCacheByMessageId.get(oldest);
|
|
228
|
+
imessageReplyCacheByMessageId.delete(oldest);
|
|
229
|
+
deletedMessageIds.push(oldest);
|
|
230
|
+
if (oldEntry?.shortId) {
|
|
231
|
+
imessageShortIdToUuid.delete(oldEntry.shortId);
|
|
232
|
+
imessageUuidToShortId.delete(oldest);
|
|
233
|
+
}
|
|
234
|
+
evicted = true;
|
|
235
|
+
}
|
|
236
|
+
if (evicted) for (const messageIdToDelete of deletedMessageIds) deleteReplyCacheEntry(messageIdToDelete);
|
|
237
|
+
persistReplyCacheEntry(fullEntry);
|
|
238
|
+
return fullEntry;
|
|
239
|
+
}
|
|
240
|
+
function hasChatScope(ctx) {
|
|
241
|
+
if (!ctx) return false;
|
|
242
|
+
return Boolean(normalizeOptionalString(ctx.chatGuid) || normalizeOptionalString(ctx.chatIdentifier) || typeof ctx.chatId === "number");
|
|
243
|
+
}
|
|
244
|
+
function isCrossChatMismatch(cached, ctx) {
|
|
245
|
+
return resolveIMessageChatMatch(cached, ctx) === "mismatch";
|
|
246
|
+
}
|
|
247
|
+
function describeChatForError(values) {
|
|
248
|
+
const parts = [];
|
|
249
|
+
if (normalizeOptionalString(values.chatGuid)) parts.push("chatGuid=<redacted>");
|
|
250
|
+
if (normalizeOptionalString(values.chatIdentifier)) parts.push("chatIdentifier=<redacted>");
|
|
251
|
+
if (typeof values.chatId === "number") parts.push("chatId=<redacted>");
|
|
252
|
+
return parts.length === 0 ? "<unknown chat>" : parts.join(", ");
|
|
253
|
+
}
|
|
254
|
+
function describeMessageIdForError(inputId, inputKind) {
|
|
255
|
+
if (inputKind === "short") return `<short:${inputId.length}-digit>`;
|
|
256
|
+
return `<uuid:${inputId.slice(0, 8)}...>`;
|
|
257
|
+
}
|
|
258
|
+
function buildCrossChatError(inputId, inputKind, cached, ctx) {
|
|
259
|
+
const remediation = inputKind === "short" ? "Use a message ID from the current chat target; MessageSidFull from another chat is rejected." : "Retry with the correct chat target.";
|
|
260
|
+
return /* @__PURE__ */ new Error(`iMessage message id ${describeMessageIdForError(inputId, inputKind)} belongs to a different chat (${describeChatForError(cached)}) than the current call target (${describeChatForError(ctx)}). ${remediation}`);
|
|
261
|
+
}
|
|
262
|
+
function resolveIMessageMessageId(shortOrUuid, opts) {
|
|
263
|
+
const trimmed = shortOrUuid.trim();
|
|
264
|
+
if (!trimmed) return trimmed;
|
|
265
|
+
hydrateFromStoreOnce();
|
|
266
|
+
if (/^\d+$/.test(trimmed)) {
|
|
267
|
+
const uuid = imessageShortIdToUuid.get(trimmed);
|
|
268
|
+
if (uuid) {
|
|
269
|
+
const cached = imessageReplyCacheByMessageId.get(uuid);
|
|
270
|
+
if (opts?.chatContext && hasChatScope(opts.chatContext)) {
|
|
271
|
+
if (cached && isCrossChatMismatch(cached, opts.chatContext)) throw buildCrossChatError(trimmed, "short", cached, opts.chatContext);
|
|
272
|
+
}
|
|
273
|
+
if (opts?.requireFromMe && cached?.isFromMe !== true) throw buildFromMeError(trimmed, "short");
|
|
274
|
+
return uuid;
|
|
275
|
+
}
|
|
276
|
+
if (opts?.requireKnownShortId && !hasChatScope(opts.chatContext)) throw new Error(`iMessage short message id ${describeMessageIdForError(trimmed, "short")} requires a chat scope (chatGuid / chatIdentifier / chatId or a target).`);
|
|
277
|
+
if (opts?.requireKnownShortId) throw new Error(`iMessage short message id ${describeMessageIdForError(trimmed, "short")} is no longer available. Use MessageSidFull.`);
|
|
278
|
+
return trimmed;
|
|
279
|
+
}
|
|
280
|
+
const cached = imessageReplyCacheByMessageId.get(trimmed);
|
|
281
|
+
if (opts?.chatContext) {
|
|
282
|
+
if (cached && isCrossChatMismatch(cached, opts.chatContext)) throw buildCrossChatError(trimmed, "uuid", cached, opts.chatContext);
|
|
283
|
+
}
|
|
284
|
+
if (opts?.requireFromMe && cached?.isFromMe !== true) throw buildFromMeError(trimmed, "uuid");
|
|
285
|
+
return trimmed;
|
|
286
|
+
}
|
|
287
|
+
function isKnownFromMeIMessageMessageId(messageId, ctx) {
|
|
288
|
+
const trimmed = normalizeOptionalString(messageId);
|
|
289
|
+
if (!trimmed || !ctx.accountId || !hasChatScope(ctx)) return false;
|
|
290
|
+
hydrateFromStoreOnce();
|
|
291
|
+
const cached = imessageReplyCacheByMessageId.get(trimmed);
|
|
292
|
+
if (!cached || cached.isFromMe !== true || cached.accountId !== ctx.accountId) return false;
|
|
293
|
+
return isPositiveIMessageChatMatch(cached, ctx);
|
|
294
|
+
}
|
|
295
|
+
function buildFromMeError(inputId, inputKind) {
|
|
296
|
+
return /* @__PURE__ */ new Error(`iMessage message id ${describeMessageIdForError(inputId, inputKind)} is not one this agent sent. edit and unsend can only target messages the gateway delivered itself; messages received from other participants cannot be modified.`);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Return the most recent cached entry whose chat scope matches the supplied
|
|
300
|
+
* context. Used as a fallback when an agent calls a per-message action (e.g.
|
|
301
|
+
* `react`) without specifying a `messageId` — the natural intent is "react
|
|
302
|
+
* to the message I just received in this chat."
|
|
303
|
+
*
|
|
304
|
+
* Strict semantics for safety:
|
|
305
|
+
* - Caller must supply a chat scope. We refuse to "guess" the active chat.
|
|
306
|
+
* - Cached entry must positively match on at least one identifier kind
|
|
307
|
+
* (chatGuid, chatIdentifier, chatId, or normalized direct-DM fingerprint).
|
|
308
|
+
* We do NOT fall through on "no overlapping identifier" — that's how a
|
|
309
|
+
* cached entry from a foreign chat could be returned when the caller's
|
|
310
|
+
* context didn't share any identifier kind with the cache.
|
|
311
|
+
* - Caller must supply an accountId; we never cross account boundaries.
|
|
312
|
+
* - We only consider entries newer than `LATEST_FALLBACK_MS`. The intent
|
|
313
|
+
* of "react to the latest" is "the message I just received," not
|
|
314
|
+
* "anything in this chat from any time."
|
|
315
|
+
*/
|
|
316
|
+
function findLatestIMessageEntryForChat(ctx) {
|
|
317
|
+
if (!hasChatScope(ctx)) return;
|
|
318
|
+
if (!ctx.accountId) return;
|
|
319
|
+
const cutoff = Date.now() - LATEST_FALLBACK_MS;
|
|
320
|
+
let best;
|
|
321
|
+
for (const entry of imessageReplyCacheByMessageId.values()) {
|
|
322
|
+
if (entry.accountId !== ctx.accountId) continue;
|
|
323
|
+
if (entry.timestamp < cutoff) continue;
|
|
324
|
+
if (!isPositiveIMessageChatMatch(entry, ctx)) continue;
|
|
325
|
+
if (!best || entry.timestamp > best.timestamp) best = entry;
|
|
326
|
+
}
|
|
327
|
+
return best;
|
|
328
|
+
}
|
|
329
|
+
function resolveIMessageCachedResourceBinding(messageId, ctx) {
|
|
330
|
+
hydrateFromStoreOnce();
|
|
331
|
+
const entry = imessageReplyCacheByMessageId.get(messageId.trim());
|
|
332
|
+
if (!entry) return "unknown";
|
|
333
|
+
if (Date.now() - entry.timestamp > 216e5) return "unknown";
|
|
334
|
+
if (entry.accountId !== ctx.accountId) return "mismatch";
|
|
335
|
+
const chatMatch = resolveIMessageChatMatch(entry, ctx);
|
|
336
|
+
if (chatMatch !== "match") return chatMatch;
|
|
337
|
+
return "match";
|
|
338
|
+
}
|
|
339
|
+
function isIMessageCurrentMessageInChat(params) {
|
|
340
|
+
if (!params.accountId || !hasChatScope(params.chatContext)) return false;
|
|
341
|
+
const currentMessageId = normalizeOptionalString(String(params.currentMessageId));
|
|
342
|
+
if (!currentMessageId) return false;
|
|
343
|
+
hydrateFromStoreOnce();
|
|
344
|
+
const fullMessageId = /^\d+$/.test(currentMessageId) ? imessageShortIdToUuid.get(currentMessageId) : currentMessageId;
|
|
345
|
+
if (!fullMessageId) return false;
|
|
346
|
+
return resolveIMessageCachedResourceBinding(fullMessageId, {
|
|
347
|
+
...params.chatContext,
|
|
348
|
+
accountId: params.accountId
|
|
349
|
+
}) === "match";
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region extensions/imessage/src/cli-output.ts
|
|
353
|
+
const IMESSAGE_CLI_STDOUT_MAX_BYTES = 8388608;
|
|
354
|
+
const IMESSAGE_CLI_STDERR_TAIL_BYTES = 65536;
|
|
355
|
+
function parseLastJsonObject(stdout) {
|
|
356
|
+
const last = stdout.split(/\r?\n/u).findLast((line) => line.trim().length > 0)?.trim();
|
|
357
|
+
if (!last) return null;
|
|
358
|
+
try {
|
|
359
|
+
const value = JSON.parse(last);
|
|
360
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function runIMessageCliJsonCommand(params) {
|
|
366
|
+
const dbPath = params.dbPath?.trim();
|
|
367
|
+
const argv = [
|
|
368
|
+
expandIMessageUserPath(params.cliPath),
|
|
369
|
+
...params.args,
|
|
370
|
+
...dbPath ? ["--db", dbPath] : [],
|
|
371
|
+
"--json"
|
|
372
|
+
];
|
|
373
|
+
const result = await runCommandWithTimeout(argv, {
|
|
374
|
+
killProcessTree: true,
|
|
375
|
+
maxOutputBytes: {
|
|
376
|
+
stdout: IMESSAGE_CLI_STDOUT_MAX_BYTES,
|
|
377
|
+
stderr: IMESSAGE_CLI_STDERR_TAIL_BYTES
|
|
378
|
+
},
|
|
379
|
+
outputCapture: {
|
|
380
|
+
stdout: "head",
|
|
381
|
+
stderr: "tail"
|
|
382
|
+
},
|
|
383
|
+
terminateOnOutputLimit: { stdout: true },
|
|
384
|
+
timeoutMs: params.timeoutMs
|
|
385
|
+
});
|
|
386
|
+
if (result.termination === "timeout") throw new Error(`iMessage action timed out after ${params.timeoutMs}ms`);
|
|
387
|
+
if (result.outputLimitExceeded || result.stdoutTruncatedBytes) throw new Error(`imsg stdout exceeded ${IMESSAGE_CLI_STDOUT_MAX_BYTES} bytes`);
|
|
388
|
+
const parsed = parseLastJsonObject(result.stdout);
|
|
389
|
+
if (result.code !== 0) {
|
|
390
|
+
const detail = typeof parsed?.error === "string" && parsed.error.trim() || result.stderr.trim() || result.stdout.trim() || `imsg exited with code ${result.code}`;
|
|
391
|
+
throw new Error(detail);
|
|
392
|
+
}
|
|
393
|
+
if (!parsed) throw new Error(`imsg returned non-JSON output: ${result.stdout.trim() || result.stderr.trim()}`);
|
|
394
|
+
if (parsed.success === false) {
|
|
395
|
+
const detail = typeof parsed.error === "string" && parsed.error.trim() ? parsed.error.trim() : "iMessage action failed";
|
|
396
|
+
throw new Error(detail);
|
|
397
|
+
}
|
|
398
|
+
return parsed;
|
|
399
|
+
}
|
|
400
|
+
//#endregion
|
|
401
|
+
//#region extensions/imessage/src/bridge-recovery.ts
|
|
402
|
+
const BRIDGE_RECOVERY_TIMEOUT_MS = 3e4;
|
|
403
|
+
const recoveries = /* @__PURE__ */ new Map();
|
|
404
|
+
/**
|
|
405
|
+
* Re-inject the private bridge after imsg has positively identified it as
|
|
406
|
+
* unresponsive. This deliberately does not retry the failed mutation: imsg may
|
|
407
|
+
* still reconcile a published send, so replaying it could duplicate a message.
|
|
408
|
+
*/
|
|
409
|
+
function recoverIMessageBridge(cliPath) {
|
|
410
|
+
const existing = recoveries.get(cliPath);
|
|
411
|
+
if (existing) return existing;
|
|
412
|
+
const recovery = runIMessageCliJsonCommand({
|
|
413
|
+
cliPath,
|
|
414
|
+
args: ["launch"],
|
|
415
|
+
timeoutMs: BRIDGE_RECOVERY_TIMEOUT_MS
|
|
416
|
+
}).then(() => void 0).finally(() => {
|
|
417
|
+
recoveries.delete(cliPath);
|
|
418
|
+
});
|
|
419
|
+
recoveries.set(cliPath, recovery);
|
|
420
|
+
return recovery;
|
|
421
|
+
}
|
|
422
|
+
//#endregion
|
|
13
423
|
//#region extensions/imessage/src/client.ts
|
|
14
424
|
var IMessageRpcRequestError = class extends Error {
|
|
15
425
|
constructor(message, code, data) {
|
|
@@ -19,6 +429,16 @@ var IMessageRpcRequestError = class extends Error {
|
|
|
19
429
|
this.name = "IMessageRpcRequestError";
|
|
20
430
|
}
|
|
21
431
|
};
|
|
432
|
+
function isIMessageBridgeStall(error) {
|
|
433
|
+
if (!(error instanceof Error)) return false;
|
|
434
|
+
return error.message.includes("Timed out waiting for response");
|
|
435
|
+
}
|
|
436
|
+
const BRIDGE_STALL_GUIDANCE = "The imsg private API bridge stopped responding. Run `imsg launch` to re-inject the dylib, then `openclaw channels status --probe` to refresh capability detection.";
|
|
437
|
+
function describeIMessageBridgeStall(error) {
|
|
438
|
+
if (error instanceof IMessageRpcRequestError) return new IMessageRpcRequestError(`${error.message} ${BRIDGE_STALL_GUIDANCE}`, error.code, error.data);
|
|
439
|
+
if (error instanceof Error) return new Error(`${error.message} ${BRIDGE_STALL_GUIDANCE}`, { cause: error });
|
|
440
|
+
return error;
|
|
441
|
+
}
|
|
22
442
|
const PUBLIC_IMESSAGE_FULL_DISK_ACCESS_ERROR = "imsg cannot access ~/Library/Messages/chat.db. Grant Full Disk Access to the Gateway/launcher process and restart Gateway.";
|
|
23
443
|
const IMESSAGE_RPC_CLOSE_GRACE_MS = 500;
|
|
24
444
|
function isTestEnv() {
|
|
@@ -44,7 +464,8 @@ var IMessageRpcClient = class {
|
|
|
44
464
|
this.stderrDecoder = new StringDecoder("utf8");
|
|
45
465
|
this.nextId = 1;
|
|
46
466
|
this.publicProcessError = null;
|
|
47
|
-
this.
|
|
467
|
+
this.configuredCliPath = opts.cliPath?.trim() || "imsg";
|
|
468
|
+
this.cliPath = expandIMessageUserPath(this.configuredCliPath);
|
|
48
469
|
const dbPath = opts.dbPath?.trim();
|
|
49
470
|
this.dbPath = dbPath ? opts.remoteHost?.trim() ? dbPath : resolveUserPath(dbPath) : void 0;
|
|
50
471
|
this.runtime = opts.runtime;
|
|
@@ -129,7 +550,20 @@ var IMessageRpcClient = class {
|
|
|
129
550
|
} catch (err) {
|
|
130
551
|
this.failTransport(err, this.child);
|
|
131
552
|
}
|
|
132
|
-
|
|
553
|
+
try {
|
|
554
|
+
return await response;
|
|
555
|
+
} catch (err) {
|
|
556
|
+
if (isIMessageBridgeStall(err)) {
|
|
557
|
+
invalidateCachedIMessagePrivateApiStatus(this.configuredCliPath);
|
|
558
|
+
try {
|
|
559
|
+
await recoverIMessageBridge(this.configuredCliPath);
|
|
560
|
+
} catch (recoveryError) {
|
|
561
|
+
this.runtime?.error?.(`imessage: automatic bridge recovery failed: ${formatErrorMessage(recoveryError)}`);
|
|
562
|
+
}
|
|
563
|
+
throw describeIMessageBridgeStall(err);
|
|
564
|
+
}
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
133
567
|
}
|
|
134
568
|
async stopChild(child) {
|
|
135
569
|
try {
|
|
@@ -329,107 +763,61 @@ function codeDelimiter(content) {
|
|
|
329
763
|
const longestRun = Math.max(0, ...[...content.matchAll(/`+/gu)].map((match) => match[0].length));
|
|
330
764
|
return "`".repeat(longestRun + 1);
|
|
331
765
|
}
|
|
332
|
-
function
|
|
333
|
-
const protections = [];
|
|
334
|
-
let markdown = "";
|
|
335
|
-
let cursor = 0;
|
|
336
|
-
for (const match of input.matchAll(/__[\p{L}_][\p{L}\p{N}_]*__/gu)) {
|
|
337
|
-
const identifier = match[0];
|
|
338
|
-
const start = match.index ?? 0;
|
|
339
|
-
const end = start + identifier.length;
|
|
340
|
-
const before = input[start - 1];
|
|
341
|
-
const beforeBefore = input[start - 2];
|
|
342
|
-
const after = input[end];
|
|
343
|
-
const member = before === ".";
|
|
344
|
-
const call = after === "(";
|
|
345
|
-
const functionArgument = before === "(" && /[\p{L}\p{N}_]/u.test(beforeBefore ?? "");
|
|
346
|
-
if (!member && !call && !functionArgument && !(after === "[")) continue;
|
|
347
|
-
let token = `OCdunder${protections.length}token`;
|
|
348
|
-
while (input.includes(token)) token += "x";
|
|
349
|
-
markdown += input.slice(cursor, start) + token;
|
|
350
|
-
protections.push({
|
|
351
|
-
token,
|
|
352
|
-
identifier
|
|
353
|
-
});
|
|
354
|
-
cursor = end;
|
|
355
|
-
}
|
|
356
|
-
return {
|
|
357
|
-
markdown: markdown + input.slice(cursor),
|
|
358
|
-
protections
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
function applyTextEdits(text, edits) {
|
|
362
|
-
const ordered = edits.toSorted((left, right) => left.start - right.start);
|
|
766
|
+
function restoreCodeMarkers(text, ranges, codeRanges) {
|
|
363
767
|
let rendered = "";
|
|
364
768
|
let cursor = 0;
|
|
365
|
-
|
|
366
|
-
rendered += text.slice(cursor, edit.start) + edit.text;
|
|
367
|
-
cursor = edit.end;
|
|
368
|
-
}
|
|
369
|
-
rendered += text.slice(cursor);
|
|
370
|
-
return {
|
|
371
|
-
text: rendered,
|
|
372
|
-
mapOffset: (offset) => offset + ordered.reduce((delta, edit) => delta + (edit.end <= offset ? edit.text.length - edit.end + edit.start : 0), 0)
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
function restoreDunderIdentifiers(text, ranges, codeRanges, protections) {
|
|
376
|
-
const edited = applyTextEdits(text, protections.flatMap((protection) => {
|
|
377
|
-
const start = text.indexOf(protection.token);
|
|
378
|
-
return start === -1 ? [] : [{
|
|
379
|
-
start,
|
|
380
|
-
end: start + protection.token.length,
|
|
381
|
-
text: protection.identifier
|
|
382
|
-
}];
|
|
383
|
-
}));
|
|
384
|
-
const mapRange = (range) => ({
|
|
385
|
-
...range,
|
|
386
|
-
start: edited.mapOffset(range.start),
|
|
387
|
-
length: edited.mapOffset(range.start + range.length) - edited.mapOffset(range.start)
|
|
388
|
-
});
|
|
389
|
-
return {
|
|
390
|
-
text: edited.text,
|
|
391
|
-
ranges: ranges.map(mapRange),
|
|
392
|
-
codeRanges: codeRanges.map(mapRange)
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
function restoreCodeMarkers(text, ranges, codeRanges) {
|
|
396
|
-
const edited = applyTextEdits(text, codeRanges.map((range) => {
|
|
769
|
+
const edits = codeRanges.map((range) => {
|
|
397
770
|
const end = range.start + range.length;
|
|
398
771
|
const content = text.slice(range.start, end);
|
|
399
772
|
const marker = codeDelimiter(content);
|
|
400
773
|
const padding = content.startsWith("`") || content.endsWith("`") ? " " : "";
|
|
774
|
+
rendered += text.slice(cursor, range.start) + `${marker}${padding}${content}${padding}${marker}`;
|
|
775
|
+
cursor = end;
|
|
401
776
|
return {
|
|
402
|
-
start: range.start,
|
|
403
777
|
end,
|
|
404
|
-
|
|
778
|
+
shift: rendered.length - end
|
|
405
779
|
};
|
|
406
|
-
})
|
|
780
|
+
});
|
|
781
|
+
rendered += text.slice(cursor);
|
|
782
|
+
const mapOffset = (offset) => {
|
|
783
|
+
let low = 0;
|
|
784
|
+
let high = edits.length;
|
|
785
|
+
while (low < high) {
|
|
786
|
+
const middle = low + Math.floor((high - low) / 2);
|
|
787
|
+
const edit = edits[middle];
|
|
788
|
+
if (edit && edit.end <= offset) low = middle + 1;
|
|
789
|
+
else high = middle;
|
|
790
|
+
}
|
|
791
|
+
return offset + (edits[low - 1]?.shift ?? 0);
|
|
792
|
+
};
|
|
407
793
|
return {
|
|
408
|
-
text:
|
|
409
|
-
ranges: ranges.map((range) =>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
794
|
+
text: rendered,
|
|
795
|
+
ranges: ranges.map((range) => {
|
|
796
|
+
const start = mapOffset(range.start);
|
|
797
|
+
return {
|
|
798
|
+
...range,
|
|
799
|
+
start,
|
|
800
|
+
length: mapOffset(range.start + range.length) - start
|
|
801
|
+
};
|
|
802
|
+
})
|
|
414
803
|
};
|
|
415
804
|
}
|
|
416
805
|
function extractMarkdownFormatRuns(input) {
|
|
417
|
-
const
|
|
418
|
-
const ir = markdownToIR(protectedDunders.markdown, {
|
|
806
|
+
const ir = markdownToIR(input, {
|
|
419
807
|
autolink: false,
|
|
420
808
|
enableHtmlUnderline: true,
|
|
421
809
|
headingStyle: "rich",
|
|
422
810
|
linkify: false,
|
|
811
|
+
preserveDunderIdentifiers: true,
|
|
423
812
|
preserveSourceBlockSpacing: true
|
|
424
813
|
});
|
|
425
814
|
const rendered = renderMarkdownWithAttributedRanges(ir, { styleMap: IMESSAGE_STYLE_MAP }, IMESSAGE_FORMAT_PROFILE);
|
|
426
815
|
const code = renderMarkdownWithAttributedRanges(ir, { styleMap: { code: "code" } }, IMESSAGE_CODE_PROFILE);
|
|
427
|
-
|
|
816
|
+
return restoreCodeMarkers(rendered.text, rendered.ranges.map(({ start, length, style }) => ({
|
|
428
817
|
start,
|
|
429
818
|
length,
|
|
430
819
|
styles: [style]
|
|
431
|
-
})), code.ranges
|
|
432
|
-
return restoreCodeMarkers(dunders.text, dunders.ranges, dunders.codeRanges);
|
|
820
|
+
})), code.ranges);
|
|
433
821
|
}
|
|
434
822
|
//#endregion
|
|
435
823
|
//#region extensions/imessage/src/monitor/sanitize-outbound.ts
|
|
@@ -893,4 +1281,4 @@ function sanitizeOutboundText(text) {
|
|
|
893
1281
|
return cleaned;
|
|
894
1282
|
}
|
|
895
1283
|
//#endregion
|
|
896
|
-
export { IMessageRpcRequestError as a,
|
|
1284
|
+
export { isIMessageEmailChatIdentifier as _, IMessageRpcRequestError as a, findLatestIMessageEntryForChat as c, rememberIMessageReplyCache as d, resolveIMessageCachedResourceBinding as f, chatContextFromIMessageTarget as g, DEFAULT_IMESSAGE_SEND_TIMEOUT_MS as h, extractMarkdownFormatRuns as i, isIMessageCurrentMessageInChat as l, DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS as m, sanitizeIMessageFinalOutboundText as n, createIMessageRpcClient as o, resolveIMessageMessageId as p, sanitizeOutboundText as r, runIMessageCliJsonCommand as s, protectIMessageFencedRoleMarkers as t, isKnownFromMeIMessageMessageId as u, normalizeDirectChatIdentifier as v, resolveIMessageDirectChatService as y };
|