@riddledc/openclaw-riddledc 0.9.0 → 0.9.2

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.
package/CHECKSUMS.txt CHANGED
@@ -1,4 +1,4 @@
1
- 73579dcc6d9a149e5af0393db55cc09ea8f577a7902ab5271d30d1fb5606990e dist/index.cjs
1
+ d6ac8bd939d92590308b0293b254e197ab326064044435f645373ca289923cc7 dist/index.cjs
2
2
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
3
3
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
4
- 4b2ea1733e0fed32177f31fc850703b9c888ea8d69dd66ce83907f2c8b758855 dist/index.js
4
+ 4b17886fc543f50ded46e6240427ec6652dde694285642ae10463e03aa10c364 dist/index.js
package/dist/index.cjs CHANGED
@@ -30,6 +30,11 @@ var import_node_child_process = require("child_process");
30
30
  var import_node_util = require("util");
31
31
  var execFile = (0, import_node_util.promisify)(import_node_child_process.execFile);
32
32
  var INLINE_CAP = 50 * 1024;
33
+ var PREVIEW_REQUEST_TIMEOUT_MS = 3e4;
34
+ var PREVIEW_UPLOAD_TIMEOUT_MS = 5 * 6e4;
35
+ var PREVIEW_ARTIFACT_TIMEOUT_MS = 6e4;
36
+ var PREVIEW_RETRY_ATTEMPTS = 3;
37
+ var PREVIEW_RETRY_BASE_DELAY_MS = 750;
33
38
  function getCfg(api) {
34
39
  const cfg = api?.config ?? {};
35
40
  const pluginCfg = cfg?.plugins?.entries?.["openclaw-riddledc"]?.config ?? {};
@@ -75,6 +80,76 @@ function abToBase64(ab) {
75
80
  function getWorkspacePath(api) {
76
81
  return api?.workspacePath ?? process.cwd();
77
82
  }
83
+ function describeError(err) {
84
+ const anyErr = err;
85
+ const parts = [];
86
+ if (err instanceof Error) parts.push(err.message);
87
+ else parts.push(String(err));
88
+ const cause = anyErr?.cause;
89
+ if (cause) {
90
+ const causeParts = [
91
+ cause.code ? `code=${cause.code}` : "",
92
+ cause.name ? `name=${cause.name}` : "",
93
+ cause.message ? `message=${cause.message}` : ""
94
+ ].filter(Boolean);
95
+ if (causeParts.length) parts.push(`cause: ${causeParts.join(" ")}`);
96
+ }
97
+ return parts.join("; ");
98
+ }
99
+ async function fetchWithTimeout(url, init, timeoutMs, label) {
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
102
+ try {
103
+ return await fetch(url, { ...init, signal: controller.signal });
104
+ } catch (err) {
105
+ if (err?.name === "AbortError") {
106
+ throw new Error(`${label} timed out after ${Math.round(timeoutMs / 1e3)}s`);
107
+ }
108
+ throw err;
109
+ } finally {
110
+ clearTimeout(timer);
111
+ }
112
+ }
113
+ function isTransientFetchError(err) {
114
+ const text = describeError(err).toLowerCase();
115
+ return [
116
+ "fetch failed",
117
+ "timed out",
118
+ "timeout",
119
+ "econnreset",
120
+ "econnrefused",
121
+ "etimedout",
122
+ "eai_again",
123
+ "socket",
124
+ "network",
125
+ "und_err",
126
+ "terminated"
127
+ ].some((needle) => text.includes(needle));
128
+ }
129
+ function sleep(ms) {
130
+ return new Promise((resolve) => setTimeout(resolve, ms));
131
+ }
132
+ async function fetchWithRetry(url, init, timeoutMs, label, opts = {}) {
133
+ const attempts = Math.max(1, opts.attempts ?? PREVIEW_RETRY_ATTEMPTS);
134
+ const baseDelayMs = opts.baseDelayMs ?? PREVIEW_RETRY_BASE_DELAY_MS;
135
+ let lastErr;
136
+ for (let attempt = 1; attempt <= attempts; attempt++) {
137
+ try {
138
+ return await fetchWithTimeout(url, init, timeoutMs, label);
139
+ } catch (err) {
140
+ lastErr = err;
141
+ if (attempt >= attempts || !isTransientFetchError(err)) break;
142
+ const jitterMs = Math.floor(Math.random() * 250);
143
+ const delayMs = Math.min(baseDelayMs * Math.pow(2, attempt - 1) + jitterMs, 5e3);
144
+ console.warn(`[openclaw-riddledc] ${label} attempt ${attempt}/${attempts} failed: ${describeError(err)}; retrying in ${delayMs}ms`);
145
+ await sleep(delayMs);
146
+ }
147
+ }
148
+ throw new Error(`${label} failed after ${attempts} attempts: ${describeError(lastErr)}`);
149
+ }
150
+ function isAlreadyStartedResponse(status, body) {
151
+ return status === 409 && /already in status:\s*(queued|running|complete|completed)/i.test(body);
152
+ }
78
153
  async function writeArtifact(workspace, subdir, filename, content) {
79
154
  const dir = (0, import_node_path.join)(workspace, "riddle", subdir);
80
155
  await (0, import_promises.mkdir)(dir, { recursive: true });
@@ -823,11 +898,16 @@ function register(api) {
823
898
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
824
899
  }
825
900
  const endpoint = baseUrl.replace(/\/$/, "");
826
- const createRes = await fetch(`${endpoint}/v1/preview`, {
827
- method: "POST",
828
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
829
- body: JSON.stringify({ framework: params.framework || "spa" })
830
- });
901
+ let createRes;
902
+ try {
903
+ createRes = await fetchWithRetry(`${endpoint}/v1/preview`, {
904
+ method: "POST",
905
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
906
+ body: JSON.stringify({ framework: params.framework || "spa" })
907
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview create");
908
+ } catch (e) {
909
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError(e)}` }, null, 2) }] };
910
+ }
831
911
  if (!createRes.ok) {
832
912
  const err = await createRes.text();
833
913
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
@@ -837,11 +917,16 @@ function register(api) {
837
917
  try {
838
918
  await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 6e4 });
839
919
  const tarData = await (0, import_promises.readFile)(tarball);
840
- const uploadRes = await fetch(created.upload_url, {
841
- method: "PUT",
842
- headers: { "Content-Type": "application/gzip" },
843
- body: tarData
844
- });
920
+ let uploadRes;
921
+ try {
922
+ uploadRes = await fetchWithRetry(created.upload_url, {
923
+ method: "PUT",
924
+ headers: { "Content-Type": "application/gzip" },
925
+ body: tarData
926
+ }, PREVIEW_UPLOAD_TIMEOUT_MS, "preview upload");
927
+ } catch (e) {
928
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: ${describeError(e)}` }, null, 2) }] };
929
+ }
845
930
  if (!uploadRes.ok) {
846
931
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
847
932
  }
@@ -851,10 +936,15 @@ function register(api) {
851
936
  } catch {
852
937
  }
853
938
  }
854
- const publishRes = await fetch(`${endpoint}/v1/preview/${created.id}/publish`, {
855
- method: "POST",
856
- headers: { Authorization: `Bearer ${apiKey}` }
857
- });
939
+ let publishRes;
940
+ try {
941
+ publishRes = await fetchWithTimeout(`${endpoint}/v1/preview/${created.id}/publish`, {
942
+ method: "POST",
943
+ headers: { Authorization: `Bearer ${apiKey}` }
944
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview publish");
945
+ } catch (e) {
946
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: ${describeError(e)}` }, null, 2) }] };
947
+ }
858
948
  if (!publishRes.ok) {
859
949
  const err = await publishRes.text();
860
950
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` }, null, 2) }] };
@@ -890,10 +980,15 @@ function register(api) {
890
980
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
891
981
  }
892
982
  assertAllowedBaseUrl(baseUrl);
893
- const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
894
- method: "DELETE",
895
- headers: { Authorization: `Bearer ${apiKey}` }
896
- });
983
+ let res;
984
+ try {
985
+ res = await fetchWithTimeout(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
986
+ method: "DELETE",
987
+ headers: { Authorization: `Bearer ${apiKey}` }
988
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview delete");
989
+ } catch (e) {
990
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: ${describeError(e)}` }, null, 2) }] };
991
+ }
897
992
  if (!res.ok) {
898
993
  const err = await res.text();
899
994
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: HTTP ${res.status} ${err}` }, null, 2) }] };
@@ -951,11 +1046,16 @@ function register(api) {
951
1046
  const envBody = {};
952
1047
  if (hasSensitiveEnv) envBody.env = params.sensitive_env;
953
1048
  if (hasLocalStorage) envBody.localStorage = params.localStorage;
954
- const envRes = await fetch(`${endpoint}/v1/server-preview/env`, {
955
- method: "POST",
956
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
957
- body: JSON.stringify(envBody)
958
- });
1049
+ let envRes;
1050
+ try {
1051
+ envRes = await fetchWithTimeout(`${endpoint}/v1/server-preview/env`, {
1052
+ method: "POST",
1053
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1054
+ body: JSON.stringify(envBody)
1055
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview env store");
1056
+ } catch (e) {
1057
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError(e)}` }, null, 2) }] };
1058
+ }
959
1059
  if (!envRes.ok) {
960
1060
  const err = await envRes.text();
961
1061
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
@@ -981,11 +1081,16 @@ function register(api) {
981
1081
  if (params.navigation_timeout) createBody.navigation_timeout = params.navigation_timeout;
982
1082
  if (params.color_scheme) createBody.color_scheme = params.color_scheme;
983
1083
  if (params.viewport) createBody.viewport = params.viewport;
984
- const createRes = await fetch(`${endpoint}/v1/server-preview`, {
985
- method: "POST",
986
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
987
- body: JSON.stringify(createBody)
988
- });
1084
+ let createRes;
1085
+ try {
1086
+ createRes = await fetchWithRetry(`${endpoint}/v1/server-preview`, {
1087
+ method: "POST",
1088
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1089
+ body: JSON.stringify(createBody)
1090
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview create");
1091
+ } catch (e) {
1092
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError(e)}` }, null, 2) }] };
1093
+ }
989
1094
  if (!createRes.ok) {
990
1095
  const err = await createRes.text();
991
1096
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
@@ -997,11 +1102,16 @@ function register(api) {
997
1102
  const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
998
1103
  await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
999
1104
  const tarData = await (0, import_promises.readFile)(tarball);
1000
- const uploadRes = await fetch(created.upload_url, {
1001
- method: "PUT",
1002
- headers: { "Content-Type": "application/gzip" },
1003
- body: tarData
1004
- });
1105
+ let uploadRes;
1106
+ try {
1107
+ uploadRes = await fetchWithRetry(created.upload_url, {
1108
+ method: "PUT",
1109
+ headers: { "Content-Type": "application/gzip" },
1110
+ body: tarData
1111
+ }, PREVIEW_UPLOAD_TIMEOUT_MS, "server preview upload");
1112
+ } catch (e) {
1113
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError(e)}` }, null, 2) }] };
1114
+ }
1005
1115
  if (!uploadRes.ok) {
1006
1116
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
1007
1117
  }
@@ -1011,21 +1121,35 @@ function register(api) {
1011
1121
  } catch {
1012
1122
  }
1013
1123
  }
1014
- const startRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
1015
- method: "POST",
1016
- headers: { Authorization: `Bearer ${apiKey}` }
1017
- });
1124
+ let startRes;
1125
+ try {
1126
+ startRes = await fetchWithRetry(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
1127
+ method: "POST",
1128
+ headers: { Authorization: `Bearer ${apiKey}` }
1129
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview start");
1130
+ } catch (e) {
1131
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError(e)}` }, null, 2) }] };
1132
+ }
1018
1133
  if (!startRes.ok) {
1019
1134
  const err = await startRes.text();
1020
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1135
+ if (isAlreadyStartedResponse(startRes.status, err)) {
1136
+ console.warn(`[openclaw-riddledc] server preview start returned ${startRes.status} for ${created.job_id}; continuing to poll`);
1137
+ } else {
1138
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1139
+ }
1021
1140
  }
1022
1141
  const timeoutMs = ((params.timeout || 120) + 60) * 1e3;
1023
1142
  const pollStart = Date.now();
1024
1143
  const POLL_INTERVAL = 3e3;
1025
1144
  while (Date.now() - pollStart < timeoutMs) {
1026
- const statusRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}`, {
1027
- headers: { Authorization: `Bearer ${apiKey}` }
1028
- });
1145
+ let statusRes;
1146
+ try {
1147
+ statusRes = await fetchWithRetry(`${endpoint}/v1/server-preview/${created.job_id}`, {
1148
+ headers: { Authorization: `Bearer ${apiKey}` }
1149
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview poll", { attempts: 2 });
1150
+ } catch (e) {
1151
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError(e)}` }, null, 2) }] };
1152
+ }
1029
1153
  if (!statusRes.ok) {
1030
1154
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
1031
1155
  }
@@ -1044,7 +1168,7 @@ function register(api) {
1044
1168
  for (const output of result.outputs) {
1045
1169
  if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
1046
1170
  try {
1047
- const imgRes = await fetch(output.url);
1171
+ const imgRes = await fetchWithTimeout(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "server preview artifact download");
1048
1172
  if (imgRes.ok) {
1049
1173
  const buf = await imgRes.arrayBuffer();
1050
1174
  const base64 = Buffer.from(buf).toString("base64");
@@ -1120,11 +1244,16 @@ function register(api) {
1120
1244
  const envBody = {};
1121
1245
  if (hasSensitiveEnv) envBody.env = params.sensitive_env;
1122
1246
  if (hasLocalStorage) envBody.localStorage = params.localStorage;
1123
- const envRes = await fetch(`${endpoint}/v1/build-preview/env`, {
1124
- method: "POST",
1125
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1126
- body: JSON.stringify(envBody)
1127
- });
1247
+ let envRes;
1248
+ try {
1249
+ envRes = await fetchWithTimeout(`${endpoint}/v1/build-preview/env`, {
1250
+ method: "POST",
1251
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1252
+ body: JSON.stringify(envBody)
1253
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview env store");
1254
+ } catch (e) {
1255
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError(e)}` }, null, 2) }] };
1256
+ }
1128
1257
  if (!envRes.ok) {
1129
1258
  const err = await envRes.text();
1130
1259
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
@@ -1152,11 +1281,16 @@ function register(api) {
1152
1281
  if (params.color_scheme) createBody.color_scheme = params.color_scheme;
1153
1282
  if (params.viewport) createBody.viewport = params.viewport;
1154
1283
  if (params.audit) createBody.audit = true;
1155
- const createRes = await fetch(`${endpoint}/v1/build-preview`, {
1156
- method: "POST",
1157
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1158
- body: JSON.stringify(createBody)
1159
- });
1284
+ let createRes;
1285
+ try {
1286
+ createRes = await fetchWithRetry(`${endpoint}/v1/build-preview`, {
1287
+ method: "POST",
1288
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1289
+ body: JSON.stringify(createBody)
1290
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview create");
1291
+ } catch (e) {
1292
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError(e)}` }, null, 2) }] };
1293
+ }
1160
1294
  if (!createRes.ok) {
1161
1295
  const err = await createRes.text();
1162
1296
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
@@ -1168,11 +1302,16 @@ function register(api) {
1168
1302
  const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
1169
1303
  await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1170
1304
  const tarData = await (0, import_promises.readFile)(tarball);
1171
- const uploadRes = await fetch(created.upload_url, {
1172
- method: "PUT",
1173
- headers: { "Content-Type": "application/gzip" },
1174
- body: tarData
1175
- });
1305
+ let uploadRes;
1306
+ try {
1307
+ uploadRes = await fetchWithRetry(created.upload_url, {
1308
+ method: "PUT",
1309
+ headers: { "Content-Type": "application/gzip" },
1310
+ body: tarData
1311
+ }, PREVIEW_UPLOAD_TIMEOUT_MS, "build preview upload");
1312
+ } catch (e) {
1313
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError(e)}` }, null, 2) }] };
1314
+ }
1176
1315
  if (!uploadRes.ok) {
1177
1316
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
1178
1317
  }
@@ -1182,21 +1321,35 @@ function register(api) {
1182
1321
  } catch {
1183
1322
  }
1184
1323
  }
1185
- const startRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
1186
- method: "POST",
1187
- headers: { Authorization: `Bearer ${apiKey}` }
1188
- });
1324
+ let startRes;
1325
+ try {
1326
+ startRes = await fetchWithRetry(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
1327
+ method: "POST",
1328
+ headers: { Authorization: `Bearer ${apiKey}` }
1329
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview start");
1330
+ } catch (e) {
1331
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError(e)}` }, null, 2) }] };
1332
+ }
1189
1333
  if (!startRes.ok) {
1190
1334
  const err = await startRes.text();
1191
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1335
+ if (isAlreadyStartedResponse(startRes.status, err)) {
1336
+ console.warn(`[openclaw-riddledc] build preview start returned ${startRes.status} for ${created.job_id}; continuing to poll`);
1337
+ } else {
1338
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1339
+ }
1192
1340
  }
1193
1341
  const timeoutMs = ((params.timeout || 180) + 120) * 1e3;
1194
1342
  const pollStart = Date.now();
1195
1343
  const POLL_INTERVAL = 3e3;
1196
1344
  while (Date.now() - pollStart < timeoutMs) {
1197
- const statusRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}`, {
1198
- headers: { Authorization: `Bearer ${apiKey}` }
1199
- });
1345
+ let statusRes;
1346
+ try {
1347
+ statusRes = await fetchWithRetry(`${endpoint}/v1/build-preview/${created.job_id}`, {
1348
+ headers: { Authorization: `Bearer ${apiKey}` }
1349
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview poll", { attempts: 2 });
1350
+ } catch (e) {
1351
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError(e)}` }, null, 2) }] };
1352
+ }
1200
1353
  if (!statusRes.ok) {
1201
1354
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
1202
1355
  }
@@ -1219,7 +1372,7 @@ function register(api) {
1219
1372
  for (const output of result.outputs) {
1220
1373
  if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
1221
1374
  try {
1222
- const imgRes = await fetch(output.url);
1375
+ const imgRes = await fetchWithTimeout(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "build preview artifact download");
1223
1376
  if (imgRes.ok) {
1224
1377
  const buf = await imgRes.arrayBuffer();
1225
1378
  const base64 = Buffer.from(buf).toString("base64");
@@ -1299,13 +1452,23 @@ function register(api) {
1299
1452
  url: import_typebox.Type.Optional(import_typebox.Type.String({ description: "URL to navigate to" })),
1300
1453
  script: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Playwright script to execute" })),
1301
1454
  steps: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.Any(), { description: "Declarative steps (alternative to script)" })),
1302
- timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max execution time in seconds (default: 60)" }))
1455
+ timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max execution time in seconds (default: 60)" })),
1456
+ stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Default: false" })),
1457
+ custom_storage: import_typebox.Type.Optional(import_typebox.Type.Object({
1458
+ bucket: import_typebox.Type.String(),
1459
+ region: import_typebox.Type.Optional(import_typebox.Type.String()),
1460
+ prefix: import_typebox.Type.Optional(import_typebox.Type.String()),
1461
+ cdn_domain: import_typebox.Type.Optional(import_typebox.Type.String()),
1462
+ role_arn: import_typebox.Type.Optional(import_typebox.Type.String())
1463
+ }, { description: "BYOB storage config" }))
1303
1464
  }),
1304
1465
  async execute(_id, params) {
1305
1466
  const { apiKey, baseUrl } = getCfg(api);
1306
1467
  if (!apiKey) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
1307
1468
  assertAllowedBaseUrl(baseUrl);
1308
1469
  const payload = { timeout_sec: params.timeout_sec || 60 };
1470
+ if (params.stealth) payload.stealth = true;
1471
+ if (params.custom_storage) payload.custom_storage = params.custom_storage;
1309
1472
  if (params.script) {
1310
1473
  payload.script = params.script;
1311
1474
  payload.url = params.url;
package/dist/index.js CHANGED
@@ -6,6 +6,11 @@ import { execFile as execFileCb } from "child_process";
6
6
  import { promisify } from "util";
7
7
  var execFile = promisify(execFileCb);
8
8
  var INLINE_CAP = 50 * 1024;
9
+ var PREVIEW_REQUEST_TIMEOUT_MS = 3e4;
10
+ var PREVIEW_UPLOAD_TIMEOUT_MS = 5 * 6e4;
11
+ var PREVIEW_ARTIFACT_TIMEOUT_MS = 6e4;
12
+ var PREVIEW_RETRY_ATTEMPTS = 3;
13
+ var PREVIEW_RETRY_BASE_DELAY_MS = 750;
9
14
  function getCfg(api) {
10
15
  const cfg = api?.config ?? {};
11
16
  const pluginCfg = cfg?.plugins?.entries?.["openclaw-riddledc"]?.config ?? {};
@@ -51,6 +56,76 @@ function abToBase64(ab) {
51
56
  function getWorkspacePath(api) {
52
57
  return api?.workspacePath ?? process.cwd();
53
58
  }
59
+ function describeError(err) {
60
+ const anyErr = err;
61
+ const parts = [];
62
+ if (err instanceof Error) parts.push(err.message);
63
+ else parts.push(String(err));
64
+ const cause = anyErr?.cause;
65
+ if (cause) {
66
+ const causeParts = [
67
+ cause.code ? `code=${cause.code}` : "",
68
+ cause.name ? `name=${cause.name}` : "",
69
+ cause.message ? `message=${cause.message}` : ""
70
+ ].filter(Boolean);
71
+ if (causeParts.length) parts.push(`cause: ${causeParts.join(" ")}`);
72
+ }
73
+ return parts.join("; ");
74
+ }
75
+ async function fetchWithTimeout(url, init, timeoutMs, label) {
76
+ const controller = new AbortController();
77
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
78
+ try {
79
+ return await fetch(url, { ...init, signal: controller.signal });
80
+ } catch (err) {
81
+ if (err?.name === "AbortError") {
82
+ throw new Error(`${label} timed out after ${Math.round(timeoutMs / 1e3)}s`);
83
+ }
84
+ throw err;
85
+ } finally {
86
+ clearTimeout(timer);
87
+ }
88
+ }
89
+ function isTransientFetchError(err) {
90
+ const text = describeError(err).toLowerCase();
91
+ return [
92
+ "fetch failed",
93
+ "timed out",
94
+ "timeout",
95
+ "econnreset",
96
+ "econnrefused",
97
+ "etimedout",
98
+ "eai_again",
99
+ "socket",
100
+ "network",
101
+ "und_err",
102
+ "terminated"
103
+ ].some((needle) => text.includes(needle));
104
+ }
105
+ function sleep(ms) {
106
+ return new Promise((resolve) => setTimeout(resolve, ms));
107
+ }
108
+ async function fetchWithRetry(url, init, timeoutMs, label, opts = {}) {
109
+ const attempts = Math.max(1, opts.attempts ?? PREVIEW_RETRY_ATTEMPTS);
110
+ const baseDelayMs = opts.baseDelayMs ?? PREVIEW_RETRY_BASE_DELAY_MS;
111
+ let lastErr;
112
+ for (let attempt = 1; attempt <= attempts; attempt++) {
113
+ try {
114
+ return await fetchWithTimeout(url, init, timeoutMs, label);
115
+ } catch (err) {
116
+ lastErr = err;
117
+ if (attempt >= attempts || !isTransientFetchError(err)) break;
118
+ const jitterMs = Math.floor(Math.random() * 250);
119
+ const delayMs = Math.min(baseDelayMs * Math.pow(2, attempt - 1) + jitterMs, 5e3);
120
+ console.warn(`[openclaw-riddledc] ${label} attempt ${attempt}/${attempts} failed: ${describeError(err)}; retrying in ${delayMs}ms`);
121
+ await sleep(delayMs);
122
+ }
123
+ }
124
+ throw new Error(`${label} failed after ${attempts} attempts: ${describeError(lastErr)}`);
125
+ }
126
+ function isAlreadyStartedResponse(status, body) {
127
+ return status === 409 && /already in status:\s*(queued|running|complete|completed)/i.test(body);
128
+ }
54
129
  async function writeArtifact(workspace, subdir, filename, content) {
55
130
  const dir = join(workspace, "riddle", subdir);
56
131
  await mkdir(dir, { recursive: true });
@@ -799,11 +874,16 @@ function register(api) {
799
874
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
800
875
  }
801
876
  const endpoint = baseUrl.replace(/\/$/, "");
802
- const createRes = await fetch(`${endpoint}/v1/preview`, {
803
- method: "POST",
804
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
805
- body: JSON.stringify({ framework: params.framework || "spa" })
806
- });
877
+ let createRes;
878
+ try {
879
+ createRes = await fetchWithRetry(`${endpoint}/v1/preview`, {
880
+ method: "POST",
881
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
882
+ body: JSON.stringify({ framework: params.framework || "spa" })
883
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview create");
884
+ } catch (e) {
885
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError(e)}` }, null, 2) }] };
886
+ }
807
887
  if (!createRes.ok) {
808
888
  const err = await createRes.text();
809
889
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
@@ -813,11 +893,16 @@ function register(api) {
813
893
  try {
814
894
  await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 6e4 });
815
895
  const tarData = await readFile(tarball);
816
- const uploadRes = await fetch(created.upload_url, {
817
- method: "PUT",
818
- headers: { "Content-Type": "application/gzip" },
819
- body: tarData
820
- });
896
+ let uploadRes;
897
+ try {
898
+ uploadRes = await fetchWithRetry(created.upload_url, {
899
+ method: "PUT",
900
+ headers: { "Content-Type": "application/gzip" },
901
+ body: tarData
902
+ }, PREVIEW_UPLOAD_TIMEOUT_MS, "preview upload");
903
+ } catch (e) {
904
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: ${describeError(e)}` }, null, 2) }] };
905
+ }
821
906
  if (!uploadRes.ok) {
822
907
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
823
908
  }
@@ -827,10 +912,15 @@ function register(api) {
827
912
  } catch {
828
913
  }
829
914
  }
830
- const publishRes = await fetch(`${endpoint}/v1/preview/${created.id}/publish`, {
831
- method: "POST",
832
- headers: { Authorization: `Bearer ${apiKey}` }
833
- });
915
+ let publishRes;
916
+ try {
917
+ publishRes = await fetchWithTimeout(`${endpoint}/v1/preview/${created.id}/publish`, {
918
+ method: "POST",
919
+ headers: { Authorization: `Bearer ${apiKey}` }
920
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview publish");
921
+ } catch (e) {
922
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: ${describeError(e)}` }, null, 2) }] };
923
+ }
834
924
  if (!publishRes.ok) {
835
925
  const err = await publishRes.text();
836
926
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` }, null, 2) }] };
@@ -866,10 +956,15 @@ function register(api) {
866
956
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
867
957
  }
868
958
  assertAllowedBaseUrl(baseUrl);
869
- const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
870
- method: "DELETE",
871
- headers: { Authorization: `Bearer ${apiKey}` }
872
- });
959
+ let res;
960
+ try {
961
+ res = await fetchWithTimeout(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
962
+ method: "DELETE",
963
+ headers: { Authorization: `Bearer ${apiKey}` }
964
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview delete");
965
+ } catch (e) {
966
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: ${describeError(e)}` }, null, 2) }] };
967
+ }
873
968
  if (!res.ok) {
874
969
  const err = await res.text();
875
970
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: HTTP ${res.status} ${err}` }, null, 2) }] };
@@ -927,11 +1022,16 @@ function register(api) {
927
1022
  const envBody = {};
928
1023
  if (hasSensitiveEnv) envBody.env = params.sensitive_env;
929
1024
  if (hasLocalStorage) envBody.localStorage = params.localStorage;
930
- const envRes = await fetch(`${endpoint}/v1/server-preview/env`, {
931
- method: "POST",
932
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
933
- body: JSON.stringify(envBody)
934
- });
1025
+ let envRes;
1026
+ try {
1027
+ envRes = await fetchWithTimeout(`${endpoint}/v1/server-preview/env`, {
1028
+ method: "POST",
1029
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1030
+ body: JSON.stringify(envBody)
1031
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview env store");
1032
+ } catch (e) {
1033
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError(e)}` }, null, 2) }] };
1034
+ }
935
1035
  if (!envRes.ok) {
936
1036
  const err = await envRes.text();
937
1037
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
@@ -957,11 +1057,16 @@ function register(api) {
957
1057
  if (params.navigation_timeout) createBody.navigation_timeout = params.navigation_timeout;
958
1058
  if (params.color_scheme) createBody.color_scheme = params.color_scheme;
959
1059
  if (params.viewport) createBody.viewport = params.viewport;
960
- const createRes = await fetch(`${endpoint}/v1/server-preview`, {
961
- method: "POST",
962
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
963
- body: JSON.stringify(createBody)
964
- });
1060
+ let createRes;
1061
+ try {
1062
+ createRes = await fetchWithRetry(`${endpoint}/v1/server-preview`, {
1063
+ method: "POST",
1064
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1065
+ body: JSON.stringify(createBody)
1066
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview create");
1067
+ } catch (e) {
1068
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError(e)}` }, null, 2) }] };
1069
+ }
965
1070
  if (!createRes.ok) {
966
1071
  const err = await createRes.text();
967
1072
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
@@ -973,11 +1078,16 @@ function register(api) {
973
1078
  const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
974
1079
  await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
975
1080
  const tarData = await readFile(tarball);
976
- const uploadRes = await fetch(created.upload_url, {
977
- method: "PUT",
978
- headers: { "Content-Type": "application/gzip" },
979
- body: tarData
980
- });
1081
+ let uploadRes;
1082
+ try {
1083
+ uploadRes = await fetchWithRetry(created.upload_url, {
1084
+ method: "PUT",
1085
+ headers: { "Content-Type": "application/gzip" },
1086
+ body: tarData
1087
+ }, PREVIEW_UPLOAD_TIMEOUT_MS, "server preview upload");
1088
+ } catch (e) {
1089
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError(e)}` }, null, 2) }] };
1090
+ }
981
1091
  if (!uploadRes.ok) {
982
1092
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
983
1093
  }
@@ -987,21 +1097,35 @@ function register(api) {
987
1097
  } catch {
988
1098
  }
989
1099
  }
990
- const startRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
991
- method: "POST",
992
- headers: { Authorization: `Bearer ${apiKey}` }
993
- });
1100
+ let startRes;
1101
+ try {
1102
+ startRes = await fetchWithRetry(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
1103
+ method: "POST",
1104
+ headers: { Authorization: `Bearer ${apiKey}` }
1105
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview start");
1106
+ } catch (e) {
1107
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError(e)}` }, null, 2) }] };
1108
+ }
994
1109
  if (!startRes.ok) {
995
1110
  const err = await startRes.text();
996
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1111
+ if (isAlreadyStartedResponse(startRes.status, err)) {
1112
+ console.warn(`[openclaw-riddledc] server preview start returned ${startRes.status} for ${created.job_id}; continuing to poll`);
1113
+ } else {
1114
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1115
+ }
997
1116
  }
998
1117
  const timeoutMs = ((params.timeout || 120) + 60) * 1e3;
999
1118
  const pollStart = Date.now();
1000
1119
  const POLL_INTERVAL = 3e3;
1001
1120
  while (Date.now() - pollStart < timeoutMs) {
1002
- const statusRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}`, {
1003
- headers: { Authorization: `Bearer ${apiKey}` }
1004
- });
1121
+ let statusRes;
1122
+ try {
1123
+ statusRes = await fetchWithRetry(`${endpoint}/v1/server-preview/${created.job_id}`, {
1124
+ headers: { Authorization: `Bearer ${apiKey}` }
1125
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview poll", { attempts: 2 });
1126
+ } catch (e) {
1127
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError(e)}` }, null, 2) }] };
1128
+ }
1005
1129
  if (!statusRes.ok) {
1006
1130
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
1007
1131
  }
@@ -1020,7 +1144,7 @@ function register(api) {
1020
1144
  for (const output of result.outputs) {
1021
1145
  if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
1022
1146
  try {
1023
- const imgRes = await fetch(output.url);
1147
+ const imgRes = await fetchWithTimeout(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "server preview artifact download");
1024
1148
  if (imgRes.ok) {
1025
1149
  const buf = await imgRes.arrayBuffer();
1026
1150
  const base64 = Buffer.from(buf).toString("base64");
@@ -1096,11 +1220,16 @@ function register(api) {
1096
1220
  const envBody = {};
1097
1221
  if (hasSensitiveEnv) envBody.env = params.sensitive_env;
1098
1222
  if (hasLocalStorage) envBody.localStorage = params.localStorage;
1099
- const envRes = await fetch(`${endpoint}/v1/build-preview/env`, {
1100
- method: "POST",
1101
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1102
- body: JSON.stringify(envBody)
1103
- });
1223
+ let envRes;
1224
+ try {
1225
+ envRes = await fetchWithTimeout(`${endpoint}/v1/build-preview/env`, {
1226
+ method: "POST",
1227
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1228
+ body: JSON.stringify(envBody)
1229
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview env store");
1230
+ } catch (e) {
1231
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError(e)}` }, null, 2) }] };
1232
+ }
1104
1233
  if (!envRes.ok) {
1105
1234
  const err = await envRes.text();
1106
1235
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
@@ -1128,11 +1257,16 @@ function register(api) {
1128
1257
  if (params.color_scheme) createBody.color_scheme = params.color_scheme;
1129
1258
  if (params.viewport) createBody.viewport = params.viewport;
1130
1259
  if (params.audit) createBody.audit = true;
1131
- const createRes = await fetch(`${endpoint}/v1/build-preview`, {
1132
- method: "POST",
1133
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1134
- body: JSON.stringify(createBody)
1135
- });
1260
+ let createRes;
1261
+ try {
1262
+ createRes = await fetchWithRetry(`${endpoint}/v1/build-preview`, {
1263
+ method: "POST",
1264
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1265
+ body: JSON.stringify(createBody)
1266
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview create");
1267
+ } catch (e) {
1268
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError(e)}` }, null, 2) }] };
1269
+ }
1136
1270
  if (!createRes.ok) {
1137
1271
  const err = await createRes.text();
1138
1272
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
@@ -1144,11 +1278,16 @@ function register(api) {
1144
1278
  const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
1145
1279
  await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1146
1280
  const tarData = await readFile(tarball);
1147
- const uploadRes = await fetch(created.upload_url, {
1148
- method: "PUT",
1149
- headers: { "Content-Type": "application/gzip" },
1150
- body: tarData
1151
- });
1281
+ let uploadRes;
1282
+ try {
1283
+ uploadRes = await fetchWithRetry(created.upload_url, {
1284
+ method: "PUT",
1285
+ headers: { "Content-Type": "application/gzip" },
1286
+ body: tarData
1287
+ }, PREVIEW_UPLOAD_TIMEOUT_MS, "build preview upload");
1288
+ } catch (e) {
1289
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError(e)}` }, null, 2) }] };
1290
+ }
1152
1291
  if (!uploadRes.ok) {
1153
1292
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
1154
1293
  }
@@ -1158,21 +1297,35 @@ function register(api) {
1158
1297
  } catch {
1159
1298
  }
1160
1299
  }
1161
- const startRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
1162
- method: "POST",
1163
- headers: { Authorization: `Bearer ${apiKey}` }
1164
- });
1300
+ let startRes;
1301
+ try {
1302
+ startRes = await fetchWithRetry(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
1303
+ method: "POST",
1304
+ headers: { Authorization: `Bearer ${apiKey}` }
1305
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview start");
1306
+ } catch (e) {
1307
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError(e)}` }, null, 2) }] };
1308
+ }
1165
1309
  if (!startRes.ok) {
1166
1310
  const err = await startRes.text();
1167
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1311
+ if (isAlreadyStartedResponse(startRes.status, err)) {
1312
+ console.warn(`[openclaw-riddledc] build preview start returned ${startRes.status} for ${created.job_id}; continuing to poll`);
1313
+ } else {
1314
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1315
+ }
1168
1316
  }
1169
1317
  const timeoutMs = ((params.timeout || 180) + 120) * 1e3;
1170
1318
  const pollStart = Date.now();
1171
1319
  const POLL_INTERVAL = 3e3;
1172
1320
  while (Date.now() - pollStart < timeoutMs) {
1173
- const statusRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}`, {
1174
- headers: { Authorization: `Bearer ${apiKey}` }
1175
- });
1321
+ let statusRes;
1322
+ try {
1323
+ statusRes = await fetchWithRetry(`${endpoint}/v1/build-preview/${created.job_id}`, {
1324
+ headers: { Authorization: `Bearer ${apiKey}` }
1325
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview poll", { attempts: 2 });
1326
+ } catch (e) {
1327
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError(e)}` }, null, 2) }] };
1328
+ }
1176
1329
  if (!statusRes.ok) {
1177
1330
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
1178
1331
  }
@@ -1195,7 +1348,7 @@ function register(api) {
1195
1348
  for (const output of result.outputs) {
1196
1349
  if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
1197
1350
  try {
1198
- const imgRes = await fetch(output.url);
1351
+ const imgRes = await fetchWithTimeout(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "build preview artifact download");
1199
1352
  if (imgRes.ok) {
1200
1353
  const buf = await imgRes.arrayBuffer();
1201
1354
  const base64 = Buffer.from(buf).toString("base64");
@@ -1275,13 +1428,23 @@ function register(api) {
1275
1428
  url: Type.Optional(Type.String({ description: "URL to navigate to" })),
1276
1429
  script: Type.Optional(Type.String({ description: "Playwright script to execute" })),
1277
1430
  steps: Type.Optional(Type.Array(Type.Any(), { description: "Declarative steps (alternative to script)" })),
1278
- timeout_sec: Type.Optional(Type.Number({ description: "Max execution time in seconds (default: 60)" }))
1431
+ timeout_sec: Type.Optional(Type.Number({ description: "Max execution time in seconds (default: 60)" })),
1432
+ stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Default: false" })),
1433
+ custom_storage: Type.Optional(Type.Object({
1434
+ bucket: Type.String(),
1435
+ region: Type.Optional(Type.String()),
1436
+ prefix: Type.Optional(Type.String()),
1437
+ cdn_domain: Type.Optional(Type.String()),
1438
+ role_arn: Type.Optional(Type.String())
1439
+ }, { description: "BYOB storage config" }))
1279
1440
  }),
1280
1441
  async execute(_id, params) {
1281
1442
  const { apiKey, baseUrl } = getCfg(api);
1282
1443
  if (!apiKey) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
1283
1444
  assertAllowedBaseUrl(baseUrl);
1284
1445
  const payload = { timeout_sec: params.timeout_sec || 60 };
1446
+ if (params.stealth) payload.stealth = true;
1447
+ if (params.custom_storage) payload.custom_storage = params.custom_storage;
1285
1448
  if (params.script) {
1286
1449
  payload.script = params.script;
1287
1450
  payload.url = params.url;
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-riddledc",
3
3
  "name": "Riddle",
4
4
  "description": "Riddle (riddledc.com) hosted browser API tools for OpenClaw agents.",
5
- "version": "0.9.0",
5
+ "version": "0.9.2",
6
6
  "notes": "0.8.0: Added riddle_build_preview for Dockerfile-based builds with image caching.",
7
7
  "type": "plugin",
8
8
  "bundledSkills": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/openclaw-riddledc",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "OpenClaw integration package for RiddleDC (no secrets).",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",