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