clawdbot 2026.1.4-1 → 2026.1.5-1

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 (116) hide show
  1. package/CHANGELOG.md +32 -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 +9 -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/signal/client.js +0 -134
  89. package/dist/signal/daemon.js +0 -69
  90. package/dist/signal/index.js +0 -3
  91. package/dist/signal/monitor.js +0 -336
  92. package/dist/signal/probe.js +0 -46
  93. package/dist/signal/send.js +0 -91
  94. package/dist/slack/actions.js +0 -97
  95. package/dist/slack/index.js +0 -5
  96. package/dist/slack/monitor.js +0 -1029
  97. package/dist/slack/probe.js +0 -47
  98. package/dist/slack/send.js +0 -131
  99. package/dist/slack/token.js +0 -10
  100. package/dist/tui/commands.js +0 -74
  101. package/dist/tui/components/assistant-message.js +0 -16
  102. package/dist/tui/components/chat-log.js +0 -92
  103. package/dist/tui/components/custom-editor.js +0 -53
  104. package/dist/tui/components/selectors.js +0 -8
  105. package/dist/tui/components/tool-execution.js +0 -111
  106. package/dist/tui/components/user-message.js +0 -17
  107. package/dist/tui/gateway-chat.js +0 -140
  108. package/dist/tui/layout.js +0 -41
  109. package/dist/tui/message-list.js +0 -57
  110. package/dist/tui/theme/theme.js +0 -80
  111. package/dist/tui/theme.js +0 -25
  112. package/dist/tui/tui.js +0 -708
  113. package/dist/wizard/clack-prompter.js +0 -56
  114. package/dist/wizard/onboarding.js +0 -452
  115. package/dist/wizard/prompts.js +0 -6
  116. package/dist/wizard/session.js +0 -203
@@ -1,69 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- export function classifySignalCliLogLine(line) {
3
- const trimmed = line.trim();
4
- if (!trimmed)
5
- return null;
6
- // signal-cli commonly writes all logs to stderr; treat severity explicitly.
7
- if (/\b(ERROR|WARN|WARNING)\b/.test(trimmed))
8
- return "error";
9
- // Some signal-cli failures are not tagged with WARN/ERROR but should still be surfaced loudly.
10
- if (/\b(FAILED|SEVERE|EXCEPTION)\b/i.test(trimmed))
11
- return "error";
12
- return "log";
13
- }
14
- function buildDaemonArgs(opts) {
15
- const args = [];
16
- if (opts.account) {
17
- args.push("-a", opts.account);
18
- }
19
- args.push("daemon");
20
- args.push("--http", `${opts.httpHost}:${opts.httpPort}`);
21
- args.push("--no-receive-stdout");
22
- if (opts.receiveMode) {
23
- args.push("--receive-mode", opts.receiveMode);
24
- }
25
- if (opts.ignoreAttachments)
26
- args.push("--ignore-attachments");
27
- if (opts.ignoreStories)
28
- args.push("--ignore-stories");
29
- if (opts.sendReadReceipts)
30
- args.push("--send-read-receipts");
31
- return args;
32
- }
33
- export function spawnSignalDaemon(opts) {
34
- const args = buildDaemonArgs(opts);
35
- const child = spawn(opts.cliPath, args, {
36
- stdio: ["ignore", "pipe", "pipe"],
37
- });
38
- const log = opts.runtime?.log ?? (() => { });
39
- const error = opts.runtime?.error ?? (() => { });
40
- child.stdout?.on("data", (data) => {
41
- for (const line of data.toString().split(/\r?\n/)) {
42
- const kind = classifySignalCliLogLine(line);
43
- if (kind === "log")
44
- log(`signal-cli: ${line.trim()}`);
45
- else if (kind === "error")
46
- error(`signal-cli: ${line.trim()}`);
47
- }
48
- });
49
- child.stderr?.on("data", (data) => {
50
- for (const line of data.toString().split(/\r?\n/)) {
51
- const kind = classifySignalCliLogLine(line);
52
- if (kind === "log")
53
- log(`signal-cli: ${line.trim()}`);
54
- else if (kind === "error")
55
- error(`signal-cli: ${line.trim()}`);
56
- }
57
- });
58
- child.on("error", (err) => {
59
- error(`signal-cli spawn error: ${String(err)}`);
60
- });
61
- return {
62
- pid: child.pid ?? undefined,
63
- stop: () => {
64
- if (!child.killed) {
65
- child.kill("SIGTERM");
66
- }
67
- },
68
- };
69
- }
@@ -1,3 +0,0 @@
1
- export { monitorSignalProvider } from "./monitor.js";
2
- export { probeSignal } from "./probe.js";
3
- export { sendMessageSignal } from "./send.js";
@@ -1,336 +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 { saveMediaBuffer } from "../media/store.js";
9
- import { normalizeE164 } from "../utils.js";
10
- import { signalCheck, signalRpcRequest, streamSignalEvents } from "./client.js";
11
- import { spawnSignalDaemon } from "./daemon.js";
12
- import { sendMessageSignal } from "./send.js";
13
- function resolveRuntime(opts) {
14
- return (opts.runtime ?? {
15
- log: console.log,
16
- error: console.error,
17
- exit: (code) => {
18
- throw new Error(`exit ${code}`);
19
- },
20
- });
21
- }
22
- function resolveBaseUrl(opts) {
23
- const cfg = loadConfig();
24
- const signalCfg = cfg.signal;
25
- if (opts.baseUrl?.trim())
26
- return opts.baseUrl.trim();
27
- if (signalCfg?.httpUrl?.trim())
28
- return signalCfg.httpUrl.trim();
29
- const host = opts.httpHost ?? signalCfg?.httpHost ?? "127.0.0.1";
30
- const port = opts.httpPort ?? signalCfg?.httpPort ?? 8080;
31
- return `http://${host}:${port}`;
32
- }
33
- function resolveAccount(opts) {
34
- const cfg = loadConfig();
35
- return opts.account?.trim() || cfg.signal?.account?.trim() || undefined;
36
- }
37
- function resolveAllowFrom(opts) {
38
- const cfg = loadConfig();
39
- const raw = opts.allowFrom ?? cfg.signal?.allowFrom ?? [];
40
- return raw.map((entry) => String(entry).trim()).filter(Boolean);
41
- }
42
- function isAllowedSender(sender, allowFrom) {
43
- if (allowFrom.length === 0)
44
- return true;
45
- if (allowFrom.includes("*"))
46
- return true;
47
- const normalizedAllow = allowFrom
48
- .map((entry) => entry.replace(/^signal:/i, ""))
49
- .map((entry) => normalizeE164(entry));
50
- const normalizedSender = normalizeE164(sender);
51
- return normalizedAllow.includes(normalizedSender);
52
- }
53
- async function waitForSignalDaemonReady(params) {
54
- const started = Date.now();
55
- let lastError = null;
56
- while (Date.now() - started < params.timeoutMs) {
57
- if (params.abortSignal?.aborted)
58
- return;
59
- const res = await signalCheck(params.baseUrl, 1000);
60
- if (res.ok)
61
- return;
62
- lastError =
63
- res.error ?? (res.status ? `HTTP ${res.status}` : "unreachable");
64
- await new Promise((r) => setTimeout(r, 150));
65
- }
66
- params.runtime.error?.(danger(`daemon not ready after ${params.timeoutMs}ms (${lastError ?? "unknown error"})`));
67
- throw new Error(`signal daemon not ready (${lastError ?? "unknown error"})`);
68
- }
69
- async function fetchAttachment(params) {
70
- const { attachment } = params;
71
- if (!attachment?.id)
72
- return null;
73
- if (attachment.size && attachment.size > params.maxBytes) {
74
- throw new Error(`Signal attachment ${attachment.id} exceeds ${(params.maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
75
- }
76
- const rpcParams = {
77
- id: attachment.id,
78
- };
79
- if (params.account)
80
- rpcParams.account = params.account;
81
- if (params.groupId)
82
- rpcParams.groupId = params.groupId;
83
- else if (params.sender)
84
- rpcParams.recipient = params.sender;
85
- else
86
- return null;
87
- const result = await signalRpcRequest("getAttachment", rpcParams, { baseUrl: params.baseUrl });
88
- if (!result?.data)
89
- return null;
90
- const buffer = Buffer.from(result.data, "base64");
91
- const saved = await saveMediaBuffer(buffer, attachment.contentType ?? undefined, "inbound", params.maxBytes);
92
- return { path: saved.path, contentType: saved.contentType };
93
- }
94
- async function deliverReplies(params) {
95
- const { replies, target, baseUrl, account, runtime, maxBytes, textLimit } = params;
96
- for (const payload of replies) {
97
- const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
98
- const text = payload.text ?? "";
99
- if (!text && mediaList.length === 0)
100
- continue;
101
- if (mediaList.length === 0) {
102
- for (const chunk of chunkText(text, textLimit)) {
103
- await sendMessageSignal(target, chunk, {
104
- baseUrl,
105
- account,
106
- maxBytes,
107
- });
108
- }
109
- }
110
- else {
111
- let first = true;
112
- for (const url of mediaList) {
113
- const caption = first ? text : "";
114
- first = false;
115
- await sendMessageSignal(target, caption, {
116
- baseUrl,
117
- account,
118
- mediaUrl: url,
119
- maxBytes,
120
- });
121
- }
122
- }
123
- runtime.log?.(`delivered reply to ${target}`);
124
- }
125
- }
126
- export async function monitorSignalProvider(opts = {}) {
127
- const runtime = resolveRuntime(opts);
128
- const cfg = loadConfig();
129
- const textLimit = resolveTextChunkLimit(cfg, "signal");
130
- const baseUrl = resolveBaseUrl(opts);
131
- const account = resolveAccount(opts);
132
- const allowFrom = resolveAllowFrom(opts);
133
- const mediaMaxBytes = (opts.mediaMaxMb ?? cfg.signal?.mediaMaxMb ?? 8) * 1024 * 1024;
134
- const ignoreAttachments = opts.ignoreAttachments ?? cfg.signal?.ignoreAttachments ?? false;
135
- const autoStart = opts.autoStart ?? cfg.signal?.autoStart ?? !cfg.signal?.httpUrl;
136
- let daemonHandle = null;
137
- if (autoStart) {
138
- const cliPath = opts.cliPath ?? cfg.signal?.cliPath ?? "signal-cli";
139
- const httpHost = opts.httpHost ?? cfg.signal?.httpHost ?? "127.0.0.1";
140
- const httpPort = opts.httpPort ?? cfg.signal?.httpPort ?? 8080;
141
- daemonHandle = spawnSignalDaemon({
142
- cliPath,
143
- account,
144
- httpHost,
145
- httpPort,
146
- receiveMode: opts.receiveMode ?? cfg.signal?.receiveMode,
147
- ignoreAttachments: opts.ignoreAttachments ?? cfg.signal?.ignoreAttachments,
148
- ignoreStories: opts.ignoreStories ?? cfg.signal?.ignoreStories,
149
- sendReadReceipts: opts.sendReadReceipts ?? cfg.signal?.sendReadReceipts,
150
- runtime,
151
- });
152
- }
153
- const onAbort = () => {
154
- daemonHandle?.stop();
155
- };
156
- opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
157
- try {
158
- if (daemonHandle) {
159
- await waitForSignalDaemonReady({
160
- baseUrl,
161
- abortSignal: opts.abortSignal,
162
- timeoutMs: 10_000,
163
- runtime,
164
- });
165
- }
166
- const handleEvent = async (event) => {
167
- if (event.event !== "receive" || !event.data)
168
- return;
169
- let payload = null;
170
- try {
171
- payload = JSON.parse(event.data);
172
- }
173
- catch (err) {
174
- runtime.error?.(`failed to parse event: ${String(err)}`);
175
- return;
176
- }
177
- if (payload?.exception?.message) {
178
- runtime.error?.(`receive exception: ${payload.exception.message}`);
179
- }
180
- const envelope = payload?.envelope;
181
- if (!envelope)
182
- return;
183
- if (envelope.syncMessage)
184
- return;
185
- const dataMessage = envelope.dataMessage ?? envelope.editMessage?.dataMessage;
186
- if (!dataMessage)
187
- return;
188
- const sender = envelope.sourceNumber?.trim();
189
- if (!sender)
190
- return;
191
- if (account && normalizeE164(sender) === normalizeE164(account)) {
192
- return;
193
- }
194
- if (!isAllowedSender(sender, allowFrom)) {
195
- logVerbose(`Blocked signal sender ${sender} (not in allowFrom)`);
196
- return;
197
- }
198
- const groupId = dataMessage.groupInfo?.groupId ?? undefined;
199
- const groupName = dataMessage.groupInfo?.groupName ?? undefined;
200
- const isGroup = Boolean(groupId);
201
- const messageText = (dataMessage.message ?? "").trim();
202
- let mediaPath;
203
- let mediaType;
204
- let placeholder = "";
205
- const firstAttachment = dataMessage.attachments?.[0];
206
- if (firstAttachment?.id && !ignoreAttachments) {
207
- try {
208
- const fetched = await fetchAttachment({
209
- baseUrl,
210
- account,
211
- attachment: firstAttachment,
212
- sender,
213
- groupId,
214
- maxBytes: mediaMaxBytes,
215
- });
216
- if (fetched) {
217
- mediaPath = fetched.path;
218
- mediaType =
219
- fetched.contentType ?? firstAttachment.contentType ?? undefined;
220
- }
221
- }
222
- catch (err) {
223
- runtime.error?.(danger(`attachment fetch failed: ${String(err)}`));
224
- }
225
- }
226
- const kind = mediaKindFromMime(mediaType ?? undefined);
227
- if (kind) {
228
- placeholder = `<media:${kind}>`;
229
- }
230
- else if (dataMessage.attachments?.length) {
231
- placeholder = "<media:attachment>";
232
- }
233
- const bodyText = messageText || placeholder || dataMessage.quote?.text?.trim() || "";
234
- if (!bodyText)
235
- return;
236
- const fromLabel = isGroup
237
- ? `${groupName ?? "Signal Group"} id:${groupId}`
238
- : `${envelope.sourceName ?? sender} id:${sender}`;
239
- const body = formatAgentEnvelope({
240
- surface: "Signal",
241
- from: fromLabel,
242
- timestamp: envelope.timestamp ?? undefined,
243
- body: bodyText,
244
- });
245
- const ctxPayload = {
246
- Body: body,
247
- From: isGroup ? `group:${groupId}` : `signal:${sender}`,
248
- To: isGroup ? `group:${groupId}` : `signal:${sender}`,
249
- ChatType: isGroup ? "group" : "direct",
250
- GroupSubject: isGroup ? (groupName ?? undefined) : undefined,
251
- SenderName: envelope.sourceName ?? sender,
252
- Surface: "signal",
253
- MessageSid: envelope.timestamp ? String(envelope.timestamp) : undefined,
254
- Timestamp: envelope.timestamp ?? undefined,
255
- MediaPath: mediaPath,
256
- MediaType: mediaType,
257
- MediaUrl: mediaPath,
258
- };
259
- if (!isGroup) {
260
- const sessionCfg = cfg.session;
261
- const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
262
- const storePath = resolveStorePath(sessionCfg?.store);
263
- await updateLastRoute({
264
- storePath,
265
- sessionKey: mainKey,
266
- channel: "signal",
267
- to: normalizeE164(sender),
268
- });
269
- }
270
- if (shouldLogVerbose()) {
271
- const preview = body.slice(0, 200).replace(/\n/g, "\\n");
272
- logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`);
273
- }
274
- let blockSendChain = Promise.resolve();
275
- const sendBlockReply = (payload) => {
276
- if (!payload?.text &&
277
- !payload?.mediaUrl &&
278
- !(payload?.mediaUrls?.length ?? 0)) {
279
- return;
280
- }
281
- blockSendChain = blockSendChain
282
- .then(async () => {
283
- await deliverReplies({
284
- replies: [payload],
285
- target: ctxPayload.To,
286
- baseUrl,
287
- account,
288
- runtime,
289
- maxBytes: mediaMaxBytes,
290
- textLimit,
291
- });
292
- })
293
- .catch((err) => {
294
- runtime.error?.(danger(`signal block reply failed: ${String(err)}`));
295
- });
296
- };
297
- const replyResult = await getReplyFromConfig(ctxPayload, { onBlockReply: sendBlockReply }, cfg);
298
- const replies = replyResult
299
- ? Array.isArray(replyResult)
300
- ? replyResult
301
- : [replyResult]
302
- : [];
303
- await blockSendChain;
304
- if (replies.length === 0)
305
- return;
306
- await deliverReplies({
307
- replies,
308
- target: ctxPayload.To,
309
- baseUrl,
310
- account,
311
- runtime,
312
- maxBytes: mediaMaxBytes,
313
- textLimit,
314
- });
315
- };
316
- await streamSignalEvents({
317
- baseUrl,
318
- account,
319
- abortSignal: opts.abortSignal,
320
- onEvent: (event) => {
321
- void handleEvent(event).catch((err) => {
322
- runtime.error?.(`event handler failed: ${String(err)}`);
323
- });
324
- },
325
- });
326
- }
327
- catch (err) {
328
- if (opts.abortSignal?.aborted)
329
- return;
330
- throw err;
331
- }
332
- finally {
333
- opts.abortSignal?.removeEventListener("abort", onAbort);
334
- daemonHandle?.stop();
335
- }
336
- }
@@ -1,46 +0,0 @@
1
- import { signalCheck, signalRpcRequest } from "./client.js";
2
- function parseSignalVersion(value) {
3
- if (typeof value === "string" && value.trim())
4
- return value.trim();
5
- if (typeof value === "object" && value !== null) {
6
- const version = value.version;
7
- if (typeof version === "string" && version.trim())
8
- return version.trim();
9
- }
10
- return null;
11
- }
12
- export async function probeSignal(baseUrl, timeoutMs) {
13
- const started = Date.now();
14
- const result = {
15
- ok: false,
16
- status: null,
17
- error: null,
18
- elapsedMs: 0,
19
- version: null,
20
- };
21
- const check = await signalCheck(baseUrl, timeoutMs);
22
- if (!check.ok) {
23
- return {
24
- ...result,
25
- status: check.status ?? null,
26
- error: check.error ?? "unreachable",
27
- elapsedMs: Date.now() - started,
28
- };
29
- }
30
- try {
31
- const version = await signalRpcRequest("version", undefined, {
32
- baseUrl,
33
- timeoutMs,
34
- });
35
- result.version = parseSignalVersion(version);
36
- }
37
- catch (err) {
38
- result.error = err instanceof Error ? err.message : String(err);
39
- }
40
- return {
41
- ...result,
42
- ok: true,
43
- status: check.status ?? null,
44
- elapsedMs: Date.now() - started,
45
- };
46
- }
@@ -1,91 +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 { signalRpcRequest } from "./client.js";
6
- function resolveBaseUrl(explicit) {
7
- const cfg = loadConfig();
8
- const signalCfg = cfg.signal;
9
- if (explicit?.trim())
10
- return explicit.trim();
11
- if (signalCfg?.httpUrl?.trim())
12
- return signalCfg.httpUrl.trim();
13
- const host = signalCfg?.httpHost?.trim() || "127.0.0.1";
14
- const port = signalCfg?.httpPort ?? 8080;
15
- return `http://${host}:${port}`;
16
- }
17
- function resolveAccount(explicit) {
18
- const cfg = loadConfig();
19
- const signalCfg = cfg.signal;
20
- const account = explicit?.trim() || signalCfg?.account?.trim();
21
- return account || undefined;
22
- }
23
- function parseTarget(raw) {
24
- let value = raw.trim();
25
- if (!value)
26
- throw new Error("Signal recipient is required");
27
- const lower = value.toLowerCase();
28
- if (lower.startsWith("signal:")) {
29
- value = value.slice("signal:".length).trim();
30
- }
31
- const normalized = value.toLowerCase();
32
- if (normalized.startsWith("group:")) {
33
- return { type: "group", groupId: value.slice("group:".length).trim() };
34
- }
35
- if (normalized.startsWith("username:")) {
36
- return {
37
- type: "username",
38
- username: value.slice("username:".length).trim(),
39
- };
40
- }
41
- if (normalized.startsWith("u:")) {
42
- return { type: "username", username: value.trim() };
43
- }
44
- return { type: "recipient", recipient: value };
45
- }
46
- async function resolveAttachment(mediaUrl, maxBytes) {
47
- const media = await loadWebMedia(mediaUrl, maxBytes);
48
- const saved = await saveMediaBuffer(media.buffer, media.contentType ?? undefined, "outbound", maxBytes);
49
- return { path: saved.path, contentType: saved.contentType };
50
- }
51
- export async function sendMessageSignal(to, text, opts = {}) {
52
- const baseUrl = resolveBaseUrl(opts.baseUrl);
53
- const account = resolveAccount(opts.account);
54
- const target = parseTarget(to);
55
- let message = text ?? "";
56
- const maxBytes = opts.maxBytes ?? 8 * 1024 * 1024;
57
- let attachments;
58
- if (opts.mediaUrl?.trim()) {
59
- const resolved = await resolveAttachment(opts.mediaUrl.trim(), maxBytes);
60
- attachments = [resolved.path];
61
- const kind = mediaKindFromMime(resolved.contentType ?? undefined);
62
- if (!message && kind) {
63
- // Avoid sending an empty body when only attachments exist.
64
- message = kind === "image" ? "<media:image>" : `<media:${kind}>`;
65
- }
66
- }
67
- if (!message.trim() && (!attachments || attachments.length === 0)) {
68
- throw new Error("Signal send requires text or media");
69
- }
70
- const params = { message };
71
- if (account)
72
- params.account = account;
73
- if (attachments && attachments.length > 0) {
74
- params.attachments = attachments;
75
- }
76
- if (target.type === "recipient") {
77
- params.recipient = [target.recipient];
78
- }
79
- else if (target.type === "group") {
80
- params.groupId = target.groupId;
81
- }
82
- else if (target.type === "username") {
83
- params.username = [target.username];
84
- }
85
- const result = await signalRpcRequest("send", params, { baseUrl, timeoutMs: opts.timeoutMs });
86
- const timestamp = result?.timestamp;
87
- return {
88
- messageId: timestamp ? String(timestamp) : "unknown",
89
- timestamp,
90
- };
91
- }
@@ -1,97 +0,0 @@
1
- import { WebClient } from "@slack/web-api";
2
- import { loadConfig } from "../config/config.js";
3
- import { sendMessageSlack } from "./send.js";
4
- import { resolveSlackBotToken } from "./token.js";
5
- function resolveToken(explicit) {
6
- const cfgToken = loadConfig().slack?.botToken;
7
- const token = resolveSlackBotToken(explicit ?? process.env.SLACK_BOT_TOKEN ?? cfgToken ?? undefined);
8
- if (!token) {
9
- throw new Error("SLACK_BOT_TOKEN or slack.botToken is required for Slack actions");
10
- }
11
- return token;
12
- }
13
- function normalizeEmoji(raw) {
14
- const trimmed = raw.trim();
15
- if (!trimmed) {
16
- throw new Error("Emoji is required for Slack reactions");
17
- }
18
- return trimmed.replace(/^:+|:+$/g, "");
19
- }
20
- async function getClient(opts = {}) {
21
- const token = resolveToken(opts.token);
22
- return opts.client ?? new WebClient(token);
23
- }
24
- export async function reactSlackMessage(channelId, messageId, emoji, opts = {}) {
25
- const client = await getClient(opts);
26
- await client.reactions.add({
27
- channel: channelId,
28
- timestamp: messageId,
29
- name: normalizeEmoji(emoji),
30
- });
31
- }
32
- export async function listSlackReactions(channelId, messageId, opts = {}) {
33
- const client = await getClient(opts);
34
- const result = await client.reactions.get({
35
- channel: channelId,
36
- timestamp: messageId,
37
- full: true,
38
- });
39
- const message = result.message;
40
- return message?.reactions ?? [];
41
- }
42
- export async function sendSlackMessage(to, content, opts = {}) {
43
- return await sendMessageSlack(to, content, {
44
- token: opts.token,
45
- mediaUrl: opts.mediaUrl,
46
- client: opts.client,
47
- });
48
- }
49
- export async function editSlackMessage(channelId, messageId, content, opts = {}) {
50
- const client = await getClient(opts);
51
- await client.chat.update({
52
- channel: channelId,
53
- ts: messageId,
54
- text: content,
55
- });
56
- }
57
- export async function deleteSlackMessage(channelId, messageId, opts = {}) {
58
- const client = await getClient(opts);
59
- await client.chat.delete({
60
- channel: channelId,
61
- ts: messageId,
62
- });
63
- }
64
- export async function readSlackMessages(channelId, opts = {}) {
65
- const client = await getClient(opts);
66
- const result = await client.conversations.history({
67
- channel: channelId,
68
- limit: opts.limit,
69
- latest: opts.before,
70
- oldest: opts.after,
71
- });
72
- return {
73
- messages: (result.messages ?? []),
74
- hasMore: Boolean(result.has_more),
75
- };
76
- }
77
- export async function getSlackMemberInfo(userId, opts = {}) {
78
- const client = await getClient(opts);
79
- return await client.users.info({ user: userId });
80
- }
81
- export async function listSlackEmojis(opts = {}) {
82
- const client = await getClient(opts);
83
- return await client.emoji.list();
84
- }
85
- export async function pinSlackMessage(channelId, messageId, opts = {}) {
86
- const client = await getClient(opts);
87
- await client.pins.add({ channel: channelId, timestamp: messageId });
88
- }
89
- export async function unpinSlackMessage(channelId, messageId, opts = {}) {
90
- const client = await getClient(opts);
91
- await client.pins.remove({ channel: channelId, timestamp: messageId });
92
- }
93
- export async function listSlackPins(channelId, opts = {}) {
94
- const client = await getClient(opts);
95
- const result = await client.pins.list({ channel: channelId });
96
- return (result.items ?? []);
97
- }
@@ -1,5 +0,0 @@
1
- export { deleteSlackMessage, editSlackMessage, getSlackMemberInfo, listSlackEmojis, listSlackPins, listSlackReactions, pinSlackMessage, reactSlackMessage, readSlackMessages, sendSlackMessage, unpinSlackMessage, } from "./actions.js";
2
- export { monitorSlackProvider } from "./monitor.js";
3
- export { probeSlack } from "./probe.js";
4
- export { sendMessageSlack } from "./send.js";
5
- export { resolveSlackAppToken, resolveSlackBotToken } from "./token.js";