@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 +2 -2
- package/dist/index.cjs +216 -12
- package/dist/index.js +216 -12
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
package/CHECKSUMS.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
041dfffc9dfaaa099cb6243c3512574254b91a80d19e64a68b078d3fc97bd4c8 dist/index.cjs
|
|
2
2
|
94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
|
|
3
3
|
94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -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
|
+
"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",
|