@riddledc/openclaw-riddledc 0.5.4 → 0.5.6

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
- db809d10ed37ca00b918bcf01f53d92a7a179e9eeea8c03fcc3ebeba67f26269 dist/index.cjs
1
+ 041dfffc9dfaaa099cb6243c3512574254b91a80d19e64a68b078d3fc97bd4c8 dist/index.cjs
2
2
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
3
3
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
4
- 06d6a6e3dcc832ed0f4be2f74a2e271aaabc79469210fef2a21d4b1f3830f8de dist/index.js
4
+ b4d87078938b29cd38097688ac435c4fe328a561963212b53c678a2525c8b86a dist/index.js
package/dist/index.cjs CHANGED
@@ -99,7 +99,8 @@ async function applySafetySpec(result, opts) {
99
99
  }
100
100
  if (base64Data) {
101
101
  const ref = await writeArtifactBinary(opts.workspace, "screenshots", `${jobId}.png`, base64Data);
102
- result.screenshot = { saved: ref.path, sizeBytes: ref.sizeBytes };
102
+ const cdnUrl = typeof result.screenshot === "object" ? result.screenshot.url : void 0;
103
+ result.screenshot = { saved: ref.path, sizeBytes: ref.sizeBytes, ...cdnUrl ? { url: cdnUrl } : {} };
103
104
  }
104
105
  }
105
106
  if (Array.isArray(result.screenshots)) {
@@ -107,6 +108,7 @@ async function applySafetySpec(result, opts) {
107
108
  for (let i = 0; i < result.screenshots.length; i++) {
108
109
  const ss = result.screenshots[i];
109
110
  let base64Data = null;
111
+ const cdnUrl = typeof ss === "object" ? ss.url : void 0;
110
112
  if (typeof ss === "string") {
111
113
  base64Data = ss;
112
114
  } else if (typeof ss === "object" && ss.data) {
@@ -114,7 +116,7 @@ async function applySafetySpec(result, opts) {
114
116
  }
115
117
  if (base64Data) {
116
118
  const ref = await writeArtifactBinary(opts.workspace, "screenshots", `${jobId}-${i}.png`, base64Data);
117
- savedRefs.push({ saved: ref.path, sizeBytes: ref.sizeBytes });
119
+ savedRefs.push({ saved: ref.path, sizeBytes: ref.sizeBytes, ...cdnUrl ? { url: cdnUrl } : {} });
118
120
  }
119
121
  }
120
122
  result.screenshots = savedRefs;
@@ -146,6 +148,95 @@ async function applySafetySpec(result, opts) {
146
148
  }
147
149
  }
148
150
  }
151
+ async function pollJobStatus(baseUrl, apiKey, jobId, maxWaitMs) {
152
+ const start = Date.now();
153
+ const POLL_INTERVAL = 2e3;
154
+ while (Date.now() - start < maxWaitMs) {
155
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}`, {
156
+ headers: { Authorization: `Bearer ${apiKey}` }
157
+ });
158
+ if (!res.ok) {
159
+ return { status: "poll_error", error: `HTTP ${res.status}` };
160
+ }
161
+ const data = await res.json();
162
+ if (data.status === "completed" || data.status === "completed_timeout" || data.status === "completed_error" || data.status === "failed") {
163
+ return data;
164
+ }
165
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
166
+ }
167
+ return { status: "poll_timeout", error: `Job ${jobId} did not complete within ${maxWaitMs}ms` };
168
+ }
169
+ async function fetchArtifactsAndBuild(baseUrl, apiKey, jobId, include) {
170
+ const res = await fetch(
171
+ `${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}/artifacts?include=${include.join(",")}`,
172
+ { headers: { Authorization: `Bearer ${apiKey}` } }
173
+ );
174
+ if (!res.ok) {
175
+ return { error: `Artifacts fetch failed: HTTP ${res.status}` };
176
+ }
177
+ const data = await res.json();
178
+ const artifacts = data.artifacts || [];
179
+ const result = {};
180
+ if (data.status) result._artifactsStatus = data.status;
181
+ if (data.timeout) result._timeout = data.timeout;
182
+ if (data.error) result._error = data.error;
183
+ const screenshots = artifacts.filter((a) => a.name && /\.(png|jpg|jpeg)$/i.test(a.name));
184
+ if (screenshots.length > 0) {
185
+ result.screenshots = [];
186
+ for (const ss of screenshots) {
187
+ if (!ss.url) continue;
188
+ try {
189
+ const imgRes = await fetch(ss.url);
190
+ if (imgRes.ok) {
191
+ const buf = await imgRes.arrayBuffer();
192
+ result.screenshots.push({
193
+ name: ss.name,
194
+ data: `data:image/png;base64,${Buffer.from(buf).toString("base64")}`,
195
+ size: buf.byteLength,
196
+ url: ss.url
197
+ });
198
+ }
199
+ } catch {
200
+ }
201
+ }
202
+ if (result.screenshots.length > 0) {
203
+ result.screenshot = result.screenshots[0];
204
+ }
205
+ }
206
+ const consoleArtifact = artifacts.find((a) => a.name === "console.json");
207
+ if (consoleArtifact?.url) {
208
+ try {
209
+ const cRes = await fetch(consoleArtifact.url);
210
+ if (cRes.ok) {
211
+ result.console = await cRes.json();
212
+ }
213
+ } catch {
214
+ }
215
+ }
216
+ const resultArtifact = artifacts.find((a) => a.name === "result.json");
217
+ if (resultArtifact?.url) {
218
+ try {
219
+ const rRes = await fetch(resultArtifact.url);
220
+ if (rRes.ok) {
221
+ result.result = await rRes.json();
222
+ }
223
+ } catch {
224
+ }
225
+ }
226
+ if (include.includes("har")) {
227
+ const harArtifact = artifacts.find((a) => a.name === "network.har");
228
+ if (harArtifact?.url) {
229
+ try {
230
+ const hRes = await fetch(harArtifact.url);
231
+ if (hRes.ok) {
232
+ result.har = await hRes.json();
233
+ }
234
+ } catch {
235
+ }
236
+ }
237
+ }
238
+ return result;
239
+ }
149
240
  async function runWithDefaults(api, payload, defaults) {
150
241
  const { apiKey, baseUrl } = getCfg(api);
151
242
  if (!apiKey) {
@@ -159,8 +250,12 @@ async function runWithDefaults(api, payload, defaults) {
159
250
  const userInclude = payload.include ?? [];
160
251
  const userRequestedHar = userInclude.includes("har");
161
252
  const harInline = !!payload.harInline;
253
+ const returnAsync = !!defaults?.returnAsync;
162
254
  const merged = { ...payload };
163
255
  delete merged.harInline;
256
+ if (returnAsync) {
257
+ merged.sync = false;
258
+ }
164
259
  const defaultInc = defaults?.include ?? ["screenshot", "console"];
165
260
  merged.include = Array.from(/* @__PURE__ */ new Set([...userInclude, ...defaultInc]));
166
261
  merged.inlineConsole = merged.inlineConsole ?? true;
@@ -171,6 +266,46 @@ async function runWithDefaults(api, payload, defaults) {
171
266
  const out = { ok: true, mode };
172
267
  const { contentType, body, headers, status } = await postRun(baseUrl, apiKey, merged);
173
268
  out.rawContentType = contentType ?? void 0;
269
+ const workspace = getWorkspacePath(api);
270
+ if (status === 408) {
271
+ let jobIdFrom408;
272
+ try {
273
+ const json408 = JSON.parse(Buffer.from(body).toString("utf8"));
274
+ jobIdFrom408 = json408.job_id;
275
+ } catch {
276
+ }
277
+ if (!jobIdFrom408) {
278
+ out.ok = false;
279
+ out.error = "Sync poll timed out but no job_id in 408 response";
280
+ return out;
281
+ }
282
+ out.job_id = jobIdFrom408;
283
+ if (returnAsync) {
284
+ out.status = "submitted";
285
+ return out;
286
+ }
287
+ const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
288
+ const pollMaxMs = scriptTimeoutMs + 3e4;
289
+ const jobStatus = await pollJobStatus(baseUrl, apiKey, jobIdFrom408, pollMaxMs);
290
+ if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
291
+ out.ok = false;
292
+ out.status = jobStatus.status;
293
+ out.error = jobStatus.error || `Job ${jobStatus.status}`;
294
+ return out;
295
+ }
296
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, jobIdFrom408, merged.include);
297
+ Object.assign(out, artifacts);
298
+ out.job_id = jobIdFrom408;
299
+ out.status = jobStatus.status;
300
+ out.duration_ms = jobStatus.duration_ms;
301
+ if (jobStatus.status !== "completed") {
302
+ out.ok = false;
303
+ if (jobStatus.timeout) out.timeout = jobStatus.timeout;
304
+ if (jobStatus.error) out.error = jobStatus.error;
305
+ }
306
+ await applySafetySpec(out, { workspace, harInline });
307
+ return out;
308
+ }
174
309
  if (status >= 400) {
175
310
  try {
176
311
  const txt2 = Buffer.from(body).toString("utf8");
@@ -189,15 +324,42 @@ async function runWithDefaults(api, payload, defaults) {
189
324
  const duration = headers.get("x-duration-ms");
190
325
  out.duration_ms = duration ? Number(duration) : void 0;
191
326
  out.sync = true;
192
- const workspace2 = getWorkspacePath(api);
193
- await applySafetySpec(out, { workspace: workspace2, harInline });
327
+ await applySafetySpec(out, { workspace, harInline });
194
328
  return out;
195
329
  }
196
330
  const txt = Buffer.from(body).toString("utf8");
197
331
  const json = JSON.parse(txt);
198
332
  Object.assign(out, json);
199
333
  out.job_id = json.job_id ?? json.jobId ?? out.job_id;
200
- const workspace = getWorkspacePath(api);
334
+ if (status === 202 && out.job_id && json.status_url) {
335
+ if (returnAsync) {
336
+ out.status = "submitted";
337
+ return out;
338
+ }
339
+ const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
340
+ const pollMaxMs = scriptTimeoutMs + 3e4;
341
+ const jobStatus = await pollJobStatus(baseUrl, apiKey, out.job_id, pollMaxMs);
342
+ if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
343
+ out.ok = false;
344
+ out.status = jobStatus.status;
345
+ out.error = jobStatus.error || `Job ${jobStatus.status}`;
346
+ return out;
347
+ }
348
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, out.job_id, merged.include);
349
+ Object.assign(out, artifacts);
350
+ out.status = jobStatus.status;
351
+ out.duration_ms = jobStatus.duration_ms;
352
+ if (jobStatus.status !== "completed") {
353
+ out.ok = false;
354
+ if (jobStatus.timeout) out.timeout = jobStatus.timeout;
355
+ if (jobStatus.error) out.error = jobStatus.error;
356
+ }
357
+ await applySafetySpec(out, { workspace, harInline });
358
+ return out;
359
+ }
360
+ if (json.status === "completed_timeout" || json.status === "completed_error") {
361
+ out.ok = false;
362
+ }
201
363
  await applySafetySpec(out, { workspace, harInline });
202
364
  return out;
203
365
  }
@@ -205,19 +367,61 @@ function register(api) {
205
367
  api.registerTool(
206
368
  {
207
369
  name: "riddle_run",
208
- 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.',
370
+ 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).',
209
371
  parameters: import_typebox.Type.Object({
210
- payload: import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())
372
+ payload: import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any()),
373
+ async: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
211
374
  }),
212
375
  async execute(_id, params) {
213
376
  const result = await runWithDefaults(api, params.payload, {
214
- include: ["screenshot", "console", "result"]
377
+ include: ["screenshot", "console", "result"],
378
+ returnAsync: !!params.async
215
379
  });
216
380
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
217
381
  }
218
382
  },
219
383
  { optional: true }
220
384
  );
385
+ api.registerTool(
386
+ {
387
+ name: "riddle_poll",
388
+ 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.",
389
+ parameters: import_typebox.Type.Object({
390
+ job_id: import_typebox.Type.String({ description: "Job ID returned by an async riddle_* call" }),
391
+ include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Artifacts to fetch on completion (default: screenshot, console, result)" })),
392
+ harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean())
393
+ }),
394
+ async execute(_id, params) {
395
+ if (!params.job_id || typeof params.job_id !== "string") throw new Error("job_id must be a string");
396
+ const { apiKey, baseUrl } = getCfg(api);
397
+ if (!apiKey) {
398
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
399
+ }
400
+ assertAllowedBaseUrl(baseUrl);
401
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${params.job_id}`, {
402
+ headers: { Authorization: `Bearer ${apiKey}` }
403
+ });
404
+ if (!res.ok) {
405
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: params.job_id, error: `HTTP ${res.status}` }, null, 2) }] };
406
+ }
407
+ const data = await res.json();
408
+ if (data.status !== "completed" && data.status !== "completed_timeout" && data.status !== "completed_error" && data.status !== "failed") {
409
+ 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) }] };
410
+ }
411
+ const include = params.include ?? ["screenshot", "console", "result"];
412
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, params.job_id, include);
413
+ const out = { ok: data.status === "completed", job_id: params.job_id, status: data.status, duration_ms: data.duration_ms, ...artifacts };
414
+ if (data.status !== "completed") {
415
+ if (data.timeout) out.timeout = data.timeout;
416
+ if (data.error) out.error = data.error;
417
+ }
418
+ const workspace = getWorkspacePath(api);
419
+ await applySafetySpec(out, { workspace, harInline: !!params.harInline });
420
+ return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
421
+ }
422
+ },
423
+ { optional: true }
424
+ );
221
425
  api.registerTool(
222
426
  {
223
427
  name: "riddle_screenshot",
@@ -299,7 +503,7 @@ function register(api) {
299
503
  api.registerTool(
300
504
  {
301
505
  name: "riddle_steps",
302
- 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.`,
506
+ 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).`,
303
507
  parameters: import_typebox.Type.Object({
304
508
  steps: import_typebox.Type.Array(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
305
509
  timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number()),
@@ -316,7 +520,8 @@ function register(api) {
316
520
  options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
317
521
  include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
318
522
  harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
319
- sync: import_typebox.Type.Optional(import_typebox.Type.Boolean())
523
+ sync: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
524
+ async: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
320
525
  }),
321
526
  async execute(_id, params) {
322
527
  if (!Array.isArray(params.steps)) throw new Error("steps must be an array");
@@ -330,7 +535,7 @@ function register(api) {
330
535
  if (Object.keys(opts).length > 0) payload.options = opts;
331
536
  if (params.include) payload.include = params.include;
332
537
  if (params.harInline) payload.harInline = params.harInline;
333
- const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"] });
538
+ const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
334
539
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
335
540
  }
336
541
  },
@@ -339,7 +544,7 @@ function register(api) {
339
544
  api.registerTool(
340
545
  {
341
546
  name: "riddle_script",
342
- 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.',
547
+ 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).',
343
548
  parameters: import_typebox.Type.Object({
344
549
  script: import_typebox.Type.String(),
345
550
  timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number()),
@@ -356,7 +561,8 @@ function register(api) {
356
561
  options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
357
562
  include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
358
563
  harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
359
- sync: import_typebox.Type.Optional(import_typebox.Type.Boolean())
564
+ sync: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
565
+ async: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
360
566
  }),
361
567
  async execute(_id, params) {
362
568
  if (!params.script || typeof params.script !== "string") throw new Error("script must be a string");
@@ -370,7 +576,7 @@ function register(api) {
370
576
  if (Object.keys(opts).length > 0) payload.options = opts;
371
577
  if (params.include) payload.include = params.include;
372
578
  if (params.harInline) payload.harInline = params.harInline;
373
- const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"] });
579
+ const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
374
580
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
375
581
  }
376
582
  },
package/dist/index.js CHANGED
@@ -75,7 +75,8 @@ async function applySafetySpec(result, opts) {
75
75
  }
76
76
  if (base64Data) {
77
77
  const ref = await writeArtifactBinary(opts.workspace, "screenshots", `${jobId}.png`, base64Data);
78
- result.screenshot = { saved: ref.path, sizeBytes: ref.sizeBytes };
78
+ const cdnUrl = typeof result.screenshot === "object" ? result.screenshot.url : void 0;
79
+ result.screenshot = { saved: ref.path, sizeBytes: ref.sizeBytes, ...cdnUrl ? { url: cdnUrl } : {} };
79
80
  }
80
81
  }
81
82
  if (Array.isArray(result.screenshots)) {
@@ -83,6 +84,7 @@ async function applySafetySpec(result, opts) {
83
84
  for (let i = 0; i < result.screenshots.length; i++) {
84
85
  const ss = result.screenshots[i];
85
86
  let base64Data = null;
87
+ const cdnUrl = typeof ss === "object" ? ss.url : void 0;
86
88
  if (typeof ss === "string") {
87
89
  base64Data = ss;
88
90
  } else if (typeof ss === "object" && ss.data) {
@@ -90,7 +92,7 @@ async function applySafetySpec(result, opts) {
90
92
  }
91
93
  if (base64Data) {
92
94
  const ref = await writeArtifactBinary(opts.workspace, "screenshots", `${jobId}-${i}.png`, base64Data);
93
- savedRefs.push({ saved: ref.path, sizeBytes: ref.sizeBytes });
95
+ savedRefs.push({ saved: ref.path, sizeBytes: ref.sizeBytes, ...cdnUrl ? { url: cdnUrl } : {} });
94
96
  }
95
97
  }
96
98
  result.screenshots = savedRefs;
@@ -122,6 +124,95 @@ async function applySafetySpec(result, opts) {
122
124
  }
123
125
  }
124
126
  }
127
+ async function pollJobStatus(baseUrl, apiKey, jobId, maxWaitMs) {
128
+ const start = Date.now();
129
+ const POLL_INTERVAL = 2e3;
130
+ while (Date.now() - start < maxWaitMs) {
131
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}`, {
132
+ headers: { Authorization: `Bearer ${apiKey}` }
133
+ });
134
+ if (!res.ok) {
135
+ return { status: "poll_error", error: `HTTP ${res.status}` };
136
+ }
137
+ const data = await res.json();
138
+ if (data.status === "completed" || data.status === "completed_timeout" || data.status === "completed_error" || data.status === "failed") {
139
+ return data;
140
+ }
141
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
142
+ }
143
+ return { status: "poll_timeout", error: `Job ${jobId} did not complete within ${maxWaitMs}ms` };
144
+ }
145
+ async function fetchArtifactsAndBuild(baseUrl, apiKey, jobId, include) {
146
+ const res = await fetch(
147
+ `${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}/artifacts?include=${include.join(",")}`,
148
+ { headers: { Authorization: `Bearer ${apiKey}` } }
149
+ );
150
+ if (!res.ok) {
151
+ return { error: `Artifacts fetch failed: HTTP ${res.status}` };
152
+ }
153
+ const data = await res.json();
154
+ const artifacts = data.artifacts || [];
155
+ const result = {};
156
+ if (data.status) result._artifactsStatus = data.status;
157
+ if (data.timeout) result._timeout = data.timeout;
158
+ if (data.error) result._error = data.error;
159
+ const screenshots = artifacts.filter((a) => a.name && /\.(png|jpg|jpeg)$/i.test(a.name));
160
+ if (screenshots.length > 0) {
161
+ result.screenshots = [];
162
+ for (const ss of screenshots) {
163
+ if (!ss.url) continue;
164
+ try {
165
+ const imgRes = await fetch(ss.url);
166
+ if (imgRes.ok) {
167
+ const buf = await imgRes.arrayBuffer();
168
+ result.screenshots.push({
169
+ name: ss.name,
170
+ data: `data:image/png;base64,${Buffer.from(buf).toString("base64")}`,
171
+ size: buf.byteLength,
172
+ url: ss.url
173
+ });
174
+ }
175
+ } catch {
176
+ }
177
+ }
178
+ if (result.screenshots.length > 0) {
179
+ result.screenshot = result.screenshots[0];
180
+ }
181
+ }
182
+ const consoleArtifact = artifacts.find((a) => a.name === "console.json");
183
+ if (consoleArtifact?.url) {
184
+ try {
185
+ const cRes = await fetch(consoleArtifact.url);
186
+ if (cRes.ok) {
187
+ result.console = await cRes.json();
188
+ }
189
+ } catch {
190
+ }
191
+ }
192
+ const resultArtifact = artifacts.find((a) => a.name === "result.json");
193
+ if (resultArtifact?.url) {
194
+ try {
195
+ const rRes = await fetch(resultArtifact.url);
196
+ if (rRes.ok) {
197
+ result.result = await rRes.json();
198
+ }
199
+ } catch {
200
+ }
201
+ }
202
+ if (include.includes("har")) {
203
+ const harArtifact = artifacts.find((a) => a.name === "network.har");
204
+ if (harArtifact?.url) {
205
+ try {
206
+ const hRes = await fetch(harArtifact.url);
207
+ if (hRes.ok) {
208
+ result.har = await hRes.json();
209
+ }
210
+ } catch {
211
+ }
212
+ }
213
+ }
214
+ return result;
215
+ }
125
216
  async function runWithDefaults(api, payload, defaults) {
126
217
  const { apiKey, baseUrl } = getCfg(api);
127
218
  if (!apiKey) {
@@ -135,8 +226,12 @@ async function runWithDefaults(api, payload, defaults) {
135
226
  const userInclude = payload.include ?? [];
136
227
  const userRequestedHar = userInclude.includes("har");
137
228
  const harInline = !!payload.harInline;
229
+ const returnAsync = !!defaults?.returnAsync;
138
230
  const merged = { ...payload };
139
231
  delete merged.harInline;
232
+ if (returnAsync) {
233
+ merged.sync = false;
234
+ }
140
235
  const defaultInc = defaults?.include ?? ["screenshot", "console"];
141
236
  merged.include = Array.from(/* @__PURE__ */ new Set([...userInclude, ...defaultInc]));
142
237
  merged.inlineConsole = merged.inlineConsole ?? true;
@@ -147,6 +242,46 @@ async function runWithDefaults(api, payload, defaults) {
147
242
  const out = { ok: true, mode };
148
243
  const { contentType, body, headers, status } = await postRun(baseUrl, apiKey, merged);
149
244
  out.rawContentType = contentType ?? void 0;
245
+ const workspace = getWorkspacePath(api);
246
+ if (status === 408) {
247
+ let jobIdFrom408;
248
+ try {
249
+ const json408 = JSON.parse(Buffer.from(body).toString("utf8"));
250
+ jobIdFrom408 = json408.job_id;
251
+ } catch {
252
+ }
253
+ if (!jobIdFrom408) {
254
+ out.ok = false;
255
+ out.error = "Sync poll timed out but no job_id in 408 response";
256
+ return out;
257
+ }
258
+ out.job_id = jobIdFrom408;
259
+ if (returnAsync) {
260
+ out.status = "submitted";
261
+ return out;
262
+ }
263
+ const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
264
+ const pollMaxMs = scriptTimeoutMs + 3e4;
265
+ const jobStatus = await pollJobStatus(baseUrl, apiKey, jobIdFrom408, pollMaxMs);
266
+ if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
267
+ out.ok = false;
268
+ out.status = jobStatus.status;
269
+ out.error = jobStatus.error || `Job ${jobStatus.status}`;
270
+ return out;
271
+ }
272
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, jobIdFrom408, merged.include);
273
+ Object.assign(out, artifacts);
274
+ out.job_id = jobIdFrom408;
275
+ out.status = jobStatus.status;
276
+ out.duration_ms = jobStatus.duration_ms;
277
+ if (jobStatus.status !== "completed") {
278
+ out.ok = false;
279
+ if (jobStatus.timeout) out.timeout = jobStatus.timeout;
280
+ if (jobStatus.error) out.error = jobStatus.error;
281
+ }
282
+ await applySafetySpec(out, { workspace, harInline });
283
+ return out;
284
+ }
150
285
  if (status >= 400) {
151
286
  try {
152
287
  const txt2 = Buffer.from(body).toString("utf8");
@@ -165,15 +300,42 @@ async function runWithDefaults(api, payload, defaults) {
165
300
  const duration = headers.get("x-duration-ms");
166
301
  out.duration_ms = duration ? Number(duration) : void 0;
167
302
  out.sync = true;
168
- const workspace2 = getWorkspacePath(api);
169
- await applySafetySpec(out, { workspace: workspace2, harInline });
303
+ await applySafetySpec(out, { workspace, harInline });
170
304
  return out;
171
305
  }
172
306
  const txt = Buffer.from(body).toString("utf8");
173
307
  const json = JSON.parse(txt);
174
308
  Object.assign(out, json);
175
309
  out.job_id = json.job_id ?? json.jobId ?? out.job_id;
176
- const workspace = getWorkspacePath(api);
310
+ if (status === 202 && out.job_id && json.status_url) {
311
+ if (returnAsync) {
312
+ out.status = "submitted";
313
+ return out;
314
+ }
315
+ const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
316
+ const pollMaxMs = scriptTimeoutMs + 3e4;
317
+ const jobStatus = await pollJobStatus(baseUrl, apiKey, out.job_id, pollMaxMs);
318
+ if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
319
+ out.ok = false;
320
+ out.status = jobStatus.status;
321
+ out.error = jobStatus.error || `Job ${jobStatus.status}`;
322
+ return out;
323
+ }
324
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, out.job_id, merged.include);
325
+ Object.assign(out, artifacts);
326
+ out.status = jobStatus.status;
327
+ out.duration_ms = jobStatus.duration_ms;
328
+ if (jobStatus.status !== "completed") {
329
+ out.ok = false;
330
+ if (jobStatus.timeout) out.timeout = jobStatus.timeout;
331
+ if (jobStatus.error) out.error = jobStatus.error;
332
+ }
333
+ await applySafetySpec(out, { workspace, harInline });
334
+ return out;
335
+ }
336
+ if (json.status === "completed_timeout" || json.status === "completed_error") {
337
+ out.ok = false;
338
+ }
177
339
  await applySafetySpec(out, { workspace, harInline });
178
340
  return out;
179
341
  }
@@ -181,19 +343,61 @@ function register(api) {
181
343
  api.registerTool(
182
344
  {
183
345
  name: "riddle_run",
184
- 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.',
346
+ 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).',
185
347
  parameters: Type.Object({
186
- payload: Type.Record(Type.String(), Type.Any())
348
+ payload: Type.Record(Type.String(), Type.Any()),
349
+ async: Type.Optional(Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
187
350
  }),
188
351
  async execute(_id, params) {
189
352
  const result = await runWithDefaults(api, params.payload, {
190
- include: ["screenshot", "console", "result"]
353
+ include: ["screenshot", "console", "result"],
354
+ returnAsync: !!params.async
191
355
  });
192
356
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
193
357
  }
194
358
  },
195
359
  { optional: true }
196
360
  );
361
+ api.registerTool(
362
+ {
363
+ name: "riddle_poll",
364
+ 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.",
365
+ parameters: Type.Object({
366
+ job_id: Type.String({ description: "Job ID returned by an async riddle_* call" }),
367
+ include: Type.Optional(Type.Array(Type.String(), { description: "Artifacts to fetch on completion (default: screenshot, console, result)" })),
368
+ harInline: Type.Optional(Type.Boolean())
369
+ }),
370
+ async execute(_id, params) {
371
+ if (!params.job_id || typeof params.job_id !== "string") throw new Error("job_id must be a string");
372
+ const { apiKey, baseUrl } = getCfg(api);
373
+ if (!apiKey) {
374
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
375
+ }
376
+ assertAllowedBaseUrl(baseUrl);
377
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${params.job_id}`, {
378
+ headers: { Authorization: `Bearer ${apiKey}` }
379
+ });
380
+ if (!res.ok) {
381
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: params.job_id, error: `HTTP ${res.status}` }, null, 2) }] };
382
+ }
383
+ const data = await res.json();
384
+ if (data.status !== "completed" && data.status !== "completed_timeout" && data.status !== "completed_error" && data.status !== "failed") {
385
+ 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) }] };
386
+ }
387
+ const include = params.include ?? ["screenshot", "console", "result"];
388
+ const artifacts = await fetchArtifactsAndBuild(baseUrl, apiKey, params.job_id, include);
389
+ const out = { ok: data.status === "completed", job_id: params.job_id, status: data.status, duration_ms: data.duration_ms, ...artifacts };
390
+ if (data.status !== "completed") {
391
+ if (data.timeout) out.timeout = data.timeout;
392
+ if (data.error) out.error = data.error;
393
+ }
394
+ const workspace = getWorkspacePath(api);
395
+ await applySafetySpec(out, { workspace, harInline: !!params.harInline });
396
+ return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
397
+ }
398
+ },
399
+ { optional: true }
400
+ );
197
401
  api.registerTool(
198
402
  {
199
403
  name: "riddle_screenshot",
@@ -275,7 +479,7 @@ function register(api) {
275
479
  api.registerTool(
276
480
  {
277
481
  name: "riddle_steps",
278
- 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.`,
482
+ 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).`,
279
483
  parameters: Type.Object({
280
484
  steps: Type.Array(Type.Record(Type.String(), Type.Any())),
281
485
  timeout_sec: Type.Optional(Type.Number()),
@@ -292,7 +496,8 @@ function register(api) {
292
496
  options: Type.Optional(Type.Record(Type.String(), Type.Any())),
293
497
  include: Type.Optional(Type.Array(Type.String())),
294
498
  harInline: Type.Optional(Type.Boolean()),
295
- sync: Type.Optional(Type.Boolean())
499
+ sync: Type.Optional(Type.Boolean()),
500
+ async: Type.Optional(Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
296
501
  }),
297
502
  async execute(_id, params) {
298
503
  if (!Array.isArray(params.steps)) throw new Error("steps must be an array");
@@ -306,7 +511,7 @@ function register(api) {
306
511
  if (Object.keys(opts).length > 0) payload.options = opts;
307
512
  if (params.include) payload.include = params.include;
308
513
  if (params.harInline) payload.harInline = params.harInline;
309
- const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"] });
514
+ const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
310
515
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
311
516
  }
312
517
  },
@@ -315,7 +520,7 @@ function register(api) {
315
520
  api.registerTool(
316
521
  {
317
522
  name: "riddle_script",
318
- 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.',
523
+ 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).',
319
524
  parameters: Type.Object({
320
525
  script: Type.String(),
321
526
  timeout_sec: Type.Optional(Type.Number()),
@@ -332,7 +537,8 @@ function register(api) {
332
537
  options: Type.Optional(Type.Record(Type.String(), Type.Any())),
333
538
  include: Type.Optional(Type.Array(Type.String())),
334
539
  harInline: Type.Optional(Type.Boolean()),
335
- sync: Type.Optional(Type.Boolean())
540
+ sync: Type.Optional(Type.Boolean()),
541
+ async: Type.Optional(Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." }))
336
542
  }),
337
543
  async execute(_id, params) {
338
544
  if (!params.script || typeof params.script !== "string") throw new Error("script must be a string");
@@ -346,7 +552,7 @@ function register(api) {
346
552
  if (Object.keys(opts).length > 0) payload.options = opts;
347
553
  if (params.include) payload.include = params.include;
348
554
  if (params.harInline) payload.harInline = params.harInline;
349
- const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"] });
555
+ const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
350
556
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
351
557
  }
352
558
  },
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-riddledc",
3
3
  "name": "Riddle",
4
4
  "description": "Riddle (riddledc.com) hosted browser API tools for OpenClaw agents.",
5
- "version": "0.5.4",
5
+ "version": "0.5.6",
6
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.",
7
7
  "type": "plugin",
8
8
  "bundledSkills": [],
@@ -33,6 +33,7 @@
33
33
  "riddle_steps",
34
34
  "riddle_script",
35
35
  "riddle_run",
36
+ "riddle_poll",
36
37
  "riddle_scrape",
37
38
  "riddle_map",
38
39
  "riddle_crawl",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/openclaw-riddledc",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "OpenClaw integration package for RiddleDC (no secrets).",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",