@riddledc/openclaw-riddledc 0.9.1 → 0.9.3

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/dist/index.cjs CHANGED
@@ -24,12 +24,231 @@ __export(index_exports, {
24
24
  });
25
25
  module.exports = __toCommonJS(index_exports);
26
26
  var import_typebox = require("@sinclair/typebox");
27
+ var import_promises2 = require("fs/promises");
28
+ var import_node_path2 = require("path");
29
+ var import_node_child_process2 = require("child_process");
30
+ var import_node_util2 = require("util");
31
+
32
+ // src/core.ts
33
+ var import_node_child_process = require("child_process");
27
34
  var import_promises = require("fs/promises");
28
35
  var import_node_path = require("path");
29
- var import_node_child_process = require("child_process");
30
36
  var import_node_util = require("util");
31
37
  var execFile = (0, import_node_util.promisify)(import_node_child_process.execFile);
32
38
  var INLINE_CAP = 50 * 1024;
39
+ var PREVIEW_REQUEST_TIMEOUT_MS = 3e4;
40
+ var PREVIEW_UPLOAD_TIMEOUT_MS = 5 * 6e4;
41
+ var PREVIEW_RETRY_ATTEMPTS = 3;
42
+ var PREVIEW_RETRY_BASE_DELAY_MS = 750;
43
+ function configFromOpenClawApi(api) {
44
+ const cfg = api?.config ?? {};
45
+ const pluginCfg = cfg?.plugins?.entries?.["openclaw-riddledc"]?.config ?? {};
46
+ return {
47
+ apiKey: process.env.RIDDLE_API_KEY || pluginCfg.apiKey,
48
+ baseUrl: pluginCfg.baseUrl || "https://api.riddledc.com",
49
+ workspace: api?.workspacePath ?? process.cwd()
50
+ };
51
+ }
52
+ function requireConfig(config) {
53
+ const apiKey = config.apiKey || process.env.RIDDLE_API_KEY;
54
+ const baseUrl = config.baseUrl || "https://api.riddledc.com";
55
+ if (!apiKey) {
56
+ throw new Error("Missing Riddle API key. Set RIDDLE_API_KEY env var or configure a Riddle API key.");
57
+ }
58
+ assertAllowedBaseUrl(baseUrl);
59
+ return { apiKey, baseUrl, workspace: config.workspace || process.cwd() };
60
+ }
61
+ function assertAllowedBaseUrl(baseUrl) {
62
+ const url = new URL(baseUrl);
63
+ if (url.protocol !== "https:") throw new Error(`Riddle baseUrl must be https: (${baseUrl})`);
64
+ if (url.hostname !== "api.riddledc.com") {
65
+ throw new Error(`Refusing to use non-official Riddle host: ${url.hostname}`);
66
+ }
67
+ }
68
+ function describeError(err) {
69
+ const anyErr = err;
70
+ const parts = [];
71
+ if (err instanceof Error) parts.push(err.message);
72
+ else parts.push(String(err));
73
+ const cause = anyErr?.cause;
74
+ if (cause) {
75
+ const causeParts = [
76
+ cause.code ? `code=${cause.code}` : "",
77
+ cause.name ? `name=${cause.name}` : "",
78
+ cause.message ? `message=${cause.message}` : ""
79
+ ].filter(Boolean);
80
+ if (causeParts.length) parts.push(`cause: ${causeParts.join(" ")}`);
81
+ }
82
+ return parts.join("; ");
83
+ }
84
+ async function fetchWithTimeout(url, init, timeoutMs, label) {
85
+ const controller = new AbortController();
86
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
87
+ try {
88
+ return await fetch(url, { ...init, signal: controller.signal });
89
+ } catch (err) {
90
+ if (err?.name === "AbortError") {
91
+ throw new Error(`${label} timed out after ${Math.round(timeoutMs / 1e3)}s`);
92
+ }
93
+ throw err;
94
+ } finally {
95
+ clearTimeout(timer);
96
+ }
97
+ }
98
+ function isTransientFetchError(err) {
99
+ const text = describeError(err).toLowerCase();
100
+ return [
101
+ "fetch failed",
102
+ "timed out",
103
+ "timeout",
104
+ "econnreset",
105
+ "econnrefused",
106
+ "etimedout",
107
+ "eai_again",
108
+ "socket",
109
+ "network",
110
+ "und_err",
111
+ "terminated"
112
+ ].some((needle) => text.includes(needle));
113
+ }
114
+ function sleep(ms) {
115
+ return new Promise((resolve) => setTimeout(resolve, ms));
116
+ }
117
+ async function fetchWithRetry(url, init, timeoutMs, label, opts = {}) {
118
+ const attempts = Math.max(1, opts.attempts ?? PREVIEW_RETRY_ATTEMPTS);
119
+ const baseDelayMs = opts.baseDelayMs ?? PREVIEW_RETRY_BASE_DELAY_MS;
120
+ let lastErr;
121
+ for (let attempt = 1; attempt <= attempts; attempt++) {
122
+ try {
123
+ return await fetchWithTimeout(url, init, timeoutMs, label);
124
+ } catch (err) {
125
+ lastErr = err;
126
+ if (attempt >= attempts || !isTransientFetchError(err)) break;
127
+ const jitterMs = Math.floor(Math.random() * 250);
128
+ const delayMs = Math.min(baseDelayMs * Math.pow(2, attempt - 1) + jitterMs, 5e3);
129
+ console.warn(`[openclaw-riddledc] ${label} attempt ${attempt}/${attempts} failed: ${describeError(err)}; retrying in ${delayMs}ms`);
130
+ await sleep(delayMs);
131
+ }
132
+ }
133
+ throw new Error(`${label} failed after ${attempts} attempts: ${describeError(lastErr)}`);
134
+ }
135
+ async function assertDirectory(dir) {
136
+ if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
137
+ try {
138
+ const st = await (0, import_promises.stat)(dir);
139
+ if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
140
+ } catch (e) {
141
+ return { ok: false, error: `Cannot access directory: ${e.message}` };
142
+ }
143
+ return null;
144
+ }
145
+ async function tarDirectory(dir, tarball, excludes, timeout) {
146
+ const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
147
+ await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout });
148
+ return (0, import_promises.readFile)(tarball);
149
+ }
150
+ async function createStaticPreview(config, params) {
151
+ let cfg;
152
+ try {
153
+ cfg = requireConfig(config);
154
+ } catch (err) {
155
+ return { ok: false, error: err.message };
156
+ }
157
+ const dirError = await assertDirectory(params.directory);
158
+ if (dirError) return dirError;
159
+ const endpoint = cfg.baseUrl.replace(/\/$/, "");
160
+ let createRes;
161
+ try {
162
+ createRes = await fetchWithRetry(`${endpoint}/v1/preview`, {
163
+ method: "POST",
164
+ headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
165
+ body: JSON.stringify({ framework: params.framework || "spa" })
166
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview create");
167
+ } catch (e) {
168
+ return { ok: false, error: `Create failed: ${describeError(e)}` };
169
+ }
170
+ if (!createRes.ok) {
171
+ const err = await createRes.text();
172
+ return { ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` };
173
+ }
174
+ const created = await createRes.json();
175
+ const tarball = `/tmp/riddle-preview-${created.id}.tar.gz`;
176
+ try {
177
+ const tarData = await tarDirectory(params.directory, tarball, [], 6e4);
178
+ let uploadRes;
179
+ try {
180
+ uploadRes = await fetchWithRetry(created.upload_url, {
181
+ method: "PUT",
182
+ headers: { "Content-Type": "application/gzip" },
183
+ body: tarData
184
+ }, PREVIEW_UPLOAD_TIMEOUT_MS, "preview upload");
185
+ } catch (e) {
186
+ return { ok: false, id: created.id, error: `Upload failed: ${describeError(e)}` };
187
+ }
188
+ if (!uploadRes.ok) {
189
+ return { ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` };
190
+ }
191
+ } finally {
192
+ try {
193
+ await (0, import_promises.rm)(tarball, { force: true });
194
+ } catch {
195
+ }
196
+ }
197
+ let publishRes;
198
+ try {
199
+ publishRes = await fetchWithTimeout(`${endpoint}/v1/preview/${created.id}/publish`, {
200
+ method: "POST",
201
+ headers: { Authorization: `Bearer ${cfg.apiKey}` }
202
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview publish");
203
+ } catch (e) {
204
+ return { ok: false, id: created.id, error: `Publish failed: ${describeError(e)}` };
205
+ }
206
+ if (!publishRes.ok) {
207
+ const err = await publishRes.text();
208
+ return { ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` };
209
+ }
210
+ const published = await publishRes.json();
211
+ return {
212
+ ok: true,
213
+ id: published.id,
214
+ preview_url: published.preview_url,
215
+ file_count: published.file_count,
216
+ total_bytes: published.total_bytes,
217
+ expires_at: created.expires_at
218
+ };
219
+ }
220
+ async function deleteStaticPreview(config, id) {
221
+ let cfg;
222
+ try {
223
+ cfg = requireConfig(config);
224
+ } catch (err) {
225
+ return { ok: false, error: err.message };
226
+ }
227
+ let res;
228
+ try {
229
+ res = await fetchWithTimeout(`${cfg.baseUrl.replace(/\/$/, "")}/v1/preview/${id}`, {
230
+ method: "DELETE",
231
+ headers: { Authorization: `Bearer ${cfg.apiKey}` }
232
+ }, PREVIEW_REQUEST_TIMEOUT_MS, "preview delete");
233
+ } catch (e) {
234
+ return { ok: false, error: `Delete failed: ${describeError(e)}` };
235
+ }
236
+ if (!res.ok) {
237
+ const err = await res.text();
238
+ return { ok: false, error: `Delete failed: HTTP ${res.status} ${err}` };
239
+ }
240
+ const data = await res.json();
241
+ return { ok: true, deleted: true, files_removed: data.files_removed };
242
+ }
243
+
244
+ // src/index.ts
245
+ var execFile2 = (0, import_node_util2.promisify)(import_node_child_process2.execFile);
246
+ var INLINE_CAP2 = 50 * 1024;
247
+ var PREVIEW_REQUEST_TIMEOUT_MS2 = 3e4;
248
+ var PREVIEW_UPLOAD_TIMEOUT_MS2 = 5 * 6e4;
249
+ var PREVIEW_ARTIFACT_TIMEOUT_MS = 6e4;
250
+ var PREVIEW_RETRY_ATTEMPTS2 = 3;
251
+ var PREVIEW_RETRY_BASE_DELAY_MS2 = 750;
33
252
  function getCfg(api) {
34
253
  const cfg = api?.config ?? {};
35
254
  const pluginCfg = cfg?.plugins?.entries?.["openclaw-riddledc"]?.config ?? {};
@@ -38,7 +257,7 @@ function getCfg(api) {
38
257
  baseUrl: pluginCfg.baseUrl || "https://api.riddledc.com"
39
258
  };
40
259
  }
41
- function assertAllowedBaseUrl(baseUrl) {
260
+ function assertAllowedBaseUrl2(baseUrl) {
42
261
  const url = new URL(baseUrl);
43
262
  if (url.protocol !== "https:") throw new Error(`Riddle baseUrl must be https: (${baseUrl})`);
44
263
  if (url.hostname !== "api.riddledc.com") {
@@ -75,20 +294,90 @@ function abToBase64(ab) {
75
294
  function getWorkspacePath(api) {
76
295
  return api?.workspacePath ?? process.cwd();
77
296
  }
297
+ function describeError2(err) {
298
+ const anyErr = err;
299
+ const parts = [];
300
+ if (err instanceof Error) parts.push(err.message);
301
+ else parts.push(String(err));
302
+ const cause = anyErr?.cause;
303
+ if (cause) {
304
+ const causeParts = [
305
+ cause.code ? `code=${cause.code}` : "",
306
+ cause.name ? `name=${cause.name}` : "",
307
+ cause.message ? `message=${cause.message}` : ""
308
+ ].filter(Boolean);
309
+ if (causeParts.length) parts.push(`cause: ${causeParts.join(" ")}`);
310
+ }
311
+ return parts.join("; ");
312
+ }
313
+ async function fetchWithTimeout2(url, init, timeoutMs, label) {
314
+ const controller = new AbortController();
315
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
316
+ try {
317
+ return await fetch(url, { ...init, signal: controller.signal });
318
+ } catch (err) {
319
+ if (err?.name === "AbortError") {
320
+ throw new Error(`${label} timed out after ${Math.round(timeoutMs / 1e3)}s`);
321
+ }
322
+ throw err;
323
+ } finally {
324
+ clearTimeout(timer);
325
+ }
326
+ }
327
+ function isTransientFetchError2(err) {
328
+ const text = describeError2(err).toLowerCase();
329
+ return [
330
+ "fetch failed",
331
+ "timed out",
332
+ "timeout",
333
+ "econnreset",
334
+ "econnrefused",
335
+ "etimedout",
336
+ "eai_again",
337
+ "socket",
338
+ "network",
339
+ "und_err",
340
+ "terminated"
341
+ ].some((needle) => text.includes(needle));
342
+ }
343
+ function sleep2(ms) {
344
+ return new Promise((resolve) => setTimeout(resolve, ms));
345
+ }
346
+ async function fetchWithRetry2(url, init, timeoutMs, label, opts = {}) {
347
+ const attempts = Math.max(1, opts.attempts ?? PREVIEW_RETRY_ATTEMPTS2);
348
+ const baseDelayMs = opts.baseDelayMs ?? PREVIEW_RETRY_BASE_DELAY_MS2;
349
+ let lastErr;
350
+ for (let attempt = 1; attempt <= attempts; attempt++) {
351
+ try {
352
+ return await fetchWithTimeout2(url, init, timeoutMs, label);
353
+ } catch (err) {
354
+ lastErr = err;
355
+ if (attempt >= attempts || !isTransientFetchError2(err)) break;
356
+ const jitterMs = Math.floor(Math.random() * 250);
357
+ const delayMs = Math.min(baseDelayMs * Math.pow(2, attempt - 1) + jitterMs, 5e3);
358
+ console.warn(`[openclaw-riddledc] ${label} attempt ${attempt}/${attempts} failed: ${describeError2(err)}; retrying in ${delayMs}ms`);
359
+ await sleep2(delayMs);
360
+ }
361
+ }
362
+ throw new Error(`${label} failed after ${attempts} attempts: ${describeError2(lastErr)}`);
363
+ }
364
+ function isAlreadyStartedResponse(status, body) {
365
+ return status === 409 && /already in status:\s*(queued|running|complete|completed)/i.test(body);
366
+ }
78
367
  async function writeArtifact(workspace, subdir, filename, content) {
79
- const dir = (0, import_node_path.join)(workspace, "riddle", subdir);
80
- await (0, import_promises.mkdir)(dir, { recursive: true });
81
- const filePath = (0, import_node_path.join)(dir, filename);
368
+ const dir = (0, import_node_path2.join)(workspace, "riddle", subdir);
369
+ await (0, import_promises2.mkdir)(dir, { recursive: true });
370
+ const filePath = (0, import_node_path2.join)(dir, filename);
82
371
  const buf = Buffer.from(content, "utf8");
83
- await (0, import_promises.writeFile)(filePath, buf);
372
+ await (0, import_promises2.writeFile)(filePath, buf);
84
373
  return { path: filePath, sizeBytes: buf.byteLength };
85
374
  }
86
375
  async function writeArtifactBinary(workspace, subdir, filename, base64Content) {
87
- const dir = (0, import_node_path.join)(workspace, "riddle", subdir);
88
- await (0, import_promises.mkdir)(dir, { recursive: true });
89
- const filePath = (0, import_node_path.join)(dir, filename);
376
+ const dir = (0, import_node_path2.join)(workspace, "riddle", subdir);
377
+ await (0, import_promises2.mkdir)(dir, { recursive: true });
378
+ const filePath = (0, import_node_path2.join)(dir, filename);
90
379
  const buf = Buffer.from(base64Content, "base64");
91
- await (0, import_promises.writeFile)(filePath, buf);
380
+ await (0, import_promises2.writeFile)(filePath, buf);
92
381
  return { path: filePath, sizeBytes: buf.byteLength };
93
382
  }
94
383
  async function applySafetySpec(result, opts) {
@@ -132,10 +421,10 @@ async function applySafetySpec(result, opts) {
132
421
  if (result.har != null) {
133
422
  const harStr = typeof result.har === "string" ? result.har : JSON.stringify(result.har);
134
423
  const harBytes = Buffer.byteLength(harStr, "utf8");
135
- if (opts.harInline && harBytes <= INLINE_CAP) {
424
+ if (opts.harInline && harBytes <= INLINE_CAP2) {
136
425
  } else {
137
426
  const ref = await writeArtifact(opts.workspace, "har", `${jobId}.har.json`, harStr);
138
- if (opts.harInline && harBytes > INLINE_CAP) {
427
+ if (opts.harInline && harBytes > INLINE_CAP2) {
139
428
  result.har = { saved: ref.path, sizeBytes: ref.sizeBytes, warning: "Exceeded 50KB inline cap; wrote to file" };
140
429
  } else {
141
430
  result.har = { saved: ref.path, sizeBytes: ref.sizeBytes };
@@ -145,7 +434,7 @@ async function applySafetySpec(result, opts) {
145
434
  if (result.console != null) {
146
435
  const consoleStr = typeof result.console === "string" ? result.console : JSON.stringify(result.console);
147
436
  const consoleBytes = Buffer.byteLength(consoleStr, "utf8");
148
- if (consoleBytes > INLINE_CAP) {
437
+ if (consoleBytes > INLINE_CAP2) {
149
438
  const ref = await writeArtifact(opts.workspace, "console", `${jobId}.log`, consoleStr);
150
439
  result.console = { saved: ref.path, sizeBytes: ref.sizeBytes };
151
440
  }
@@ -248,7 +537,7 @@ async function runWithDefaults(api, payload, defaults) {
248
537
  error: "Missing Riddle API key. Set RIDDLE_API_KEY env var or plugins.entries.riddle.config.apiKey."
249
538
  };
250
539
  }
251
- assertAllowedBaseUrl(baseUrl);
540
+ assertAllowedBaseUrl2(baseUrl);
252
541
  const mode = detectMode(payload);
253
542
  const userInclude = payload.include ?? [];
254
543
  const userRequestedHar = userInclude.includes("har");
@@ -400,7 +689,7 @@ function register(api) {
400
689
  if (!apiKey) {
401
690
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
402
691
  }
403
- assertAllowedBaseUrl(baseUrl);
692
+ assertAllowedBaseUrl2(baseUrl);
404
693
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${params.job_id}`, {
405
694
  headers: { Authorization: `Bearer ${apiKey}` }
406
695
  });
@@ -809,70 +1098,8 @@ function register(api) {
809
1098
  framework: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Framework hint: 'spa' (default) or 'static'" }))
810
1099
  }),
811
1100
  async execute(_id, params) {
812
- const { apiKey, baseUrl } = getCfg(api);
813
- if (!apiKey) {
814
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
815
- }
816
- assertAllowedBaseUrl(baseUrl);
817
- const dir = params.directory;
818
- if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
819
- try {
820
- const st = await (0, import_promises.stat)(dir);
821
- if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
822
- } catch (e) {
823
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
824
- }
825
- 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
- });
831
- if (!createRes.ok) {
832
- const err = await createRes.text();
833
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
834
- }
835
- const created = await createRes.json();
836
- const tarball = `/tmp/riddle-preview-${created.id}.tar.gz`;
837
- try {
838
- await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 6e4 });
839
- 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
- });
845
- if (!uploadRes.ok) {
846
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
847
- }
848
- } finally {
849
- try {
850
- await (0, import_promises.rm)(tarball, { force: true });
851
- } catch {
852
- }
853
- }
854
- const publishRes = await fetch(`${endpoint}/v1/preview/${created.id}/publish`, {
855
- method: "POST",
856
- headers: { Authorization: `Bearer ${apiKey}` }
857
- });
858
- if (!publishRes.ok) {
859
- const err = await publishRes.text();
860
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` }, null, 2) }] };
861
- }
862
- const published = await publishRes.json();
863
- return {
864
- content: [{
865
- type: "text",
866
- text: JSON.stringify({
867
- ok: true,
868
- id: published.id,
869
- preview_url: published.preview_url,
870
- file_count: published.file_count,
871
- total_bytes: published.total_bytes,
872
- expires_at: created.expires_at
873
- }, null, 2)
874
- }]
875
- };
1101
+ const result = await createStaticPreview(configFromOpenClawApi(api), params);
1102
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
876
1103
  }
877
1104
  },
878
1105
  { optional: true }
@@ -885,21 +1112,8 @@ function register(api) {
885
1112
  id: import_typebox.Type.String({ description: "Preview ID (e.g. pv_a1b2c3d4)" })
886
1113
  }),
887
1114
  async execute(_id, params) {
888
- const { apiKey, baseUrl } = getCfg(api);
889
- if (!apiKey) {
890
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
891
- }
892
- assertAllowedBaseUrl(baseUrl);
893
- const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
894
- method: "DELETE",
895
- headers: { Authorization: `Bearer ${apiKey}` }
896
- });
897
- if (!res.ok) {
898
- const err = await res.text();
899
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: HTTP ${res.status} ${err}` }, null, 2) }] };
900
- }
901
- const data = await res.json();
902
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted: true, files_removed: data.files_removed }, null, 2) }] };
1115
+ const result = await deleteStaticPreview(configFromOpenClawApi(api), params.id);
1116
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
903
1117
  }
904
1118
  },
905
1119
  { optional: true }
@@ -934,11 +1148,11 @@ function register(api) {
934
1148
  if (!apiKey) {
935
1149
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
936
1150
  }
937
- assertAllowedBaseUrl(baseUrl);
1151
+ assertAllowedBaseUrl2(baseUrl);
938
1152
  const dir = params.directory;
939
1153
  if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
940
1154
  try {
941
- const st = await (0, import_promises.stat)(dir);
1155
+ const st = await (0, import_promises2.stat)(dir);
942
1156
  if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
943
1157
  } catch (e) {
944
1158
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
@@ -951,11 +1165,16 @@ function register(api) {
951
1165
  const envBody = {};
952
1166
  if (hasSensitiveEnv) envBody.env = params.sensitive_env;
953
1167
  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
- });
1168
+ let envRes;
1169
+ try {
1170
+ envRes = await fetchWithTimeout2(`${endpoint}/v1/server-preview/env`, {
1171
+ method: "POST",
1172
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1173
+ body: JSON.stringify(envBody)
1174
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "server preview env store");
1175
+ } catch (e) {
1176
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError2(e)}` }, null, 2) }] };
1177
+ }
959
1178
  if (!envRes.ok) {
960
1179
  const err = await envRes.text();
961
1180
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
@@ -981,11 +1200,16 @@ function register(api) {
981
1200
  if (params.navigation_timeout) createBody.navigation_timeout = params.navigation_timeout;
982
1201
  if (params.color_scheme) createBody.color_scheme = params.color_scheme;
983
1202
  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
- });
1203
+ let createRes;
1204
+ try {
1205
+ createRes = await fetchWithRetry2(`${endpoint}/v1/server-preview`, {
1206
+ method: "POST",
1207
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1208
+ body: JSON.stringify(createBody)
1209
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "server preview create");
1210
+ } catch (e) {
1211
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError2(e)}` }, null, 2) }] };
1212
+ }
989
1213
  if (!createRes.ok) {
990
1214
  const err = await createRes.text();
991
1215
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
@@ -995,37 +1219,56 @@ function register(api) {
995
1219
  try {
996
1220
  const excludes = params.exclude || [".git", "*.log"];
997
1221
  const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
998
- await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
999
- 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
- });
1222
+ await execFile2("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1223
+ const tarData = await (0, import_promises2.readFile)(tarball);
1224
+ let uploadRes;
1225
+ try {
1226
+ uploadRes = await fetchWithRetry2(created.upload_url, {
1227
+ method: "PUT",
1228
+ headers: { "Content-Type": "application/gzip" },
1229
+ body: tarData
1230
+ }, PREVIEW_UPLOAD_TIMEOUT_MS2, "server preview upload");
1231
+ } catch (e) {
1232
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError2(e)}` }, null, 2) }] };
1233
+ }
1005
1234
  if (!uploadRes.ok) {
1006
1235
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
1007
1236
  }
1008
1237
  } finally {
1009
1238
  try {
1010
- await (0, import_promises.rm)(tarball, { force: true });
1239
+ await (0, import_promises2.rm)(tarball, { force: true });
1011
1240
  } catch {
1012
1241
  }
1013
1242
  }
1014
- const startRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
1015
- method: "POST",
1016
- headers: { Authorization: `Bearer ${apiKey}` }
1017
- });
1243
+ let startRes;
1244
+ try {
1245
+ startRes = await fetchWithRetry2(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
1246
+ method: "POST",
1247
+ headers: { Authorization: `Bearer ${apiKey}` }
1248
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "server preview start");
1249
+ } catch (e) {
1250
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError2(e)}` }, null, 2) }] };
1251
+ }
1018
1252
  if (!startRes.ok) {
1019
1253
  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) }] };
1254
+ if (isAlreadyStartedResponse(startRes.status, err)) {
1255
+ console.warn(`[openclaw-riddledc] server preview start returned ${startRes.status} for ${created.job_id}; continuing to poll`);
1256
+ } else {
1257
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1258
+ }
1021
1259
  }
1022
1260
  const timeoutMs = ((params.timeout || 120) + 60) * 1e3;
1023
1261
  const pollStart = Date.now();
1024
1262
  const POLL_INTERVAL = 3e3;
1025
1263
  while (Date.now() - pollStart < timeoutMs) {
1026
- const statusRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}`, {
1027
- headers: { Authorization: `Bearer ${apiKey}` }
1028
- });
1264
+ let statusRes;
1265
+ try {
1266
+ statusRes = await fetchWithRetry2(`${endpoint}/v1/server-preview/${created.job_id}`, {
1267
+ headers: { Authorization: `Bearer ${apiKey}` }
1268
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "server preview poll", { attempts: 2 });
1269
+ } catch (e) {
1270
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError2(e)}` }, null, 2) }] };
1271
+ }
1029
1272
  if (!statusRes.ok) {
1030
1273
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
1031
1274
  }
@@ -1044,7 +1287,7 @@ function register(api) {
1044
1287
  for (const output of result.outputs) {
1045
1288
  if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
1046
1289
  try {
1047
- const imgRes = await fetch(output.url);
1290
+ const imgRes = await fetchWithTimeout2(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "server preview artifact download");
1048
1291
  if (imgRes.ok) {
1049
1292
  const buf = await imgRes.arrayBuffer();
1050
1293
  const base64 = Buffer.from(buf).toString("base64");
@@ -1098,17 +1341,17 @@ function register(api) {
1098
1341
  if (!apiKey) {
1099
1342
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
1100
1343
  }
1101
- assertAllowedBaseUrl(baseUrl);
1344
+ assertAllowedBaseUrl2(baseUrl);
1102
1345
  const dir = params.directory;
1103
1346
  if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
1104
1347
  try {
1105
- const st = await (0, import_promises.stat)(dir);
1348
+ const st = await (0, import_promises2.stat)(dir);
1106
1349
  if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
1107
1350
  } catch (e) {
1108
1351
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
1109
1352
  }
1110
1353
  try {
1111
- await (0, import_promises.stat)(`${dir}/Dockerfile`);
1354
+ await (0, import_promises2.stat)(`${dir}/Dockerfile`);
1112
1355
  } catch {
1113
1356
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `No Dockerfile found at ${dir}/Dockerfile. riddle_build_preview requires a Dockerfile at the root of the directory.` }, null, 2) }] };
1114
1357
  }
@@ -1120,11 +1363,16 @@ function register(api) {
1120
1363
  const envBody = {};
1121
1364
  if (hasSensitiveEnv) envBody.env = params.sensitive_env;
1122
1365
  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
- });
1366
+ let envRes;
1367
+ try {
1368
+ envRes = await fetchWithTimeout2(`${endpoint}/v1/build-preview/env`, {
1369
+ method: "POST",
1370
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1371
+ body: JSON.stringify(envBody)
1372
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "build preview env store");
1373
+ } catch (e) {
1374
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError2(e)}` }, null, 2) }] };
1375
+ }
1128
1376
  if (!envRes.ok) {
1129
1377
  const err = await envRes.text();
1130
1378
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
@@ -1152,11 +1400,16 @@ function register(api) {
1152
1400
  if (params.color_scheme) createBody.color_scheme = params.color_scheme;
1153
1401
  if (params.viewport) createBody.viewport = params.viewport;
1154
1402
  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
- });
1403
+ let createRes;
1404
+ try {
1405
+ createRes = await fetchWithRetry2(`${endpoint}/v1/build-preview`, {
1406
+ method: "POST",
1407
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1408
+ body: JSON.stringify(createBody)
1409
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "build preview create");
1410
+ } catch (e) {
1411
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError2(e)}` }, null, 2) }] };
1412
+ }
1160
1413
  if (!createRes.ok) {
1161
1414
  const err = await createRes.text();
1162
1415
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
@@ -1166,37 +1419,56 @@ function register(api) {
1166
1419
  try {
1167
1420
  const excludes = params.exclude || [".git", "*.log"];
1168
1421
  const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
1169
- await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1170
- 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
- });
1422
+ await execFile2("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1423
+ const tarData = await (0, import_promises2.readFile)(tarball);
1424
+ let uploadRes;
1425
+ try {
1426
+ uploadRes = await fetchWithRetry2(created.upload_url, {
1427
+ method: "PUT",
1428
+ headers: { "Content-Type": "application/gzip" },
1429
+ body: tarData
1430
+ }, PREVIEW_UPLOAD_TIMEOUT_MS2, "build preview upload");
1431
+ } catch (e) {
1432
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError2(e)}` }, null, 2) }] };
1433
+ }
1176
1434
  if (!uploadRes.ok) {
1177
1435
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
1178
1436
  }
1179
1437
  } finally {
1180
1438
  try {
1181
- await (0, import_promises.rm)(tarball, { force: true });
1439
+ await (0, import_promises2.rm)(tarball, { force: true });
1182
1440
  } catch {
1183
1441
  }
1184
1442
  }
1185
- const startRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
1186
- method: "POST",
1187
- headers: { Authorization: `Bearer ${apiKey}` }
1188
- });
1443
+ let startRes;
1444
+ try {
1445
+ startRes = await fetchWithRetry2(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
1446
+ method: "POST",
1447
+ headers: { Authorization: `Bearer ${apiKey}` }
1448
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "build preview start");
1449
+ } catch (e) {
1450
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError2(e)}` }, null, 2) }] };
1451
+ }
1189
1452
  if (!startRes.ok) {
1190
1453
  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) }] };
1454
+ if (isAlreadyStartedResponse(startRes.status, err)) {
1455
+ console.warn(`[openclaw-riddledc] build preview start returned ${startRes.status} for ${created.job_id}; continuing to poll`);
1456
+ } else {
1457
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
1458
+ }
1192
1459
  }
1193
1460
  const timeoutMs = ((params.timeout || 180) + 120) * 1e3;
1194
1461
  const pollStart = Date.now();
1195
1462
  const POLL_INTERVAL = 3e3;
1196
1463
  while (Date.now() - pollStart < timeoutMs) {
1197
- const statusRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}`, {
1198
- headers: { Authorization: `Bearer ${apiKey}` }
1199
- });
1464
+ let statusRes;
1465
+ try {
1466
+ statusRes = await fetchWithRetry2(`${endpoint}/v1/build-preview/${created.job_id}`, {
1467
+ headers: { Authorization: `Bearer ${apiKey}` }
1468
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "build preview poll", { attempts: 2 });
1469
+ } catch (e) {
1470
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError2(e)}` }, null, 2) }] };
1471
+ }
1200
1472
  if (!statusRes.ok) {
1201
1473
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
1202
1474
  }
@@ -1219,7 +1491,7 @@ function register(api) {
1219
1491
  for (const output of result.outputs) {
1220
1492
  if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
1221
1493
  try {
1222
- const imgRes = await fetch(output.url);
1494
+ const imgRes = await fetchWithTimeout2(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "build preview artifact download");
1223
1495
  if (imgRes.ok) {
1224
1496
  const buf = await imgRes.arrayBuffer();
1225
1497
  const base64 = Buffer.from(buf).toString("base64");
@@ -1244,7 +1516,7 @@ function register(api) {
1244
1516
  async function riddleApiFetch(api2, method, path, body) {
1245
1517
  const { apiKey, baseUrl } = getCfg(api2);
1246
1518
  if (!apiKey) throw new Error("Missing Riddle API key. Set RIDDLE_API_KEY or configure in plugin settings.");
1247
- assertAllowedBaseUrl(baseUrl);
1519
+ assertAllowedBaseUrl2(baseUrl);
1248
1520
  const url = `${baseUrl.replace(/\/$/, "")}${path}`;
1249
1521
  const res = await fetch(url, {
1250
1522
  method,
@@ -1312,7 +1584,7 @@ function register(api) {
1312
1584
  async execute(_id, params) {
1313
1585
  const { apiKey, baseUrl } = getCfg(api);
1314
1586
  if (!apiKey) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
1315
- assertAllowedBaseUrl(baseUrl);
1587
+ assertAllowedBaseUrl2(baseUrl);
1316
1588
  const payload = { timeout_sec: params.timeout_sec || 60 };
1317
1589
  if (params.stealth) payload.stealth = true;
1318
1590
  if (params.custom_storage) payload.custom_storage = params.custom_storage;