@openclaw/crabline 0.1.10 → 0.1.12

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 (197) hide show
  1. package/README.md +99 -12
  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 +264 -50
  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/matrix-ids.d.ts +5 -0
  31. package/dist/src/matrix-ids.js +83 -0
  32. package/dist/src/matrix-ids.js.map +1 -0
  33. package/dist/src/openclaw/artifact-generation.d.ts +12 -2
  34. package/dist/src/openclaw/artifact-generation.js +436 -15
  35. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  36. package/dist/src/openclaw/bridges/matrix.js +21 -4
  37. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  38. package/dist/src/openclaw/bridges/mattermost.js +13 -8
  39. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  40. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  41. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  42. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  43. package/dist/src/openclaw/bridges/signal.js +15 -6
  44. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  45. package/dist/src/openclaw/bridges/slack.js +116 -23
  46. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  47. package/dist/src/openclaw/bridges/telegram.js +45 -19
  48. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  49. package/dist/src/openclaw/bridges/whatsapp.js +32 -16
  50. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  51. package/dist/src/openclaw/bridges/zalo.js +8 -3
  52. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  53. package/dist/src/openclaw/private-file.d.ts +40 -3
  54. package/dist/src/openclaw/private-file.js +2070 -95
  55. package/dist/src/openclaw/private-file.js.map +1 -1
  56. package/dist/src/openclaw/shared.d.ts +1 -0
  57. package/dist/src/openclaw/shared.js +48 -13
  58. package/dist/src/openclaw/shared.js.map +1 -1
  59. package/dist/src/openclaw/smoke-lock.d.ts +9 -0
  60. package/dist/src/openclaw/smoke-lock.js +696 -187
  61. package/dist/src/openclaw/smoke-lock.js.map +1 -1
  62. package/dist/src/openclaw.js +352 -29
  63. package/dist/src/openclaw.js.map +1 -1
  64. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  65. package/dist/src/platform/process-owned-lock.js +1698 -0
  66. package/dist/src/platform/process-owned-lock.js.map +1 -0
  67. package/dist/src/platform/recorder-directory.d.ts +1 -0
  68. package/dist/src/platform/recorder-directory.js +14 -0
  69. package/dist/src/platform/recorder-directory.js.map +1 -0
  70. package/dist/src/platform/windows-acl.d.ts +18 -0
  71. package/dist/src/platform/windows-acl.js +1367 -0
  72. package/dist/src/platform/windows-acl.js.map +1 -0
  73. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  74. package/dist/src/platform/windows-lock-root.js +98 -0
  75. package/dist/src/platform/windows-lock-root.js.map +1 -0
  76. package/dist/src/providers/builtin/discord.d.ts +4 -0
  77. package/dist/src/providers/builtin/discord.js +52 -9
  78. package/dist/src/providers/builtin/discord.js.map +1 -1
  79. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  80. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  81. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  82. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  83. package/dist/src/providers/builtin/feishu.js +96 -26
  84. package/dist/src/providers/builtin/feishu.js.map +1 -1
  85. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  86. package/dist/src/providers/builtin/googlechat.js +95 -26
  87. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  88. package/dist/src/providers/builtin/imessage.js +60 -17
  89. package/dist/src/providers/builtin/imessage.js.map +1 -1
  90. package/dist/src/providers/builtin/loopback.js +88 -35
  91. package/dist/src/providers/builtin/loopback.js.map +1 -1
  92. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  93. package/dist/src/providers/builtin/matrix.js +78 -21
  94. package/dist/src/providers/builtin/matrix.js.map +1 -1
  95. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  96. package/dist/src/providers/builtin/mattermost.js +138 -24
  97. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  98. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  99. package/dist/src/providers/builtin/msteams.js +118 -38
  100. package/dist/src/providers/builtin/msteams.js.map +1 -1
  101. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  102. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  103. package/dist/src/providers/builtin/script.js +1057 -146
  104. package/dist/src/providers/builtin/script.js.map +1 -1
  105. package/dist/src/providers/builtin/slack.d.ts +8 -1
  106. package/dist/src/providers/builtin/slack.js +327 -24
  107. package/dist/src/providers/builtin/slack.js.map +1 -1
  108. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  109. package/dist/src/providers/builtin/telegram.js +55 -14
  110. package/dist/src/providers/builtin/telegram.js.map +1 -1
  111. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  112. package/dist/src/providers/builtin/whatsapp.js +157 -29
  113. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  114. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  115. package/dist/src/providers/builtin/zalo.js +31 -7
  116. package/dist/src/providers/builtin/zalo.js.map +1 -1
  117. package/dist/src/providers/catalog.d.ts +1 -1
  118. package/dist/src/providers/catalog.js +1 -1
  119. package/dist/src/providers/catalog.js.map +1 -1
  120. package/dist/src/providers/local-mock.d.ts +4 -0
  121. package/dist/src/providers/local-mock.js +162 -35
  122. package/dist/src/providers/local-mock.js.map +1 -1
  123. package/dist/src/providers/native-ids.d.ts +2 -0
  124. package/dist/src/providers/native-ids.js +3 -0
  125. package/dist/src/providers/native-ids.js.map +1 -1
  126. package/dist/src/providers/recorder.d.ts +14 -3
  127. package/dist/src/providers/recorder.js +532 -42
  128. package/dist/src/providers/recorder.js.map +1 -1
  129. package/dist/src/providers/registry.d.ts +1 -0
  130. package/dist/src/providers/registry.js +65 -20
  131. package/dist/src/providers/registry.js.map +1 -1
  132. package/dist/src/providers/signed-jwt.d.ts +6 -1
  133. package/dist/src/providers/signed-jwt.js +241 -50
  134. package/dist/src/providers/signed-jwt.js.map +1 -1
  135. package/dist/src/providers/slack-ids.d.ts +1 -0
  136. package/dist/src/providers/slack-ids.js +5 -0
  137. package/dist/src/providers/slack-ids.js.map +1 -1
  138. package/dist/src/providers/target-normalizers.d.ts +9 -1
  139. package/dist/src/providers/target-normalizers.js +177 -58
  140. package/dist/src/providers/target-normalizers.js.map +1 -1
  141. package/dist/src/providers/webhook-server.d.ts +3 -0
  142. package/dist/src/providers/webhook-server.js +268 -47
  143. package/dist/src/providers/webhook-server.js.map +1 -1
  144. package/dist/src/servers/http.d.ts +14 -2
  145. package/dist/src/servers/http.js +229 -58
  146. package/dist/src/servers/http.js.map +1 -1
  147. package/dist/src/servers/matrix.d.ts +3 -0
  148. package/dist/src/servers/matrix.js +268 -110
  149. package/dist/src/servers/matrix.js.map +1 -1
  150. package/dist/src/servers/mattermost.d.ts +5 -0
  151. package/dist/src/servers/mattermost.js +389 -91
  152. package/dist/src/servers/mattermost.js.map +1 -1
  153. package/dist/src/servers/recorder.d.ts +11 -0
  154. package/dist/src/servers/recorder.js +1241 -22
  155. package/dist/src/servers/recorder.js.map +1 -1
  156. package/dist/src/servers/signal.d.ts +1 -0
  157. package/dist/src/servers/signal.js +279 -44
  158. package/dist/src/servers/signal.js.map +1 -1
  159. package/dist/src/servers/slack.d.ts +15 -0
  160. package/dist/src/servers/slack.js +259 -51
  161. package/dist/src/servers/slack.js.map +1 -1
  162. package/dist/src/servers/telegram-identity.d.ts +6 -0
  163. package/dist/src/servers/telegram-identity.js +27 -0
  164. package/dist/src/servers/telegram-identity.js.map +1 -0
  165. package/dist/src/servers/telegram.d.ts +17 -6
  166. package/dist/src/servers/telegram.js +1193 -118
  167. package/dist/src/servers/telegram.js.map +1 -1
  168. package/dist/src/servers/webhook-target.d.ts +12 -4
  169. package/dist/src/servers/webhook-target.js +226 -28
  170. package/dist/src/servers/webhook-target.js.map +1 -1
  171. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  172. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  173. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  174. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  175. package/dist/src/servers/whatsapp-jid.js +9 -5
  176. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  177. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  178. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  179. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  180. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  181. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  182. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  183. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  184. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  185. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  186. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  187. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  188. package/dist/src/servers/whatsapp.d.ts +7 -0
  189. package/dist/src/servers/whatsapp.js +149 -24
  190. package/dist/src/servers/whatsapp.js.map +1 -1
  191. package/dist/src/servers/zalo.d.ts +1 -0
  192. package/dist/src/servers/zalo.js +193 -82
  193. package/dist/src/servers/zalo.js.map +1 -1
  194. package/docs/channel-setup.md +185 -43
  195. package/fixtures/examples/crabline.example.yaml +3 -2
  196. package/package.json +18 -10
  197. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -1,17 +1,28 @@
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 = {
16
27
  mattermost: (shared, commandOptions) => ({
17
28
  ...shared,
@@ -126,9 +137,150 @@ async function writeOutput(stream, value) {
126
137
  async function print(value) {
127
138
  return await writeOutput(process.stdout, `${value}\n`);
128
139
  }
140
+ class WatchShutdownDeadlineError extends CrablineError {
141
+ }
142
+ function createWatchShutdownDeadline() {
143
+ let deadlineAt;
144
+ const controller = new AbortController();
145
+ return {
146
+ async race(operation, operationName) {
147
+ let timer;
148
+ let removeStartListener;
149
+ const deadline = new Promise((_, reject) => {
150
+ const arm = () => {
151
+ timer = setTimeout(() => {
152
+ reject(new WatchShutdownDeadlineError(`Provider ${operationName} did not settle within ${WATCH_SHUTDOWN_GRACE_MS}ms during watch shutdown.`, { kind: "timeout" }));
153
+ }, Math.max(0, deadlineAt - Date.now()));
154
+ };
155
+ if (controller.signal.aborted) {
156
+ arm();
157
+ return;
158
+ }
159
+ controller.signal.addEventListener("abort", arm, { once: true });
160
+ removeStartListener = () => controller.signal.removeEventListener("abort", arm);
161
+ });
162
+ try {
163
+ return await Promise.race([Promise.resolve(operation), deadline]);
164
+ }
165
+ finally {
166
+ removeStartListener?.();
167
+ if (timer) {
168
+ clearTimeout(timer);
169
+ }
170
+ }
171
+ },
172
+ start() {
173
+ if (deadlineAt !== undefined) {
174
+ return;
175
+ }
176
+ deadlineAt = Date.now() + WATCH_SHUTDOWN_GRACE_MS;
177
+ controller.abort();
178
+ },
179
+ };
180
+ }
181
+ function containsWatchShutdownDeadline(error, seen = new Set()) {
182
+ if (error instanceof WatchShutdownDeadlineError) {
183
+ return true;
184
+ }
185
+ if (!error || typeof error !== "object" || seen.has(error)) {
186
+ return false;
187
+ }
188
+ seen.add(error);
189
+ if (error instanceof AggregateError) {
190
+ return error.errors.some((entry) => containsWatchShutdownDeadline(entry, seen));
191
+ }
192
+ return "cause" in error && containsWatchShutdownDeadline(error.cause, seen);
193
+ }
129
194
  async function withManifest(options, action) {
130
195
  return action(await loadManifest(options.config));
131
196
  }
197
+ function rejectedSecretOption(flags, optionName, envName) {
198
+ return new Option(flags).hideHelp().argParser(() => {
199
+ throw new CrablineError(`${optionName} no longer accepts credential values in command-line arguments. Use ${envName} or --credentials-fd.`, { kind: "config" });
200
+ });
201
+ }
202
+ function parseCredentialsFd(value) {
203
+ if (!/^[0-9]+$/u.test(value)) {
204
+ throw new CrablineError("Serve credentials file descriptor must be 0 or an integer greater than 2.", { kind: "config" });
205
+ }
206
+ const fd = Number(value);
207
+ if (!Number.isSafeInteger(fd) || fd === 1 || fd === 2 || fd > 2_147_483_647) {
208
+ throw new CrablineError("Serve credentials file descriptor must be 0 or an integer greater than 2.", { kind: "config" });
209
+ }
210
+ return fd;
211
+ }
212
+ async function readBoundedFileDescriptor(fd) {
213
+ const stream = createReadStream("", { autoClose: fd !== 0, fd });
214
+ const chunks = [];
215
+ let totalBytes = 0;
216
+ try {
217
+ for await (const chunk of stream) {
218
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
219
+ totalBytes += buffer.byteLength;
220
+ if (totalBytes > MAX_SERVE_CREDENTIALS_BYTES) {
221
+ throw new CrablineError(`Serve credentials JSON exceeds ${MAX_SERVE_CREDENTIALS_BYTES} bytes.`, { kind: "config" });
222
+ }
223
+ chunks.push(buffer);
224
+ }
225
+ }
226
+ catch (error) {
227
+ stream.destroy();
228
+ if (error instanceof CrablineError) {
229
+ throw error;
230
+ }
231
+ throw new CrablineError(`Unable to read serve credentials from file descriptor ${fd}.`, {
232
+ cause: error,
233
+ kind: "config",
234
+ });
235
+ }
236
+ finally {
237
+ if (fd !== 0 && !stream.closed) {
238
+ stream.destroy();
239
+ await finished(stream).catch(() => undefined);
240
+ }
241
+ }
242
+ return Buffer.concat(chunks).toString("utf8");
243
+ }
244
+ function parseServeCredentialsJson(input) {
245
+ let parsed;
246
+ try {
247
+ parsed = JSON.parse(input);
248
+ }
249
+ catch (error) {
250
+ throw new CrablineError("Serve credentials input must be valid JSON.", {
251
+ cause: error,
252
+ kind: "config",
253
+ });
254
+ }
255
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
256
+ throw new CrablineError("Serve credentials JSON must be an object.", { kind: "config" });
257
+ }
258
+ const values = {};
259
+ for (const [key, value] of Object.entries(parsed)) {
260
+ if (!SERVE_CREDENTIAL_ENV.some(([name]) => name === key)) {
261
+ throw new CrablineError("Serve credentials JSON contains unsupported fields.", {
262
+ kind: "config",
263
+ });
264
+ }
265
+ if (typeof value !== "string") {
266
+ throw new CrablineError("Serve credentials JSON values must be strings.", {
267
+ kind: "config",
268
+ });
269
+ }
270
+ values[key] = value;
271
+ }
272
+ return values;
273
+ }
274
+ async function resolveServeCredentials(credentialsFd) {
275
+ const overrides = credentialsFd === undefined
276
+ ? {}
277
+ : parseServeCredentialsJson(await readBoundedFileDescriptor(parseCredentialsFd(credentialsFd)));
278
+ return Object.fromEntries(SERVE_CREDENTIAL_ENV.flatMap(([name, envName]) => {
279
+ const override = overrides[name];
280
+ const value = override ?? process.env[envName];
281
+ return value === undefined ? [] : [[name, value]];
282
+ }));
283
+ }
132
284
  export function createProgram(setExitCode = (code) => {
133
285
  process.exitCode = code;
134
286
  }, dependencies = {}) {
@@ -138,6 +290,13 @@ export function createProgram(setExitCode = (code) => {
138
290
  const publish = dependencies.publishReadyFile ?? publishReadyFileUnlocked;
139
291
  const removeReady = dependencies.removeReadyFile ?? removeReadyFileIfOwned;
140
292
  const startServer = dependencies.startServer ?? startCrablineServer;
293
+ const formatCliJson = (value) => {
294
+ const formatted = formatJsonResult(value);
295
+ if (!formatted.ok) {
296
+ setExitCode(EXIT_CODES.FAILURE);
297
+ }
298
+ return formatted.output;
299
+ };
141
300
  program
142
301
  .exitOverride()
143
302
  .name("crabline")
@@ -162,7 +321,7 @@ export function createProgram(setExitCode = (code) => {
162
321
  })),
163
322
  support: registry.catalog,
164
323
  };
165
- await print(options.json ? formatJson(payload) : renderProvidersText(payload));
324
+ await print(options.json ? formatCliJson(payload) : renderProvidersText(payload));
166
325
  });
167
326
  program
168
327
  .command("fixtures")
@@ -171,7 +330,7 @@ export function createProgram(setExitCode = (code) => {
171
330
  const options = program.opts();
172
331
  const { manifest } = await loadManifest(options.config);
173
332
  await print(options.json
174
- ? formatJson(manifest.fixtures)
333
+ ? formatCliJson(manifest.fixtures)
175
334
  : manifest.fixtures
176
335
  .map((fixture) => `${sanitizeTerminalText(fixture.id, true)} ${sanitizeTerminalText(fixture.mode, true)} provider=${sanitizeTerminalText(fixture.provider, true)} target=${sanitizeTerminalText(fixture.target.id, true)}`)
177
336
  .join("\n"));
@@ -194,8 +353,8 @@ export function createProgram(setExitCode = (code) => {
194
353
  modeOverride: "probe",
195
354
  registry,
196
355
  });
197
- await print(options.json ? formatJson(result) : formatRunResultText(result));
198
356
  setExitCode(computeExitCode(result));
357
+ await print(options.json ? formatCliJson(result) : formatRunResultText(result));
199
358
  });
200
359
  for (const mode of ["send", "roundtrip", "agent"]) {
201
360
  program
@@ -212,8 +371,8 @@ export function createProgram(setExitCode = (code) => {
212
371
  modeOverride: mode,
213
372
  registry,
214
373
  });
215
- await print(options.json ? formatJson(result) : formatRunResultText(result));
216
374
  setExitCode(computeExitCode(result));
375
+ await print(options.json ? formatCliJson(result) : formatRunResultText(result));
217
376
  });
218
377
  }
219
378
  program
@@ -229,8 +388,8 @@ export function createProgram(setExitCode = (code) => {
229
388
  manifestPath: path,
230
389
  registry,
231
390
  });
232
- await print(options.json ? formatJson(result) : formatRunResultText(result));
233
391
  setExitCode(computeExitCode(result));
392
+ await print(options.json ? formatCliJson(result) : formatRunResultText(result));
234
393
  });
235
394
  program
236
395
  .command("watch <fixtureId>")
@@ -250,12 +409,18 @@ export function createProgram(setExitCode = (code) => {
250
409
  });
251
410
  }
252
411
  const controller = new AbortController();
412
+ const shutdownDeadline = createWatchShutdownDeadline();
253
413
  let iterator;
254
414
  const stopWatch = onceAsync(async () => {
415
+ shutdownDeadline.start();
255
416
  controller.abort();
256
- await iterator?.return?.();
417
+ const returnResult = iterator?.return?.();
418
+ if (returnResult) {
419
+ await shutdownDeadline.race(returnResult, "watch iterator return");
420
+ }
257
421
  });
258
422
  const shutdown = installShutdownHandler(stopWatch);
423
+ const shutdownSettled = shutdown.wait().then(() => WATCH_SHUTDOWN_REACHED);
259
424
  const lifecycleErrors = [];
260
425
  try {
261
426
  const watchContext = {
@@ -282,14 +447,17 @@ export function createProgram(setExitCode = (code) => {
282
447
  const watch = provider.watch(watchContext);
283
448
  iterator = watch[Symbol.asyncIterator]();
284
449
  while (true) {
285
- const result = await iterator.next();
450
+ const result = await Promise.race([iterator.next(), shutdownSettled]);
451
+ if (typeof result === "symbol") {
452
+ break;
453
+ }
286
454
  if (result.done) {
287
455
  break;
288
456
  }
289
457
  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)}`);
458
+ const written = await shutdownDeadline.race(print(options.json
459
+ ? formatCliJson(message)
460
+ : `${sanitizeTerminalText(message.sentAt, true)} ${sanitizeTerminalText(message.author, true)} ${sanitizeTerminalText(message.text, true)}`), "watch output write");
293
461
  if (!written) {
294
462
  break;
295
463
  }
@@ -307,7 +475,10 @@ export function createProgram(setExitCode = (code) => {
307
475
  }
308
476
  }
309
477
  try {
310
- await provider.cleanup?.();
478
+ const cleanup = provider.cleanup?.();
479
+ if (cleanup) {
480
+ await shutdownDeadline.race(cleanup, "cleanup");
481
+ }
311
482
  }
312
483
  catch (error) {
313
484
  if (!lifecycleErrors.includes(error)) {
@@ -327,17 +498,24 @@ export function createProgram(setExitCode = (code) => {
327
498
  .description("Start a local provider server that OpenClaw live adapters can target")
328
499
  .option("--host <host>", "Bind host", "127.0.0.1")
329
500
  .option("--port <port>", "Bind port", "0")
330
- .option("--admin-token <token>", "Admin token for inbound test messages (or CRABLINE_ADMIN_TOKEN)")
331
501
  .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
502
  .option("--bot-username <username>", "Mattermost, Telegram, or Zalo bot username", "crabline_bot")
503
+ .option("--credentials-fd <fd>", "Read JSON credentials from fd 0 or an inherited fd; values override CRABLINE_* env")
335
504
  .option("--recorder <path>", "JSONL recorder path")
336
505
  .option("--ready-file <path>", "Write the server runtime manifest to this path")
337
506
  .option("--self-jid <jid>", "WhatsApp self JID")
338
507
  .option("--show-secrets", "Print provider credentials in text output", false)
339
- .option("--signing-secret <secret>", "Slack signing secret (or CRABLINE_SIGNING_SECRET)")
340
508
  .option("--once", "Start, print the runtime manifest, and stop immediately", false)
509
+ .addOption(rejectedSecretOption("--access-token <token>", "--access-token", "CRABLINE_ACCESS_TOKEN"))
510
+ .addOption(rejectedSecretOption("--admin-token <token>", "--admin-token", "CRABLINE_ADMIN_TOKEN"))
511
+ .addOption(rejectedSecretOption("--bot-token <token>", "--bot-token", "CRABLINE_BOT_TOKEN"))
512
+ .addOption(rejectedSecretOption("--signing-secret <secret>", "--signing-secret", "CRABLINE_SIGNING_SECRET"))
513
+ .addHelpText("after", [
514
+ "",
515
+ "Credential JSON fields: accessToken, adminToken, botToken, signingSecret",
516
+ "Environment fallbacks: CRABLINE_ACCESS_TOKEN, CRABLINE_ADMIN_TOKEN,",
517
+ " CRABLINE_BOT_TOKEN, CRABLINE_SIGNING_SECRET",
518
+ ].join("\n"))
341
519
  .action(async (provider, commandOptions) => {
342
520
  const options = program.opts();
343
521
  if (!isCrablineServerChannel(provider)) {
@@ -345,10 +523,6 @@ export function createProgram(setExitCode = (code) => {
345
523
  kind: "config",
346
524
  });
347
525
  }
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
526
  if (!/^[0-9]+$/u.test(commandOptions.port)) {
353
527
  throw new CrablineError(`Invalid local server port: ${commandOptions.port}`, {
354
528
  kind: "config",
@@ -366,6 +540,11 @@ export function createProgram(setExitCode = (code) => {
366
540
  kind: "config",
367
541
  });
368
542
  }
543
+ const credentials = await resolveServeCredentials(commandOptions.credentialsFd);
544
+ commandOptions.accessToken = credentials.accessToken;
545
+ commandOptions.adminToken = credentials.adminToken;
546
+ commandOptions.botToken = credentials.botToken;
547
+ commandOptions.signingSecret = credentials.signingSecret;
369
548
  let server;
370
549
  let startupSettled = false;
371
550
  let settleStartup;
@@ -389,14 +568,41 @@ export function createProgram(setExitCode = (code) => {
389
568
  settlePublication?.();
390
569
  }
391
570
  };
571
+ let releaseReadyLease;
572
+ let publishedReady;
392
573
  const close = onceAsync(async () => {
393
574
  await startup;
394
575
  await publication;
395
- await server?.close();
576
+ const lifecycleErrors = [];
577
+ if (commandOptions.readyFile && publishedReady) {
578
+ try {
579
+ await removeReady(commandOptions.readyFile, publishedReady.contents, publishedReady.identity);
580
+ }
581
+ catch (error) {
582
+ lifecycleErrors.push(error);
583
+ }
584
+ }
585
+ let serverClosed = false;
586
+ try {
587
+ await server?.close();
588
+ serverClosed = true;
589
+ }
590
+ catch (error) {
591
+ lifecycleErrors.push(error);
592
+ }
593
+ if (serverClosed && releaseReadyLease) {
594
+ try {
595
+ await releaseReadyLease();
596
+ }
597
+ catch (error) {
598
+ lifecycleErrors.push(error);
599
+ }
600
+ }
601
+ if (lifecycleErrors.length > 0) {
602
+ throw combineLifecycleErrors(lifecycleErrors, "Crabline server shutdown failed.");
603
+ }
396
604
  });
397
605
  const shutdown = installShutdownHandler(close);
398
- let releaseReadyLease;
399
- let publishedReady;
400
606
  let actionFailed = false;
401
607
  let actionError;
402
608
  try {
@@ -424,8 +630,13 @@ export function createProgram(setExitCode = (code) => {
424
630
  await shutdown.wait();
425
631
  }
426
632
  else {
633
+ let outputWritten = true;
427
634
  try {
428
- const payload = formatJson(server.manifest);
635
+ const formatted = formatJsonResult(server.manifest);
636
+ if (!formatted.ok) {
637
+ throw new Error("Unable to serialize server manifest.");
638
+ }
639
+ const payload = formatted.output;
429
640
  if (commandOptions.readyFile) {
430
641
  const readyContents = `${payload}\n`;
431
642
  publishedReady = {
@@ -433,14 +644,25 @@ export function createProgram(setExitCode = (code) => {
433
644
  identity: await publish(commandOptions.readyFile, readyContents),
434
645
  };
435
646
  }
436
- await print(options.json
437
- ? payload
438
- : renderServeText(server.manifest, commandOptions.showSecrets === true));
647
+ if (shutdown.requested()) {
648
+ outputWritten = false;
649
+ finishPublication();
650
+ await shutdown.wait();
651
+ }
652
+ else {
653
+ outputWritten = await print(options.json
654
+ ? payload
655
+ : renderServeText(server.manifest, commandOptions.showSecrets === true));
656
+ if (!outputWritten) {
657
+ finishPublication();
658
+ await close();
659
+ }
660
+ }
439
661
  }
440
662
  finally {
441
663
  finishPublication();
442
664
  }
443
- if (shutdown.requested() || !commandOptions.once) {
665
+ if (outputWritten && (shutdown.requested() || !commandOptions.once)) {
444
666
  await shutdown.wait();
445
667
  }
446
668
  }
@@ -453,23 +675,12 @@ export function createProgram(setExitCode = (code) => {
453
675
  finishStartup();
454
676
  finishPublication();
455
677
  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
- }
678
+ try {
679
+ await close();
680
+ }
681
+ catch (error) {
682
+ if (!actionFailed || error !== actionError) {
683
+ cleanupErrors.push(error);
473
684
  }
474
685
  }
475
686
  shutdown.dispose();
@@ -486,8 +697,8 @@ export function createProgram(setExitCode = (code) => {
486
697
  const findings = diagnose(manifest);
487
698
  const ok = findings.length === 0;
488
699
  const payload = { findings, ok };
489
- await print(options.json ? formatJson(payload) : ok ? "doctor ok" : findings.join("\n"));
490
700
  setExitCode(ok ? 0 : 10);
701
+ await print(options.json ? formatCliJson(payload) : ok ? "doctor ok" : findings.join("\n"));
491
702
  });
492
703
  return program;
493
704
  }
@@ -842,11 +1053,11 @@ function diagnose(manifest) {
842
1053
  }
843
1054
  return findings;
844
1055
  }
845
- export async function runCli(argv) {
1056
+ export async function runCli(argv, options = {}) {
846
1057
  let exitCode = 0;
847
1058
  const program = createProgram((code) => {
848
1059
  exitCode = code;
849
- });
1060
+ }, options.dependencies);
850
1061
  const parserErrors = [];
851
1062
  const parserOutput = [];
852
1063
  const bufferParserErrors = (command) => {
@@ -896,6 +1107,9 @@ export async function runCli(argv) {
896
1107
  else {
897
1108
  await writeOutput(process.stderr, `${ensureErrorMessage(error)}\n`);
898
1109
  }
1110
+ if (options.forceExit && containsWatchShutdownDeadline(error)) {
1111
+ options.forceExit(errorExitCode);
1112
+ }
899
1113
  return errorExitCode;
900
1114
  }
901
1115
  }