@openclaw/crabline 0.1.11 → 0.1.13

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 (214) hide show
  1. package/README.md +137 -23
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +281 -52
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/index.d.ts +12 -10
  31. package/dist/src/index.js +6 -5
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/matrix-ids.d.ts +5 -0
  34. package/dist/src/matrix-ids.js +83 -0
  35. package/dist/src/matrix-ids.js.map +1 -0
  36. package/dist/src/openclaw/artifact-generation.d.ts +7 -6
  37. package/dist/src/openclaw/artifact-generation.js +385 -41
  38. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  39. package/dist/src/openclaw/bridges/discord.d.ts +1 -0
  40. package/dist/src/openclaw/bridges/discord.js +149 -0
  41. package/dist/src/openclaw/bridges/discord.js.map +1 -0
  42. package/dist/src/openclaw/bridges/matrix.js +22 -5
  43. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  44. package/dist/src/openclaw/bridges/mattermost.js +14 -9
  45. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  46. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  47. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  48. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  49. package/dist/src/openclaw/bridges/signal.js +16 -7
  50. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  51. package/dist/src/openclaw/bridges/slack.js +117 -24
  52. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  53. package/dist/src/openclaw/bridges/telegram.js +46 -20
  54. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  55. package/dist/src/openclaw/bridges/whatsapp.js +33 -17
  56. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  57. package/dist/src/openclaw/bridges/zalo.js +9 -4
  58. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  59. package/dist/src/openclaw/outbound-contract.js +3 -0
  60. package/dist/src/openclaw/outbound-contract.js.map +1 -1
  61. package/dist/src/openclaw/private-file.d.ts +38 -3
  62. package/dist/src/openclaw/private-file.js +2008 -87
  63. package/dist/src/openclaw/private-file.js.map +1 -1
  64. package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
  65. package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
  66. package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
  67. package/dist/src/openclaw/shared.d.ts +10 -17
  68. package/dist/src/openclaw/shared.js +53 -19
  69. package/dist/src/openclaw/shared.js.map +1 -1
  70. package/dist/src/openclaw.d.ts +3 -5
  71. package/dist/src/openclaw.js +245 -60
  72. package/dist/src/openclaw.js.map +1 -1
  73. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  74. package/dist/src/platform/process-owned-lock.js +1698 -0
  75. package/dist/src/platform/process-owned-lock.js.map +1 -0
  76. package/dist/src/platform/recorder-directory.d.ts +1 -0
  77. package/dist/src/platform/recorder-directory.js +14 -0
  78. package/dist/src/platform/recorder-directory.js.map +1 -0
  79. package/dist/src/platform/windows-acl.d.ts +18 -0
  80. package/dist/src/platform/windows-acl.js +1367 -0
  81. package/dist/src/platform/windows-acl.js.map +1 -0
  82. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  83. package/dist/src/platform/windows-lock-root.js +98 -0
  84. package/dist/src/platform/windows-lock-root.js.map +1 -0
  85. package/dist/src/providers/builtin/discord.d.ts +4 -0
  86. package/dist/src/providers/builtin/discord.js +52 -9
  87. package/dist/src/providers/builtin/discord.js.map +1 -1
  88. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  89. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  90. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  91. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  92. package/dist/src/providers/builtin/feishu.js +96 -26
  93. package/dist/src/providers/builtin/feishu.js.map +1 -1
  94. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  95. package/dist/src/providers/builtin/googlechat.js +95 -26
  96. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  97. package/dist/src/providers/builtin/imessage.js +60 -17
  98. package/dist/src/providers/builtin/imessage.js.map +1 -1
  99. package/dist/src/providers/builtin/loopback.js +88 -35
  100. package/dist/src/providers/builtin/loopback.js.map +1 -1
  101. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  102. package/dist/src/providers/builtin/matrix.js +78 -21
  103. package/dist/src/providers/builtin/matrix.js.map +1 -1
  104. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  105. package/dist/src/providers/builtin/mattermost.js +138 -24
  106. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  107. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  108. package/dist/src/providers/builtin/msteams.js +118 -38
  109. package/dist/src/providers/builtin/msteams.js.map +1 -1
  110. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  111. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  112. package/dist/src/providers/builtin/script.js +1057 -146
  113. package/dist/src/providers/builtin/script.js.map +1 -1
  114. package/dist/src/providers/builtin/slack.d.ts +8 -1
  115. package/dist/src/providers/builtin/slack.js +327 -24
  116. package/dist/src/providers/builtin/slack.js.map +1 -1
  117. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  118. package/dist/src/providers/builtin/telegram.js +55 -14
  119. package/dist/src/providers/builtin/telegram.js.map +1 -1
  120. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  121. package/dist/src/providers/builtin/whatsapp.js +157 -29
  122. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  123. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  124. package/dist/src/providers/builtin/zalo.js +31 -7
  125. package/dist/src/providers/builtin/zalo.js.map +1 -1
  126. package/dist/src/providers/catalog.d.ts +1 -1
  127. package/dist/src/providers/catalog.js +1 -1
  128. package/dist/src/providers/catalog.js.map +1 -1
  129. package/dist/src/providers/local-mock.d.ts +4 -0
  130. package/dist/src/providers/local-mock.js +162 -35
  131. package/dist/src/providers/local-mock.js.map +1 -1
  132. package/dist/src/providers/native-ids.d.ts +2 -0
  133. package/dist/src/providers/native-ids.js +3 -0
  134. package/dist/src/providers/native-ids.js.map +1 -1
  135. package/dist/src/providers/recorder.d.ts +14 -3
  136. package/dist/src/providers/recorder.js +521 -41
  137. package/dist/src/providers/recorder.js.map +1 -1
  138. package/dist/src/providers/registry.d.ts +1 -0
  139. package/dist/src/providers/registry.js +65 -20
  140. package/dist/src/providers/registry.js.map +1 -1
  141. package/dist/src/providers/signed-jwt.d.ts +6 -1
  142. package/dist/src/providers/signed-jwt.js +241 -50
  143. package/dist/src/providers/signed-jwt.js.map +1 -1
  144. package/dist/src/providers/slack-ids.d.ts +1 -0
  145. package/dist/src/providers/slack-ids.js +5 -0
  146. package/dist/src/providers/slack-ids.js.map +1 -1
  147. package/dist/src/providers/target-normalizers.d.ts +9 -1
  148. package/dist/src/providers/target-normalizers.js +177 -58
  149. package/dist/src/providers/target-normalizers.js.map +1 -1
  150. package/dist/src/providers/webhook-server.d.ts +3 -0
  151. package/dist/src/providers/webhook-server.js +268 -47
  152. package/dist/src/providers/webhook-server.js.map +1 -1
  153. package/dist/src/servers/discord.d.ts +40 -0
  154. package/dist/src/servers/discord.js +842 -0
  155. package/dist/src/servers/discord.js.map +1 -0
  156. package/dist/src/servers/http.d.ts +14 -2
  157. package/dist/src/servers/http.js +229 -58
  158. package/dist/src/servers/http.js.map +1 -1
  159. package/dist/src/servers/index.d.ts +10 -4
  160. package/dist/src/servers/index.js +4 -0
  161. package/dist/src/servers/index.js.map +1 -1
  162. package/dist/src/servers/matrix.d.ts +3 -0
  163. package/dist/src/servers/matrix.js +268 -110
  164. package/dist/src/servers/matrix.js.map +1 -1
  165. package/dist/src/servers/mattermost.d.ts +5 -0
  166. package/dist/src/servers/mattermost.js +389 -91
  167. package/dist/src/servers/mattermost.js.map +1 -1
  168. package/dist/src/servers/recorder.d.ts +11 -0
  169. package/dist/src/servers/recorder.js +1072 -69
  170. package/dist/src/servers/recorder.js.map +1 -1
  171. package/dist/src/servers/signal.d.ts +1 -0
  172. package/dist/src/servers/signal.js +279 -44
  173. package/dist/src/servers/signal.js.map +1 -1
  174. package/dist/src/servers/slack.d.ts +15 -0
  175. package/dist/src/servers/slack.js +270 -54
  176. package/dist/src/servers/slack.js.map +1 -1
  177. package/dist/src/servers/telegram-identity.d.ts +6 -0
  178. package/dist/src/servers/telegram-identity.js +27 -0
  179. package/dist/src/servers/telegram-identity.js.map +1 -0
  180. package/dist/src/servers/telegram.d.ts +17 -6
  181. package/dist/src/servers/telegram.js +1218 -107
  182. package/dist/src/servers/telegram.js.map +1 -1
  183. package/dist/src/servers/webhook-target.d.ts +12 -4
  184. package/dist/src/servers/webhook-target.js +226 -28
  185. package/dist/src/servers/webhook-target.js.map +1 -1
  186. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  187. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  188. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  189. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  190. package/dist/src/servers/whatsapp-jid.js +9 -5
  191. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  192. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  193. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  194. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  195. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  196. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  197. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  198. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  199. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  200. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  201. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  202. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  203. package/dist/src/servers/whatsapp.d.ts +7 -1
  204. package/dist/src/servers/whatsapp.js +149 -25
  205. package/dist/src/servers/whatsapp.js.map +1 -1
  206. package/dist/src/servers/zalo.d.ts +1 -0
  207. package/dist/src/servers/zalo.js +193 -82
  208. package/dist/src/servers/zalo.js.map +1 -1
  209. package/docs/channel-setup.md +251 -64
  210. package/fixtures/examples/crabline.example.yaml +3 -2
  211. package/package.json +18 -10
  212. package/dist/src/openclaw/smoke-lock.js +0 -1004
  213. package/dist/src/openclaw/smoke-lock.js.map +0 -1
  214. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -1,18 +1,36 @@
1
- import { Command, CommanderError } from "commander";
1
+ import { Command, CommanderError, Option } from "commander";
2
2
  import { randomUUID } from "node:crypto";
3
- import { constants as fsConstants } from "node:fs";
3
+ import { constants as fsConstants, createReadStream } from "node:fs";
4
4
  import fs from "node:fs/promises";
5
5
  import nodePath from "node:path";
6
+ import { finished } from "node:stream/promises";
6
7
  import { lock } from "proper-lockfile";
7
8
  import { loadManifest } from "../config/load.js";
9
+ import { EXIT_CODES } from "../core/exit-codes.js";
8
10
  import { createRegistry } from "../providers/registry.js";
9
- import { formatJson, formatRunResultText, sanitizeTerminalText } from "../core/reporters.js";
11
+ import { formatJson, formatJsonResult, formatRunResultText, sanitizeTerminalText, } from "../core/reporters.js";
10
12
  import { assertScriptStdinPayloadSize, computeExitCode, runFixtureCommand, runSuite, } from "../core/run.js";
11
13
  import { CrablineError, ensureErrorMessage } from "../core/errors.js";
12
14
  import { isCrablineServerChannel, startCrablineServer, } from "../servers/index.js";
13
15
  const READY_FILE_LEASE_STALE_MS = 60_000;
14
16
  const READY_FILE_LEASE_UPDATE_MS = 1_000;
17
+ const MAX_SERVE_CREDENTIALS_BYTES = 64 * 1024;
18
+ const WATCH_SHUTDOWN_GRACE_MS = 250;
19
+ const WATCH_SHUTDOWN_REACHED = Symbol("watch shutdown reached");
20
+ const SERVE_CREDENTIAL_ENV = [
21
+ ["accessToken", "CRABLINE_ACCESS_TOKEN"],
22
+ ["adminToken", "CRABLINE_ADMIN_TOKEN"],
23
+ ["botToken", "CRABLINE_BOT_TOKEN"],
24
+ ["signingSecret", "CRABLINE_SIGNING_SECRET"],
25
+ ];
15
26
  const SERVE_PARAM_FACTORIES = {
27
+ discord: (shared, commandOptions) => ({
28
+ ...shared,
29
+ adminToken: commandOptions.adminToken,
30
+ botToken: commandOptions.botToken,
31
+ botUsername: commandOptions.botUsername,
32
+ channel: "discord",
33
+ }),
16
34
  mattermost: (shared, commandOptions) => ({
17
35
  ...shared,
18
36
  adminToken: commandOptions.adminToken,
@@ -126,9 +144,150 @@ async function writeOutput(stream, value) {
126
144
  async function print(value) {
127
145
  return await writeOutput(process.stdout, `${value}\n`);
128
146
  }
147
+ class WatchShutdownDeadlineError extends CrablineError {
148
+ }
149
+ function createWatchShutdownDeadline() {
150
+ let deadlineAt;
151
+ const controller = new AbortController();
152
+ return {
153
+ async race(operation, operationName) {
154
+ let timer;
155
+ let removeStartListener;
156
+ const deadline = new Promise((_, reject) => {
157
+ const arm = () => {
158
+ timer = setTimeout(() => {
159
+ reject(new WatchShutdownDeadlineError(`Provider ${operationName} did not settle within ${WATCH_SHUTDOWN_GRACE_MS}ms during watch shutdown.`, { kind: "timeout" }));
160
+ }, Math.max(0, deadlineAt - Date.now()));
161
+ };
162
+ if (controller.signal.aborted) {
163
+ arm();
164
+ return;
165
+ }
166
+ controller.signal.addEventListener("abort", arm, { once: true });
167
+ removeStartListener = () => controller.signal.removeEventListener("abort", arm);
168
+ });
169
+ try {
170
+ return await Promise.race([Promise.resolve(operation), deadline]);
171
+ }
172
+ finally {
173
+ removeStartListener?.();
174
+ if (timer) {
175
+ clearTimeout(timer);
176
+ }
177
+ }
178
+ },
179
+ start() {
180
+ if (deadlineAt !== undefined) {
181
+ return;
182
+ }
183
+ deadlineAt = Date.now() + WATCH_SHUTDOWN_GRACE_MS;
184
+ controller.abort();
185
+ },
186
+ };
187
+ }
188
+ function containsWatchShutdownDeadline(error, seen = new Set()) {
189
+ if (error instanceof WatchShutdownDeadlineError) {
190
+ return true;
191
+ }
192
+ if (!error || typeof error !== "object" || seen.has(error)) {
193
+ return false;
194
+ }
195
+ seen.add(error);
196
+ if (error instanceof AggregateError) {
197
+ return error.errors.some((entry) => containsWatchShutdownDeadline(entry, seen));
198
+ }
199
+ return "cause" in error && containsWatchShutdownDeadline(error.cause, seen);
200
+ }
129
201
  async function withManifest(options, action) {
130
202
  return action(await loadManifest(options.config));
131
203
  }
204
+ function rejectedSecretOption(flags, optionName, envName) {
205
+ return new Option(flags).hideHelp().argParser(() => {
206
+ throw new CrablineError(`${optionName} no longer accepts credential values in command-line arguments. Use ${envName} or --credentials-fd.`, { kind: "config" });
207
+ });
208
+ }
209
+ function parseCredentialsFd(value) {
210
+ if (!/^[0-9]+$/u.test(value)) {
211
+ throw new CrablineError("Serve credentials file descriptor must be 0 or an integer greater than 2.", { kind: "config" });
212
+ }
213
+ const fd = Number(value);
214
+ if (!Number.isSafeInteger(fd) || fd === 1 || fd === 2 || fd > 2_147_483_647) {
215
+ throw new CrablineError("Serve credentials file descriptor must be 0 or an integer greater than 2.", { kind: "config" });
216
+ }
217
+ return fd;
218
+ }
219
+ async function readBoundedFileDescriptor(fd) {
220
+ const stream = createReadStream("", { autoClose: fd !== 0, fd });
221
+ const chunks = [];
222
+ let totalBytes = 0;
223
+ try {
224
+ for await (const chunk of stream) {
225
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
226
+ totalBytes += buffer.byteLength;
227
+ if (totalBytes > MAX_SERVE_CREDENTIALS_BYTES) {
228
+ throw new CrablineError(`Serve credentials JSON exceeds ${MAX_SERVE_CREDENTIALS_BYTES} bytes.`, { kind: "config" });
229
+ }
230
+ chunks.push(buffer);
231
+ }
232
+ }
233
+ catch (error) {
234
+ stream.destroy();
235
+ if (error instanceof CrablineError) {
236
+ throw error;
237
+ }
238
+ throw new CrablineError(`Unable to read serve credentials from file descriptor ${fd}.`, {
239
+ cause: error,
240
+ kind: "config",
241
+ });
242
+ }
243
+ finally {
244
+ if (fd !== 0 && !stream.closed) {
245
+ stream.destroy();
246
+ await finished(stream).catch(() => undefined);
247
+ }
248
+ }
249
+ return Buffer.concat(chunks).toString("utf8");
250
+ }
251
+ function parseServeCredentialsJson(input) {
252
+ let parsed;
253
+ try {
254
+ parsed = JSON.parse(input);
255
+ }
256
+ catch (error) {
257
+ throw new CrablineError("Serve credentials input must be valid JSON.", {
258
+ cause: error,
259
+ kind: "config",
260
+ });
261
+ }
262
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
263
+ throw new CrablineError("Serve credentials JSON must be an object.", { kind: "config" });
264
+ }
265
+ const values = {};
266
+ for (const [key, value] of Object.entries(parsed)) {
267
+ if (!SERVE_CREDENTIAL_ENV.some(([name]) => name === key)) {
268
+ throw new CrablineError("Serve credentials JSON contains unsupported fields.", {
269
+ kind: "config",
270
+ });
271
+ }
272
+ if (typeof value !== "string") {
273
+ throw new CrablineError("Serve credentials JSON values must be strings.", {
274
+ kind: "config",
275
+ });
276
+ }
277
+ values[key] = value;
278
+ }
279
+ return values;
280
+ }
281
+ async function resolveServeCredentials(credentialsFd) {
282
+ const overrides = credentialsFd === undefined
283
+ ? {}
284
+ : parseServeCredentialsJson(await readBoundedFileDescriptor(parseCredentialsFd(credentialsFd)));
285
+ return Object.fromEntries(SERVE_CREDENTIAL_ENV.flatMap(([name, envName]) => {
286
+ const override = overrides[name];
287
+ const value = override ?? process.env[envName];
288
+ return value === undefined ? [] : [[name, value]];
289
+ }));
290
+ }
132
291
  export function createProgram(setExitCode = (code) => {
133
292
  process.exitCode = code;
134
293
  }, dependencies = {}) {
@@ -138,6 +297,13 @@ export function createProgram(setExitCode = (code) => {
138
297
  const publish = dependencies.publishReadyFile ?? publishReadyFileUnlocked;
139
298
  const removeReady = dependencies.removeReadyFile ?? removeReadyFileIfOwned;
140
299
  const startServer = dependencies.startServer ?? startCrablineServer;
300
+ const formatCliJson = (value) => {
301
+ const formatted = formatJsonResult(value);
302
+ if (!formatted.ok) {
303
+ setExitCode(EXIT_CODES.FAILURE);
304
+ }
305
+ return formatted.output;
306
+ };
141
307
  program
142
308
  .exitOverride()
143
309
  .name("crabline")
@@ -162,7 +328,7 @@ export function createProgram(setExitCode = (code) => {
162
328
  })),
163
329
  support: registry.catalog,
164
330
  };
165
- await print(options.json ? formatJson(payload) : renderProvidersText(payload));
331
+ await print(options.json ? formatCliJson(payload) : renderProvidersText(payload));
166
332
  });
167
333
  program
168
334
  .command("fixtures")
@@ -171,7 +337,7 @@ export function createProgram(setExitCode = (code) => {
171
337
  const options = program.opts();
172
338
  const { manifest } = await loadManifest(options.config);
173
339
  await print(options.json
174
- ? formatJson(manifest.fixtures)
340
+ ? formatCliJson(manifest.fixtures)
175
341
  : manifest.fixtures
176
342
  .map((fixture) => `${sanitizeTerminalText(fixture.id, true)} ${sanitizeTerminalText(fixture.mode, true)} provider=${sanitizeTerminalText(fixture.provider, true)} target=${sanitizeTerminalText(fixture.target.id, true)}`)
177
343
  .join("\n"));
@@ -194,8 +360,8 @@ export function createProgram(setExitCode = (code) => {
194
360
  modeOverride: "probe",
195
361
  registry,
196
362
  });
197
- await print(options.json ? formatJson(result) : formatRunResultText(result));
198
363
  setExitCode(computeExitCode(result));
364
+ await print(options.json ? formatCliJson(result) : formatRunResultText(result));
199
365
  });
200
366
  for (const mode of ["send", "roundtrip", "agent"]) {
201
367
  program
@@ -212,8 +378,8 @@ export function createProgram(setExitCode = (code) => {
212
378
  modeOverride: mode,
213
379
  registry,
214
380
  });
215
- await print(options.json ? formatJson(result) : formatRunResultText(result));
216
381
  setExitCode(computeExitCode(result));
382
+ await print(options.json ? formatCliJson(result) : formatRunResultText(result));
217
383
  });
218
384
  }
219
385
  program
@@ -229,8 +395,8 @@ export function createProgram(setExitCode = (code) => {
229
395
  manifestPath: path,
230
396
  registry,
231
397
  });
232
- await print(options.json ? formatJson(result) : formatRunResultText(result));
233
398
  setExitCode(computeExitCode(result));
399
+ await print(options.json ? formatCliJson(result) : formatRunResultText(result));
234
400
  });
235
401
  program
236
402
  .command("watch <fixtureId>")
@@ -250,12 +416,18 @@ export function createProgram(setExitCode = (code) => {
250
416
  });
251
417
  }
252
418
  const controller = new AbortController();
419
+ const shutdownDeadline = createWatchShutdownDeadline();
253
420
  let iterator;
254
421
  const stopWatch = onceAsync(async () => {
422
+ shutdownDeadline.start();
255
423
  controller.abort();
256
- await iterator?.return?.();
424
+ const returnResult = iterator?.return?.();
425
+ if (returnResult) {
426
+ await shutdownDeadline.race(returnResult, "watch iterator return");
427
+ }
257
428
  });
258
429
  const shutdown = installShutdownHandler(stopWatch);
430
+ const shutdownSettled = shutdown.wait().then(() => WATCH_SHUTDOWN_REACHED);
259
431
  const lifecycleErrors = [];
260
432
  try {
261
433
  const watchContext = {
@@ -282,14 +454,17 @@ export function createProgram(setExitCode = (code) => {
282
454
  const watch = provider.watch(watchContext);
283
455
  iterator = watch[Symbol.asyncIterator]();
284
456
  while (true) {
285
- const result = await iterator.next();
457
+ const result = await Promise.race([iterator.next(), shutdownSettled]);
458
+ if (typeof result === "symbol") {
459
+ break;
460
+ }
286
461
  if (result.done) {
287
462
  break;
288
463
  }
289
464
  const message = result.value;
290
- const written = await print(options.json
291
- ? formatJson(message)
292
- : `${sanitizeTerminalText(message.sentAt, true)} ${sanitizeTerminalText(message.author, true)} ${sanitizeTerminalText(message.text, true)}`);
465
+ const written = await shutdownDeadline.race(print(options.json
466
+ ? formatCliJson(message)
467
+ : `${sanitizeTerminalText(message.sentAt, true)} ${sanitizeTerminalText(message.author, true)} ${sanitizeTerminalText(message.text, true)}`), "watch output write");
293
468
  if (!written) {
294
469
  break;
295
470
  }
@@ -307,7 +482,10 @@ export function createProgram(setExitCode = (code) => {
307
482
  }
308
483
  }
309
484
  try {
310
- await provider.cleanup?.();
485
+ const cleanup = provider.cleanup?.();
486
+ if (cleanup) {
487
+ await shutdownDeadline.race(cleanup, "cleanup");
488
+ }
311
489
  }
312
490
  catch (error) {
313
491
  if (!lifecycleErrors.includes(error)) {
@@ -327,17 +505,24 @@ export function createProgram(setExitCode = (code) => {
327
505
  .description("Start a local provider server that OpenClaw live adapters can target")
328
506
  .option("--host <host>", "Bind host", "127.0.0.1")
329
507
  .option("--port <port>", "Bind port", "0")
330
- .option("--admin-token <token>", "Admin token for inbound test messages (or CRABLINE_ADMIN_TOKEN)")
331
508
  .option("--account <number>", "Signal account number")
332
- .option("--access-token <token>", "Matrix or WhatsApp access token (or CRABLINE_ACCESS_TOKEN)")
333
- .option("--bot-token <token>", "Mattermost, Slack, Telegram, or Zalo bot token (or CRABLINE_BOT_TOKEN)")
334
- .option("--bot-username <username>", "Mattermost, Telegram, or Zalo bot username", "crabline_bot")
509
+ .option("--bot-username <username>", "Discord, Mattermost, Telegram, or Zalo bot username", "crabline_bot")
510
+ .option("--credentials-fd <fd>", "Read JSON credentials from fd 0 or an inherited fd; values override CRABLINE_* env")
335
511
  .option("--recorder <path>", "JSONL recorder path")
336
512
  .option("--ready-file <path>", "Write the server runtime manifest to this path")
337
513
  .option("--self-jid <jid>", "WhatsApp self JID")
338
514
  .option("--show-secrets", "Print provider credentials in text output", false)
339
- .option("--signing-secret <secret>", "Slack signing secret (or CRABLINE_SIGNING_SECRET)")
340
515
  .option("--once", "Start, print the runtime manifest, and stop immediately", false)
516
+ .addOption(rejectedSecretOption("--access-token <token>", "--access-token", "CRABLINE_ACCESS_TOKEN"))
517
+ .addOption(rejectedSecretOption("--admin-token <token>", "--admin-token", "CRABLINE_ADMIN_TOKEN"))
518
+ .addOption(rejectedSecretOption("--bot-token <token>", "--bot-token", "CRABLINE_BOT_TOKEN"))
519
+ .addOption(rejectedSecretOption("--signing-secret <secret>", "--signing-secret", "CRABLINE_SIGNING_SECRET"))
520
+ .addHelpText("after", [
521
+ "",
522
+ "Credential JSON fields: accessToken, adminToken, botToken, signingSecret",
523
+ "Environment fallbacks: CRABLINE_ACCESS_TOKEN, CRABLINE_ADMIN_TOKEN,",
524
+ " CRABLINE_BOT_TOKEN, CRABLINE_SIGNING_SECRET",
525
+ ].join("\n"))
341
526
  .action(async (provider, commandOptions) => {
342
527
  const options = program.opts();
343
528
  if (!isCrablineServerChannel(provider)) {
@@ -345,10 +530,6 @@ export function createProgram(setExitCode = (code) => {
345
530
  kind: "config",
346
531
  });
347
532
  }
348
- commandOptions.accessToken ??= process.env.CRABLINE_ACCESS_TOKEN;
349
- commandOptions.adminToken ??= process.env.CRABLINE_ADMIN_TOKEN;
350
- commandOptions.botToken ??= process.env.CRABLINE_BOT_TOKEN;
351
- commandOptions.signingSecret ??= process.env.CRABLINE_SIGNING_SECRET;
352
533
  if (!/^[0-9]+$/u.test(commandOptions.port)) {
353
534
  throw new CrablineError(`Invalid local server port: ${commandOptions.port}`, {
354
535
  kind: "config",
@@ -366,6 +547,11 @@ export function createProgram(setExitCode = (code) => {
366
547
  kind: "config",
367
548
  });
368
549
  }
550
+ const credentials = await resolveServeCredentials(commandOptions.credentialsFd);
551
+ commandOptions.accessToken = credentials.accessToken;
552
+ commandOptions.adminToken = credentials.adminToken;
553
+ commandOptions.botToken = credentials.botToken;
554
+ commandOptions.signingSecret = credentials.signingSecret;
369
555
  let server;
370
556
  let startupSettled = false;
371
557
  let settleStartup;
@@ -389,14 +575,41 @@ export function createProgram(setExitCode = (code) => {
389
575
  settlePublication?.();
390
576
  }
391
577
  };
578
+ let releaseReadyLease;
579
+ let publishedReady;
392
580
  const close = onceAsync(async () => {
393
581
  await startup;
394
582
  await publication;
395
- await server?.close();
583
+ const lifecycleErrors = [];
584
+ if (commandOptions.readyFile && publishedReady) {
585
+ try {
586
+ await removeReady(commandOptions.readyFile, publishedReady.contents, publishedReady.identity);
587
+ }
588
+ catch (error) {
589
+ lifecycleErrors.push(error);
590
+ }
591
+ }
592
+ let serverClosed = false;
593
+ try {
594
+ await server?.close();
595
+ serverClosed = true;
596
+ }
597
+ catch (error) {
598
+ lifecycleErrors.push(error);
599
+ }
600
+ if (serverClosed && releaseReadyLease) {
601
+ try {
602
+ await releaseReadyLease();
603
+ }
604
+ catch (error) {
605
+ lifecycleErrors.push(error);
606
+ }
607
+ }
608
+ if (lifecycleErrors.length > 0) {
609
+ throw combineLifecycleErrors(lifecycleErrors, "Crabline server shutdown failed.");
610
+ }
396
611
  });
397
612
  const shutdown = installShutdownHandler(close);
398
- let releaseReadyLease;
399
- let publishedReady;
400
613
  let actionFailed = false;
401
614
  let actionError;
402
615
  try {
@@ -424,8 +637,13 @@ export function createProgram(setExitCode = (code) => {
424
637
  await shutdown.wait();
425
638
  }
426
639
  else {
640
+ let outputWritten = true;
427
641
  try {
428
- const payload = formatJson(server.manifest);
642
+ const formatted = formatJsonResult(server.manifest);
643
+ if (!formatted.ok) {
644
+ throw new Error("Unable to serialize server manifest.");
645
+ }
646
+ const payload = formatted.output;
429
647
  if (commandOptions.readyFile) {
430
648
  const readyContents = `${payload}\n`;
431
649
  publishedReady = {
@@ -433,14 +651,25 @@ export function createProgram(setExitCode = (code) => {
433
651
  identity: await publish(commandOptions.readyFile, readyContents),
434
652
  };
435
653
  }
436
- await print(options.json
437
- ? payload
438
- : renderServeText(server.manifest, commandOptions.showSecrets === true));
654
+ if (shutdown.requested()) {
655
+ outputWritten = false;
656
+ finishPublication();
657
+ await shutdown.wait();
658
+ }
659
+ else {
660
+ outputWritten = await print(options.json
661
+ ? payload
662
+ : renderServeText(server.manifest, commandOptions.showSecrets === true));
663
+ if (!outputWritten) {
664
+ finishPublication();
665
+ await close();
666
+ }
667
+ }
439
668
  }
440
669
  finally {
441
670
  finishPublication();
442
671
  }
443
- if (shutdown.requested() || !commandOptions.once) {
672
+ if (outputWritten && (shutdown.requested() || !commandOptions.once)) {
444
673
  await shutdown.wait();
445
674
  }
446
675
  }
@@ -453,23 +682,12 @@ export function createProgram(setExitCode = (code) => {
453
682
  finishStartup();
454
683
  finishPublication();
455
684
  const cleanupErrors = [];
456
- const readyToRemove = publishedReady;
457
- for (const cleanup of [
458
- close,
459
- ...(commandOptions.readyFile && readyToRemove
460
- ? [
461
- () => removeReady(commandOptions.readyFile, readyToRemove.contents, readyToRemove.identity),
462
- ]
463
- : []),
464
- ...(releaseReadyLease ? [releaseReadyLease] : []),
465
- ]) {
466
- try {
467
- await cleanup();
468
- }
469
- catch (error) {
470
- if (!actionFailed || error !== actionError) {
471
- cleanupErrors.push(error);
472
- }
685
+ try {
686
+ await close();
687
+ }
688
+ catch (error) {
689
+ if (!actionFailed || error !== actionError) {
690
+ cleanupErrors.push(error);
473
691
  }
474
692
  }
475
693
  shutdown.dispose();
@@ -486,8 +704,8 @@ export function createProgram(setExitCode = (code) => {
486
704
  const findings = diagnose(manifest);
487
705
  const ok = findings.length === 0;
488
706
  const payload = { findings, ok };
489
- await print(options.json ? formatJson(payload) : ok ? "doctor ok" : findings.join("\n"));
490
707
  setExitCode(ok ? 0 : 10);
708
+ await print(options.json ? formatCliJson(payload) : ok ? "doctor ok" : findings.join("\n"));
491
709
  });
492
710
  return program;
493
711
  }
@@ -732,6 +950,15 @@ function renderServeText(manifest, showSecrets) {
732
950
  }
733
951
  function renderServeProviderFields(manifest, showSecrets) {
734
952
  const secret = (value) => (showSecrets ? value : "<redacted>");
953
+ if (manifest.provider === "discord") {
954
+ return [
955
+ ` adminToken: ${secret(manifest.adminToken)}`,
956
+ ` botToken: ${secret(manifest.botToken)}`,
957
+ ` applicationId: ${manifest.applicationId}`,
958
+ ` botUserId: ${manifest.botUserId}`,
959
+ ` gateway: ${manifest.endpoints.gatewayUrl}`,
960
+ ];
961
+ }
735
962
  if (manifest.provider === "mattermost") {
736
963
  return [
737
964
  ` adminToken: ${secret(manifest.adminToken)}`,
@@ -773,7 +1000,6 @@ function renderServeProviderFields(manifest, showSecrets) {
773
1000
  ` baileysWebSocket: ${baileysWebSocketUrl}`,
774
1001
  ` messages: ${manifest.endpoints.messagesUrl}`,
775
1002
  ` phoneNumber: ${manifest.endpoints.phoneNumberUrl}`,
776
- ` status: ${manifest.endpoints.statusUrl}`,
777
1003
  ` selfJid: ${manifest.selfJid}`,
778
1004
  ];
779
1005
  }
@@ -842,11 +1068,11 @@ function diagnose(manifest) {
842
1068
  }
843
1069
  return findings;
844
1070
  }
845
- export async function runCli(argv) {
1071
+ export async function runCli(argv, options = {}) {
846
1072
  let exitCode = 0;
847
1073
  const program = createProgram((code) => {
848
1074
  exitCode = code;
849
- });
1075
+ }, options.dependencies);
850
1076
  const parserErrors = [];
851
1077
  const parserOutput = [];
852
1078
  const bufferParserErrors = (command) => {
@@ -896,6 +1122,9 @@ export async function runCli(argv) {
896
1122
  else {
897
1123
  await writeOutput(process.stderr, `${ensureErrorMessage(error)}\n`);
898
1124
  }
1125
+ if (options.forceExit && containsWatchShutdownDeadline(error)) {
1126
+ options.forceExit(errorExitCode);
1127
+ }
899
1128
  return errorExitCode;
900
1129
  }
901
1130
  }