clawdbot 2026.1.4-1 → 2026.1.5

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.
Files changed (117) hide show
  1. package/CHANGELOG.md +26 -6
  2. package/README.md +26 -1
  3. package/dist/agents/pi-embedded-runner.js +2 -0
  4. package/dist/agents/pi-embedded-subscribe.js +18 -3
  5. package/dist/agents/pi-tools.js +45 -6
  6. package/dist/agents/tools/browser-tool.js +38 -89
  7. package/dist/agents/tools/cron-tool.js +8 -8
  8. package/dist/agents/workspace.js +8 -1
  9. package/dist/auto-reply/command-detection.js +26 -0
  10. package/dist/auto-reply/reply/agent-runner.js +15 -8
  11. package/dist/auto-reply/reply/commands.js +36 -25
  12. package/dist/auto-reply/reply/directive-handling.js +4 -2
  13. package/dist/auto-reply/reply/directives.js +12 -0
  14. package/dist/auto-reply/reply/session-updates.js +2 -4
  15. package/dist/auto-reply/reply.js +26 -4
  16. package/dist/browser/config.js +22 -4
  17. package/dist/browser/profiles-service.js +3 -1
  18. package/dist/browser/profiles.js +14 -3
  19. package/dist/canvas-host/a2ui/.bundle.hash +2 -0
  20. package/dist/cli/gateway-cli.js +2 -2
  21. package/dist/cli/profile.js +81 -0
  22. package/dist/cli/program.js +10 -1
  23. package/dist/cli/run-main.js +33 -0
  24. package/dist/commands/configure.js +5 -0
  25. package/dist/commands/onboard-providers.js +1 -1
  26. package/dist/commands/setup.js +4 -1
  27. package/dist/config/defaults.js +56 -0
  28. package/dist/config/io.js +47 -6
  29. package/dist/config/paths.js +2 -2
  30. package/dist/config/port-defaults.js +32 -0
  31. package/dist/config/sessions.js +3 -2
  32. package/dist/config/validation.js +2 -2
  33. package/dist/config/zod-schema.js +16 -0
  34. package/dist/discord/monitor.js +75 -266
  35. package/dist/entry.js +16 -0
  36. package/dist/gateway/call.js +8 -1
  37. package/dist/gateway/server-methods/chat.js +1 -1
  38. package/dist/gateway/server.js +14 -3
  39. package/dist/index.js +2 -2
  40. package/dist/infra/control-ui-assets.js +118 -0
  41. package/dist/infra/dotenv.js +15 -0
  42. package/dist/infra/shell-env.js +79 -0
  43. package/dist/infra/system-events.js +50 -23
  44. package/dist/macos/relay.js +8 -2
  45. package/dist/telegram/bot.js +24 -1
  46. package/dist/utils.js +8 -2
  47. package/dist/web/auto-reply.js +18 -21
  48. package/dist/web/inbound.js +5 -1
  49. package/dist/web/qr-image.js +4 -4
  50. package/dist/web/session.js +2 -3
  51. package/docs/agent.md +0 -2
  52. package/docs/assets/markdown.css +4 -1
  53. package/docs/audio.md +0 -2
  54. package/docs/clawd.md +0 -2
  55. package/docs/configuration.md +62 -3
  56. package/docs/docs.json +9 -1
  57. package/docs/faq.md +32 -7
  58. package/docs/gateway.md +28 -0
  59. package/docs/images.md +0 -2
  60. package/docs/index.md +2 -4
  61. package/docs/mac/icon.md +1 -1
  62. package/docs/nix.md +57 -11
  63. package/docs/onboarding.md +0 -2
  64. package/docs/refactor/webagent-session.md +0 -2
  65. package/docs/research/memory.md +1 -1
  66. package/docs/skills.md +0 -2
  67. package/docs/templates/AGENTS.md +2 -2
  68. package/docs/tools.md +15 -0
  69. package/docs/whatsapp.md +2 -0
  70. package/package.json +8 -16
  71. package/dist/control-ui/assets/index-BFID3yAA.css +0 -1
  72. package/dist/control-ui/assets/index-CE_axlTS.js +0 -2235
  73. package/dist/control-ui/assets/index-CE_axlTS.js.map +0 -1
  74. package/dist/control-ui/index.html +0 -15
  75. package/dist/daemon/constants.js +0 -10
  76. package/dist/daemon/launchd.js +0 -276
  77. package/dist/daemon/legacy.js +0 -63
  78. package/dist/daemon/program-args.js +0 -76
  79. package/dist/daemon/schtasks.js +0 -257
  80. package/dist/daemon/service.js +0 -60
  81. package/dist/daemon/systemd.js +0 -266
  82. package/dist/imessage/client.js +0 -165
  83. package/dist/imessage/index.js +0 -3
  84. package/dist/imessage/monitor.js +0 -272
  85. package/dist/imessage/probe.js +0 -26
  86. package/dist/imessage/send.js +0 -83
  87. package/dist/imessage/targets.js +0 -176
  88. package/dist/sessions/send-policy.js +0 -68
  89. package/dist/signal/client.js +0 -134
  90. package/dist/signal/daemon.js +0 -69
  91. package/dist/signal/index.js +0 -3
  92. package/dist/signal/monitor.js +0 -336
  93. package/dist/signal/probe.js +0 -46
  94. package/dist/signal/send.js +0 -91
  95. package/dist/slack/actions.js +0 -97
  96. package/dist/slack/index.js +0 -5
  97. package/dist/slack/monitor.js +0 -1029
  98. package/dist/slack/probe.js +0 -47
  99. package/dist/slack/send.js +0 -131
  100. package/dist/slack/token.js +0 -10
  101. package/dist/tui/commands.js +0 -74
  102. package/dist/tui/components/assistant-message.js +0 -16
  103. package/dist/tui/components/chat-log.js +0 -92
  104. package/dist/tui/components/custom-editor.js +0 -53
  105. package/dist/tui/components/selectors.js +0 -8
  106. package/dist/tui/components/tool-execution.js +0 -111
  107. package/dist/tui/components/user-message.js +0 -17
  108. package/dist/tui/gateway-chat.js +0 -140
  109. package/dist/tui/layout.js +0 -41
  110. package/dist/tui/message-list.js +0 -57
  111. package/dist/tui/theme/theme.js +0 -80
  112. package/dist/tui/theme.js +0 -25
  113. package/dist/tui/tui.js +0 -708
  114. package/dist/wizard/clack-prompter.js +0 -56
  115. package/dist/wizard/onboarding.js +0 -452
  116. package/dist/wizard/prompts.js +0 -6
  117. package/dist/wizard/session.js +0 -203
@@ -1,272 +0,0 @@
1
- import { chunkText, resolveTextChunkLimit } from "../auto-reply/chunk.js";
2
- import { formatAgentEnvelope } from "../auto-reply/envelope.js";
3
- import { getReplyFromConfig } from "../auto-reply/reply.js";
4
- import { loadConfig } from "../config/config.js";
5
- import { resolveStorePath, updateLastRoute } from "../config/sessions.js";
6
- import { danger, logVerbose, shouldLogVerbose } from "../globals.js";
7
- import { mediaKindFromMime } from "../media/constants.js";
8
- import { createIMessageRpcClient } from "./client.js";
9
- import { sendMessageIMessage } from "./send.js";
10
- import { formatIMessageChatTarget, isAllowedIMessageSender, normalizeIMessageHandle, } from "./targets.js";
11
- function resolveRuntime(opts) {
12
- return (opts.runtime ?? {
13
- log: console.log,
14
- error: console.error,
15
- exit: (code) => {
16
- throw new Error(`exit ${code}`);
17
- },
18
- });
19
- }
20
- function resolveAllowFrom(opts) {
21
- const cfg = loadConfig();
22
- const raw = opts.allowFrom ?? cfg.imessage?.allowFrom ?? [];
23
- return raw.map((entry) => String(entry).trim()).filter(Boolean);
24
- }
25
- function resolveMentionRegexes(cfg) {
26
- return (cfg.routing?.groupChat?.mentionPatterns
27
- ?.map((pattern) => {
28
- try {
29
- return new RegExp(pattern, "i");
30
- }
31
- catch {
32
- return null;
33
- }
34
- })
35
- .filter((val) => Boolean(val)) ?? []);
36
- }
37
- function resolveGroupRequireMention(cfg, opts, chatId) {
38
- if (typeof opts.requireMention === "boolean")
39
- return opts.requireMention;
40
- const groupId = chatId != null ? String(chatId) : undefined;
41
- if (groupId) {
42
- const groupConfig = cfg.imessage?.groups?.[groupId];
43
- if (typeof groupConfig?.requireMention === "boolean") {
44
- return groupConfig.requireMention;
45
- }
46
- }
47
- const groupDefault = cfg.imessage?.groups?.["*"]?.requireMention;
48
- if (typeof groupDefault === "boolean")
49
- return groupDefault;
50
- return true;
51
- }
52
- function isMentioned(text, regexes) {
53
- if (!text)
54
- return false;
55
- const cleaned = text
56
- .replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u206f]/g, "")
57
- .toLowerCase();
58
- return regexes.some((re) => re.test(cleaned));
59
- }
60
- async function deliverReplies(params) {
61
- const { replies, target, client, runtime, maxBytes, textLimit } = params;
62
- for (const payload of replies) {
63
- const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
64
- const text = payload.text ?? "";
65
- if (!text && mediaList.length === 0)
66
- continue;
67
- if (mediaList.length === 0) {
68
- for (const chunk of chunkText(text, textLimit)) {
69
- await sendMessageIMessage(target, chunk, { maxBytes, client });
70
- }
71
- }
72
- else {
73
- let first = true;
74
- for (const url of mediaList) {
75
- const caption = first ? text : "";
76
- first = false;
77
- await sendMessageIMessage(target, caption, {
78
- mediaUrl: url,
79
- maxBytes,
80
- client,
81
- });
82
- }
83
- }
84
- runtime.log?.(`imessage: delivered reply to ${target}`);
85
- }
86
- }
87
- export async function monitorIMessageProvider(opts = {}) {
88
- const runtime = resolveRuntime(opts);
89
- const cfg = loadConfig();
90
- const textLimit = resolveTextChunkLimit(cfg, "imessage");
91
- const allowFrom = resolveAllowFrom(opts);
92
- const mentionRegexes = resolveMentionRegexes(cfg);
93
- const includeAttachments = opts.includeAttachments ?? cfg.imessage?.includeAttachments ?? false;
94
- const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.imessage?.mediaMaxMb ?? 16) * 1024 * 1024;
95
- const handleMessage = async (raw) => {
96
- const params = raw;
97
- const message = params?.message ?? null;
98
- if (!message)
99
- return;
100
- const senderRaw = message.sender ?? "";
101
- const sender = senderRaw.trim();
102
- if (!sender)
103
- return;
104
- if (message.is_from_me)
105
- return;
106
- const chatId = message.chat_id ?? undefined;
107
- const chatGuid = message.chat_guid ?? undefined;
108
- const chatIdentifier = message.chat_identifier ?? undefined;
109
- const isGroup = Boolean(message.is_group);
110
- if (isGroup && !chatId)
111
- return;
112
- if (!isAllowedIMessageSender({
113
- allowFrom,
114
- sender,
115
- chatId: chatId ?? undefined,
116
- chatGuid,
117
- chatIdentifier,
118
- })) {
119
- logVerbose(`Blocked iMessage sender ${sender} (not in allowFrom)`);
120
- return;
121
- }
122
- const messageText = (message.text ?? "").trim();
123
- const mentioned = isGroup ? isMentioned(messageText, mentionRegexes) : true;
124
- const requireMention = resolveGroupRequireMention(cfg, opts, chatId);
125
- if (isGroup && requireMention && !mentioned) {
126
- logVerbose(`imessage: skipping group message (no mention)`);
127
- return;
128
- }
129
- const attachments = includeAttachments ? (message.attachments ?? []) : [];
130
- const firstAttachment = attachments?.find((entry) => entry?.original_path && !entry?.missing);
131
- const mediaPath = firstAttachment?.original_path ?? undefined;
132
- const mediaType = firstAttachment?.mime_type ?? undefined;
133
- const kind = mediaKindFromMime(mediaType ?? undefined);
134
- const placeholder = kind
135
- ? `<media:${kind}>`
136
- : attachments?.length
137
- ? "<media:attachment>"
138
- : "";
139
- const bodyText = messageText || placeholder;
140
- if (!bodyText)
141
- return;
142
- const chatTarget = formatIMessageChatTarget(chatId);
143
- const fromLabel = isGroup
144
- ? `${message.chat_name || "iMessage Group"} id:${chatId ?? "unknown"}`
145
- : `${normalizeIMessageHandle(sender)} id:${sender}`;
146
- const createdAt = message.created_at
147
- ? Date.parse(message.created_at)
148
- : undefined;
149
- const body = formatAgentEnvelope({
150
- surface: "iMessage",
151
- from: fromLabel,
152
- timestamp: createdAt,
153
- body: bodyText,
154
- });
155
- const ctxPayload = {
156
- Body: body,
157
- From: isGroup ? `group:${chatId}` : `imessage:${sender}`,
158
- To: chatTarget || `imessage:${sender}`,
159
- ChatType: isGroup ? "group" : "direct",
160
- GroupSubject: isGroup ? (message.chat_name ?? undefined) : undefined,
161
- GroupMembers: isGroup
162
- ? (message.participants ?? []).filter(Boolean).join(", ")
163
- : undefined,
164
- SenderName: sender,
165
- Surface: "imessage",
166
- MessageSid: message.id ? String(message.id) : undefined,
167
- Timestamp: createdAt,
168
- MediaPath: mediaPath,
169
- MediaType: mediaType,
170
- MediaUrl: mediaPath,
171
- WasMentioned: mentioned,
172
- };
173
- if (!isGroup) {
174
- const sessionCfg = cfg.session;
175
- const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
176
- const storePath = resolveStorePath(sessionCfg?.store);
177
- const to = chatTarget || sender;
178
- if (to) {
179
- await updateLastRoute({
180
- storePath,
181
- sessionKey: mainKey,
182
- channel: "imessage",
183
- to,
184
- });
185
- }
186
- }
187
- if (shouldLogVerbose()) {
188
- const preview = body.slice(0, 200).replace(/\n/g, "\\n");
189
- logVerbose(`imessage inbound: chatId=${chatId ?? "unknown"} from=${ctxPayload.From} len=${body.length} preview="${preview}"`);
190
- }
191
- let blockSendChain = Promise.resolve();
192
- const sendBlockReply = (payload) => {
193
- if (!payload?.text &&
194
- !payload?.mediaUrl &&
195
- !(payload?.mediaUrls?.length ?? 0)) {
196
- return;
197
- }
198
- blockSendChain = blockSendChain
199
- .then(async () => {
200
- await deliverReplies({
201
- replies: [payload],
202
- target: ctxPayload.To,
203
- client,
204
- runtime,
205
- maxBytes: mediaMaxBytes,
206
- textLimit,
207
- });
208
- })
209
- .catch((err) => {
210
- runtime.error?.(danger(`imessage block reply failed: ${String(err)}`));
211
- });
212
- };
213
- const replyResult = await getReplyFromConfig(ctxPayload, { onBlockReply: sendBlockReply }, cfg);
214
- const replies = replyResult
215
- ? Array.isArray(replyResult)
216
- ? replyResult
217
- : [replyResult]
218
- : [];
219
- await blockSendChain;
220
- if (replies.length === 0)
221
- return;
222
- await deliverReplies({
223
- replies,
224
- target: ctxPayload.To,
225
- client,
226
- runtime,
227
- maxBytes: mediaMaxBytes,
228
- textLimit,
229
- });
230
- };
231
- const client = await createIMessageRpcClient({
232
- cliPath: opts.cliPath ?? cfg.imessage?.cliPath,
233
- dbPath: opts.dbPath ?? cfg.imessage?.dbPath,
234
- runtime,
235
- onNotification: (msg) => {
236
- if (msg.method === "message") {
237
- void handleMessage(msg.params).catch((err) => {
238
- runtime.error?.(`imessage: handler failed: ${String(err)}`);
239
- });
240
- }
241
- else if (msg.method === "error") {
242
- runtime.error?.(`imessage: watch error ${JSON.stringify(msg.params)}`);
243
- }
244
- },
245
- });
246
- let subscriptionId = null;
247
- const abort = opts.abortSignal;
248
- const onAbort = () => {
249
- if (subscriptionId) {
250
- void client.request("watch.unsubscribe", {
251
- subscription: subscriptionId,
252
- });
253
- }
254
- void client.stop();
255
- };
256
- abort?.addEventListener("abort", onAbort, { once: true });
257
- try {
258
- const result = await client.request("watch.subscribe", { attachments: includeAttachments });
259
- subscriptionId = result?.subscription ?? null;
260
- await client.waitForClose();
261
- }
262
- catch (err) {
263
- if (abort?.aborted)
264
- return;
265
- runtime.error?.(danger(`imessage: monitor failed: ${String(err)}`));
266
- throw err;
267
- }
268
- finally {
269
- abort?.removeEventListener("abort", onAbort);
270
- await client.stop();
271
- }
272
- }
@@ -1,26 +0,0 @@
1
- import { detectBinary } from "../commands/onboard-helpers.js";
2
- import { loadConfig } from "../config/config.js";
3
- import { createIMessageRpcClient } from "./client.js";
4
- export async function probeIMessage(timeoutMs = 2000) {
5
- const cfg = loadConfig();
6
- const cliPath = cfg.imessage?.cliPath?.trim() || "imsg";
7
- const dbPath = cfg.imessage?.dbPath?.trim();
8
- const detected = await detectBinary(cliPath);
9
- if (!detected) {
10
- return { ok: false, error: `imsg not found (${cliPath})` };
11
- }
12
- const client = await createIMessageRpcClient({
13
- cliPath,
14
- dbPath,
15
- });
16
- try {
17
- await client.request("chats.list", { limit: 1 }, { timeoutMs });
18
- return { ok: true };
19
- }
20
- catch (err) {
21
- return { ok: false, error: String(err) };
22
- }
23
- finally {
24
- await client.stop();
25
- }
26
- }
@@ -1,83 +0,0 @@
1
- import { loadConfig } from "../config/config.js";
2
- import { mediaKindFromMime } from "../media/constants.js";
3
- import { saveMediaBuffer } from "../media/store.js";
4
- import { loadWebMedia } from "../web/media.js";
5
- import { createIMessageRpcClient } from "./client.js";
6
- import { formatIMessageChatTarget, parseIMessageTarget, } from "./targets.js";
7
- function resolveCliPath(explicit) {
8
- const cfg = loadConfig();
9
- return explicit?.trim() || cfg.imessage?.cliPath?.trim() || "imsg";
10
- }
11
- function resolveDbPath(explicit) {
12
- const cfg = loadConfig();
13
- return explicit?.trim() || cfg.imessage?.dbPath?.trim() || undefined;
14
- }
15
- function resolveService(explicit) {
16
- const cfg = loadConfig();
17
- return (explicit || cfg.imessage?.service || "auto");
18
- }
19
- function resolveRegion(explicit) {
20
- const cfg = loadConfig();
21
- return explicit?.trim() || cfg.imessage?.region?.trim() || "US";
22
- }
23
- async function resolveAttachment(mediaUrl, maxBytes) {
24
- const media = await loadWebMedia(mediaUrl, maxBytes);
25
- const saved = await saveMediaBuffer(media.buffer, media.contentType ?? undefined, "outbound", maxBytes);
26
- return { path: saved.path, contentType: saved.contentType };
27
- }
28
- export async function sendMessageIMessage(to, text, opts = {}) {
29
- const cliPath = resolveCliPath(opts.cliPath);
30
- const dbPath = resolveDbPath(opts.dbPath);
31
- const target = parseIMessageTarget(opts.chatId ? formatIMessageChatTarget(opts.chatId) : to);
32
- const service = opts.service ?? (target.kind === "handle" ? target.service : undefined);
33
- const region = resolveRegion(opts.region);
34
- const maxBytes = opts.maxBytes ?? 16 * 1024 * 1024;
35
- let message = text ?? "";
36
- let filePath;
37
- if (opts.mediaUrl?.trim()) {
38
- const resolved = await resolveAttachment(opts.mediaUrl.trim(), maxBytes);
39
- filePath = resolved.path;
40
- if (!message.trim()) {
41
- const kind = mediaKindFromMime(resolved.contentType ?? undefined);
42
- if (kind)
43
- message = kind === "image" ? "<media:image>" : `<media:${kind}>`;
44
- }
45
- }
46
- if (!message.trim() && !filePath) {
47
- throw new Error("iMessage send requires text or media");
48
- }
49
- const params = {
50
- text: message,
51
- service: resolveService(service),
52
- region,
53
- };
54
- if (filePath)
55
- params.file = filePath;
56
- if (target.kind === "chat_id") {
57
- params.chat_id = target.chatId;
58
- }
59
- else if (target.kind === "chat_guid") {
60
- params.chat_guid = target.chatGuid;
61
- }
62
- else if (target.kind === "chat_identifier") {
63
- params.chat_identifier = target.chatIdentifier;
64
- }
65
- else {
66
- params.to = target.to;
67
- }
68
- const client = opts.client ?? (await createIMessageRpcClient({ cliPath, dbPath }));
69
- const shouldClose = !opts.client;
70
- try {
71
- const result = await client.request("send", params, {
72
- timeoutMs: opts.timeoutMs,
73
- });
74
- return {
75
- messageId: result?.ok ? "ok" : "unknown",
76
- };
77
- }
78
- finally {
79
- if (shouldClose) {
80
- await client.stop();
81
- }
82
- }
83
- }
@@ -1,176 +0,0 @@
1
- import { normalizeE164 } from "../utils.js";
2
- const CHAT_ID_PREFIXES = ["chat_id:", "chatid:", "chat:"];
3
- const CHAT_GUID_PREFIXES = ["chat_guid:", "chatguid:", "guid:"];
4
- const CHAT_IDENTIFIER_PREFIXES = [
5
- "chat_identifier:",
6
- "chatidentifier:",
7
- "chatident:",
8
- ];
9
- const SERVICE_PREFIXES = [
10
- { prefix: "imessage:", service: "imessage" },
11
- { prefix: "sms:", service: "sms" },
12
- { prefix: "auto:", service: "auto" },
13
- ];
14
- function stripPrefix(value, prefix) {
15
- return value.slice(prefix.length).trim();
16
- }
17
- export function normalizeIMessageHandle(raw) {
18
- const trimmed = raw.trim();
19
- if (!trimmed)
20
- return "";
21
- const lowered = trimmed.toLowerCase();
22
- if (lowered.startsWith("imessage:"))
23
- return normalizeIMessageHandle(trimmed.slice(9));
24
- if (lowered.startsWith("sms:"))
25
- return normalizeIMessageHandle(trimmed.slice(4));
26
- if (lowered.startsWith("auto:"))
27
- return normalizeIMessageHandle(trimmed.slice(5));
28
- if (trimmed.includes("@"))
29
- return trimmed.toLowerCase();
30
- const normalized = normalizeE164(trimmed);
31
- if (normalized)
32
- return normalized;
33
- return trimmed.replace(/\s+/g, "");
34
- }
35
- export function parseIMessageTarget(raw) {
36
- const trimmed = raw.trim();
37
- if (!trimmed)
38
- throw new Error("iMessage target is required");
39
- const lower = trimmed.toLowerCase();
40
- for (const { prefix, service } of SERVICE_PREFIXES) {
41
- if (lower.startsWith(prefix)) {
42
- const remainder = stripPrefix(trimmed, prefix);
43
- if (!remainder)
44
- throw new Error(`${prefix} target is required`);
45
- const remainderLower = remainder.toLowerCase();
46
- const isChatTarget = CHAT_ID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
47
- CHAT_GUID_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
48
- CHAT_IDENTIFIER_PREFIXES.some((p) => remainderLower.startsWith(p)) ||
49
- remainderLower.startsWith("group:");
50
- if (isChatTarget) {
51
- return parseIMessageTarget(remainder);
52
- }
53
- return { kind: "handle", to: remainder, service };
54
- }
55
- }
56
- for (const prefix of CHAT_ID_PREFIXES) {
57
- if (lower.startsWith(prefix)) {
58
- const value = stripPrefix(trimmed, prefix);
59
- const chatId = Number.parseInt(value, 10);
60
- if (!Number.isFinite(chatId)) {
61
- throw new Error(`Invalid chat_id: ${value}`);
62
- }
63
- return { kind: "chat_id", chatId };
64
- }
65
- }
66
- for (const prefix of CHAT_GUID_PREFIXES) {
67
- if (lower.startsWith(prefix)) {
68
- const value = stripPrefix(trimmed, prefix);
69
- if (!value)
70
- throw new Error("chat_guid is required");
71
- return { kind: "chat_guid", chatGuid: value };
72
- }
73
- }
74
- for (const prefix of CHAT_IDENTIFIER_PREFIXES) {
75
- if (lower.startsWith(prefix)) {
76
- const value = stripPrefix(trimmed, prefix);
77
- if (!value)
78
- throw new Error("chat_identifier is required");
79
- return { kind: "chat_identifier", chatIdentifier: value };
80
- }
81
- }
82
- if (lower.startsWith("group:")) {
83
- const value = stripPrefix(trimmed, "group:");
84
- const chatId = Number.parseInt(value, 10);
85
- if (Number.isFinite(chatId)) {
86
- return { kind: "chat_id", chatId };
87
- }
88
- if (!value)
89
- throw new Error("group target is required");
90
- return { kind: "chat_guid", chatGuid: value };
91
- }
92
- return { kind: "handle", to: trimmed, service: "auto" };
93
- }
94
- export function parseIMessageAllowTarget(raw) {
95
- const trimmed = raw.trim();
96
- if (!trimmed)
97
- return { kind: "handle", handle: "" };
98
- const lower = trimmed.toLowerCase();
99
- for (const { prefix } of SERVICE_PREFIXES) {
100
- if (lower.startsWith(prefix)) {
101
- const remainder = stripPrefix(trimmed, prefix);
102
- if (!remainder)
103
- return { kind: "handle", handle: "" };
104
- return parseIMessageAllowTarget(remainder);
105
- }
106
- }
107
- for (const prefix of CHAT_ID_PREFIXES) {
108
- if (lower.startsWith(prefix)) {
109
- const value = stripPrefix(trimmed, prefix);
110
- const chatId = Number.parseInt(value, 10);
111
- if (Number.isFinite(chatId))
112
- return { kind: "chat_id", chatId };
113
- }
114
- }
115
- for (const prefix of CHAT_GUID_PREFIXES) {
116
- if (lower.startsWith(prefix)) {
117
- const value = stripPrefix(trimmed, prefix);
118
- if (value)
119
- return { kind: "chat_guid", chatGuid: value };
120
- }
121
- }
122
- for (const prefix of CHAT_IDENTIFIER_PREFIXES) {
123
- if (lower.startsWith(prefix)) {
124
- const value = stripPrefix(trimmed, prefix);
125
- if (value)
126
- return { kind: "chat_identifier", chatIdentifier: value };
127
- }
128
- }
129
- if (lower.startsWith("group:")) {
130
- const value = stripPrefix(trimmed, "group:");
131
- const chatId = Number.parseInt(value, 10);
132
- if (Number.isFinite(chatId))
133
- return { kind: "chat_id", chatId };
134
- if (value)
135
- return { kind: "chat_guid", chatGuid: value };
136
- }
137
- return { kind: "handle", handle: normalizeIMessageHandle(trimmed) };
138
- }
139
- export function isAllowedIMessageSender(params) {
140
- const allowFrom = params.allowFrom.map((entry) => String(entry).trim());
141
- if (allowFrom.length === 0)
142
- return true;
143
- if (allowFrom.includes("*"))
144
- return true;
145
- const senderNormalized = normalizeIMessageHandle(params.sender);
146
- const chatId = params.chatId ?? undefined;
147
- const chatGuid = params.chatGuid?.trim();
148
- const chatIdentifier = params.chatIdentifier?.trim();
149
- for (const entry of allowFrom) {
150
- if (!entry)
151
- continue;
152
- const parsed = parseIMessageAllowTarget(entry);
153
- if (parsed.kind === "chat_id" && chatId !== undefined) {
154
- if (parsed.chatId === chatId)
155
- return true;
156
- }
157
- else if (parsed.kind === "chat_guid" && chatGuid) {
158
- if (parsed.chatGuid === chatGuid)
159
- return true;
160
- }
161
- else if (parsed.kind === "chat_identifier" && chatIdentifier) {
162
- if (parsed.chatIdentifier === chatIdentifier)
163
- return true;
164
- }
165
- else if (parsed.kind === "handle" && senderNormalized) {
166
- if (parsed.handle === senderNormalized)
167
- return true;
168
- }
169
- }
170
- return false;
171
- }
172
- export function formatIMessageChatTarget(chatId) {
173
- if (!chatId || !Number.isFinite(chatId))
174
- return "";
175
- return `chat_id:${chatId}`;
176
- }
@@ -1,68 +0,0 @@
1
- export function normalizeSendPolicy(raw) {
2
- const value = raw?.trim().toLowerCase();
3
- if (value === "allow")
4
- return "allow";
5
- if (value === "deny")
6
- return "deny";
7
- return undefined;
8
- }
9
- function normalizeMatchValue(raw) {
10
- const value = raw?.trim().toLowerCase();
11
- return value ? value : undefined;
12
- }
13
- function deriveSurfaceFromKey(key) {
14
- if (!key)
15
- return undefined;
16
- const parts = key.split(":").filter(Boolean);
17
- if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
18
- return normalizeMatchValue(parts[0]);
19
- }
20
- return undefined;
21
- }
22
- function deriveChatTypeFromKey(key) {
23
- if (!key)
24
- return undefined;
25
- if (key.startsWith("group:") || key.includes(":group:"))
26
- return "group";
27
- if (key.includes(":channel:"))
28
- return "room";
29
- return undefined;
30
- }
31
- export function resolveSendPolicy(params) {
32
- const override = normalizeSendPolicy(params.entry?.sendPolicy);
33
- if (override)
34
- return override;
35
- const policy = params.cfg.session?.sendPolicy;
36
- if (!policy)
37
- return "allow";
38
- const surface = normalizeMatchValue(params.surface) ??
39
- normalizeMatchValue(params.entry?.surface) ??
40
- normalizeMatchValue(params.entry?.lastChannel) ??
41
- deriveSurfaceFromKey(params.sessionKey);
42
- const chatType = normalizeMatchValue(params.chatType ?? params.entry?.chatType) ??
43
- normalizeMatchValue(deriveChatTypeFromKey(params.sessionKey));
44
- const sessionKey = params.sessionKey ?? "";
45
- let allowedMatch = false;
46
- for (const rule of policy.rules ?? []) {
47
- if (!rule)
48
- continue;
49
- const action = normalizeSendPolicy(rule.action) ?? "allow";
50
- const match = rule.match ?? {};
51
- const matchSurface = normalizeMatchValue(match.surface);
52
- const matchChatType = normalizeMatchValue(match.chatType);
53
- const matchPrefix = normalizeMatchValue(match.keyPrefix);
54
- if (matchSurface && matchSurface !== surface)
55
- continue;
56
- if (matchChatType && matchChatType !== chatType)
57
- continue;
58
- if (matchPrefix && !sessionKey.startsWith(matchPrefix))
59
- continue;
60
- if (action === "deny")
61
- return "deny";
62
- allowedMatch = true;
63
- }
64
- if (allowedMatch)
65
- return "allow";
66
- const fallback = normalizeSendPolicy(policy.default);
67
- return fallback ?? "allow";
68
- }