@riddledc/openclaw-riddledc 0.5.5 → 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
- 21e09d68db3add27f41481e3ef75bdffa14bc4ae496285c7df2757590104f48b dist/index.cjs
1
+ 041dfffc9dfaaa099cb6243c3512574254b91a80d19e64a68b078d3fc97bd4c8 dist/index.cjs
2
2
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
3
3
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
4
- 63fe7e972b3fa5392e23902122409fbc6d59fa73e0751f0414fd06b9092a7fcd dist/index.js
4
+ b4d87078938b29cd38097688ac435c4fe328a561963212b53c678a2525c8b86a dist/index.js
package/dist/index.cjs CHANGED
@@ -148,6 +148,95 @@ async function applySafetySpec(result, opts) {
148
148
  }
149
149
  }
150
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
+ }
151
240
  async function runWithDefaults(api, payload, defaults) {
152
241
  const { apiKey, baseUrl } = getCfg(api);
153
242
  if (!apiKey) {
@@ -161,8 +250,12 @@ async function runWithDefaults(api, payload, defaults) {
161
250
  const userInclude = payload.include ?? [];
162
251
  const userRequestedHar = userInclude.includes("har");
163
252
  const harInline = !!payload.harInline;
253
+ const returnAsync = !!defaults?.returnAsync;
164
254
  const merged = { ...payload };
165
255
  delete merged.harInline;
256
+ if (returnAsync) {
257
+ merged.sync = false;
258
+ }
166
259
  const defaultInc = defaults?.include ?? ["screenshot", "console"];
167
260
  merged.include = Array.from(/* @__PURE__ */ new Set([...userInclude, ...defaultInc]));
168
261
  merged.inlineConsole = merged.inlineConsole ?? true;
@@ -173,6 +266,46 @@ async function runWithDefaults(api, payload, defaults) {
173
266
  const out = { ok: true, mode };
174
267
  const { contentType, body, headers, status } = await postRun(baseUrl, apiKey, merged);
175
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
+ }
176
309
  if (status >= 400) {
177
310
  try {
178
311
  const txt2 = Buffer.from(body).toString("utf8");
@@ -191,15 +324,42 @@ async function runWithDefaults(api, payload, defaults) {
191
324
  const duration = headers.get("x-duration-ms");
192
325
  out.duration_ms = duration ? Number(duration) : void 0;
193
326
  out.sync = true;
194
- const workspace2 = getWorkspacePath(api);
195
- await applySafetySpec(out, { workspace: workspace2, harInline });
327
+ await applySafetySpec(out, { workspace, harInline });
196
328
  return out;
197
329
  }
198
330
  const txt = Buffer.from(body).toString("utf8");
199
331
  const json = JSON.parse(txt);
200
332
  Object.assign(out, json);
201
333
  out.job_id = json.job_id ?? json.jobId ?? out.job_id;
202
- 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
+ }
203
363
  await applySafetySpec(out, { workspace, harInline });
204
364
  return out;
205
365
  }
@@ -207,19 +367,61 @@ function register(api) {
207
367
  api.registerTool(
208
368
  {
209
369
  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.',
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).',
211
371
  parameters: import_typebox.Type.Object({
212
- 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." }))
213
374
  }),
214
375
  async execute(_id, params) {
215
376
  const result = await runWithDefaults(api, params.payload, {
216
- include: ["screenshot", "console", "result"]
377
+ include: ["screenshot", "console", "result"],
378
+ returnAsync: !!params.async
217
379
  });
218
380
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
219
381
  }
220
382
  },
221
383
  { optional: true }
222
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
+ );
223
425
  api.registerTool(
224
426
  {
225
427
  name: "riddle_screenshot",
@@ -301,7 +503,7 @@ function register(api) {
301
503
  api.registerTool(
302
504
  {
303
505
  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.`,
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).`,
305
507
  parameters: import_typebox.Type.Object({
306
508
  steps: import_typebox.Type.Array(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
307
509
  timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number()),
@@ -318,7 +520,8 @@ function register(api) {
318
520
  options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
319
521
  include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
320
522
  harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
321
- 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." }))
322
525
  }),
323
526
  async execute(_id, params) {
324
527
  if (!Array.isArray(params.steps)) throw new Error("steps must be an array");
@@ -332,7 +535,7 @@ function register(api) {
332
535
  if (Object.keys(opts).length > 0) payload.options = opts;
333
536
  if (params.include) payload.include = params.include;
334
537
  if (params.harInline) payload.harInline = params.harInline;
335
- 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 });
336
539
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
337
540
  }
338
541
  },
@@ -341,7 +544,7 @@ function register(api) {
341
544
  api.registerTool(
342
545
  {
343
546
  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.',
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).',
345
548
  parameters: import_typebox.Type.Object({
346
549
  script: import_typebox.Type.String(),
347
550
  timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number()),
@@ -358,7 +561,8 @@ function register(api) {
358
561
  options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
359
562
  include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
360
563
  harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
361
- 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." }))
362
566
  }),
363
567
  async execute(_id, params) {
364
568
  if (!params.script || typeof params.script !== "string") throw new Error("script must be a string");
@@ -372,7 +576,7 @@ function register(api) {
372
576
  if (Object.keys(opts).length > 0) payload.options = opts;
373
577
  if (params.include) payload.include = params.include;
374
578
  if (params.harInline) payload.harInline = params.harInline;
375
- 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 });
376
580
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
377
581
  }
378
582
  },
package/dist/index.js CHANGED
@@ -124,6 +124,95 @@ async function applySafetySpec(result, opts) {
124
124
  }
125
125
  }
126
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
+ }
127
216
  async function runWithDefaults(api, payload, defaults) {
128
217
  const { apiKey, baseUrl } = getCfg(api);
129
218
  if (!apiKey) {
@@ -137,8 +226,12 @@ async function runWithDefaults(api, payload, defaults) {
137
226
  const userInclude = payload.include ?? [];
138
227
  const userRequestedHar = userInclude.includes("har");
139
228
  const harInline = !!payload.harInline;
229
+ const returnAsync = !!defaults?.returnAsync;
140
230
  const merged = { ...payload };
141
231
  delete merged.harInline;
232
+ if (returnAsync) {
233
+ merged.sync = false;
234
+ }
142
235
  const defaultInc = defaults?.include ?? ["screenshot", "console"];
143
236
  merged.include = Array.from(/* @__PURE__ */ new Set([...userInclude, ...defaultInc]));
144
237
  merged.inlineConsole = merged.inlineConsole ?? true;
@@ -149,6 +242,46 @@ async function runWithDefaults(api, payload, defaults) {
149
242
  const out = { ok: true, mode };
150
243
  const { contentType, body, headers, status } = await postRun(baseUrl, apiKey, merged);
151
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
+ }
152
285
  if (status >= 400) {
153
286
  try {
154
287
  const txt2 = Buffer.from(body).toString("utf8");
@@ -167,15 +300,42 @@ async function runWithDefaults(api, payload, defaults) {
167
300
  const duration = headers.get("x-duration-ms");
168
301
  out.duration_ms = duration ? Number(duration) : void 0;
169
302
  out.sync = true;
170
- const workspace2 = getWorkspacePath(api);
171
- await applySafetySpec(out, { workspace: workspace2, harInline });
303
+ await applySafetySpec(out, { workspace, harInline });
172
304
  return out;
173
305
  }
174
306
  const txt = Buffer.from(body).toString("utf8");
175
307
  const json = JSON.parse(txt);
176
308
  Object.assign(out, json);
177
309
  out.job_id = json.job_id ?? json.jobId ?? out.job_id;
178
- 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
+ }
179
339
  await applySafetySpec(out, { workspace, harInline });
180
340
  return out;
181
341
  }
@@ -183,19 +343,61 @@ function register(api) {
183
343
  api.registerTool(
184
344
  {
185
345
  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.',
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).',
187
347
  parameters: Type.Object({
188
- 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." }))
189
350
  }),
190
351
  async execute(_id, params) {
191
352
  const result = await runWithDefaults(api, params.payload, {
192
- include: ["screenshot", "console", "result"]
353
+ include: ["screenshot", "console", "result"],
354
+ returnAsync: !!params.async
193
355
  });
194
356
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
195
357
  }
196
358
  },
197
359
  { optional: true }
198
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
+ );
199
401
  api.registerTool(
200
402
  {
201
403
  name: "riddle_screenshot",
@@ -277,7 +479,7 @@ function register(api) {
277
479
  api.registerTool(
278
480
  {
279
481
  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.`,
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).`,
281
483
  parameters: Type.Object({
282
484
  steps: Type.Array(Type.Record(Type.String(), Type.Any())),
283
485
  timeout_sec: Type.Optional(Type.Number()),
@@ -294,7 +496,8 @@ function register(api) {
294
496
  options: Type.Optional(Type.Record(Type.String(), Type.Any())),
295
497
  include: Type.Optional(Type.Array(Type.String())),
296
498
  harInline: Type.Optional(Type.Boolean()),
297
- 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." }))
298
501
  }),
299
502
  async execute(_id, params) {
300
503
  if (!Array.isArray(params.steps)) throw new Error("steps must be an array");
@@ -308,7 +511,7 @@ function register(api) {
308
511
  if (Object.keys(opts).length > 0) payload.options = opts;
309
512
  if (params.include) payload.include = params.include;
310
513
  if (params.harInline) payload.harInline = params.harInline;
311
- 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 });
312
515
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
313
516
  }
314
517
  },
@@ -317,7 +520,7 @@ function register(api) {
317
520
  api.registerTool(
318
521
  {
319
522
  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.',
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).',
321
524
  parameters: Type.Object({
322
525
  script: Type.String(),
323
526
  timeout_sec: Type.Optional(Type.Number()),
@@ -334,7 +537,8 @@ function register(api) {
334
537
  options: Type.Optional(Type.Record(Type.String(), Type.Any())),
335
538
  include: Type.Optional(Type.Array(Type.String())),
336
539
  harInline: Type.Optional(Type.Boolean()),
337
- 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." }))
338
542
  }),
339
543
  async execute(_id, params) {
340
544
  if (!params.script || typeof params.script !== "string") throw new Error("script must be a string");
@@ -348,7 +552,7 @@ function register(api) {
348
552
  if (Object.keys(opts).length > 0) payload.options = opts;
349
553
  if (params.include) payload.include = params.include;
350
554
  if (params.harInline) payload.harInline = params.harInline;
351
- 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 });
352
556
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
353
557
  }
354
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.5",
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.5",
3
+ "version": "0.5.6",
4
4
  "description": "OpenClaw integration package for RiddleDC (no secrets).",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",