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,134 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- const DEFAULT_TIMEOUT_MS = 10_000;
3
- function normalizeBaseUrl(url) {
4
- const trimmed = url.trim();
5
- if (!trimmed) {
6
- throw new Error("Signal base URL is required");
7
- }
8
- if (/^https?:\/\//i.test(trimmed))
9
- return trimmed.replace(/\/+$/, "");
10
- return `http://${trimmed}`.replace(/\/+$/, "");
11
- }
12
- async function fetchWithTimeout(url, init, timeoutMs) {
13
- const controller = new AbortController();
14
- const timer = setTimeout(() => controller.abort(), timeoutMs);
15
- try {
16
- return await fetch(url, { ...init, signal: controller.signal });
17
- }
18
- finally {
19
- clearTimeout(timer);
20
- }
21
- }
22
- export async function signalRpcRequest(method, params, opts) {
23
- const baseUrl = normalizeBaseUrl(opts.baseUrl);
24
- const id = randomUUID();
25
- const body = JSON.stringify({
26
- jsonrpc: "2.0",
27
- method,
28
- params,
29
- id,
30
- });
31
- const res = await fetchWithTimeout(`${baseUrl}/api/v1/rpc`, {
32
- method: "POST",
33
- headers: { "Content-Type": "application/json" },
34
- body,
35
- }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
36
- if (res.status === 201) {
37
- return undefined;
38
- }
39
- const text = await res.text();
40
- if (!text) {
41
- throw new Error(`Signal RPC empty response (status ${res.status})`);
42
- }
43
- const parsed = JSON.parse(text);
44
- if (parsed.error) {
45
- const code = parsed.error.code ?? "unknown";
46
- const msg = parsed.error.message ?? "Signal RPC error";
47
- throw new Error(`Signal RPC ${code}: ${msg}`);
48
- }
49
- return parsed.result;
50
- }
51
- export async function signalCheck(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
52
- const normalized = normalizeBaseUrl(baseUrl);
53
- try {
54
- const res = await fetchWithTimeout(`${normalized}/api/v1/check`, { method: "GET" }, timeoutMs);
55
- if (!res.ok) {
56
- return { ok: false, status: res.status, error: `HTTP ${res.status}` };
57
- }
58
- return { ok: true, status: res.status, error: null };
59
- }
60
- catch (err) {
61
- return {
62
- ok: false,
63
- status: null,
64
- error: err instanceof Error ? err.message : String(err),
65
- };
66
- }
67
- }
68
- export async function streamSignalEvents(params) {
69
- const baseUrl = normalizeBaseUrl(params.baseUrl);
70
- const url = new URL(`${baseUrl}/api/v1/events`);
71
- if (params.account)
72
- url.searchParams.set("account", params.account);
73
- const res = await fetch(url, {
74
- method: "GET",
75
- headers: { Accept: "text/event-stream" },
76
- signal: params.abortSignal,
77
- });
78
- if (!res.ok || !res.body) {
79
- throw new Error(`Signal SSE failed (${res.status} ${res.statusText || "error"})`);
80
- }
81
- const reader = res.body.getReader();
82
- const decoder = new TextDecoder();
83
- let buffer = "";
84
- let currentEvent = {};
85
- const flushEvent = () => {
86
- if (!currentEvent.data && !currentEvent.event && !currentEvent.id)
87
- return;
88
- params.onEvent({
89
- event: currentEvent.event,
90
- data: currentEvent.data,
91
- id: currentEvent.id,
92
- });
93
- currentEvent = {};
94
- };
95
- while (true) {
96
- const { value, done } = await reader.read();
97
- if (done)
98
- break;
99
- buffer += decoder.decode(value, { stream: true });
100
- let lineEnd = buffer.indexOf("\n");
101
- while (lineEnd !== -1) {
102
- let line = buffer.slice(0, lineEnd);
103
- buffer = buffer.slice(lineEnd + 1);
104
- if (line.endsWith("\r"))
105
- line = line.slice(0, -1);
106
- if (line === "") {
107
- flushEvent();
108
- lineEnd = buffer.indexOf("\n");
109
- continue;
110
- }
111
- if (line.startsWith(":")) {
112
- lineEnd = buffer.indexOf("\n");
113
- continue;
114
- }
115
- const [rawField, ...rest] = line.split(":");
116
- const field = rawField.trim();
117
- const rawValue = rest.join(":");
118
- const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
119
- if (field === "event") {
120
- currentEvent.event = value;
121
- }
122
- else if (field === "data") {
123
- currentEvent.data = currentEvent.data
124
- ? `${currentEvent.data}\n${value}`
125
- : value;
126
- }
127
- else if (field === "id") {
128
- currentEvent.id = value;
129
- }
130
- lineEnd = buffer.indexOf("\n");
131
- }
132
- }
133
- flushEvent();
134
- }
@@ -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
- }