@riddledc/openclaw-riddledc 0.9.2 → 0.9.4

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,17 +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;
33
39
  var PREVIEW_REQUEST_TIMEOUT_MS = 3e4;
34
40
  var PREVIEW_UPLOAD_TIMEOUT_MS = 5 * 6e4;
35
- var PREVIEW_ARTIFACT_TIMEOUT_MS = 6e4;
36
41
  var PREVIEW_RETRY_ATTEMPTS = 3;
37
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;
38
252
  function getCfg(api) {
39
253
  const cfg = api?.config ?? {};
40
254
  const pluginCfg = cfg?.plugins?.entries?.["openclaw-riddledc"]?.config ?? {};
@@ -43,7 +257,7 @@ function getCfg(api) {
43
257
  baseUrl: pluginCfg.baseUrl || "https://api.riddledc.com"
44
258
  };
45
259
  }
46
- function assertAllowedBaseUrl(baseUrl) {
260
+ function assertAllowedBaseUrl2(baseUrl) {
47
261
  const url = new URL(baseUrl);
48
262
  if (url.protocol !== "https:") throw new Error(`Riddle baseUrl must be https: (${baseUrl})`);
49
263
  if (url.hostname !== "api.riddledc.com") {
@@ -80,7 +294,7 @@ function abToBase64(ab) {
80
294
  function getWorkspacePath(api) {
81
295
  return api?.workspacePath ?? process.cwd();
82
296
  }
83
- function describeError(err) {
297
+ function describeError2(err) {
84
298
  const anyErr = err;
85
299
  const parts = [];
86
300
  if (err instanceof Error) parts.push(err.message);
@@ -96,7 +310,7 @@ function describeError(err) {
96
310
  }
97
311
  return parts.join("; ");
98
312
  }
99
- async function fetchWithTimeout(url, init, timeoutMs, label) {
313
+ async function fetchWithTimeout2(url, init, timeoutMs, label) {
100
314
  const controller = new AbortController();
101
315
  const timer = setTimeout(() => controller.abort(), timeoutMs);
102
316
  try {
@@ -110,8 +324,8 @@ async function fetchWithTimeout(url, init, timeoutMs, label) {
110
324
  clearTimeout(timer);
111
325
  }
112
326
  }
113
- function isTransientFetchError(err) {
114
- const text = describeError(err).toLowerCase();
327
+ function isTransientFetchError2(err) {
328
+ const text = describeError2(err).toLowerCase();
115
329
  return [
116
330
  "fetch failed",
117
331
  "timed out",
@@ -126,44 +340,59 @@ function isTransientFetchError(err) {
126
340
  "terminated"
127
341
  ].some((needle) => text.includes(needle));
128
342
  }
129
- function sleep(ms) {
343
+ function sleep2(ms) {
130
344
  return new Promise((resolve) => setTimeout(resolve, ms));
131
345
  }
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;
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;
135
349
  let lastErr;
136
350
  for (let attempt = 1; attempt <= attempts; attempt++) {
137
351
  try {
138
- return await fetchWithTimeout(url, init, timeoutMs, label);
352
+ return await fetchWithTimeout2(url, init, timeoutMs, label);
139
353
  } catch (err) {
140
354
  lastErr = err;
141
- if (attempt >= attempts || !isTransientFetchError(err)) break;
355
+ if (attempt >= attempts || !isTransientFetchError2(err)) break;
142
356
  const jitterMs = Math.floor(Math.random() * 250);
143
357
  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);
358
+ console.warn(`[openclaw-riddledc] ${label} attempt ${attempt}/${attempts} failed: ${describeError2(err)}; retrying in ${delayMs}ms`);
359
+ await sleep2(delayMs);
146
360
  }
147
361
  }
148
- throw new Error(`${label} failed after ${attempts} attempts: ${describeError(lastErr)}`);
362
+ throw new Error(`${label} failed after ${attempts} attempts: ${describeError2(lastErr)}`);
149
363
  }
150
364
  function isAlreadyStartedResponse(status, body) {
151
365
  return status === 409 && /already in status:\s*(queued|running|complete|completed)/i.test(body);
152
366
  }
367
+ function previewTimeoutToolResult(jobId, timeoutMs, lastStatusData, extras = {}) {
368
+ const lastStatus = lastStatusData?.status ?? "unknown";
369
+ const result = {
370
+ ok: false,
371
+ job_id: jobId,
372
+ status: lastStatus,
373
+ outputs: lastStatusData?.outputs || [],
374
+ compute_seconds: lastStatusData?.compute_seconds,
375
+ egress_bytes: lastStatusData?.egress_bytes,
376
+ error: `Job did not complete within ${timeoutMs / 1e3}s; last status was ${lastStatus}`,
377
+ ...extras
378
+ };
379
+ if (lastStatusData?.error) result.server_error = lastStatusData.error;
380
+ return result;
381
+ }
153
382
  async function writeArtifact(workspace, subdir, filename, content) {
154
- const dir = (0, import_node_path.join)(workspace, "riddle", subdir);
155
- await (0, import_promises.mkdir)(dir, { recursive: true });
156
- const filePath = (0, import_node_path.join)(dir, filename);
383
+ const dir = (0, import_node_path2.join)(workspace, "riddle", subdir);
384
+ await (0, import_promises2.mkdir)(dir, { recursive: true });
385
+ const filePath = (0, import_node_path2.join)(dir, filename);
157
386
  const buf = Buffer.from(content, "utf8");
158
- await (0, import_promises.writeFile)(filePath, buf);
387
+ await (0, import_promises2.writeFile)(filePath, buf);
159
388
  return { path: filePath, sizeBytes: buf.byteLength };
160
389
  }
161
390
  async function writeArtifactBinary(workspace, subdir, filename, base64Content) {
162
- const dir = (0, import_node_path.join)(workspace, "riddle", subdir);
163
- await (0, import_promises.mkdir)(dir, { recursive: true });
164
- const filePath = (0, import_node_path.join)(dir, filename);
391
+ const dir = (0, import_node_path2.join)(workspace, "riddle", subdir);
392
+ await (0, import_promises2.mkdir)(dir, { recursive: true });
393
+ const filePath = (0, import_node_path2.join)(dir, filename);
165
394
  const buf = Buffer.from(base64Content, "base64");
166
- await (0, import_promises.writeFile)(filePath, buf);
395
+ await (0, import_promises2.writeFile)(filePath, buf);
167
396
  return { path: filePath, sizeBytes: buf.byteLength };
168
397
  }
169
398
  async function applySafetySpec(result, opts) {
@@ -207,10 +436,10 @@ async function applySafetySpec(result, opts) {
207
436
  if (result.har != null) {
208
437
  const harStr = typeof result.har === "string" ? result.har : JSON.stringify(result.har);
209
438
  const harBytes = Buffer.byteLength(harStr, "utf8");
210
- if (opts.harInline && harBytes <= INLINE_CAP) {
439
+ if (opts.harInline && harBytes <= INLINE_CAP2) {
211
440
  } else {
212
441
  const ref = await writeArtifact(opts.workspace, "har", `${jobId}.har.json`, harStr);
213
- if (opts.harInline && harBytes > INLINE_CAP) {
442
+ if (opts.harInline && harBytes > INLINE_CAP2) {
214
443
  result.har = { saved: ref.path, sizeBytes: ref.sizeBytes, warning: "Exceeded 50KB inline cap; wrote to file" };
215
444
  } else {
216
445
  result.har = { saved: ref.path, sizeBytes: ref.sizeBytes };
@@ -220,7 +449,7 @@ async function applySafetySpec(result, opts) {
220
449
  if (result.console != null) {
221
450
  const consoleStr = typeof result.console === "string" ? result.console : JSON.stringify(result.console);
222
451
  const consoleBytes = Buffer.byteLength(consoleStr, "utf8");
223
- if (consoleBytes > INLINE_CAP) {
452
+ if (consoleBytes > INLINE_CAP2) {
224
453
  const ref = await writeArtifact(opts.workspace, "console", `${jobId}.log`, consoleStr);
225
454
  result.console = { saved: ref.path, sizeBytes: ref.sizeBytes };
226
455
  }
@@ -323,7 +552,7 @@ async function runWithDefaults(api, payload, defaults) {
323
552
  error: "Missing Riddle API key. Set RIDDLE_API_KEY env var or plugins.entries.riddle.config.apiKey."
324
553
  };
325
554
  }
326
- assertAllowedBaseUrl(baseUrl);
555
+ assertAllowedBaseUrl2(baseUrl);
327
556
  const mode = detectMode(payload);
328
557
  const userInclude = payload.include ?? [];
329
558
  const userRequestedHar = userInclude.includes("har");
@@ -475,7 +704,7 @@ function register(api) {
475
704
  if (!apiKey) {
476
705
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
477
706
  }
478
- assertAllowedBaseUrl(baseUrl);
707
+ assertAllowedBaseUrl2(baseUrl);
479
708
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${params.job_id}`, {
480
709
  headers: { Authorization: `Bearer ${apiKey}` }
481
710
  });
@@ -884,85 +1113,8 @@ function register(api) {
884
1113
  framework: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Framework hint: 'spa' (default) or 'static'" }))
885
1114
  }),
886
1115
  async execute(_id, params) {
887
- const { apiKey, baseUrl } = getCfg(api);
888
- if (!apiKey) {
889
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
890
- }
891
- assertAllowedBaseUrl(baseUrl);
892
- const dir = params.directory;
893
- if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
894
- try {
895
- const st = await (0, import_promises.stat)(dir);
896
- if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
897
- } catch (e) {
898
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
899
- }
900
- const endpoint = baseUrl.replace(/\/$/, "");
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
- }
911
- if (!createRes.ok) {
912
- const err = await createRes.text();
913
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
914
- }
915
- const created = await createRes.json();
916
- const tarball = `/tmp/riddle-preview-${created.id}.tar.gz`;
917
- try {
918
- await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 6e4 });
919
- const tarData = await (0, import_promises.readFile)(tarball);
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
- }
930
- if (!uploadRes.ok) {
931
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
932
- }
933
- } finally {
934
- try {
935
- await (0, import_promises.rm)(tarball, { force: true });
936
- } catch {
937
- }
938
- }
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
- }
948
- if (!publishRes.ok) {
949
- const err = await publishRes.text();
950
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` }, null, 2) }] };
951
- }
952
- const published = await publishRes.json();
953
- return {
954
- content: [{
955
- type: "text",
956
- text: JSON.stringify({
957
- ok: true,
958
- id: published.id,
959
- preview_url: published.preview_url,
960
- file_count: published.file_count,
961
- total_bytes: published.total_bytes,
962
- expires_at: created.expires_at
963
- }, null, 2)
964
- }]
965
- };
1116
+ const result = await createStaticPreview(configFromOpenClawApi(api), params);
1117
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
966
1118
  }
967
1119
  },
968
1120
  { optional: true }
@@ -975,26 +1127,8 @@ function register(api) {
975
1127
  id: import_typebox.Type.String({ description: "Preview ID (e.g. pv_a1b2c3d4)" })
976
1128
  }),
977
1129
  async execute(_id, params) {
978
- const { apiKey, baseUrl } = getCfg(api);
979
- if (!apiKey) {
980
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
981
- }
982
- assertAllowedBaseUrl(baseUrl);
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
- }
992
- if (!res.ok) {
993
- const err = await res.text();
994
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: HTTP ${res.status} ${err}` }, null, 2) }] };
995
- }
996
- const data = await res.json();
997
- return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted: true, files_removed: data.files_removed }, null, 2) }] };
1130
+ const result = await deleteStaticPreview(configFromOpenClawApi(api), params.id);
1131
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
998
1132
  }
999
1133
  },
1000
1134
  { optional: true }
@@ -1029,11 +1163,11 @@ function register(api) {
1029
1163
  if (!apiKey) {
1030
1164
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
1031
1165
  }
1032
- assertAllowedBaseUrl(baseUrl);
1166
+ assertAllowedBaseUrl2(baseUrl);
1033
1167
  const dir = params.directory;
1034
1168
  if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
1035
1169
  try {
1036
- const st = await (0, import_promises.stat)(dir);
1170
+ const st = await (0, import_promises2.stat)(dir);
1037
1171
  if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
1038
1172
  } catch (e) {
1039
1173
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
@@ -1048,13 +1182,13 @@ function register(api) {
1048
1182
  if (hasLocalStorage) envBody.localStorage = params.localStorage;
1049
1183
  let envRes;
1050
1184
  try {
1051
- envRes = await fetchWithTimeout(`${endpoint}/v1/server-preview/env`, {
1185
+ envRes = await fetchWithTimeout2(`${endpoint}/v1/server-preview/env`, {
1052
1186
  method: "POST",
1053
1187
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1054
1188
  body: JSON.stringify(envBody)
1055
- }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview env store");
1189
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "server preview env store");
1056
1190
  } catch (e) {
1057
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError(e)}` }, null, 2) }] };
1191
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError2(e)}` }, null, 2) }] };
1058
1192
  }
1059
1193
  if (!envRes.ok) {
1060
1194
  const err = await envRes.text();
@@ -1083,13 +1217,13 @@ function register(api) {
1083
1217
  if (params.viewport) createBody.viewport = params.viewport;
1084
1218
  let createRes;
1085
1219
  try {
1086
- createRes = await fetchWithRetry(`${endpoint}/v1/server-preview`, {
1220
+ createRes = await fetchWithRetry2(`${endpoint}/v1/server-preview`, {
1087
1221
  method: "POST",
1088
1222
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1089
1223
  body: JSON.stringify(createBody)
1090
- }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview create");
1224
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "server preview create");
1091
1225
  } catch (e) {
1092
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError(e)}` }, null, 2) }] };
1226
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError2(e)}` }, null, 2) }] };
1093
1227
  }
1094
1228
  if (!createRes.ok) {
1095
1229
  const err = await createRes.text();
@@ -1100,35 +1234,35 @@ function register(api) {
1100
1234
  try {
1101
1235
  const excludes = params.exclude || [".git", "*.log"];
1102
1236
  const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
1103
- await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1104
- const tarData = await (0, import_promises.readFile)(tarball);
1237
+ await execFile2("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1238
+ const tarData = await (0, import_promises2.readFile)(tarball);
1105
1239
  let uploadRes;
1106
1240
  try {
1107
- uploadRes = await fetchWithRetry(created.upload_url, {
1241
+ uploadRes = await fetchWithRetry2(created.upload_url, {
1108
1242
  method: "PUT",
1109
1243
  headers: { "Content-Type": "application/gzip" },
1110
1244
  body: tarData
1111
- }, PREVIEW_UPLOAD_TIMEOUT_MS, "server preview upload");
1245
+ }, PREVIEW_UPLOAD_TIMEOUT_MS2, "server preview upload");
1112
1246
  } catch (e) {
1113
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError(e)}` }, null, 2) }] };
1247
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError2(e)}` }, null, 2) }] };
1114
1248
  }
1115
1249
  if (!uploadRes.ok) {
1116
1250
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
1117
1251
  }
1118
1252
  } finally {
1119
1253
  try {
1120
- await (0, import_promises.rm)(tarball, { force: true });
1254
+ await (0, import_promises2.rm)(tarball, { force: true });
1121
1255
  } catch {
1122
1256
  }
1123
1257
  }
1124
1258
  let startRes;
1125
1259
  try {
1126
- startRes = await fetchWithRetry(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
1260
+ startRes = await fetchWithRetry2(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
1127
1261
  method: "POST",
1128
1262
  headers: { Authorization: `Bearer ${apiKey}` }
1129
- }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview start");
1263
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "server preview start");
1130
1264
  } catch (e) {
1131
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError(e)}` }, null, 2) }] };
1265
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError2(e)}` }, null, 2) }] };
1132
1266
  }
1133
1267
  if (!startRes.ok) {
1134
1268
  const err = await startRes.text();
@@ -1141,19 +1275,21 @@ function register(api) {
1141
1275
  const timeoutMs = ((params.timeout || 120) + 60) * 1e3;
1142
1276
  const pollStart = Date.now();
1143
1277
  const POLL_INTERVAL = 3e3;
1278
+ let lastStatusData = null;
1144
1279
  while (Date.now() - pollStart < timeoutMs) {
1145
1280
  let statusRes;
1146
1281
  try {
1147
- statusRes = await fetchWithRetry(`${endpoint}/v1/server-preview/${created.job_id}`, {
1282
+ statusRes = await fetchWithRetry2(`${endpoint}/v1/server-preview/${created.job_id}`, {
1148
1283
  headers: { Authorization: `Bearer ${apiKey}` }
1149
- }, PREVIEW_REQUEST_TIMEOUT_MS, "server preview poll", { attempts: 2 });
1284
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "server preview poll", { attempts: 2 });
1150
1285
  } catch (e) {
1151
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError(e)}` }, null, 2) }] };
1286
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError2(e)}` }, null, 2) }] };
1152
1287
  }
1153
1288
  if (!statusRes.ok) {
1154
1289
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
1155
1290
  }
1156
1291
  const statusData = await statusRes.json();
1292
+ lastStatusData = statusData;
1157
1293
  if (statusData.status === "complete" || statusData.status === "completed" || statusData.status === "failed") {
1158
1294
  const result = {
1159
1295
  ok: statusData.status === "complete" || statusData.status === "completed",
@@ -1168,7 +1304,7 @@ function register(api) {
1168
1304
  for (const output of result.outputs) {
1169
1305
  if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
1170
1306
  try {
1171
- const imgRes = await fetchWithTimeout(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "server preview artifact download");
1307
+ const imgRes = await fetchWithTimeout2(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "server preview artifact download");
1172
1308
  if (imgRes.ok) {
1173
1309
  const buf = await imgRes.arrayBuffer();
1174
1310
  const base64 = Buffer.from(buf).toString("base64");
@@ -1185,7 +1321,7 @@ function register(api) {
1185
1321
  }
1186
1322
  await new Promise((r) => setTimeout(r, POLL_INTERVAL));
1187
1323
  }
1188
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Job did not complete within ${timeoutMs / 1e3}s` }, null, 2) }] };
1324
+ return { content: [{ type: "text", text: JSON.stringify(previewTimeoutToolResult(created.job_id, timeoutMs, lastStatusData), null, 2) }] };
1189
1325
  }
1190
1326
  },
1191
1327
  { optional: true }
@@ -1222,17 +1358,17 @@ function register(api) {
1222
1358
  if (!apiKey) {
1223
1359
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
1224
1360
  }
1225
- assertAllowedBaseUrl(baseUrl);
1361
+ assertAllowedBaseUrl2(baseUrl);
1226
1362
  const dir = params.directory;
1227
1363
  if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
1228
1364
  try {
1229
- const st = await (0, import_promises.stat)(dir);
1365
+ const st = await (0, import_promises2.stat)(dir);
1230
1366
  if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
1231
1367
  } catch (e) {
1232
1368
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
1233
1369
  }
1234
1370
  try {
1235
- await (0, import_promises.stat)(`${dir}/Dockerfile`);
1371
+ await (0, import_promises2.stat)(`${dir}/Dockerfile`);
1236
1372
  } catch {
1237
1373
  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) }] };
1238
1374
  }
@@ -1246,13 +1382,13 @@ function register(api) {
1246
1382
  if (hasLocalStorage) envBody.localStorage = params.localStorage;
1247
1383
  let envRes;
1248
1384
  try {
1249
- envRes = await fetchWithTimeout(`${endpoint}/v1/build-preview/env`, {
1385
+ envRes = await fetchWithTimeout2(`${endpoint}/v1/build-preview/env`, {
1250
1386
  method: "POST",
1251
1387
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1252
1388
  body: JSON.stringify(envBody)
1253
- }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview env store");
1389
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "build preview env store");
1254
1390
  } catch (e) {
1255
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError(e)}` }, null, 2) }] };
1391
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: ${describeError2(e)}` }, null, 2) }] };
1256
1392
  }
1257
1393
  if (!envRes.ok) {
1258
1394
  const err = await envRes.text();
@@ -1283,13 +1419,13 @@ function register(api) {
1283
1419
  if (params.audit) createBody.audit = true;
1284
1420
  let createRes;
1285
1421
  try {
1286
- createRes = await fetchWithRetry(`${endpoint}/v1/build-preview`, {
1422
+ createRes = await fetchWithRetry2(`${endpoint}/v1/build-preview`, {
1287
1423
  method: "POST",
1288
1424
  headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
1289
1425
  body: JSON.stringify(createBody)
1290
- }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview create");
1426
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "build preview create");
1291
1427
  } catch (e) {
1292
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError(e)}` }, null, 2) }] };
1428
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: ${describeError2(e)}` }, null, 2) }] };
1293
1429
  }
1294
1430
  if (!createRes.ok) {
1295
1431
  const err = await createRes.text();
@@ -1300,35 +1436,35 @@ function register(api) {
1300
1436
  try {
1301
1437
  const excludes = params.exclude || [".git", "*.log"];
1302
1438
  const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
1303
- await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1304
- const tarData = await (0, import_promises.readFile)(tarball);
1439
+ await execFile2("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
1440
+ const tarData = await (0, import_promises2.readFile)(tarball);
1305
1441
  let uploadRes;
1306
1442
  try {
1307
- uploadRes = await fetchWithRetry(created.upload_url, {
1443
+ uploadRes = await fetchWithRetry2(created.upload_url, {
1308
1444
  method: "PUT",
1309
1445
  headers: { "Content-Type": "application/gzip" },
1310
1446
  body: tarData
1311
- }, PREVIEW_UPLOAD_TIMEOUT_MS, "build preview upload");
1447
+ }, PREVIEW_UPLOAD_TIMEOUT_MS2, "build preview upload");
1312
1448
  } catch (e) {
1313
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError(e)}` }, null, 2) }] };
1449
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: ${describeError2(e)}` }, null, 2) }] };
1314
1450
  }
1315
1451
  if (!uploadRes.ok) {
1316
1452
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
1317
1453
  }
1318
1454
  } finally {
1319
1455
  try {
1320
- await (0, import_promises.rm)(tarball, { force: true });
1456
+ await (0, import_promises2.rm)(tarball, { force: true });
1321
1457
  } catch {
1322
1458
  }
1323
1459
  }
1324
1460
  let startRes;
1325
1461
  try {
1326
- startRes = await fetchWithRetry(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
1462
+ startRes = await fetchWithRetry2(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
1327
1463
  method: "POST",
1328
1464
  headers: { Authorization: `Bearer ${apiKey}` }
1329
- }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview start");
1465
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "build preview start");
1330
1466
  } catch (e) {
1331
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError(e)}` }, null, 2) }] };
1467
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: ${describeError2(e)}` }, null, 2) }] };
1332
1468
  }
1333
1469
  if (!startRes.ok) {
1334
1470
  const err = await startRes.text();
@@ -1341,19 +1477,21 @@ function register(api) {
1341
1477
  const timeoutMs = ((params.timeout || 180) + 120) * 1e3;
1342
1478
  const pollStart = Date.now();
1343
1479
  const POLL_INTERVAL = 3e3;
1480
+ let lastStatusData = null;
1344
1481
  while (Date.now() - pollStart < timeoutMs) {
1345
1482
  let statusRes;
1346
1483
  try {
1347
- statusRes = await fetchWithRetry(`${endpoint}/v1/build-preview/${created.job_id}`, {
1484
+ statusRes = await fetchWithRetry2(`${endpoint}/v1/build-preview/${created.job_id}`, {
1348
1485
  headers: { Authorization: `Bearer ${apiKey}` }
1349
- }, PREVIEW_REQUEST_TIMEOUT_MS, "build preview poll", { attempts: 2 });
1486
+ }, PREVIEW_REQUEST_TIMEOUT_MS2, "build preview poll", { attempts: 2 });
1350
1487
  } catch (e) {
1351
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError(e)}` }, null, 2) }] };
1488
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: ${describeError2(e)}` }, null, 2) }] };
1352
1489
  }
1353
1490
  if (!statusRes.ok) {
1354
1491
  return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
1355
1492
  }
1356
1493
  const statusData = await statusRes.json();
1494
+ lastStatusData = statusData;
1357
1495
  if (statusData.status === "complete" || statusData.status === "completed" || statusData.status === "failed") {
1358
1496
  const result = {
1359
1497
  ok: statusData.status === "complete" || statusData.status === "completed",
@@ -1372,7 +1510,7 @@ function register(api) {
1372
1510
  for (const output of result.outputs) {
1373
1511
  if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
1374
1512
  try {
1375
- const imgRes = await fetchWithTimeout(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "build preview artifact download");
1513
+ const imgRes = await fetchWithTimeout2(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, "build preview artifact download");
1376
1514
  if (imgRes.ok) {
1377
1515
  const buf = await imgRes.arrayBuffer();
1378
1516
  const base64 = Buffer.from(buf).toString("base64");
@@ -1389,7 +1527,12 @@ function register(api) {
1389
1527
  }
1390
1528
  await new Promise((r) => setTimeout(r, POLL_INTERVAL));
1391
1529
  }
1392
- return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Job did not complete within ${timeoutMs / 1e3}s` }, null, 2) }] };
1530
+ return { content: [{ type: "text", text: JSON.stringify(previewTimeoutToolResult(created.job_id, timeoutMs, lastStatusData, {
1531
+ build_duration_ms: lastStatusData?.build_duration_ms,
1532
+ ...lastStatusData?.build_log ? { build_log: lastStatusData.build_log } : {},
1533
+ ...lastStatusData?.container_log ? { container_log: lastStatusData.container_log } : {},
1534
+ ...lastStatusData?.audit ? { audit: lastStatusData.audit } : {}
1535
+ }), null, 2) }] };
1393
1536
  }
1394
1537
  },
1395
1538
  { optional: true }
@@ -1397,7 +1540,7 @@ function register(api) {
1397
1540
  async function riddleApiFetch(api2, method, path, body) {
1398
1541
  const { apiKey, baseUrl } = getCfg(api2);
1399
1542
  if (!apiKey) throw new Error("Missing Riddle API key. Set RIDDLE_API_KEY or configure in plugin settings.");
1400
- assertAllowedBaseUrl(baseUrl);
1543
+ assertAllowedBaseUrl2(baseUrl);
1401
1544
  const url = `${baseUrl.replace(/\/$/, "")}${path}`;
1402
1545
  const res = await fetch(url, {
1403
1546
  method,
@@ -1465,7 +1608,7 @@ function register(api) {
1465
1608
  async execute(_id, params) {
1466
1609
  const { apiKey, baseUrl } = getCfg(api);
1467
1610
  if (!apiKey) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
1468
- assertAllowedBaseUrl(baseUrl);
1611
+ assertAllowedBaseUrl2(baseUrl);
1469
1612
  const payload = { timeout_sec: params.timeout_sec || 60 };
1470
1613
  if (params.stealth) payload.stealth = true;
1471
1614
  if (params.custom_storage) payload.custom_storage = params.custom_storage;