@riddledc/openclaw-riddledc 0.5.5 → 0.6.0

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
- 21e09d68db3add27f41481e3ef75bdffa14bc4ae496285c7df2757590104f48b dist/index.cjs
1
+ 876e2b78ca5f38ca4da27097761bcdb99882a5724f025d5befd5ba78710c5832 dist/index.cjs
2
2
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
3
3
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
4
- 63fe7e972b3fa5392e23902122409fbc6d59fa73e0751f0414fd06b9092a7fcd dist/index.js
4
+ c481cef6e2fd6aca8d7aef042bc7b8cb5fc12274afe9c50ba10947e398c566ce dist/index.js
package/dist/index.cjs CHANGED
@@ -26,6 +26,9 @@ module.exports = __toCommonJS(index_exports);
26
26
  var import_typebox = require("@sinclair/typebox");
27
27
  var import_promises = require("fs/promises");
28
28
  var import_node_path = require("path");
29
+ var import_node_child_process = require("child_process");
30
+ var import_node_util = require("util");
31
+ var execFile = (0, import_node_util.promisify)(import_node_child_process.execFile);
29
32
  var INLINE_CAP = 50 * 1024;
30
33
  function getCfg(api) {
31
34
  const cfg = api?.config ?? {};
@@ -148,6 +151,95 @@ async function applySafetySpec(result, opts) {
148
151
  }
149
152
  }
150
153
  }
154
+ async function pollJobStatus(baseUrl, apiKey, jobId, maxWaitMs) {
155
+ const start = Date.now();
156
+ const POLL_INTERVAL = 2e3;
157
+ while (Date.now() - start < maxWaitMs) {
158
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}`, {
159
+ headers: { Authorization: `Bearer ${apiKey}` }
160
+ });
161
+ if (!res.ok) {
162
+ return { status: "poll_error", error: `HTTP ${res.status}` };
163
+ }
164
+ const data = await res.json();
165
+ if (data.status === "completed" || data.status === "completed_timeout" || data.status === "completed_error" || data.status === "failed") {
166
+ return data;
167
+ }
168
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
169
+ }
170
+ return { status: "poll_timeout", error: `Job ${jobId} did not complete within ${maxWaitMs}ms` };
171
+ }
172
+ async function fetchArtifactsAndBuild(baseUrl, apiKey, jobId, include) {
173
+ const res = await fetch(
174
+ `${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}/artifacts?include=${include.join(",")}`,
175
+ { headers: { Authorization: `Bearer ${apiKey}` } }
176
+ );
177
+ if (!res.ok) {
178
+ return { error: `Artifacts fetch failed: HTTP ${res.status}` };
179
+ }
180
+ const data = await res.json();
181
+ const artifacts = data.artifacts || [];
182
+ const result = {};
183
+ if (data.status) result._artifactsStatus = data.status;
184
+ if (data.timeout) result._timeout = data.timeout;
185
+ if (data.error) result._error = data.error;
186
+ const screenshots = artifacts.filter((a) => a.name && /\.(png|jpg|jpeg)$/i.test(a.name));
187
+ if (screenshots.length > 0) {
188
+ result.screenshots = [];
189
+ for (const ss of screenshots) {
190
+ if (!ss.url) continue;
191
+ try {
192
+ const imgRes = await fetch(ss.url);
193
+ if (imgRes.ok) {
194
+ const buf = await imgRes.arrayBuffer();
195
+ result.screenshots.push({
196
+ name: ss.name,
197
+ data: `data:image/png;base64,${Buffer.from(buf).toString("base64")}`,
198
+ size: buf.byteLength,
199
+ url: ss.url
200
+ });
201
+ }
202
+ } catch {
203
+ }
204
+ }
205
+ if (result.screenshots.length > 0) {
206
+ result.screenshot = result.screenshots[0];
207
+ }
208
+ }
209
+ const consoleArtifact = artifacts.find((a) => a.name === "console.json");
210
+ if (consoleArtifact?.url) {
211
+ try {
212
+ const cRes = await fetch(consoleArtifact.url);
213
+ if (cRes.ok) {
214
+ result.console = await cRes.json();
215
+ }
216
+ } catch {
217
+ }
218
+ }
219
+ const resultArtifact = artifacts.find((a) => a.name === "result.json");
220
+ if (resultArtifact?.url) {
221
+ try {
222
+ const rRes = await fetch(resultArtifact.url);
223
+ if (rRes.ok) {
224
+ result.result = await rRes.json();
225
+ }
226
+ } catch {
227
+ }
228
+ }
229
+ if (include.includes("har")) {
230
+ const harArtifact = artifacts.find((a) => a.name === "network.har");
231
+ if (harArtifact?.url) {
232
+ try {
233
+ const hRes = await fetch(harArtifact.url);
234
+ if (hRes.ok) {
235
+ result.har = await hRes.json();
236
+ }
237
+ } catch {
238
+ }
239
+ }
240
+ }
241
+ return result;
242
+ }
151
243
  async function runWithDefaults(api, payload, defaults) {
152
244
  const { apiKey, baseUrl } = getCfg(api);
153
245
  if (!apiKey) {
@@ -161,8 +253,12 @@ async function runWithDefaults(api, payload, defaults) {
161
253
  const userInclude = payload.include ?? [];
162
254
  const userRequestedHar = userInclude.includes("har");
163
255
  const harInline = !!payload.harInline;
256
+ const returnAsync = !!defaults?.returnAsync;
164
257
  const merged = { ...payload };
165
258
  delete merged.harInline;
259
+ if (returnAsync) {
260
+ merged.sync = false;
261
+ }
166
262
  const defaultInc = defaults?.include ?? ["screenshot", "console"];
167
263
  merged.include = Array.from(/* @__PURE__ */ new Set([...userInclude, ...defaultInc]));
168
264
  merged.inlineConsole = merged.inlineConsole ?? true;
@@ -173,6 +269,46 @@ async function runWithDefaults(api, payload, defaults) {
173
269
  const out = { ok: true, mode };
174
270
  const { contentType, body, headers, status } = await postRun(baseUrl, apiKey, merged);
175
271
  out.rawContentType = contentType ?? void 0;
272
+ const workspace = getWorkspacePath(api);
273
+ if (status === 408) {
274
+ let jobIdFrom408;
275
+ try {
276
+ const json408 = JSON.parse(Buffer.from(body).toString("utf8"));
277
+ jobIdFrom408 = json408.job_id;
278
+ } catch {
279
+ }
280
+ if (!jobIdFrom408) {
281
+ out.ok = false;
282
+ out.error = "Sync poll timed out but no job_id in 408 response";
283
+ return out;
284
+ }
285
+ out.job_id = jobIdFrom408;
286
+ if (returnAsync) {
287
+ out.status = "submitted";
288
+ return out;
289
+ }
290
+ const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
291
+ const pollMaxMs = scriptTimeoutMs + 3e4;
292
+ const jobStatus = await pollJobStatus(baseUrl, apiKey, jobIdFrom408, pollMaxMs);
293
+ if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
294
+ out.ok = false;
295
+ out.status = jobStatus.status;
296
+ out.error = jobStatus.error || `Job ${jobStatus.status}`;
297
+ return out;
298
+ }
299
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, jobIdFrom408, merged.include);
300
+ Object.assign(out, artifacts);
301
+ out.job_id = jobIdFrom408;
302
+ out.status = jobStatus.status;
303
+ out.duration_ms = jobStatus.duration_ms;
304
+ if (jobStatus.status !== "completed") {
305
+ out.ok = false;
306
+ if (jobStatus.timeout) out.timeout = jobStatus.timeout;
307
+ if (jobStatus.error) out.error = jobStatus.error;
308
+ }
309
+ await applySafetySpec(out, { workspace, harInline });
310
+ return out;
311
+ }
176
312
  if (status >= 400) {
177
313
  try {
178
314
  const txt2 = Buffer.from(body).toString("utf8");
@@ -191,15 +327,42 @@ async function runWithDefaults(api, payload, defaults) {
191
327
  const duration = headers.get("x-duration-ms");
192
328
  out.duration_ms = duration ? Number(duration) : void 0;
193
329
  out.sync = true;
194
- const workspace2 = getWorkspacePath(api);
195
- await applySafetySpec(out, { workspace: workspace2, harInline });
330
+ await applySafetySpec(out, { workspace, harInline });
196
331
  return out;
197
332
  }
198
333
  const txt = Buffer.from(body).toString("utf8");
199
334
  const json = JSON.parse(txt);
200
335
  Object.assign(out, json);
201
336
  out.job_id = json.job_id ?? json.jobId ?? out.job_id;
202
- const workspace = getWorkspacePath(api);
337
+ if (status === 202 && out.job_id && json.status_url) {
338
+ if (returnAsync) {
339
+ out.status = "submitted";
340
+ return out;
341
+ }
342
+ const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
343
+ const pollMaxMs = scriptTimeoutMs + 3e4;
344
+ const jobStatus = await pollJobStatus(baseUrl, apiKey, out.job_id, pollMaxMs);
345
+ if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
346
+ out.ok = false;
347
+ out.status = jobStatus.status;
348
+ out.error = jobStatus.error || `Job ${jobStatus.status}`;
349
+ return out;
350
+ }
351
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, out.job_id, merged.include);
352
+ Object.assign(out, artifacts);
353
+ out.status = jobStatus.status;
354
+ out.duration_ms = jobStatus.duration_ms;
355
+ if (jobStatus.status !== "completed") {
356
+ out.ok = false;
357
+ if (jobStatus.timeout) out.timeout = jobStatus.timeout;
358
+ if (jobStatus.error) out.error = jobStatus.error;
359
+ }
360
+ await applySafetySpec(out, { workspace, harInline });
361
+ return out;
362
+ }
363
+ if (json.status === "completed_timeout" || json.status === "completed_error") {
364
+ out.ok = false;
365
+ }
203
366
  await applySafetySpec(out, { workspace, harInline });
204
367
  return out;
205
368
  }
@@ -207,19 +370,61 @@ function register(api) {
207
370
  api.registerTool(
208
371
  {
209
372
  name: "riddle_run",
210
- description: 'Run a Riddle job (pass-through payload) against https://api.riddledc.com/v1/run. Supports url/urls/steps/script. Returns screenshot + console by default; pass include:["har"] to opt in to HAR capture.',
373
+ description: 'Run a Riddle job (pass-through payload) against https://api.riddledc.com/v1/run. Supports url/urls/steps/script. Returns screenshot + console by default; pass include:["har"] to opt in to HAR capture. Set async:true to return immediately with job_id (use riddle_poll to check status later).',
211
374
  parameters: import_typebox.Type.Object({
212
- payload: import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())
375
+ payload: import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any()),
376
+ async: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
213
377
  }),
214
378
  async execute(_id, params) {
215
379
  const result = await runWithDefaults(api, params.payload, {
216
- include: ["screenshot", "console", "result"]
380
+ include: ["screenshot", "console", "result"],
381
+ returnAsync: !!params.async
217
382
  });
218
383
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
219
384
  }
220
385
  },
221
386
  { optional: true }
222
387
  );
388
+ api.registerTool(
389
+ {
390
+ name: "riddle_poll",
391
+ description: "Poll the status of an async Riddle job. Use after submitting a job with async:true. Returns current status if still running, or full results (screenshot, console, etc.) if completed.",
392
+ parameters: import_typebox.Type.Object({
393
+ job_id: import_typebox.Type.String({ description: "Job ID returned by an async riddle_* call" }),
394
+ include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Artifacts to fetch on completion (default: screenshot, console, result)" })),
395
+ harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean())
396
+ }),
397
+ async execute(_id, params) {
398
+ if (!params.job_id || typeof params.job_id !== "string") throw new Error("job_id must be a string");
399
+ const { apiKey, baseUrl } = getCfg(api);
400
+ if (!apiKey) {
401
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
402
+ }
403
+ assertAllowedBaseUrl(baseUrl);
404
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${params.job_id}`, {
405
+ headers: { Authorization: `Bearer ${apiKey}` }
406
+ });
407
+ if (!res.ok) {
408
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: params.job_id, error: `HTTP ${res.status}` }, null, 2) }] };
409
+ }
410
+ const data = await res.json();
411
+ if (data.status !== "completed" && data.status !== "completed_timeout" && data.status !== "completed_error" && data.status !== "failed") {
412
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, job_id: params.job_id, status: data.status, message: "Job still running. Call riddle_poll again later." }, null, 2) }] };
413
+ }
414
+ const include = params.include ?? ["screenshot", "console", "result"];
415
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, params.job_id, include);
416
+ const out = { ok: data.status === "completed", job_id: params.job_id, status: data.status, duration_ms: data.duration_ms, ...artifacts };
417
+ if (data.status !== "completed") {
418
+ if (data.timeout) out.timeout = data.timeout;
419
+ if (data.error) out.error = data.error;
420
+ }
421
+ const workspace = getWorkspacePath(api);
422
+ await applySafetySpec(out, { workspace, harInline: !!params.harInline });
423
+ return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
424
+ }
425
+ },
426
+ { optional: true }
427
+ );
223
428
  api.registerTool(
224
429
  {
225
430
  name: "riddle_screenshot",
@@ -301,7 +506,7 @@ function register(api) {
301
506
  api.registerTool(
302
507
  {
303
508
  name: "riddle_steps",
304
- description: `Riddle: run a workflow in steps mode (goto/click/fill/screenshot/scrape/map/crawl/etc.). Supports authenticated sessions via cookies/localStorage. Data extraction steps: { scrape: true }, { map: { max_pages?: N } }, { crawl: { max_pages?: N, format?: 'json'|'csv' } }. Returns screenshot + console by default; pass include:["har","data","urls","dataset","sitemap"] for additional artifacts.`,
509
+ description: `Riddle: run a workflow in steps mode (goto/click/fill/screenshot/scrape/map/crawl/etc.). Supports authenticated sessions via cookies/localStorage. Data extraction steps: { scrape: true }, { map: { max_pages?: N } }, { crawl: { max_pages?: N, format?: 'json'|'csv' } }. Returns screenshot + console by default; pass include:["har","data","urls","dataset","sitemap"] for additional artifacts. Set async:true to return immediately with job_id (use riddle_poll to check status later).`,
305
510
  parameters: import_typebox.Type.Object({
306
511
  steps: import_typebox.Type.Array(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
307
512
  timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number()),
@@ -318,7 +523,8 @@ function register(api) {
318
523
  options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
319
524
  include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
320
525
  harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
321
- sync: import_typebox.Type.Optional(import_typebox.Type.Boolean())
526
+ sync: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
527
+ async: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
322
528
  }),
323
529
  async execute(_id, params) {
324
530
  if (!Array.isArray(params.steps)) throw new Error("steps must be an array");
@@ -332,7 +538,7 @@ function register(api) {
332
538
  if (Object.keys(opts).length > 0) payload.options = opts;
333
539
  if (params.include) payload.include = params.include;
334
540
  if (params.harInline) payload.harInline = params.harInline;
335
- const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"] });
541
+ const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
336
542
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
337
543
  }
338
544
  },
@@ -341,7 +547,7 @@ function register(api) {
341
547
  api.registerTool(
342
548
  {
343
549
  name: "riddle_script",
344
- description: 'Riddle: run full Playwright code (script mode). Supports authenticated sessions via cookies/localStorage. In scripts, use `await injectLocalStorage()` after navigating to the origin to apply localStorage values. Available sandbox helpers: saveScreenshot(label), saveHtml(label), saveJson(name, data), scrape(opts?), map(opts?), crawl(opts?). Returns screenshot + console by default; pass include:["har","data","urls","dataset","sitemap"] for additional artifacts.',
550
+ description: 'Riddle: run full Playwright code (script mode). Supports authenticated sessions via cookies/localStorage. In scripts, use `await injectLocalStorage()` after navigating to the origin to apply localStorage values. Available sandbox helpers: saveScreenshot(label), saveHtml(label), saveJson(name, data), scrape(opts?), map(opts?), crawl(opts?). Returns screenshot + console by default; pass include:["har","data","urls","dataset","sitemap"] for additional artifacts. Set async:true to return immediately with job_id (use riddle_poll to check status later).',
345
551
  parameters: import_typebox.Type.Object({
346
552
  script: import_typebox.Type.String(),
347
553
  timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number()),
@@ -358,7 +564,8 @@ function register(api) {
358
564
  options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
359
565
  include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
360
566
  harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
361
- sync: import_typebox.Type.Optional(import_typebox.Type.Boolean())
567
+ sync: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
568
+ async: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
362
569
  }),
363
570
  async execute(_id, params) {
364
571
  if (!params.script || typeof params.script !== "string") throw new Error("script must be a string");
@@ -372,7 +579,7 @@ function register(api) {
372
579
  if (Object.keys(opts).length > 0) payload.options = opts;
373
580
  if (params.include) payload.include = params.include;
374
581
  if (params.harInline) payload.harInline = params.harInline;
375
- const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"] });
582
+ const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
376
583
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
377
584
  }
378
585
  },
@@ -549,4 +756,108 @@ function register(api) {
549
756
  },
550
757
  { optional: true }
551
758
  );
759
+ api.registerTool(
760
+ {
761
+ name: "riddle_preview",
762
+ description: "Deploy a local build directory as an ephemeral preview site. Tars the directory, uploads to Riddle, and returns a live URL at preview.riddledc.com that can be screenshotted with other riddle_* tools. Previews auto-expire after 24 hours.",
763
+ parameters: import_typebox.Type.Object({
764
+ directory: import_typebox.Type.String({ description: "Absolute path to the build output directory (e.g. /path/to/build or /path/to/dist)" }),
765
+ framework: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Framework hint: 'spa' (default) or 'static'" }))
766
+ }),
767
+ async execute(_id, params) {
768
+ const { apiKey, baseUrl } = getCfg(api);
769
+ if (!apiKey) {
770
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
771
+ }
772
+ assertAllowedBaseUrl(baseUrl);
773
+ const dir = params.directory;
774
+ if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
775
+ try {
776
+ const st = await (0, import_promises.stat)(dir);
777
+ if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
778
+ } catch (e) {
779
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
780
+ }
781
+ const endpoint = baseUrl.replace(/\/$/, "");
782
+ const createRes = await fetch(`${endpoint}/v1/preview`, {
783
+ method: "POST",
784
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
785
+ body: JSON.stringify({ framework: params.framework || "spa" })
786
+ });
787
+ if (!createRes.ok) {
788
+ const err = await createRes.text();
789
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
790
+ }
791
+ const created = await createRes.json();
792
+ const tarball = `/tmp/riddle-preview-${created.id}.tar.gz`;
793
+ try {
794
+ await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 6e4 });
795
+ const tarData = await (0, import_promises.readFile)(tarball);
796
+ const uploadRes = await fetch(created.upload_url, {
797
+ method: "PUT",
798
+ headers: { "Content-Type": "application/gzip" },
799
+ body: tarData
800
+ });
801
+ if (!uploadRes.ok) {
802
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
803
+ }
804
+ } finally {
805
+ try {
806
+ await (0, import_promises.rm)(tarball, { force: true });
807
+ } catch {
808
+ }
809
+ }
810
+ const publishRes = await fetch(`${endpoint}/v1/preview/${created.id}/publish`, {
811
+ method: "POST",
812
+ headers: { Authorization: `Bearer ${apiKey}` }
813
+ });
814
+ if (!publishRes.ok) {
815
+ const err = await publishRes.text();
816
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` }, null, 2) }] };
817
+ }
818
+ const published = await publishRes.json();
819
+ return {
820
+ content: [{
821
+ type: "text",
822
+ text: JSON.stringify({
823
+ ok: true,
824
+ id: published.id,
825
+ preview_url: published.preview_url,
826
+ file_count: published.file_count,
827
+ total_bytes: published.total_bytes,
828
+ expires_at: created.expires_at
829
+ }, null, 2)
830
+ }]
831
+ };
832
+ }
833
+ },
834
+ { optional: true }
835
+ );
836
+ api.registerTool(
837
+ {
838
+ name: "riddle_preview_delete",
839
+ description: "Delete an ephemeral preview site created by riddle_preview. Removes all files and frees the preview ID immediately instead of waiting for auto-expiry.",
840
+ parameters: import_typebox.Type.Object({
841
+ id: import_typebox.Type.String({ description: "Preview ID (e.g. pv_a1b2c3d4)" })
842
+ }),
843
+ async execute(_id, params) {
844
+ const { apiKey, baseUrl } = getCfg(api);
845
+ if (!apiKey) {
846
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
847
+ }
848
+ assertAllowedBaseUrl(baseUrl);
849
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
850
+ method: "DELETE",
851
+ headers: { Authorization: `Bearer ${apiKey}` }
852
+ });
853
+ if (!res.ok) {
854
+ const err = await res.text();
855
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: HTTP ${res.status} ${err}` }, null, 2) }] };
856
+ }
857
+ const data = await res.json();
858
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted: true, files_removed: data.files_removed }, null, 2) }] };
859
+ }
860
+ },
861
+ { optional: true }
862
+ );
552
863
  }
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  // src/index.ts
2
2
  import { Type } from "@sinclair/typebox";
3
- import { writeFile, mkdir } from "fs/promises";
3
+ import { writeFile, mkdir, readFile, stat, rm } from "fs/promises";
4
4
  import { join } from "path";
5
+ import { execFile as execFileCb } from "child_process";
6
+ import { promisify } from "util";
7
+ var execFile = promisify(execFileCb);
5
8
  var INLINE_CAP = 50 * 1024;
6
9
  function getCfg(api) {
7
10
  const cfg = api?.config ?? {};
@@ -124,6 +127,95 @@ async function applySafetySpec(result, opts) {
124
127
  }
125
128
  }
126
129
  }
130
+ async function pollJobStatus(baseUrl, apiKey, jobId, maxWaitMs) {
131
+ const start = Date.now();
132
+ const POLL_INTERVAL = 2e3;
133
+ while (Date.now() - start < maxWaitMs) {
134
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}`, {
135
+ headers: { Authorization: `Bearer ${apiKey}` }
136
+ });
137
+ if (!res.ok) {
138
+ return { status: "poll_error", error: `HTTP ${res.status}` };
139
+ }
140
+ const data = await res.json();
141
+ if (data.status === "completed" || data.status === "completed_timeout" || data.status === "completed_error" || data.status === "failed") {
142
+ return data;
143
+ }
144
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
145
+ }
146
+ return { status: "poll_timeout", error: `Job ${jobId} did not complete within ${maxWaitMs}ms` };
147
+ }
148
+ async function fetchArtifactsAndBuild(baseUrl, apiKey, jobId, include) {
149
+ const res = await fetch(
150
+ `${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}/artifacts?include=${include.join(",")}`,
151
+ { headers: { Authorization: `Bearer ${apiKey}` } }
152
+ );
153
+ if (!res.ok) {
154
+ return { error: `Artifacts fetch failed: HTTP ${res.status}` };
155
+ }
156
+ const data = await res.json();
157
+ const artifacts = data.artifacts || [];
158
+ const result = {};
159
+ if (data.status) result._artifactsStatus = data.status;
160
+ if (data.timeout) result._timeout = data.timeout;
161
+ if (data.error) result._error = data.error;
162
+ const screenshots = artifacts.filter((a) => a.name && /\.(png|jpg|jpeg)$/i.test(a.name));
163
+ if (screenshots.length > 0) {
164
+ result.screenshots = [];
165
+ for (const ss of screenshots) {
166
+ if (!ss.url) continue;
167
+ try {
168
+ const imgRes = await fetch(ss.url);
169
+ if (imgRes.ok) {
170
+ const buf = await imgRes.arrayBuffer();
171
+ result.screenshots.push({
172
+ name: ss.name,
173
+ data: `data:image/png;base64,${Buffer.from(buf).toString("base64")}`,
174
+ size: buf.byteLength,
175
+ url: ss.url
176
+ });
177
+ }
178
+ } catch {
179
+ }
180
+ }
181
+ if (result.screenshots.length > 0) {
182
+ result.screenshot = result.screenshots[0];
183
+ }
184
+ }
185
+ const consoleArtifact = artifacts.find((a) => a.name === "console.json");
186
+ if (consoleArtifact?.url) {
187
+ try {
188
+ const cRes = await fetch(consoleArtifact.url);
189
+ if (cRes.ok) {
190
+ result.console = await cRes.json();
191
+ }
192
+ } catch {
193
+ }
194
+ }
195
+ const resultArtifact = artifacts.find((a) => a.name === "result.json");
196
+ if (resultArtifact?.url) {
197
+ try {
198
+ const rRes = await fetch(resultArtifact.url);
199
+ if (rRes.ok) {
200
+ result.result = await rRes.json();
201
+ }
202
+ } catch {
203
+ }
204
+ }
205
+ if (include.includes("har")) {
206
+ const harArtifact = artifacts.find((a) => a.name === "network.har");
207
+ if (harArtifact?.url) {
208
+ try {
209
+ const hRes = await fetch(harArtifact.url);
210
+ if (hRes.ok) {
211
+ result.har = await hRes.json();
212
+ }
213
+ } catch {
214
+ }
215
+ }
216
+ }
217
+ return result;
218
+ }
127
219
  async function runWithDefaults(api, payload, defaults) {
128
220
  const { apiKey, baseUrl } = getCfg(api);
129
221
  if (!apiKey) {
@@ -137,8 +229,12 @@ async function runWithDefaults(api, payload, defaults) {
137
229
  const userInclude = payload.include ?? [];
138
230
  const userRequestedHar = userInclude.includes("har");
139
231
  const harInline = !!payload.harInline;
232
+ const returnAsync = !!defaults?.returnAsync;
140
233
  const merged = { ...payload };
141
234
  delete merged.harInline;
235
+ if (returnAsync) {
236
+ merged.sync = false;
237
+ }
142
238
  const defaultInc = defaults?.include ?? ["screenshot", "console"];
143
239
  merged.include = Array.from(/* @__PURE__ */ new Set([...userInclude, ...defaultInc]));
144
240
  merged.inlineConsole = merged.inlineConsole ?? true;
@@ -149,6 +245,46 @@ async function runWithDefaults(api, payload, defaults) {
149
245
  const out = { ok: true, mode };
150
246
  const { contentType, body, headers, status } = await postRun(baseUrl, apiKey, merged);
151
247
  out.rawContentType = contentType ?? void 0;
248
+ const workspace = getWorkspacePath(api);
249
+ if (status === 408) {
250
+ let jobIdFrom408;
251
+ try {
252
+ const json408 = JSON.parse(Buffer.from(body).toString("utf8"));
253
+ jobIdFrom408 = json408.job_id;
254
+ } catch {
255
+ }
256
+ if (!jobIdFrom408) {
257
+ out.ok = false;
258
+ out.error = "Sync poll timed out but no job_id in 408 response";
259
+ return out;
260
+ }
261
+ out.job_id = jobIdFrom408;
262
+ if (returnAsync) {
263
+ out.status = "submitted";
264
+ return out;
265
+ }
266
+ const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
267
+ const pollMaxMs = scriptTimeoutMs + 3e4;
268
+ const jobStatus = await pollJobStatus(baseUrl, apiKey, jobIdFrom408, pollMaxMs);
269
+ if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
270
+ out.ok = false;
271
+ out.status = jobStatus.status;
272
+ out.error = jobStatus.error || `Job ${jobStatus.status}`;
273
+ return out;
274
+ }
275
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, jobIdFrom408, merged.include);
276
+ Object.assign(out, artifacts);
277
+ out.job_id = jobIdFrom408;
278
+ out.status = jobStatus.status;
279
+ out.duration_ms = jobStatus.duration_ms;
280
+ if (jobStatus.status !== "completed") {
281
+ out.ok = false;
282
+ if (jobStatus.timeout) out.timeout = jobStatus.timeout;
283
+ if (jobStatus.error) out.error = jobStatus.error;
284
+ }
285
+ await applySafetySpec(out, { workspace, harInline });
286
+ return out;
287
+ }
152
288
  if (status >= 400) {
153
289
  try {
154
290
  const txt2 = Buffer.from(body).toString("utf8");
@@ -167,15 +303,42 @@ async function runWithDefaults(api, payload, defaults) {
167
303
  const duration = headers.get("x-duration-ms");
168
304
  out.duration_ms = duration ? Number(duration) : void 0;
169
305
  out.sync = true;
170
- const workspace2 = getWorkspacePath(api);
171
- await applySafetySpec(out, { workspace: workspace2, harInline });
306
+ await applySafetySpec(out, { workspace, harInline });
172
307
  return out;
173
308
  }
174
309
  const txt = Buffer.from(body).toString("utf8");
175
310
  const json = JSON.parse(txt);
176
311
  Object.assign(out, json);
177
312
  out.job_id = json.job_id ?? json.jobId ?? out.job_id;
178
- const workspace = getWorkspacePath(api);
313
+ if (status === 202 && out.job_id && json.status_url) {
314
+ if (returnAsync) {
315
+ out.status = "submitted";
316
+ return out;
317
+ }
318
+ const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
319
+ const pollMaxMs = scriptTimeoutMs + 3e4;
320
+ const jobStatus = await pollJobStatus(baseUrl, apiKey, out.job_id, pollMaxMs);
321
+ if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
322
+ out.ok = false;
323
+ out.status = jobStatus.status;
324
+ out.error = jobStatus.error || `Job ${jobStatus.status}`;
325
+ return out;
326
+ }
327
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, out.job_id, merged.include);
328
+ Object.assign(out, artifacts);
329
+ out.status = jobStatus.status;
330
+ out.duration_ms = jobStatus.duration_ms;
331
+ if (jobStatus.status !== "completed") {
332
+ out.ok = false;
333
+ if (jobStatus.timeout) out.timeout = jobStatus.timeout;
334
+ if (jobStatus.error) out.error = jobStatus.error;
335
+ }
336
+ await applySafetySpec(out, { workspace, harInline });
337
+ return out;
338
+ }
339
+ if (json.status === "completed_timeout" || json.status === "completed_error") {
340
+ out.ok = false;
341
+ }
179
342
  await applySafetySpec(out, { workspace, harInline });
180
343
  return out;
181
344
  }
@@ -183,19 +346,61 @@ function register(api) {
183
346
  api.registerTool(
184
347
  {
185
348
  name: "riddle_run",
186
- description: 'Run a Riddle job (pass-through payload) against https://api.riddledc.com/v1/run. Supports url/urls/steps/script. Returns screenshot + console by default; pass include:["har"] to opt in to HAR capture.',
349
+ description: 'Run a Riddle job (pass-through payload) against https://api.riddledc.com/v1/run. Supports url/urls/steps/script. Returns screenshot + console by default; pass include:["har"] to opt in to HAR capture. Set async:true to return immediately with job_id (use riddle_poll to check status later).',
187
350
  parameters: Type.Object({
188
- payload: Type.Record(Type.String(), Type.Any())
351
+ payload: Type.Record(Type.String(), Type.Any()),
352
+ async: Type.Optional(Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
189
353
  }),
190
354
  async execute(_id, params) {
191
355
  const result = await runWithDefaults(api, params.payload, {
192
- include: ["screenshot", "console", "result"]
356
+ include: ["screenshot", "console", "result"],
357
+ returnAsync: !!params.async
193
358
  });
194
359
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
195
360
  }
196
361
  },
197
362
  { optional: true }
198
363
  );
364
+ api.registerTool(
365
+ {
366
+ name: "riddle_poll",
367
+ description: "Poll the status of an async Riddle job. Use after submitting a job with async:true. Returns current status if still running, or full results (screenshot, console, etc.) if completed.",
368
+ parameters: Type.Object({
369
+ job_id: Type.String({ description: "Job ID returned by an async riddle_* call" }),
370
+ include: Type.Optional(Type.Array(Type.String(), { description: "Artifacts to fetch on completion (default: screenshot, console, result)" })),
371
+ harInline: Type.Optional(Type.Boolean())
372
+ }),
373
+ async execute(_id, params) {
374
+ if (!params.job_id || typeof params.job_id !== "string") throw new Error("job_id must be a string");
375
+ const { apiKey, baseUrl } = getCfg(api);
376
+ if (!apiKey) {
377
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
378
+ }
379
+ assertAllowedBaseUrl(baseUrl);
380
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${params.job_id}`, {
381
+ headers: { Authorization: `Bearer ${apiKey}` }
382
+ });
383
+ if (!res.ok) {
384
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: params.job_id, error: `HTTP ${res.status}` }, null, 2) }] };
385
+ }
386
+ const data = await res.json();
387
+ if (data.status !== "completed" && data.status !== "completed_timeout" && data.status !== "completed_error" && data.status !== "failed") {
388
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, job_id: params.job_id, status: data.status, message: "Job still running. Call riddle_poll again later." }, null, 2) }] };
389
+ }
390
+ const include = params.include ?? ["screenshot", "console", "result"];
391
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, params.job_id, include);
392
+ const out = { ok: data.status === "completed", job_id: params.job_id, status: data.status, duration_ms: data.duration_ms, ...artifacts };
393
+ if (data.status !== "completed") {
394
+ if (data.timeout) out.timeout = data.timeout;
395
+ if (data.error) out.error = data.error;
396
+ }
397
+ const workspace = getWorkspacePath(api);
398
+ await applySafetySpec(out, { workspace, harInline: !!params.harInline });
399
+ return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
400
+ }
401
+ },
402
+ { optional: true }
403
+ );
199
404
  api.registerTool(
200
405
  {
201
406
  name: "riddle_screenshot",
@@ -277,7 +482,7 @@ function register(api) {
277
482
  api.registerTool(
278
483
  {
279
484
  name: "riddle_steps",
280
- description: `Riddle: run a workflow in steps mode (goto/click/fill/screenshot/scrape/map/crawl/etc.). Supports authenticated sessions via cookies/localStorage. Data extraction steps: { scrape: true }, { map: { max_pages?: N } }, { crawl: { max_pages?: N, format?: 'json'|'csv' } }. Returns screenshot + console by default; pass include:["har","data","urls","dataset","sitemap"] for additional artifacts.`,
485
+ description: `Riddle: run a workflow in steps mode (goto/click/fill/screenshot/scrape/map/crawl/etc.). Supports authenticated sessions via cookies/localStorage. Data extraction steps: { scrape: true }, { map: { max_pages?: N } }, { crawl: { max_pages?: N, format?: 'json'|'csv' } }. Returns screenshot + console by default; pass include:["har","data","urls","dataset","sitemap"] for additional artifacts. Set async:true to return immediately with job_id (use riddle_poll to check status later).`,
281
486
  parameters: Type.Object({
282
487
  steps: Type.Array(Type.Record(Type.String(), Type.Any())),
283
488
  timeout_sec: Type.Optional(Type.Number()),
@@ -294,7 +499,8 @@ function register(api) {
294
499
  options: Type.Optional(Type.Record(Type.String(), Type.Any())),
295
500
  include: Type.Optional(Type.Array(Type.String())),
296
501
  harInline: Type.Optional(Type.Boolean()),
297
- sync: Type.Optional(Type.Boolean())
502
+ sync: Type.Optional(Type.Boolean()),
503
+ async: Type.Optional(Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
298
504
  }),
299
505
  async execute(_id, params) {
300
506
  if (!Array.isArray(params.steps)) throw new Error("steps must be an array");
@@ -308,7 +514,7 @@ function register(api) {
308
514
  if (Object.keys(opts).length > 0) payload.options = opts;
309
515
  if (params.include) payload.include = params.include;
310
516
  if (params.harInline) payload.harInline = params.harInline;
311
- const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"] });
517
+ const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
312
518
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
313
519
  }
314
520
  },
@@ -317,7 +523,7 @@ function register(api) {
317
523
  api.registerTool(
318
524
  {
319
525
  name: "riddle_script",
320
- description: 'Riddle: run full Playwright code (script mode). Supports authenticated sessions via cookies/localStorage. In scripts, use `await injectLocalStorage()` after navigating to the origin to apply localStorage values. Available sandbox helpers: saveScreenshot(label), saveHtml(label), saveJson(name, data), scrape(opts?), map(opts?), crawl(opts?). Returns screenshot + console by default; pass include:["har","data","urls","dataset","sitemap"] for additional artifacts.',
526
+ description: 'Riddle: run full Playwright code (script mode). Supports authenticated sessions via cookies/localStorage. In scripts, use `await injectLocalStorage()` after navigating to the origin to apply localStorage values. Available sandbox helpers: saveScreenshot(label), saveHtml(label), saveJson(name, data), scrape(opts?), map(opts?), crawl(opts?). Returns screenshot + console by default; pass include:["har","data","urls","dataset","sitemap"] for additional artifacts. Set async:true to return immediately with job_id (use riddle_poll to check status later).',
321
527
  parameters: Type.Object({
322
528
  script: Type.String(),
323
529
  timeout_sec: Type.Optional(Type.Number()),
@@ -334,7 +540,8 @@ function register(api) {
334
540
  options: Type.Optional(Type.Record(Type.String(), Type.Any())),
335
541
  include: Type.Optional(Type.Array(Type.String())),
336
542
  harInline: Type.Optional(Type.Boolean()),
337
- sync: Type.Optional(Type.Boolean())
543
+ sync: Type.Optional(Type.Boolean()),
544
+ async: Type.Optional(Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
338
545
  }),
339
546
  async execute(_id, params) {
340
547
  if (!params.script || typeof params.script !== "string") throw new Error("script must be a string");
@@ -348,7 +555,7 @@ function register(api) {
348
555
  if (Object.keys(opts).length > 0) payload.options = opts;
349
556
  if (params.include) payload.include = params.include;
350
557
  if (params.harInline) payload.harInline = params.harInline;
351
- const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"] });
558
+ const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
352
559
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
353
560
  }
354
561
  },
@@ -525,6 +732,110 @@ function register(api) {
525
732
  },
526
733
  { optional: true }
527
734
  );
735
+ api.registerTool(
736
+ {
737
+ name: "riddle_preview",
738
+ description: "Deploy a local build directory as an ephemeral preview site. Tars the directory, uploads to Riddle, and returns a live URL at preview.riddledc.com that can be screenshotted with other riddle_* tools. Previews auto-expire after 24 hours.",
739
+ parameters: Type.Object({
740
+ directory: Type.String({ description: "Absolute path to the build output directory (e.g. /path/to/build or /path/to/dist)" }),
741
+ framework: Type.Optional(Type.String({ description: "Framework hint: 'spa' (default) or 'static'" }))
742
+ }),
743
+ async execute(_id, params) {
744
+ const { apiKey, baseUrl } = getCfg(api);
745
+ if (!apiKey) {
746
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
747
+ }
748
+ assertAllowedBaseUrl(baseUrl);
749
+ const dir = params.directory;
750
+ if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
751
+ try {
752
+ const st = await stat(dir);
753
+ if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
754
+ } catch (e) {
755
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
756
+ }
757
+ const endpoint = baseUrl.replace(/\/$/, "");
758
+ const createRes = await fetch(`${endpoint}/v1/preview`, {
759
+ method: "POST",
760
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
761
+ body: JSON.stringify({ framework: params.framework || "spa" })
762
+ });
763
+ if (!createRes.ok) {
764
+ const err = await createRes.text();
765
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
766
+ }
767
+ const created = await createRes.json();
768
+ const tarball = `/tmp/riddle-preview-${created.id}.tar.gz`;
769
+ try {
770
+ await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 6e4 });
771
+ const tarData = await readFile(tarball);
772
+ const uploadRes = await fetch(created.upload_url, {
773
+ method: "PUT",
774
+ headers: { "Content-Type": "application/gzip" },
775
+ body: tarData
776
+ });
777
+ if (!uploadRes.ok) {
778
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
779
+ }
780
+ } finally {
781
+ try {
782
+ await rm(tarball, { force: true });
783
+ } catch {
784
+ }
785
+ }
786
+ const publishRes = await fetch(`${endpoint}/v1/preview/${created.id}/publish`, {
787
+ method: "POST",
788
+ headers: { Authorization: `Bearer ${apiKey}` }
789
+ });
790
+ if (!publishRes.ok) {
791
+ const err = await publishRes.text();
792
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` }, null, 2) }] };
793
+ }
794
+ const published = await publishRes.json();
795
+ return {
796
+ content: [{
797
+ type: "text",
798
+ text: JSON.stringify({
799
+ ok: true,
800
+ id: published.id,
801
+ preview_url: published.preview_url,
802
+ file_count: published.file_count,
803
+ total_bytes: published.total_bytes,
804
+ expires_at: created.expires_at
805
+ }, null, 2)
806
+ }]
807
+ };
808
+ }
809
+ },
810
+ { optional: true }
811
+ );
812
+ api.registerTool(
813
+ {
814
+ name: "riddle_preview_delete",
815
+ description: "Delete an ephemeral preview site created by riddle_preview. Removes all files and frees the preview ID immediately instead of waiting for auto-expiry.",
816
+ parameters: Type.Object({
817
+ id: Type.String({ description: "Preview ID (e.g. pv_a1b2c3d4)" })
818
+ }),
819
+ async execute(_id, params) {
820
+ const { apiKey, baseUrl } = getCfg(api);
821
+ if (!apiKey) {
822
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
823
+ }
824
+ assertAllowedBaseUrl(baseUrl);
825
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
826
+ method: "DELETE",
827
+ headers: { Authorization: `Bearer ${apiKey}` }
828
+ });
829
+ if (!res.ok) {
830
+ const err = await res.text();
831
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: HTTP ${res.status} ${err}` }, null, 2) }] };
832
+ }
833
+ const data = await res.json();
834
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted: true, files_removed: data.files_removed }, null, 2) }] };
835
+ }
836
+ },
837
+ { optional: true }
838
+ );
528
839
  }
529
840
  export {
530
841
  register as default
@@ -2,14 +2,15 @@
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.5.5",
6
- "notes": "0.4.0: Added riddle_scrape, riddle_map, riddle_crawl convenience tools. Updated riddle_steps and riddle_script descriptions with data extraction capabilities.",
5
+ "version": "0.6.0",
6
+ "notes": "0.6.0: Added riddle_preview and riddle_preview_delete tools for ephemeral preview hosting at preview.riddledc.com.",
7
7
  "type": "plugin",
8
8
  "bundledSkills": [],
9
9
  "capabilities": {
10
10
  "network": {
11
11
  "egress": [
12
- "api.riddledc.com"
12
+ "api.riddledc.com",
13
+ "preview.riddledc.com"
13
14
  ],
14
15
  "enforced": true,
15
16
  "note": "Hardcoded allowlist in assertAllowedBaseUrl() - cannot be overridden by config"
@@ -33,10 +34,13 @@
33
34
  "riddle_steps",
34
35
  "riddle_script",
35
36
  "riddle_run",
37
+ "riddle_poll",
36
38
  "riddle_scrape",
37
39
  "riddle_map",
38
40
  "riddle_crawl",
39
- "riddle_visual_diff"
41
+ "riddle_visual_diff",
42
+ "riddle_preview",
43
+ "riddle_preview_delete"
40
44
  ],
41
45
  "invokes": [],
42
46
  "note": "Provides tools for agent use; does not invoke other agent tools"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/openclaw-riddledc",
3
- "version": "0.5.5",
3
+ "version": "0.6.0",
4
4
  "description": "OpenClaw integration package for RiddleDC (no secrets).",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",