@openclaw/signal 0.0.0 → 2026.6.11

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 (49) hide show
  1. package/README.md +12 -2
  2. package/dist/accounts-hOCHbEhX.js +40 -0
  3. package/dist/api.js +16 -0
  4. package/dist/approval-handler.runtime-CeJI1wow.js +158 -0
  5. package/dist/approval-resolver-BR0MioAA.js +15 -0
  6. package/dist/channel-LmY2UpOt.js +619 -0
  7. package/dist/channel-config-api.js +2 -0
  8. package/dist/channel-entry.js +18 -0
  9. package/dist/channel-plugin-api.js +2 -0
  10. package/dist/channel.runtime-B0-YpkC4.js +65 -0
  11. package/dist/config-api-KS-qhQvD.js +2 -0
  12. package/dist/config-schema-BiojLEsX.js +27 -0
  13. package/dist/contract-api.js +3 -0
  14. package/dist/identity-B6O4k8xg.js +130 -0
  15. package/dist/index.js +18 -0
  16. package/dist/install-signal-cli-CXgTF3de.js +243 -0
  17. package/dist/message-actions-Bue0g2Kc.js +548 -0
  18. package/dist/monitor-CUhIKHJo.js +1404 -0
  19. package/dist/probe-BQ_Izoya.js +48 -0
  20. package/dist/reaction-runtime-api-C_PQ45D9.js +1074 -0
  21. package/dist/reaction-runtime-api.js +2 -0
  22. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  23. package/dist/runtime-api.js +18 -0
  24. package/dist/secret-contract-api.js +5 -0
  25. package/dist/send-CBlFUkY_.js +599 -0
  26. package/dist/send.runtime-CfaZd8X4.js +2 -0
  27. package/dist/setup-entry.js +11 -0
  28. package/node_modules/ws/LICENSE +20 -0
  29. package/node_modules/ws/README.md +548 -0
  30. package/node_modules/ws/browser.js +8 -0
  31. package/node_modules/ws/index.js +22 -0
  32. package/node_modules/ws/lib/buffer-util.js +131 -0
  33. package/node_modules/ws/lib/constants.js +19 -0
  34. package/node_modules/ws/lib/event-target.js +292 -0
  35. package/node_modules/ws/lib/extension.js +203 -0
  36. package/node_modules/ws/lib/limiter.js +55 -0
  37. package/node_modules/ws/lib/permessage-deflate.js +528 -0
  38. package/node_modules/ws/lib/receiver.js +760 -0
  39. package/node_modules/ws/lib/sender.js +607 -0
  40. package/node_modules/ws/lib/stream.js +161 -0
  41. package/node_modules/ws/lib/subprotocol.js +62 -0
  42. package/node_modules/ws/lib/validation.js +152 -0
  43. package/node_modules/ws/lib/websocket-server.js +562 -0
  44. package/node_modules/ws/lib/websocket.js +1407 -0
  45. package/node_modules/ws/package.json +70 -0
  46. package/node_modules/ws/wrapper.mjs +21 -0
  47. package/npm-shrinkwrap.json +36 -0
  48. package/openclaw.plugin.json +771 -0
  49. package/package.json +77 -7
@@ -0,0 +1,65 @@
1
+ import { i as resolveSignalAccount, n as listSignalAccountIds } from "./accounts-hOCHbEhX.js";
2
+ import { c as signalNumberTextInput, i as createSignalCliPathTextInput, o as signalCompletionNote, s as signalDmPolicy } from "./channel-LmY2UpOt.js";
3
+ import { r as installSignalCli } from "./install-signal-cli-CXgTF3de.js";
4
+ import { detectBinary } from "openclaw/plugin-sdk/setup-tools";
5
+ import { createDetectedBinaryStatus, createSetupTranslator, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
6
+ //#region extensions/signal/src/setup-surface.ts
7
+ const t = createSetupTranslator();
8
+ const channel = "signal";
9
+ //#endregion
10
+ //#region extensions/signal/src/channel.runtime.ts
11
+ const signalSetupWizard = {
12
+ channel,
13
+ status: createDetectedBinaryStatus({
14
+ channelLabel: "Signal",
15
+ binaryLabel: "signal-cli",
16
+ configuredLabel: t("wizard.channels.statusConfigured"),
17
+ unconfiguredLabel: t("wizard.channels.statusNeedsSetup"),
18
+ configuredHint: t("wizard.channels.statusSignalCliFound"),
19
+ unconfiguredHint: t("wizard.channels.statusSignalCliMissing"),
20
+ configuredScore: 1,
21
+ unconfiguredScore: 0,
22
+ resolveConfigured: ({ cfg, accountId }) => accountId ? resolveSignalAccount({
23
+ cfg,
24
+ accountId
25
+ }).configured : listSignalAccountIds(cfg).some((resolvedAccountId) => resolveSignalAccount({
26
+ cfg,
27
+ accountId: resolvedAccountId
28
+ }).configured),
29
+ resolveBinaryPath: ({ cfg, accountId }) => resolveSignalAccount({
30
+ cfg,
31
+ accountId
32
+ }).config.cliPath ?? "signal-cli",
33
+ detectBinary
34
+ }),
35
+ prepare: async ({ cfg, accountId, credentialValues, runtime, prompter, options }) => {
36
+ if (!options?.allowSignalInstall) return;
37
+ const cliDetected = await detectBinary((typeof credentialValues.cliPath === "string" ? credentialValues.cliPath : void 0) ?? resolveSignalAccount({
38
+ cfg,
39
+ accountId
40
+ }).config.cliPath ?? "signal-cli");
41
+ if (!await prompter.confirm({
42
+ message: cliDetected ? t("wizard.signal.reinstallPrompt") : t("wizard.signal.installPrompt"),
43
+ initialValue: !cliDetected
44
+ })) return;
45
+ try {
46
+ const result = await installSignalCli(runtime);
47
+ if (result.ok && result.cliPath) {
48
+ await prompter.note(`Installed signal-cli at ${result.cliPath}`, "Signal");
49
+ return { credentialValues: { cliPath: result.cliPath } };
50
+ }
51
+ if (!result.ok) await prompter.note(result.error ?? "signal-cli install failed.", "Signal");
52
+ } catch (error) {
53
+ await prompter.note(`signal-cli install failed: ${String(error)}`, "Signal");
54
+ }
55
+ },
56
+ credentials: [],
57
+ textInputs: [createSignalCliPathTextInput(async ({ currentValue }) => {
58
+ return !await detectBinary(currentValue ?? "signal-cli");
59
+ }), signalNumberTextInput],
60
+ completionNote: signalCompletionNote,
61
+ dmPolicy: signalDmPolicy,
62
+ disable: (cfg) => setSetupChannelEnabled(cfg, channel, false)
63
+ };
64
+ //#endregion
65
+ export { signalSetupWizard };
@@ -0,0 +1,2 @@
1
+ import { SignalConfigSchema, buildChannelConfigSchema } from "openclaw/plugin-sdk/bundled-channel-config-schema";
2
+ export { buildChannelConfigSchema as n, SignalConfigSchema as t };
@@ -0,0 +1,27 @@
1
+ import { n as buildChannelConfigSchema, t as SignalConfigSchema } from "./config-api-KS-qhQvD.js";
2
+ //#endregion
3
+ //#region extensions/signal/src/config-schema.ts
4
+ const SignalChannelConfigSchema = buildChannelConfigSchema(SignalConfigSchema, { uiHints: {
5
+ "": {
6
+ label: "Signal",
7
+ help: "Signal channel provider configuration including account identity and DM policy behavior. Keep account mapping explicit so routing remains stable across multi-device setups."
8
+ },
9
+ dmPolicy: {
10
+ label: "Signal DM Policy",
11
+ help: "Direct message access control (\"pairing\" recommended). \"open\" requires channels.signal.allowFrom=[\"*\"]."
12
+ },
13
+ configWrites: {
14
+ label: "Signal Config Writes",
15
+ help: "Allow Signal to write config in response to channel events/commands (default: true)."
16
+ },
17
+ account: {
18
+ label: "Signal Account",
19
+ help: "Signal account identifier (phone/number handle) used to bind this channel config to a specific Signal identity. Keep this aligned with your linked device/session state."
20
+ },
21
+ configPath: {
22
+ label: "Signal CLI Config Path",
23
+ help: "Optional directory passed to signal-cli via --config when the service needs a non-default signal-cli data path."
24
+ }
25
+ } });
26
+ //#endregion
27
+ export { SignalChannelConfigSchema as t };
@@ -0,0 +1,3 @@
1
+ import { d as normalizeSignalMessagingTarget, i as isSignalSenderAllowed, u as looksLikeSignalTargetId } from "./identity-B6O4k8xg.js";
2
+ import { a as looksLikeArchive, i as installSignalCliFromRelease, n as extractSignalCliArchive, o as pickAsset, r as installSignalCli, t as downloadToFile } from "./install-signal-cli-CXgTF3de.js";
3
+ export { downloadToFile, extractSignalCliArchive, installSignalCli, installSignalCliFromRelease, isSignalSenderAllowed, looksLikeArchive, looksLikeSignalTargetId, normalizeSignalMessagingTarget, pickAsset };
@@ -0,0 +1,130 @@
1
+ import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
2
+ import { normalizeE164 } from "openclaw/plugin-sdk/text-utility-runtime";
3
+ //#region extensions/signal/src/normalize.ts
4
+ function normalizeSignalMessagingTarget(raw) {
5
+ const trimmed = raw.trim();
6
+ if (!trimmed) return;
7
+ let normalized = trimmed;
8
+ if (normalizeLowercaseStringOrEmpty(normalized).startsWith("signal:")) normalized = normalized.slice(7).trim();
9
+ if (!normalized) return;
10
+ const lower = normalizeLowercaseStringOrEmpty(normalized);
11
+ if (lower.startsWith("group:")) {
12
+ const id = normalized.slice(6).trim();
13
+ return id ? `group:${id}` : void 0;
14
+ }
15
+ if (lower.startsWith("username:")) {
16
+ const id = normalized.slice(9).trim();
17
+ return id ? normalizeLowercaseStringOrEmpty(`username:${id}`) : void 0;
18
+ }
19
+ if (lower.startsWith("u:")) {
20
+ const id = normalized.slice(2).trim();
21
+ return id ? normalizeLowercaseStringOrEmpty(`username:${id}`) : void 0;
22
+ }
23
+ if (lower.startsWith("uuid:")) {
24
+ const id = normalized.slice(5).trim();
25
+ return id ? normalizeLowercaseStringOrEmpty(id) : void 0;
26
+ }
27
+ return normalizeLowercaseStringOrEmpty(normalized);
28
+ }
29
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
30
+ const UUID_COMPACT_PATTERN = /^[0-9a-f]{32}$/i;
31
+ function looksLikeSignalTargetId(raw, normalized) {
32
+ const candidates = normalizeStringEntries([raw, normalized ?? ""]);
33
+ for (const candidate of candidates) {
34
+ if (/^(signal:)?(group:|username:|u:)/i.test(candidate)) return true;
35
+ if (/^(signal:)?uuid:/i.test(candidate)) {
36
+ const stripped = candidate.replace(/^signal:/i, "").replace(/^uuid:/i, "").trim();
37
+ if (!stripped) continue;
38
+ if (UUID_PATTERN.test(stripped) || UUID_COMPACT_PATTERN.test(stripped)) return true;
39
+ continue;
40
+ }
41
+ const withoutSignalPrefix = candidate.replace(/^signal:/i, "").trim();
42
+ if (UUID_PATTERN.test(withoutSignalPrefix) || UUID_COMPACT_PATTERN.test(withoutSignalPrefix)) return true;
43
+ if (/^\+?\d{3,}$/.test(withoutSignalPrefix)) return true;
44
+ }
45
+ return false;
46
+ }
47
+ //#endregion
48
+ //#region extensions/signal/src/uuid.ts
49
+ const UUID_HYPHENATED_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
50
+ const UUID_COMPACT_RE = /^[0-9a-f]{32}$/i;
51
+ function looksLikeUuid(value) {
52
+ if (UUID_HYPHENATED_RE.test(value) || UUID_COMPACT_RE.test(value)) return true;
53
+ const compact = value.replace(/-/g, "");
54
+ if (!/^[0-9a-f]+$/i.test(compact)) return false;
55
+ return /[a-f]/i.test(compact);
56
+ }
57
+ //#endregion
58
+ //#region extensions/signal/src/identity.ts
59
+ function stripSignalPrefix(value) {
60
+ return value.replace(/^signal:/i, "").trim();
61
+ }
62
+ function resolveSignalSender(params) {
63
+ const sourceNumber = params.sourceNumber?.trim();
64
+ if (sourceNumber) return {
65
+ kind: "phone",
66
+ raw: sourceNumber,
67
+ e164: normalizeE164(sourceNumber)
68
+ };
69
+ const sourceUuid = params.sourceUuid?.trim();
70
+ if (sourceUuid) return {
71
+ kind: "uuid",
72
+ raw: sourceUuid
73
+ };
74
+ return null;
75
+ }
76
+ function formatSignalSenderId(sender) {
77
+ return sender.kind === "phone" ? sender.e164 : `uuid:${sender.raw}`;
78
+ }
79
+ function formatSignalSenderDisplay(sender) {
80
+ return sender.kind === "phone" ? sender.e164 : `uuid:${sender.raw}`;
81
+ }
82
+ function formatSignalPairingIdLine(sender) {
83
+ if (sender.kind === "phone") return `Your Signal number: ${sender.e164}`;
84
+ return `Your Signal sender id: ${formatSignalSenderId(sender)}`;
85
+ }
86
+ function resolveSignalRecipient(sender) {
87
+ return sender.kind === "phone" ? sender.e164 : sender.raw;
88
+ }
89
+ function resolveSignalPeerId(sender) {
90
+ return sender.kind === "phone" ? sender.e164 : `uuid:${sender.raw}`;
91
+ }
92
+ function parseSignalAllowEntry(entry) {
93
+ const trimmed = entry.trim();
94
+ if (!trimmed) return null;
95
+ if (trimmed === "*") return { kind: "any" };
96
+ const stripped = stripSignalPrefix(trimmed);
97
+ if (normalizeLowercaseStringOrEmpty(stripped).startsWith("uuid:")) {
98
+ const raw = stripped.slice(5).trim();
99
+ if (!raw) return null;
100
+ return {
101
+ kind: "uuid",
102
+ raw
103
+ };
104
+ }
105
+ if (looksLikeUuid(stripped)) return {
106
+ kind: "uuid",
107
+ raw: stripped
108
+ };
109
+ return {
110
+ kind: "phone",
111
+ e164: normalizeE164(stripped)
112
+ };
113
+ }
114
+ function normalizeSignalAllowRecipient(entry) {
115
+ const parsed = parseSignalAllowEntry(entry);
116
+ if (!parsed || parsed.kind === "any") return;
117
+ return parsed.kind === "phone" ? parsed.e164 : parsed.raw;
118
+ }
119
+ function isSignalSenderAllowed(sender, allowFrom) {
120
+ if (allowFrom.length === 0) return false;
121
+ const parsed = allowFrom.map(parseSignalAllowEntry).filter((entry) => entry !== null);
122
+ if (parsed.some((entry) => entry.kind === "any")) return true;
123
+ return parsed.some((entry) => {
124
+ if (entry.kind === "phone" && sender.kind === "phone") return entry.e164 === sender.e164;
125
+ if (entry.kind === "uuid" && sender.kind === "uuid") return entry.raw === sender.raw;
126
+ return false;
127
+ });
128
+ }
129
+ //#endregion
130
+ export { normalizeSignalAllowRecipient as a, resolveSignalSender as c, normalizeSignalMessagingTarget as d, isSignalSenderAllowed as i, looksLikeUuid as l, formatSignalSenderDisplay as n, resolveSignalPeerId as o, formatSignalSenderId as r, resolveSignalRecipient as s, formatSignalPairingIdLine as t, looksLikeSignalTargetId as u };
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
+ //#region extensions/signal/index.ts
3
+ var signal_default = defineBundledChannelEntry({
4
+ id: "signal",
5
+ name: "Signal",
6
+ description: "Signal channel plugin",
7
+ importMetaUrl: import.meta.url,
8
+ plugin: {
9
+ specifier: "./channel-plugin-api.js",
10
+ exportName: "signalPlugin"
11
+ },
12
+ runtime: {
13
+ specifier: "./runtime-api.js",
14
+ exportName: "setSignalRuntime"
15
+ }
16
+ });
17
+ //#endregion
18
+ export { signal_default as default };
@@ -0,0 +1,243 @@
1
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
5
+ import { CONFIG_DIR, extractArchive, resolveBrewExecutable } from "openclaw/plugin-sdk/setup-tools";
6
+ import { createWriteStream } from "node:fs";
7
+ import { Readable, Transform } from "node:stream";
8
+ import { pipeline } from "node:stream/promises";
9
+ import { runPluginCommandWithTimeout } from "openclaw/plugin-sdk/run-command";
10
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
11
+ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
12
+ //#region extensions/signal/src/install-signal-cli.ts
13
+ const MAX_SIGNAL_CLI_ARCHIVE_BYTES = 256 * 1024 * 1024;
14
+ const SIGNAL_CLI_DOWNLOAD_TIMEOUT_MS = 5 * 6e4;
15
+ const SIGNAL_CLI_RELEASE_INFO_TIMEOUT_MS = 3e4;
16
+ const CONTENT_LENGTH_RE = /^\d+$/;
17
+ /** @internal Exported for testing. */
18
+ async function extractSignalCliArchive(archivePath, installRoot, timeoutMs) {
19
+ await extractArchive({
20
+ archivePath,
21
+ destDir: installRoot,
22
+ timeoutMs
23
+ });
24
+ }
25
+ /** @internal Exported for testing. */
26
+ function looksLikeArchive(name) {
27
+ return name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".zip");
28
+ }
29
+ function isNodeReadableStream(value) {
30
+ return Boolean(value && typeof value.pipe === "function");
31
+ }
32
+ function chunkByteLength(chunk) {
33
+ if (typeof chunk === "string") return Buffer.byteLength(chunk);
34
+ if (chunk instanceof Uint8Array) return chunk.byteLength;
35
+ return Buffer.byteLength(String(chunk));
36
+ }
37
+ /**
38
+ * Pick a native release asset from the official GitHub releases.
39
+ *
40
+ * The official signal-cli releases only publish native (GraalVM) binaries for
41
+ * x86-64 Linux. On architectures where no native asset is available this
42
+ * returns `undefined` so the caller can fall back to a different install
43
+ * strategy (e.g. Homebrew).
44
+ */
45
+ /** @internal Exported for testing. */
46
+ function pickAsset(assets, platform, arch) {
47
+ const archives = assets.filter((asset) => Boolean(asset.name && asset.browser_download_url)).filter((a) => looksLikeArchive(normalizeLowercaseStringOrEmpty(a.name)));
48
+ const byName = (pattern) => archives.find((asset) => pattern.test(normalizeLowercaseStringOrEmpty(asset.name)));
49
+ if (platform === "linux") {
50
+ if (arch === "x64") return byName(/linux-native/) || byName(/linux/) || archives[0];
51
+ return;
52
+ }
53
+ if (platform === "darwin") return byName(/macos|osx|darwin/) || archives[0];
54
+ if (platform === "win32") return byName(/windows|win/) || archives[0];
55
+ return archives[0];
56
+ }
57
+ /** @internal Exported for testing. */
58
+ async function downloadToFile(url, dest, maxRedirects = 5, maxBytes = MAX_SIGNAL_CLI_ARCHIVE_BYTES) {
59
+ let completed = false;
60
+ const { response, release } = await fetchWithSsrFGuard({
61
+ url,
62
+ maxRedirects,
63
+ requireHttps: true,
64
+ timeoutMs: SIGNAL_CLI_DOWNLOAD_TIMEOUT_MS,
65
+ capture: false,
66
+ auditContext: "signal-cli-install-archive"
67
+ });
68
+ try {
69
+ if (!response.ok || !response.body) throw new Error(`HTTP ${response.status || "?"} downloading file`);
70
+ const rawLength = response.headers.get("content-length");
71
+ if (rawLength !== null) {
72
+ const trimmedLength = rawLength.trim();
73
+ const declaredLength = CONTENT_LENGTH_RE.test(trimmedLength) ? Number(trimmedLength) : NaN;
74
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) throw new Error(`signal-cli archive exceeds the ${maxBytes}-byte download cap (declared ${declaredLength}).`);
75
+ }
76
+ let totalBytes = 0;
77
+ const body = response.body;
78
+ await pipeline(isNodeReadableStream(body) ? body : Readable.fromWeb(body), new Transform({ transform(chunk, _encoding, callback) {
79
+ totalBytes += chunkByteLength(chunk);
80
+ if (totalBytes > maxBytes) {
81
+ callback(/* @__PURE__ */ new Error(`signal-cli archive exceeded the ${maxBytes}-byte download cap.`));
82
+ return;
83
+ }
84
+ callback(null, chunk);
85
+ } }), createWriteStream(dest));
86
+ completed = true;
87
+ } finally {
88
+ await release();
89
+ if (!completed) await fs.rm(dest, { force: true }).catch(() => void 0);
90
+ }
91
+ }
92
+ async function findSignalCliBinary(root) {
93
+ const candidates = [];
94
+ const enqueue = async (dir, depth) => {
95
+ if (depth > 3) return;
96
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
97
+ for (const entry of entries) {
98
+ const full = path.join(dir, entry.name);
99
+ if (entry.isDirectory()) await enqueue(full, depth + 1);
100
+ else if (entry.isFile() && entry.name === "signal-cli") candidates.push(full);
101
+ }
102
+ };
103
+ await enqueue(root, 0);
104
+ return candidates[0] ?? null;
105
+ }
106
+ async function resolveBrewSignalCliPath(brewExe) {
107
+ try {
108
+ const result = await runPluginCommandWithTimeout({
109
+ argv: [
110
+ brewExe,
111
+ "--prefix",
112
+ "signal-cli"
113
+ ],
114
+ timeoutMs: 1e4
115
+ });
116
+ if (result.code === 0 && result.stdout.trim()) {
117
+ const prefix = result.stdout.trim();
118
+ const candidate = path.join(prefix, "bin", "signal-cli");
119
+ try {
120
+ await fs.access(candidate);
121
+ return candidate;
122
+ } catch {
123
+ return findSignalCliBinary(prefix);
124
+ }
125
+ }
126
+ } catch {}
127
+ return null;
128
+ }
129
+ async function installSignalCliViaBrew(runtime) {
130
+ const brewExe = resolveBrewExecutable();
131
+ if (!brewExe) return {
132
+ ok: false,
133
+ error: `No native signal-cli build is available for ${process.arch}. Install Homebrew (https://brew.sh) and try again, or install signal-cli manually.`
134
+ };
135
+ runtime.log(`Installing signal-cli via Homebrew (${brewExe})…`);
136
+ const result = await runPluginCommandWithTimeout({
137
+ argv: [
138
+ brewExe,
139
+ "install",
140
+ "signal-cli"
141
+ ],
142
+ timeoutMs: 15 * 6e4
143
+ });
144
+ if (result.code !== 0) return {
145
+ ok: false,
146
+ error: `brew install signal-cli failed (exit ${result.code}): ${result.stderr.trim().slice(0, 200)}`
147
+ };
148
+ const cliPath = await resolveBrewSignalCliPath(brewExe);
149
+ if (!cliPath) return {
150
+ ok: false,
151
+ error: "brew install succeeded but signal-cli binary was not found."
152
+ };
153
+ let version;
154
+ try {
155
+ version = (await runPluginCommandWithTimeout({
156
+ argv: [cliPath, "--version"],
157
+ timeoutMs: 1e4
158
+ })).stdout.trim().replace(/^signal-cli\s+/, "") || void 0;
159
+ } catch {}
160
+ return {
161
+ ok: true,
162
+ cliPath,
163
+ version
164
+ };
165
+ }
166
+ /** @internal Exported for testing. */
167
+ async function installSignalCliFromRelease(runtime) {
168
+ const { response, release } = await fetchWithSsrFGuard({
169
+ url: "https://api.github.com/repos/AsamK/signal-cli/releases/latest",
170
+ maxRedirects: 5,
171
+ requireHttps: true,
172
+ timeoutMs: SIGNAL_CLI_RELEASE_INFO_TIMEOUT_MS,
173
+ capture: false,
174
+ auditContext: "signal-cli-release-info",
175
+ init: { headers: {
176
+ "User-Agent": "openclaw",
177
+ Accept: "application/vnd.github+json"
178
+ } }
179
+ });
180
+ let payload;
181
+ try {
182
+ if (!response.ok) return {
183
+ ok: false,
184
+ error: `Failed to fetch release info (${response.status})`
185
+ };
186
+ try {
187
+ payload = await response.json();
188
+ } catch {
189
+ return {
190
+ ok: false,
191
+ error: "Failed to parse signal-cli release info."
192
+ };
193
+ }
194
+ } finally {
195
+ await release();
196
+ }
197
+ const version = payload.tag_name?.replace(/^v/, "") ?? "unknown";
198
+ const asset = pickAsset(payload.assets ?? [], process.platform, process.arch);
199
+ if (!asset) return {
200
+ ok: false,
201
+ error: "No compatible release asset found for this platform."
202
+ };
203
+ const tmpDir = await fs.mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "openclaw-signal-"));
204
+ const archivePath = path.join(tmpDir, asset.name);
205
+ runtime.log(`Downloading signal-cli ${version} (${asset.name})…`);
206
+ await downloadToFile(asset.browser_download_url, archivePath);
207
+ const installRoot = path.join(CONFIG_DIR, "tools", "signal-cli", version);
208
+ await fs.mkdir(installRoot, { recursive: true });
209
+ if (!looksLikeArchive(normalizeLowercaseStringOrEmpty(asset.name))) return {
210
+ ok: false,
211
+ error: `Unsupported archive type: ${asset.name}`
212
+ };
213
+ try {
214
+ await extractSignalCliArchive(archivePath, installRoot, 6e4);
215
+ } catch (err) {
216
+ const message = formatErrorMessage(err);
217
+ return {
218
+ ok: false,
219
+ error: `Failed to extract ${asset.name}: ${message}`
220
+ };
221
+ }
222
+ const cliPath = await findSignalCliBinary(installRoot);
223
+ if (!cliPath) return {
224
+ ok: false,
225
+ error: `signal-cli binary not found after extracting ${asset.name}`
226
+ };
227
+ await fs.chmod(cliPath, 493).catch(() => {});
228
+ return {
229
+ ok: true,
230
+ cliPath,
231
+ version
232
+ };
233
+ }
234
+ async function installSignalCli(runtime) {
235
+ if (process.platform === "win32") return {
236
+ ok: false,
237
+ error: "Signal CLI auto-install is not supported on Windows yet."
238
+ };
239
+ if (process.platform !== "linux" || process.arch === "x64") return installSignalCliFromRelease(runtime);
240
+ return installSignalCliViaBrew(runtime);
241
+ }
242
+ //#endregion
243
+ export { looksLikeArchive as a, installSignalCliFromRelease as i, extractSignalCliArchive as n, pickAsset as o, installSignalCli as r, downloadToFile as t };