@riddledc/openclaw-riddledc 0.7.0 → 0.8.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 +398 -14
- package/dist/index.js +398 -14
- package/openclaw.plugin.json +8 -3
- package/package.json +2 -2
package/CHECKSUMS.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
73579dcc6d9a149e5af0393db55cc09ea8f577a7902ab5271d30d1fb5606990e dist/index.cjs
|
|
2
2
|
94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
|
|
3
3
|
94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
|
|
4
|
-
|
|
4
|
+
4b2ea1733e0fed32177f31fc850703b9c888ea8d69dd66ce83907f2c8b758855 dist/index.js
|
package/dist/index.cjs
CHANGED
|
@@ -444,7 +444,10 @@ function register(api) {
|
|
|
444
444
|
headers: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "HTTP headers to send with requests" })),
|
|
445
445
|
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
|
|
446
446
|
include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
|
|
447
|
-
harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean())
|
|
447
|
+
harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
|
|
448
|
+
proxy: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("residential"), import_typebox.Type.Literal("isp")], { description: "Proxy tier. 'residential' routes through residential IPs with CAPTCHA solving. Adds data-based surcharge (~$19/GB). Default: no proxy (datacenter)." })),
|
|
449
|
+
proxy_options: import_typebox.Type.Optional(import_typebox.Type.Object({ country: import_typebox.Type.Optional(import_typebox.Type.String({ description: "ISO country code (default: 'us')" })), state: import_typebox.Type.Optional(import_typebox.Type.String({ description: "State/region code (e.g. 'virginia')" })), city: import_typebox.Type.Optional(import_typebox.Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
450
|
+
stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
448
451
|
}),
|
|
449
452
|
async execute(_id, params) {
|
|
450
453
|
if (!params.url || typeof params.url !== "string") throw new Error("url must be a string");
|
|
@@ -457,6 +460,9 @@ function register(api) {
|
|
|
457
460
|
if (Object.keys(opts).length > 0) payload.options = opts;
|
|
458
461
|
if (params.include) payload.include = params.include;
|
|
459
462
|
if (params.harInline) payload.harInline = params.harInline;
|
|
463
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
464
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
465
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
460
466
|
const result = await runWithDefaults(api, payload, { include: ["screenshot", "console"] });
|
|
461
467
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
462
468
|
}
|
|
@@ -482,7 +488,10 @@ function register(api) {
|
|
|
482
488
|
headers: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "HTTP headers to send with requests" })),
|
|
483
489
|
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
|
|
484
490
|
include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
|
|
485
|
-
harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean())
|
|
491
|
+
harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
|
|
492
|
+
proxy: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("residential"), import_typebox.Type.Literal("isp")], { description: "Proxy tier. 'residential' routes through residential IPs with CAPTCHA solving. Adds data-based surcharge (~$19/GB). Default: no proxy (datacenter)." })),
|
|
493
|
+
proxy_options: import_typebox.Type.Optional(import_typebox.Type.Object({ country: import_typebox.Type.Optional(import_typebox.Type.String({ description: "ISO country code (default: 'us')" })), state: import_typebox.Type.Optional(import_typebox.Type.String({ description: "State/region code (e.g. 'virginia')" })), city: import_typebox.Type.Optional(import_typebox.Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
494
|
+
stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
486
495
|
}),
|
|
487
496
|
async execute(_id, params) {
|
|
488
497
|
if (!Array.isArray(params.urls) || params.urls.some((url) => typeof url !== "string")) {
|
|
@@ -497,6 +506,9 @@ function register(api) {
|
|
|
497
506
|
if (Object.keys(opts).length > 0) payload.options = opts;
|
|
498
507
|
if (params.include) payload.include = params.include;
|
|
499
508
|
if (params.harInline) payload.harInline = params.harInline;
|
|
509
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
510
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
511
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
500
512
|
const result = await runWithDefaults(api, payload, { include: ["screenshot", "console"] });
|
|
501
513
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
502
514
|
}
|
|
@@ -524,7 +536,10 @@ function register(api) {
|
|
|
524
536
|
include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
|
|
525
537
|
harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
|
|
526
538
|
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." }))
|
|
539
|
+
async: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." })),
|
|
540
|
+
proxy: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("residential"), import_typebox.Type.Literal("isp")], { description: "Proxy tier. 'residential' routes through residential IPs with CAPTCHA solving. Adds data-based surcharge (~$19/GB). Default: no proxy (datacenter)." })),
|
|
541
|
+
proxy_options: import_typebox.Type.Optional(import_typebox.Type.Object({ country: import_typebox.Type.Optional(import_typebox.Type.String({ description: "ISO country code (default: 'us')" })), state: import_typebox.Type.Optional(import_typebox.Type.String({ description: "State/region code (e.g. 'virginia')" })), city: import_typebox.Type.Optional(import_typebox.Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
542
|
+
stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
528
543
|
}),
|
|
529
544
|
async execute(_id, params) {
|
|
530
545
|
if (!Array.isArray(params.steps)) throw new Error("steps must be an array");
|
|
@@ -538,6 +553,9 @@ function register(api) {
|
|
|
538
553
|
if (Object.keys(opts).length > 0) payload.options = opts;
|
|
539
554
|
if (params.include) payload.include = params.include;
|
|
540
555
|
if (params.harInline) payload.harInline = params.harInline;
|
|
556
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
557
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
558
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
541
559
|
const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
|
|
542
560
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
543
561
|
}
|
|
@@ -565,7 +583,10 @@ function register(api) {
|
|
|
565
583
|
include: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String())),
|
|
566
584
|
harInline: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
|
|
567
585
|
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." }))
|
|
586
|
+
async: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." })),
|
|
587
|
+
proxy: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("residential"), import_typebox.Type.Literal("isp")], { description: "Proxy tier. 'residential' routes through residential IPs with CAPTCHA solving. Adds data-based surcharge (~$19/GB). Default: no proxy (datacenter)." })),
|
|
588
|
+
proxy_options: import_typebox.Type.Optional(import_typebox.Type.Object({ country: import_typebox.Type.Optional(import_typebox.Type.String({ description: "ISO country code (default: 'us')" })), state: import_typebox.Type.Optional(import_typebox.Type.String({ description: "State/region code (e.g. 'virginia')" })), city: import_typebox.Type.Optional(import_typebox.Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
589
|
+
stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
569
590
|
}),
|
|
570
591
|
async execute(_id, params) {
|
|
571
592
|
if (!params.script || typeof params.script !== "string") throw new Error("script must be a string");
|
|
@@ -579,6 +600,9 @@ function register(api) {
|
|
|
579
600
|
if (Object.keys(opts).length > 0) payload.options = opts;
|
|
580
601
|
if (params.include) payload.include = params.include;
|
|
581
602
|
if (params.harInline) payload.harInline = params.harInline;
|
|
603
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
604
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
605
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
582
606
|
const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
|
|
583
607
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
584
608
|
}
|
|
@@ -600,7 +624,10 @@ function register(api) {
|
|
|
600
624
|
secure: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
|
|
601
625
|
httpOnly: import_typebox.Type.Optional(import_typebox.Type.Boolean())
|
|
602
626
|
}), { description: "Cookies to inject for authenticated sessions" })),
|
|
603
|
-
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any()))
|
|
627
|
+
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
|
|
628
|
+
proxy: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("residential"), import_typebox.Type.Literal("isp")], { description: "Proxy tier for blocked sites. Adds ~$19/GB surcharge." })),
|
|
629
|
+
proxy_options: import_typebox.Type.Optional(import_typebox.Type.Object({ country: import_typebox.Type.Optional(import_typebox.Type.String({ description: "ISO country code (default: 'us')" })), state: import_typebox.Type.Optional(import_typebox.Type.String({ description: "State/region code (e.g. 'virginia')" })), city: import_typebox.Type.Optional(import_typebox.Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
630
|
+
stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
604
631
|
}),
|
|
605
632
|
async execute(_id, params) {
|
|
606
633
|
const scrapeOpts = params.extract_metadata === false ? "{ extract_metadata: false }" : "";
|
|
@@ -610,6 +637,9 @@ function register(api) {
|
|
|
610
637
|
options: { ...params.options || {}, returnResult: true }
|
|
611
638
|
};
|
|
612
639
|
if (params.cookies) payload.options.cookies = params.cookies;
|
|
640
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
641
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
642
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
613
643
|
const result = await runWithDefaults(api, payload, { include: ["result", "console"] });
|
|
614
644
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
615
645
|
}
|
|
@@ -634,7 +664,10 @@ function register(api) {
|
|
|
634
664
|
secure: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
|
|
635
665
|
httpOnly: import_typebox.Type.Optional(import_typebox.Type.Boolean())
|
|
636
666
|
}), { description: "Cookies to inject for authenticated sessions" })),
|
|
637
|
-
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any()))
|
|
667
|
+
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
|
|
668
|
+
proxy: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("residential"), import_typebox.Type.Literal("isp")], { description: "Proxy tier for blocked sites. Adds ~$19/GB surcharge." })),
|
|
669
|
+
proxy_options: import_typebox.Type.Optional(import_typebox.Type.Object({ country: import_typebox.Type.Optional(import_typebox.Type.String({ description: "ISO country code (default: 'us')" })), state: import_typebox.Type.Optional(import_typebox.Type.String({ description: "State/region code (e.g. 'virginia')" })), city: import_typebox.Type.Optional(import_typebox.Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
670
|
+
stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
638
671
|
}),
|
|
639
672
|
async execute(_id, params) {
|
|
640
673
|
const mapOpts = [];
|
|
@@ -649,6 +682,9 @@ function register(api) {
|
|
|
649
682
|
options: { ...params.options || {}, returnResult: true }
|
|
650
683
|
};
|
|
651
684
|
if (params.cookies) payload.options.cookies = params.cookies;
|
|
685
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
686
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
687
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
652
688
|
const result = await runWithDefaults(api, payload, { include: ["result", "console"] });
|
|
653
689
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
654
690
|
}
|
|
@@ -676,7 +712,10 @@ function register(api) {
|
|
|
676
712
|
secure: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
|
|
677
713
|
httpOnly: import_typebox.Type.Optional(import_typebox.Type.Boolean())
|
|
678
714
|
}), { description: "Cookies to inject for authenticated sessions" })),
|
|
679
|
-
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any()))
|
|
715
|
+
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
|
|
716
|
+
proxy: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("residential"), import_typebox.Type.Literal("isp")], { description: "Proxy tier for blocked sites. Adds ~$19/GB surcharge." })),
|
|
717
|
+
proxy_options: import_typebox.Type.Optional(import_typebox.Type.Object({ country: import_typebox.Type.Optional(import_typebox.Type.String({ description: "ISO country code (default: 'us')" })), state: import_typebox.Type.Optional(import_typebox.Type.String({ description: "State/region code (e.g. 'virginia')" })), city: import_typebox.Type.Optional(import_typebox.Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
718
|
+
stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
680
719
|
}),
|
|
681
720
|
async execute(_id, params) {
|
|
682
721
|
const crawlOpts = [];
|
|
@@ -694,6 +733,9 @@ function register(api) {
|
|
|
694
733
|
options: { ...params.options || {}, returnResult: true }
|
|
695
734
|
};
|
|
696
735
|
if (params.cookies) payload.options.cookies = params.cookies;
|
|
736
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
737
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
738
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
697
739
|
const result = await runWithDefaults(api, payload, { include: ["result", "console"] });
|
|
698
740
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
699
741
|
}
|
|
@@ -731,7 +773,8 @@ function register(api) {
|
|
|
731
773
|
secure: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
|
|
732
774
|
httpOnly: import_typebox.Type.Optional(import_typebox.Type.Boolean())
|
|
733
775
|
}), { description: "Cookies for the 'after' URL" })),
|
|
734
|
-
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any()))
|
|
776
|
+
options: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.Any())),
|
|
777
|
+
stealth: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
735
778
|
}),
|
|
736
779
|
async execute(_id, params) {
|
|
737
780
|
const vdOpts = [];
|
|
@@ -750,6 +793,7 @@ function register(api) {
|
|
|
750
793
|
script: `return await visualDiff(${optsStr});`,
|
|
751
794
|
options: { ...params.options || {}, returnResult: true }
|
|
752
795
|
};
|
|
796
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
753
797
|
const result = await runWithDefaults(api, payload, { include: ["result", "console", "visual_diff"] });
|
|
754
798
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
755
799
|
}
|
|
@@ -875,7 +919,15 @@ function register(api) {
|
|
|
875
919
|
timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max execution time in seconds (default: 120, max: 600)" })),
|
|
876
920
|
readiness_path: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Path to poll for readiness (default: same as path)" })),
|
|
877
921
|
readiness_timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max seconds to wait for server readiness (default: 30)" })),
|
|
878
|
-
script: import_typebox.Type.Optional(import_typebox.Type.String({ description:
|
|
922
|
+
script: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional Playwright script to run after server is ready. Full sandbox: saveScreenshot(), scrape(), map(), crawl(), saveHtml(), saveJson(), visualDiff(). Cannot use with steps." })),
|
|
923
|
+
steps: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.Any(), { description: "Declarative steps (same as riddle_steps). Cannot use with script. Example: [{ click: '.btn' }, { screenshot: 'after-click' }]" })),
|
|
924
|
+
wait_until: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("load"), import_typebox.Type.Literal("domcontentloaded"), import_typebox.Type.Literal("networkidle")], { description: "Playwright waitUntil strategy for page.goto (default: 'load'). Use 'domcontentloaded' for SPAs that make continuous network requests." })),
|
|
925
|
+
wait_for_selector: import_typebox.Type.Optional(import_typebox.Type.String({ description: "CSS selector to wait for after page load, before running script. Solves hydration race conditions. Example: '.billing-table' or '[data-hydrated]'" })),
|
|
926
|
+
navigation_timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Seconds to wait for page.goto() navigation to complete (5-120, default: 30). Increase for slow-loading apps." })),
|
|
927
|
+
color_scheme: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("dark"), import_typebox.Type.Literal("light")], { description: "Color scheme for emulateMedia. Applied BEFORE navigation so initial render uses it." })),
|
|
928
|
+
viewport: import_typebox.Type.Optional(import_typebox.Type.Object({ width: import_typebox.Type.Number(), height: import_typebox.Type.Number() }, { description: "Browser viewport size (default: 1920x1080)" })),
|
|
929
|
+
localStorage: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "localStorage key-value pairs injected before page load (e.g. auth tokens)" })),
|
|
930
|
+
exclude: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Glob patterns to exclude from tarball. Default: ['.git', '*.log']. Add 'node_modules' only if your server doesn't need it (e.g. static file servers)." }))
|
|
879
931
|
}),
|
|
880
932
|
async execute(_id, params) {
|
|
881
933
|
const { apiKey, baseUrl } = getCfg(api);
|
|
@@ -893,11 +945,16 @@ function register(api) {
|
|
|
893
945
|
}
|
|
894
946
|
const endpoint = baseUrl.replace(/\/$/, "");
|
|
895
947
|
let envRef = null;
|
|
896
|
-
|
|
948
|
+
const hasSensitiveEnv = params.sensitive_env && Object.keys(params.sensitive_env).length > 0;
|
|
949
|
+
const hasLocalStorage = params.localStorage && Object.keys(params.localStorage).length > 0;
|
|
950
|
+
if (hasSensitiveEnv || hasLocalStorage) {
|
|
951
|
+
const envBody = {};
|
|
952
|
+
if (hasSensitiveEnv) envBody.env = params.sensitive_env;
|
|
953
|
+
if (hasLocalStorage) envBody.localStorage = params.localStorage;
|
|
897
954
|
const envRes = await fetch(`${endpoint}/v1/server-preview/env`, {
|
|
898
955
|
method: "POST",
|
|
899
956
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
900
|
-
body: JSON.stringify(
|
|
957
|
+
body: JSON.stringify(envBody)
|
|
901
958
|
});
|
|
902
959
|
if (!envRes.ok) {
|
|
903
960
|
const err = await envRes.text();
|
|
@@ -918,6 +975,12 @@ function register(api) {
|
|
|
918
975
|
if (params.readiness_path) createBody.readiness_path = params.readiness_path;
|
|
919
976
|
if (params.readiness_timeout) createBody.readiness_timeout = params.readiness_timeout;
|
|
920
977
|
if (params.script) createBody.script = params.script;
|
|
978
|
+
if (params.steps) createBody.steps = params.steps;
|
|
979
|
+
if (params.wait_until) createBody.wait_until = params.wait_until;
|
|
980
|
+
if (params.wait_for_selector) createBody.wait_for_selector = params.wait_for_selector;
|
|
981
|
+
if (params.navigation_timeout) createBody.navigation_timeout = params.navigation_timeout;
|
|
982
|
+
if (params.color_scheme) createBody.color_scheme = params.color_scheme;
|
|
983
|
+
if (params.viewport) createBody.viewport = params.viewport;
|
|
921
984
|
const createRes = await fetch(`${endpoint}/v1/server-preview`, {
|
|
922
985
|
method: "POST",
|
|
923
986
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
@@ -930,7 +993,9 @@ function register(api) {
|
|
|
930
993
|
const created = await createRes.json();
|
|
931
994
|
const tarball = `/tmp/riddle-sp-${created.job_id}.tar.gz`;
|
|
932
995
|
try {
|
|
933
|
-
|
|
996
|
+
const excludes = params.exclude || [".git", "*.log"];
|
|
997
|
+
const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
|
|
998
|
+
await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
|
|
934
999
|
const tarData = await (0, import_promises.readFile)(tarball);
|
|
935
1000
|
const uploadRes = await fetch(created.upload_url, {
|
|
936
1001
|
method: "PUT",
|
|
@@ -965,9 +1030,9 @@ function register(api) {
|
|
|
965
1030
|
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
|
|
966
1031
|
}
|
|
967
1032
|
const statusData = await statusRes.json();
|
|
968
|
-
if (statusData.status === "completed" || statusData.status === "failed") {
|
|
1033
|
+
if (statusData.status === "complete" || statusData.status === "completed" || statusData.status === "failed") {
|
|
969
1034
|
const result = {
|
|
970
|
-
ok: statusData.status === "completed",
|
|
1035
|
+
ok: statusData.status === "complete" || statusData.status === "completed",
|
|
971
1036
|
job_id: created.job_id,
|
|
972
1037
|
status: statusData.status,
|
|
973
1038
|
outputs: statusData.outputs || [],
|
|
@@ -991,6 +1056,7 @@ function register(api) {
|
|
|
991
1056
|
}
|
|
992
1057
|
}
|
|
993
1058
|
}
|
|
1059
|
+
result.screenshots = result.outputs.filter((o) => /\.(png|jpg|jpeg)$/i.test(o.name));
|
|
994
1060
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
995
1061
|
}
|
|
996
1062
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
@@ -1000,4 +1066,322 @@ function register(api) {
|
|
|
1000
1066
|
},
|
|
1001
1067
|
{ optional: true }
|
|
1002
1068
|
);
|
|
1069
|
+
api.registerTool(
|
|
1070
|
+
{
|
|
1071
|
+
name: "riddle_build_preview",
|
|
1072
|
+
description: "Build a Docker image from a Dockerfile in the project directory, run the server, and screenshot it. Unlike riddle_server_preview (which pulls a stock image), this builds a custom image from your Dockerfile \u2014 allowing pre-installed dependencies, build tools, and custom system packages. Built images are cached on the worker for 30 minutes by default for fast re-runs.",
|
|
1073
|
+
parameters: import_typebox.Type.Object({
|
|
1074
|
+
directory: import_typebox.Type.String({ description: "Absolute path to the project directory. Must contain a Dockerfile at root." }),
|
|
1075
|
+
command: import_typebox.Type.String({ description: "Command to start the server inside the built container (e.g. 'python server.py')" }),
|
|
1076
|
+
port: import_typebox.Type.Number({ description: "Port the server listens on inside the container" }),
|
|
1077
|
+
path: import_typebox.Type.Optional(import_typebox.Type.String({ description: "URL path to screenshot (default: '/')" })),
|
|
1078
|
+
env: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "Non-sensitive environment variables" })),
|
|
1079
|
+
sensitive_env: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "Sensitive environment variables. Stored securely and deleted after use." })),
|
|
1080
|
+
build_args: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "Docker build arguments (--build-arg KEY=VAL)" })),
|
|
1081
|
+
keep_image_minutes: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "How long to cache the built image on the worker (default: 30, max: 120, 0 = delete immediately)" })),
|
|
1082
|
+
timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max execution time in seconds including build (default: 180, max: 600)" })),
|
|
1083
|
+
readiness_path: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Path to poll for readiness (default: same as path)" })),
|
|
1084
|
+
readiness_timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max seconds to wait for server readiness (default: 30)" })),
|
|
1085
|
+
script: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional Playwright script to run after server is ready. Cannot use with steps." })),
|
|
1086
|
+
steps: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.Any(), { description: "Declarative steps (same as riddle_steps). Cannot use with script." })),
|
|
1087
|
+
wait_until: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("load"), import_typebox.Type.Literal("domcontentloaded"), import_typebox.Type.Literal("networkidle")], { description: "Playwright waitUntil strategy (default: 'load')" })),
|
|
1088
|
+
wait_for_selector: import_typebox.Type.Optional(import_typebox.Type.String({ description: "CSS selector to wait for after page load, before running script" })),
|
|
1089
|
+
navigation_timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Seconds to wait for page.goto() navigation to complete (5-120, default: 30). Increase for slow-loading apps." })),
|
|
1090
|
+
color_scheme: import_typebox.Type.Optional(import_typebox.Type.Union([import_typebox.Type.Literal("dark"), import_typebox.Type.Literal("light")], { description: "Color scheme for emulateMedia" })),
|
|
1091
|
+
viewport: import_typebox.Type.Optional(import_typebox.Type.Object({ width: import_typebox.Type.Number(), height: import_typebox.Type.Number() }, { description: "Browser viewport size (default: 1920x1080)" })),
|
|
1092
|
+
localStorage: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "localStorage key-value pairs injected before page load" })),
|
|
1093
|
+
exclude: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Glob patterns to exclude from tarball. Default: ['.git', '*.log']" })),
|
|
1094
|
+
audit: import_typebox.Type.Optional(import_typebox.Type.Boolean({ description: "Run security audit scan on submitted code. Returns dependency list, security findings, code summary, and risk flags." }))
|
|
1095
|
+
}),
|
|
1096
|
+
async execute(_id, params) {
|
|
1097
|
+
const { apiKey, baseUrl } = getCfg(api);
|
|
1098
|
+
if (!apiKey) {
|
|
1099
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
|
|
1100
|
+
}
|
|
1101
|
+
assertAllowedBaseUrl(baseUrl);
|
|
1102
|
+
const dir = params.directory;
|
|
1103
|
+
if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
|
|
1104
|
+
try {
|
|
1105
|
+
const st = await (0, import_promises.stat)(dir);
|
|
1106
|
+
if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
|
|
1107
|
+
} catch (e) {
|
|
1108
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
await (0, import_promises.stat)(`${dir}/Dockerfile`);
|
|
1112
|
+
} catch {
|
|
1113
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `No Dockerfile found at ${dir}/Dockerfile. riddle_build_preview requires a Dockerfile at the root of the directory.` }, null, 2) }] };
|
|
1114
|
+
}
|
|
1115
|
+
const endpoint = baseUrl.replace(/\/$/, "");
|
|
1116
|
+
let envRef = null;
|
|
1117
|
+
const hasSensitiveEnv = params.sensitive_env && Object.keys(params.sensitive_env).length > 0;
|
|
1118
|
+
const hasLocalStorage = params.localStorage && Object.keys(params.localStorage).length > 0;
|
|
1119
|
+
if (hasSensitiveEnv || hasLocalStorage) {
|
|
1120
|
+
const envBody = {};
|
|
1121
|
+
if (hasSensitiveEnv) envBody.env = params.sensitive_env;
|
|
1122
|
+
if (hasLocalStorage) envBody.localStorage = params.localStorage;
|
|
1123
|
+
const envRes = await fetch(`${endpoint}/v1/build-preview/env`, {
|
|
1124
|
+
method: "POST",
|
|
1125
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1126
|
+
body: JSON.stringify(envBody)
|
|
1127
|
+
});
|
|
1128
|
+
if (!envRes.ok) {
|
|
1129
|
+
const err = await envRes.text();
|
|
1130
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
|
|
1131
|
+
}
|
|
1132
|
+
const envData = await envRes.json();
|
|
1133
|
+
envRef = envData.env_ref;
|
|
1134
|
+
}
|
|
1135
|
+
const createBody = {
|
|
1136
|
+
command: params.command,
|
|
1137
|
+
port: params.port
|
|
1138
|
+
};
|
|
1139
|
+
if (params.path) createBody.path = params.path;
|
|
1140
|
+
if (params.env) createBody.env = params.env;
|
|
1141
|
+
if (envRef) createBody.env_ref = envRef;
|
|
1142
|
+
if (params.build_args) createBody.build_args = params.build_args;
|
|
1143
|
+
if (params.keep_image_minutes !== void 0) createBody.keep_image_minutes = params.keep_image_minutes;
|
|
1144
|
+
if (params.timeout) createBody.timeout = params.timeout;
|
|
1145
|
+
if (params.readiness_path) createBody.readiness_path = params.readiness_path;
|
|
1146
|
+
if (params.readiness_timeout) createBody.readiness_timeout = params.readiness_timeout;
|
|
1147
|
+
if (params.script) createBody.script = params.script;
|
|
1148
|
+
if (params.steps) createBody.steps = params.steps;
|
|
1149
|
+
if (params.wait_until) createBody.wait_until = params.wait_until;
|
|
1150
|
+
if (params.wait_for_selector) createBody.wait_for_selector = params.wait_for_selector;
|
|
1151
|
+
if (params.navigation_timeout) createBody.navigation_timeout = params.navigation_timeout;
|
|
1152
|
+
if (params.color_scheme) createBody.color_scheme = params.color_scheme;
|
|
1153
|
+
if (params.viewport) createBody.viewport = params.viewport;
|
|
1154
|
+
if (params.audit) createBody.audit = true;
|
|
1155
|
+
const createRes = await fetch(`${endpoint}/v1/build-preview`, {
|
|
1156
|
+
method: "POST",
|
|
1157
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1158
|
+
body: JSON.stringify(createBody)
|
|
1159
|
+
});
|
|
1160
|
+
if (!createRes.ok) {
|
|
1161
|
+
const err = await createRes.text();
|
|
1162
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
|
|
1163
|
+
}
|
|
1164
|
+
const created = await createRes.json();
|
|
1165
|
+
const tarball = `/tmp/riddle-bp-${created.job_id}.tar.gz`;
|
|
1166
|
+
try {
|
|
1167
|
+
const excludes = params.exclude || [".git", "*.log"];
|
|
1168
|
+
const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
|
|
1169
|
+
await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
|
|
1170
|
+
const tarData = await (0, import_promises.readFile)(tarball);
|
|
1171
|
+
const uploadRes = await fetch(created.upload_url, {
|
|
1172
|
+
method: "PUT",
|
|
1173
|
+
headers: { "Content-Type": "application/gzip" },
|
|
1174
|
+
body: tarData
|
|
1175
|
+
});
|
|
1176
|
+
if (!uploadRes.ok) {
|
|
1177
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
|
|
1178
|
+
}
|
|
1179
|
+
} finally {
|
|
1180
|
+
try {
|
|
1181
|
+
await (0, import_promises.rm)(tarball, { force: true });
|
|
1182
|
+
} catch {
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const startRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
|
|
1186
|
+
method: "POST",
|
|
1187
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1188
|
+
});
|
|
1189
|
+
if (!startRes.ok) {
|
|
1190
|
+
const err = await startRes.text();
|
|
1191
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
|
|
1192
|
+
}
|
|
1193
|
+
const timeoutMs = ((params.timeout || 180) + 120) * 1e3;
|
|
1194
|
+
const pollStart = Date.now();
|
|
1195
|
+
const POLL_INTERVAL = 3e3;
|
|
1196
|
+
while (Date.now() - pollStart < timeoutMs) {
|
|
1197
|
+
const statusRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}`, {
|
|
1198
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1199
|
+
});
|
|
1200
|
+
if (!statusRes.ok) {
|
|
1201
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
|
|
1202
|
+
}
|
|
1203
|
+
const statusData = await statusRes.json();
|
|
1204
|
+
if (statusData.status === "complete" || statusData.status === "completed" || statusData.status === "failed") {
|
|
1205
|
+
const result = {
|
|
1206
|
+
ok: statusData.status === "complete" || statusData.status === "completed",
|
|
1207
|
+
job_id: created.job_id,
|
|
1208
|
+
status: statusData.status,
|
|
1209
|
+
outputs: statusData.outputs || [],
|
|
1210
|
+
compute_seconds: statusData.compute_seconds,
|
|
1211
|
+
build_duration_ms: statusData.build_duration_ms,
|
|
1212
|
+
egress_bytes: statusData.egress_bytes
|
|
1213
|
+
};
|
|
1214
|
+
if (statusData.error) result.error = statusData.error;
|
|
1215
|
+
if (statusData.build_log) result.build_log = statusData.build_log;
|
|
1216
|
+
if (statusData.container_log) result.container_log = statusData.container_log;
|
|
1217
|
+
if (statusData.audit) result.audit = statusData.audit;
|
|
1218
|
+
const workspace = getWorkspacePath(api);
|
|
1219
|
+
for (const output of result.outputs) {
|
|
1220
|
+
if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
|
|
1221
|
+
try {
|
|
1222
|
+
const imgRes = await fetch(output.url);
|
|
1223
|
+
if (imgRes.ok) {
|
|
1224
|
+
const buf = await imgRes.arrayBuffer();
|
|
1225
|
+
const base64 = Buffer.from(buf).toString("base64");
|
|
1226
|
+
const ref = await writeArtifactBinary(workspace, "screenshots", `${created.job_id}-${output.name}`, base64);
|
|
1227
|
+
output.saved = ref.path;
|
|
1228
|
+
output.sizeBytes = ref.sizeBytes;
|
|
1229
|
+
}
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
result.screenshots = result.outputs.filter((o) => /\.(png|jpg|jpeg)$/i.test(o.name));
|
|
1235
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1236
|
+
}
|
|
1237
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
1238
|
+
}
|
|
1239
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Job did not complete within ${timeoutMs / 1e3}s` }, null, 2) }] };
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1242
|
+
{ optional: true }
|
|
1243
|
+
);
|
|
1244
|
+
async function riddleApiFetch(api2, method, path, body) {
|
|
1245
|
+
const { apiKey, baseUrl } = getCfg(api2);
|
|
1246
|
+
if (!apiKey) throw new Error("Missing Riddle API key. Set RIDDLE_API_KEY or configure in plugin settings.");
|
|
1247
|
+
assertAllowedBaseUrl(baseUrl);
|
|
1248
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
1249
|
+
const res = await fetch(url, {
|
|
1250
|
+
method,
|
|
1251
|
+
headers: {
|
|
1252
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1253
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
1254
|
+
},
|
|
1255
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
1256
|
+
});
|
|
1257
|
+
const data = await res.json();
|
|
1258
|
+
if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`);
|
|
1259
|
+
return data;
|
|
1260
|
+
}
|
|
1261
|
+
api.registerTool(
|
|
1262
|
+
{
|
|
1263
|
+
name: "riddle_session_create",
|
|
1264
|
+
description: "Create a persistent browser session. Sessions maintain cookies, localStorage, and auth state across multiple riddle_session_run calls. Use for multi-step auth flows (2FA, OAuth) and authenticated agent workflows. Sessions are billed per-second while warm.",
|
|
1265
|
+
parameters: import_typebox.Type.Object({
|
|
1266
|
+
name: import_typebox.Type.String({ description: "Human-readable session name (unique per API key)" }),
|
|
1267
|
+
ttl_sec: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max session lifetime in seconds (default: 3600, max: 86400)" })),
|
|
1268
|
+
idle_timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max idle time between uses in seconds (default: 600, max: 3600)" }))
|
|
1269
|
+
}),
|
|
1270
|
+
async execute(_id, params) {
|
|
1271
|
+
const data = await riddleApiFetch(api, "POST", "/v1/sessions", {
|
|
1272
|
+
name: params.name,
|
|
1273
|
+
ttl_sec: params.ttl_sec,
|
|
1274
|
+
idle_timeout_sec: params.idle_timeout_sec
|
|
1275
|
+
});
|
|
1276
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
{ optional: true }
|
|
1280
|
+
);
|
|
1281
|
+
api.registerTool(
|
|
1282
|
+
{
|
|
1283
|
+
name: "riddle_session_list",
|
|
1284
|
+
description: "List all active persistent browser sessions for the current API key.",
|
|
1285
|
+
parameters: import_typebox.Type.Object({}),
|
|
1286
|
+
async execute() {
|
|
1287
|
+
const data = await riddleApiFetch(api, "GET", "/v1/sessions");
|
|
1288
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
{ optional: true }
|
|
1292
|
+
);
|
|
1293
|
+
api.registerTool(
|
|
1294
|
+
{
|
|
1295
|
+
name: "riddle_session_run",
|
|
1296
|
+
description: "Run a Playwright script or steps in a persistent session. The browser context (cookies, localStorage, auth tokens) persists from previous calls to the same session. Use after riddle_session_create. Available helpers in script: saveScreenshot(label), saveJson(name, data), saveFile(name, buffer), scrape(), map(), crawl().",
|
|
1297
|
+
parameters: import_typebox.Type.Object({
|
|
1298
|
+
session_id: import_typebox.Type.String({ description: "Session ID from riddle_session_create" }),
|
|
1299
|
+
url: import_typebox.Type.Optional(import_typebox.Type.String({ description: "URL to navigate to" })),
|
|
1300
|
+
script: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Playwright script to execute" })),
|
|
1301
|
+
steps: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.Any(), { description: "Declarative steps (alternative to script)" })),
|
|
1302
|
+
timeout_sec: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max execution time in seconds (default: 60)" }))
|
|
1303
|
+
}),
|
|
1304
|
+
async execute(_id, params) {
|
|
1305
|
+
const { apiKey, baseUrl } = getCfg(api);
|
|
1306
|
+
if (!apiKey) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
|
|
1307
|
+
assertAllowedBaseUrl(baseUrl);
|
|
1308
|
+
const payload = { timeout_sec: params.timeout_sec || 60 };
|
|
1309
|
+
if (params.script) {
|
|
1310
|
+
payload.script = params.script;
|
|
1311
|
+
payload.url = params.url;
|
|
1312
|
+
} else if (params.steps) {
|
|
1313
|
+
payload.steps = params.steps;
|
|
1314
|
+
} else if (params.url) {
|
|
1315
|
+
payload.url = params.url;
|
|
1316
|
+
payload.script = `await page.goto('${params.url.replace(/'/g, "\\'")}'); await saveScreenshot('page');`;
|
|
1317
|
+
} else return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Provide script, steps, or url" }, null, 2) }] };
|
|
1318
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/sessions/${params.session_id}/run`, {
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1321
|
+
body: JSON.stringify(payload)
|
|
1322
|
+
});
|
|
1323
|
+
const data = await res.json();
|
|
1324
|
+
if (!res.ok) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: data.error?.message || data.error || `HTTP ${res.status}` }, null, 2) }] };
|
|
1325
|
+
if (data.job_id) {
|
|
1326
|
+
const timeoutMs = (params.timeout_sec || 60) * 1e3 + 15e3;
|
|
1327
|
+
const POLL_INTERVAL = 2e3;
|
|
1328
|
+
const started = Date.now();
|
|
1329
|
+
while (Date.now() - started < timeoutMs) {
|
|
1330
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
1331
|
+
const statusRes = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${data.job_id}`, {
|
|
1332
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1333
|
+
});
|
|
1334
|
+
if (!statusRes.ok) continue;
|
|
1335
|
+
const statusData = await statusRes.json();
|
|
1336
|
+
if (statusData.status === "completed" || statusData.status === "failed" || statusData.status === "completed_error" || statusData.status === "completed_timeout") {
|
|
1337
|
+
const result = {
|
|
1338
|
+
ok: statusData.status === "completed",
|
|
1339
|
+
job_id: data.job_id,
|
|
1340
|
+
session_id: params.session_id,
|
|
1341
|
+
status: statusData.status,
|
|
1342
|
+
outputs: statusData.outputs || [],
|
|
1343
|
+
duration_ms: statusData.duration_ms
|
|
1344
|
+
};
|
|
1345
|
+
if (statusData.error) result.error = statusData.error;
|
|
1346
|
+
const workspace = getWorkspacePath(api);
|
|
1347
|
+
for (const output of result.outputs) {
|
|
1348
|
+
if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
|
|
1349
|
+
try {
|
|
1350
|
+
const imgRes = await fetch(output.url);
|
|
1351
|
+
if (imgRes.ok) {
|
|
1352
|
+
const buf = await imgRes.arrayBuffer();
|
|
1353
|
+
const base64 = Buffer.from(buf).toString("base64");
|
|
1354
|
+
const ref = await writeArtifactBinary(workspace, "screenshots", `${data.job_id}-${output.name}`, base64);
|
|
1355
|
+
output.saved = ref.path;
|
|
1356
|
+
output.sizeBytes = ref.sizeBytes;
|
|
1357
|
+
}
|
|
1358
|
+
} catch {
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
result.screenshots = result.outputs.filter((o) => /\.(png|jpg|jpeg)$/i.test(o.name));
|
|
1363
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: data.job_id, session_id: params.session_id, error: "Job did not complete in time" }, null, 2) }] };
|
|
1367
|
+
}
|
|
1368
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, ...data }, null, 2) }] };
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
{ optional: true }
|
|
1372
|
+
);
|
|
1373
|
+
api.registerTool(
|
|
1374
|
+
{
|
|
1375
|
+
name: "riddle_session_destroy",
|
|
1376
|
+
description: "Destroy a persistent browser session, closing the browser context and cleaning up all stored state. Stops warm-time billing.",
|
|
1377
|
+
parameters: import_typebox.Type.Object({
|
|
1378
|
+
session_id: import_typebox.Type.String({ description: "Session ID to destroy" })
|
|
1379
|
+
}),
|
|
1380
|
+
async execute(_id, params) {
|
|
1381
|
+
const data = await riddleApiFetch(api, "DELETE", `/v1/sessions/${params.session_id}`);
|
|
1382
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
{ optional: true }
|
|
1386
|
+
);
|
|
1003
1387
|
}
|
package/dist/index.js
CHANGED
|
@@ -420,7 +420,10 @@ function register(api) {
|
|
|
420
420
|
headers: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "HTTP headers to send with requests" })),
|
|
421
421
|
options: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
422
422
|
include: Type.Optional(Type.Array(Type.String())),
|
|
423
|
-
harInline: Type.Optional(Type.Boolean())
|
|
423
|
+
harInline: Type.Optional(Type.Boolean()),
|
|
424
|
+
proxy: Type.Optional(Type.Union([Type.Literal("residential"), Type.Literal("isp")], { description: "Proxy tier. 'residential' routes through residential IPs with CAPTCHA solving. Adds data-based surcharge (~$19/GB). Default: no proxy (datacenter)." })),
|
|
425
|
+
proxy_options: Type.Optional(Type.Object({ country: Type.Optional(Type.String({ description: "ISO country code (default: 'us')" })), state: Type.Optional(Type.String({ description: "State/region code (e.g. 'virginia')" })), city: Type.Optional(Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
426
|
+
stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
424
427
|
}),
|
|
425
428
|
async execute(_id, params) {
|
|
426
429
|
if (!params.url || typeof params.url !== "string") throw new Error("url must be a string");
|
|
@@ -433,6 +436,9 @@ function register(api) {
|
|
|
433
436
|
if (Object.keys(opts).length > 0) payload.options = opts;
|
|
434
437
|
if (params.include) payload.include = params.include;
|
|
435
438
|
if (params.harInline) payload.harInline = params.harInline;
|
|
439
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
440
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
441
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
436
442
|
const result = await runWithDefaults(api, payload, { include: ["screenshot", "console"] });
|
|
437
443
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
438
444
|
}
|
|
@@ -458,7 +464,10 @@ function register(api) {
|
|
|
458
464
|
headers: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "HTTP headers to send with requests" })),
|
|
459
465
|
options: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
460
466
|
include: Type.Optional(Type.Array(Type.String())),
|
|
461
|
-
harInline: Type.Optional(Type.Boolean())
|
|
467
|
+
harInline: Type.Optional(Type.Boolean()),
|
|
468
|
+
proxy: Type.Optional(Type.Union([Type.Literal("residential"), Type.Literal("isp")], { description: "Proxy tier. 'residential' routes through residential IPs with CAPTCHA solving. Adds data-based surcharge (~$19/GB). Default: no proxy (datacenter)." })),
|
|
469
|
+
proxy_options: Type.Optional(Type.Object({ country: Type.Optional(Type.String({ description: "ISO country code (default: 'us')" })), state: Type.Optional(Type.String({ description: "State/region code (e.g. 'virginia')" })), city: Type.Optional(Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
470
|
+
stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
462
471
|
}),
|
|
463
472
|
async execute(_id, params) {
|
|
464
473
|
if (!Array.isArray(params.urls) || params.urls.some((url) => typeof url !== "string")) {
|
|
@@ -473,6 +482,9 @@ function register(api) {
|
|
|
473
482
|
if (Object.keys(opts).length > 0) payload.options = opts;
|
|
474
483
|
if (params.include) payload.include = params.include;
|
|
475
484
|
if (params.harInline) payload.harInline = params.harInline;
|
|
485
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
486
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
487
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
476
488
|
const result = await runWithDefaults(api, payload, { include: ["screenshot", "console"] });
|
|
477
489
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
478
490
|
}
|
|
@@ -500,7 +512,10 @@ function register(api) {
|
|
|
500
512
|
include: Type.Optional(Type.Array(Type.String())),
|
|
501
513
|
harInline: Type.Optional(Type.Boolean()),
|
|
502
514
|
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." }))
|
|
515
|
+
async: Type.Optional(Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." })),
|
|
516
|
+
proxy: Type.Optional(Type.Union([Type.Literal("residential"), Type.Literal("isp")], { description: "Proxy tier. 'residential' routes through residential IPs with CAPTCHA solving. Adds data-based surcharge (~$19/GB). Default: no proxy (datacenter)." })),
|
|
517
|
+
proxy_options: Type.Optional(Type.Object({ country: Type.Optional(Type.String({ description: "ISO country code (default: 'us')" })), state: Type.Optional(Type.String({ description: "State/region code (e.g. 'virginia')" })), city: Type.Optional(Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
518
|
+
stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
504
519
|
}),
|
|
505
520
|
async execute(_id, params) {
|
|
506
521
|
if (!Array.isArray(params.steps)) throw new Error("steps must be an array");
|
|
@@ -514,6 +529,9 @@ function register(api) {
|
|
|
514
529
|
if (Object.keys(opts).length > 0) payload.options = opts;
|
|
515
530
|
if (params.include) payload.include = params.include;
|
|
516
531
|
if (params.harInline) payload.harInline = params.harInline;
|
|
532
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
533
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
534
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
517
535
|
const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
|
|
518
536
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
519
537
|
}
|
|
@@ -541,7 +559,10 @@ function register(api) {
|
|
|
541
559
|
include: Type.Optional(Type.Array(Type.String())),
|
|
542
560
|
harInline: Type.Optional(Type.Boolean()),
|
|
543
561
|
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." }))
|
|
562
|
+
async: Type.Optional(Type.Boolean({ description: "Return job_id immediately without waiting for completion. Use riddle_poll to check status." })),
|
|
563
|
+
proxy: Type.Optional(Type.Union([Type.Literal("residential"), Type.Literal("isp")], { description: "Proxy tier. 'residential' routes through residential IPs with CAPTCHA solving. Adds data-based surcharge (~$19/GB). Default: no proxy (datacenter)." })),
|
|
564
|
+
proxy_options: Type.Optional(Type.Object({ country: Type.Optional(Type.String({ description: "ISO country code (default: 'us')" })), state: Type.Optional(Type.String({ description: "State/region code (e.g. 'virginia')" })), city: Type.Optional(Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
565
|
+
stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
545
566
|
}),
|
|
546
567
|
async execute(_id, params) {
|
|
547
568
|
if (!params.script || typeof params.script !== "string") throw new Error("script must be a string");
|
|
@@ -555,6 +576,9 @@ function register(api) {
|
|
|
555
576
|
if (Object.keys(opts).length > 0) payload.options = opts;
|
|
556
577
|
if (params.include) payload.include = params.include;
|
|
557
578
|
if (params.harInline) payload.harInline = params.harInline;
|
|
579
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
580
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
581
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
558
582
|
const result = await runWithDefaults(api, payload, { include: ["screenshot", "console", "result", "data", "urls", "dataset", "sitemap", "visual_diff"], returnAsync: !!params.async });
|
|
559
583
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
560
584
|
}
|
|
@@ -576,7 +600,10 @@ function register(api) {
|
|
|
576
600
|
secure: Type.Optional(Type.Boolean()),
|
|
577
601
|
httpOnly: Type.Optional(Type.Boolean())
|
|
578
602
|
}), { description: "Cookies to inject for authenticated sessions" })),
|
|
579
|
-
options: Type.Optional(Type.Record(Type.String(), Type.Any()))
|
|
603
|
+
options: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
604
|
+
proxy: Type.Optional(Type.Union([Type.Literal("residential"), Type.Literal("isp")], { description: "Proxy tier for blocked sites. Adds ~$19/GB surcharge." })),
|
|
605
|
+
proxy_options: Type.Optional(Type.Object({ country: Type.Optional(Type.String({ description: "ISO country code (default: 'us')" })), state: Type.Optional(Type.String({ description: "State/region code (e.g. 'virginia')" })), city: Type.Optional(Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
606
|
+
stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
580
607
|
}),
|
|
581
608
|
async execute(_id, params) {
|
|
582
609
|
const scrapeOpts = params.extract_metadata === false ? "{ extract_metadata: false }" : "";
|
|
@@ -586,6 +613,9 @@ function register(api) {
|
|
|
586
613
|
options: { ...params.options || {}, returnResult: true }
|
|
587
614
|
};
|
|
588
615
|
if (params.cookies) payload.options.cookies = params.cookies;
|
|
616
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
617
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
618
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
589
619
|
const result = await runWithDefaults(api, payload, { include: ["result", "console"] });
|
|
590
620
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
591
621
|
}
|
|
@@ -610,7 +640,10 @@ function register(api) {
|
|
|
610
640
|
secure: Type.Optional(Type.Boolean()),
|
|
611
641
|
httpOnly: Type.Optional(Type.Boolean())
|
|
612
642
|
}), { description: "Cookies to inject for authenticated sessions" })),
|
|
613
|
-
options: Type.Optional(Type.Record(Type.String(), Type.Any()))
|
|
643
|
+
options: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
644
|
+
proxy: Type.Optional(Type.Union([Type.Literal("residential"), Type.Literal("isp")], { description: "Proxy tier for blocked sites. Adds ~$19/GB surcharge." })),
|
|
645
|
+
proxy_options: Type.Optional(Type.Object({ country: Type.Optional(Type.String({ description: "ISO country code (default: 'us')" })), state: Type.Optional(Type.String({ description: "State/region code (e.g. 'virginia')" })), city: Type.Optional(Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
646
|
+
stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
614
647
|
}),
|
|
615
648
|
async execute(_id, params) {
|
|
616
649
|
const mapOpts = [];
|
|
@@ -625,6 +658,9 @@ function register(api) {
|
|
|
625
658
|
options: { ...params.options || {}, returnResult: true }
|
|
626
659
|
};
|
|
627
660
|
if (params.cookies) payload.options.cookies = params.cookies;
|
|
661
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
662
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
663
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
628
664
|
const result = await runWithDefaults(api, payload, { include: ["result", "console"] });
|
|
629
665
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
630
666
|
}
|
|
@@ -652,7 +688,10 @@ function register(api) {
|
|
|
652
688
|
secure: Type.Optional(Type.Boolean()),
|
|
653
689
|
httpOnly: Type.Optional(Type.Boolean())
|
|
654
690
|
}), { description: "Cookies to inject for authenticated sessions" })),
|
|
655
|
-
options: Type.Optional(Type.Record(Type.String(), Type.Any()))
|
|
691
|
+
options: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
692
|
+
proxy: Type.Optional(Type.Union([Type.Literal("residential"), Type.Literal("isp")], { description: "Proxy tier for blocked sites. Adds ~$19/GB surcharge." })),
|
|
693
|
+
proxy_options: Type.Optional(Type.Object({ country: Type.Optional(Type.String({ description: "ISO country code (default: 'us')" })), state: Type.Optional(Type.String({ description: "State/region code (e.g. 'virginia')" })), city: Type.Optional(Type.String({ description: "City name (e.g. 'fredericksburg')" })) })),
|
|
694
|
+
stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
656
695
|
}),
|
|
657
696
|
async execute(_id, params) {
|
|
658
697
|
const crawlOpts = [];
|
|
@@ -670,6 +709,9 @@ function register(api) {
|
|
|
670
709
|
options: { ...params.options || {}, returnResult: true }
|
|
671
710
|
};
|
|
672
711
|
if (params.cookies) payload.options.cookies = params.cookies;
|
|
712
|
+
if (params.proxy) payload.proxy = params.proxy;
|
|
713
|
+
if (params.proxy_options) payload.proxy_options = params.proxy_options;
|
|
714
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
673
715
|
const result = await runWithDefaults(api, payload, { include: ["result", "console"] });
|
|
674
716
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
675
717
|
}
|
|
@@ -707,7 +749,8 @@ function register(api) {
|
|
|
707
749
|
secure: Type.Optional(Type.Boolean()),
|
|
708
750
|
httpOnly: Type.Optional(Type.Boolean())
|
|
709
751
|
}), { description: "Cookies for the 'after' URL" })),
|
|
710
|
-
options: Type.Optional(Type.Record(Type.String(), Type.Any()))
|
|
752
|
+
options: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
753
|
+
stealth: Type.Optional(Type.Boolean({ description: "Enable stealth mode (Patchright) to bypass bot detection (Cloudflare, Vercel, Datadome). Disables console capture. Default: false" }))
|
|
711
754
|
}),
|
|
712
755
|
async execute(_id, params) {
|
|
713
756
|
const vdOpts = [];
|
|
@@ -726,6 +769,7 @@ function register(api) {
|
|
|
726
769
|
script: `return await visualDiff(${optsStr});`,
|
|
727
770
|
options: { ...params.options || {}, returnResult: true }
|
|
728
771
|
};
|
|
772
|
+
if (params.stealth) payload.stealth = params.stealth;
|
|
729
773
|
const result = await runWithDefaults(api, payload, { include: ["result", "console", "visual_diff"] });
|
|
730
774
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
731
775
|
}
|
|
@@ -851,7 +895,15 @@ function register(api) {
|
|
|
851
895
|
timeout: Type.Optional(Type.Number({ description: "Max execution time in seconds (default: 120, max: 600)" })),
|
|
852
896
|
readiness_path: Type.Optional(Type.String({ description: "Path to poll for readiness (default: same as path)" })),
|
|
853
897
|
readiness_timeout: Type.Optional(Type.Number({ description: "Max seconds to wait for server readiness (default: 30)" })),
|
|
854
|
-
script: Type.Optional(Type.String({ description:
|
|
898
|
+
script: Type.Optional(Type.String({ description: "Optional Playwright script to run after server is ready. Full sandbox: saveScreenshot(), scrape(), map(), crawl(), saveHtml(), saveJson(), visualDiff(). Cannot use with steps." })),
|
|
899
|
+
steps: Type.Optional(Type.Array(Type.Any(), { description: "Declarative steps (same as riddle_steps). Cannot use with script. Example: [{ click: '.btn' }, { screenshot: 'after-click' }]" })),
|
|
900
|
+
wait_until: Type.Optional(Type.Union([Type.Literal("load"), Type.Literal("domcontentloaded"), Type.Literal("networkidle")], { description: "Playwright waitUntil strategy for page.goto (default: 'load'). Use 'domcontentloaded' for SPAs that make continuous network requests." })),
|
|
901
|
+
wait_for_selector: Type.Optional(Type.String({ description: "CSS selector to wait for after page load, before running script. Solves hydration race conditions. Example: '.billing-table' or '[data-hydrated]'" })),
|
|
902
|
+
navigation_timeout: Type.Optional(Type.Number({ description: "Seconds to wait for page.goto() navigation to complete (5-120, default: 30). Increase for slow-loading apps." })),
|
|
903
|
+
color_scheme: Type.Optional(Type.Union([Type.Literal("dark"), Type.Literal("light")], { description: "Color scheme for emulateMedia. Applied BEFORE navigation so initial render uses it." })),
|
|
904
|
+
viewport: Type.Optional(Type.Object({ width: Type.Number(), height: Type.Number() }, { description: "Browser viewport size (default: 1920x1080)" })),
|
|
905
|
+
localStorage: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "localStorage key-value pairs injected before page load (e.g. auth tokens)" })),
|
|
906
|
+
exclude: Type.Optional(Type.Array(Type.String(), { description: "Glob patterns to exclude from tarball. Default: ['.git', '*.log']. Add 'node_modules' only if your server doesn't need it (e.g. static file servers)." }))
|
|
855
907
|
}),
|
|
856
908
|
async execute(_id, params) {
|
|
857
909
|
const { apiKey, baseUrl } = getCfg(api);
|
|
@@ -869,11 +921,16 @@ function register(api) {
|
|
|
869
921
|
}
|
|
870
922
|
const endpoint = baseUrl.replace(/\/$/, "");
|
|
871
923
|
let envRef = null;
|
|
872
|
-
|
|
924
|
+
const hasSensitiveEnv = params.sensitive_env && Object.keys(params.sensitive_env).length > 0;
|
|
925
|
+
const hasLocalStorage = params.localStorage && Object.keys(params.localStorage).length > 0;
|
|
926
|
+
if (hasSensitiveEnv || hasLocalStorage) {
|
|
927
|
+
const envBody = {};
|
|
928
|
+
if (hasSensitiveEnv) envBody.env = params.sensitive_env;
|
|
929
|
+
if (hasLocalStorage) envBody.localStorage = params.localStorage;
|
|
873
930
|
const envRes = await fetch(`${endpoint}/v1/server-preview/env`, {
|
|
874
931
|
method: "POST",
|
|
875
932
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
876
|
-
body: JSON.stringify(
|
|
933
|
+
body: JSON.stringify(envBody)
|
|
877
934
|
});
|
|
878
935
|
if (!envRes.ok) {
|
|
879
936
|
const err = await envRes.text();
|
|
@@ -894,6 +951,12 @@ function register(api) {
|
|
|
894
951
|
if (params.readiness_path) createBody.readiness_path = params.readiness_path;
|
|
895
952
|
if (params.readiness_timeout) createBody.readiness_timeout = params.readiness_timeout;
|
|
896
953
|
if (params.script) createBody.script = params.script;
|
|
954
|
+
if (params.steps) createBody.steps = params.steps;
|
|
955
|
+
if (params.wait_until) createBody.wait_until = params.wait_until;
|
|
956
|
+
if (params.wait_for_selector) createBody.wait_for_selector = params.wait_for_selector;
|
|
957
|
+
if (params.navigation_timeout) createBody.navigation_timeout = params.navigation_timeout;
|
|
958
|
+
if (params.color_scheme) createBody.color_scheme = params.color_scheme;
|
|
959
|
+
if (params.viewport) createBody.viewport = params.viewport;
|
|
897
960
|
const createRes = await fetch(`${endpoint}/v1/server-preview`, {
|
|
898
961
|
method: "POST",
|
|
899
962
|
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
@@ -906,7 +969,9 @@ function register(api) {
|
|
|
906
969
|
const created = await createRes.json();
|
|
907
970
|
const tarball = `/tmp/riddle-sp-${created.job_id}.tar.gz`;
|
|
908
971
|
try {
|
|
909
|
-
|
|
972
|
+
const excludes = params.exclude || [".git", "*.log"];
|
|
973
|
+
const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
|
|
974
|
+
await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
|
|
910
975
|
const tarData = await readFile(tarball);
|
|
911
976
|
const uploadRes = await fetch(created.upload_url, {
|
|
912
977
|
method: "PUT",
|
|
@@ -941,9 +1006,9 @@ function register(api) {
|
|
|
941
1006
|
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
|
|
942
1007
|
}
|
|
943
1008
|
const statusData = await statusRes.json();
|
|
944
|
-
if (statusData.status === "completed" || statusData.status === "failed") {
|
|
1009
|
+
if (statusData.status === "complete" || statusData.status === "completed" || statusData.status === "failed") {
|
|
945
1010
|
const result = {
|
|
946
|
-
ok: statusData.status === "completed",
|
|
1011
|
+
ok: statusData.status === "complete" || statusData.status === "completed",
|
|
947
1012
|
job_id: created.job_id,
|
|
948
1013
|
status: statusData.status,
|
|
949
1014
|
outputs: statusData.outputs || [],
|
|
@@ -967,6 +1032,7 @@ function register(api) {
|
|
|
967
1032
|
}
|
|
968
1033
|
}
|
|
969
1034
|
}
|
|
1035
|
+
result.screenshots = result.outputs.filter((o) => /\.(png|jpg|jpeg)$/i.test(o.name));
|
|
970
1036
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
971
1037
|
}
|
|
972
1038
|
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
@@ -976,6 +1042,324 @@ function register(api) {
|
|
|
976
1042
|
},
|
|
977
1043
|
{ optional: true }
|
|
978
1044
|
);
|
|
1045
|
+
api.registerTool(
|
|
1046
|
+
{
|
|
1047
|
+
name: "riddle_build_preview",
|
|
1048
|
+
description: "Build a Docker image from a Dockerfile in the project directory, run the server, and screenshot it. Unlike riddle_server_preview (which pulls a stock image), this builds a custom image from your Dockerfile \u2014 allowing pre-installed dependencies, build tools, and custom system packages. Built images are cached on the worker for 30 minutes by default for fast re-runs.",
|
|
1049
|
+
parameters: Type.Object({
|
|
1050
|
+
directory: Type.String({ description: "Absolute path to the project directory. Must contain a Dockerfile at root." }),
|
|
1051
|
+
command: Type.String({ description: "Command to start the server inside the built container (e.g. 'python server.py')" }),
|
|
1052
|
+
port: Type.Number({ description: "Port the server listens on inside the container" }),
|
|
1053
|
+
path: Type.Optional(Type.String({ description: "URL path to screenshot (default: '/')" })),
|
|
1054
|
+
env: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Non-sensitive environment variables" })),
|
|
1055
|
+
sensitive_env: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Sensitive environment variables. Stored securely and deleted after use." })),
|
|
1056
|
+
build_args: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Docker build arguments (--build-arg KEY=VAL)" })),
|
|
1057
|
+
keep_image_minutes: Type.Optional(Type.Number({ description: "How long to cache the built image on the worker (default: 30, max: 120, 0 = delete immediately)" })),
|
|
1058
|
+
timeout: Type.Optional(Type.Number({ description: "Max execution time in seconds including build (default: 180, max: 600)" })),
|
|
1059
|
+
readiness_path: Type.Optional(Type.String({ description: "Path to poll for readiness (default: same as path)" })),
|
|
1060
|
+
readiness_timeout: Type.Optional(Type.Number({ description: "Max seconds to wait for server readiness (default: 30)" })),
|
|
1061
|
+
script: Type.Optional(Type.String({ description: "Optional Playwright script to run after server is ready. Cannot use with steps." })),
|
|
1062
|
+
steps: Type.Optional(Type.Array(Type.Any(), { description: "Declarative steps (same as riddle_steps). Cannot use with script." })),
|
|
1063
|
+
wait_until: Type.Optional(Type.Union([Type.Literal("load"), Type.Literal("domcontentloaded"), Type.Literal("networkidle")], { description: "Playwright waitUntil strategy (default: 'load')" })),
|
|
1064
|
+
wait_for_selector: Type.Optional(Type.String({ description: "CSS selector to wait for after page load, before running script" })),
|
|
1065
|
+
navigation_timeout: Type.Optional(Type.Number({ description: "Seconds to wait for page.goto() navigation to complete (5-120, default: 30). Increase for slow-loading apps." })),
|
|
1066
|
+
color_scheme: Type.Optional(Type.Union([Type.Literal("dark"), Type.Literal("light")], { description: "Color scheme for emulateMedia" })),
|
|
1067
|
+
viewport: Type.Optional(Type.Object({ width: Type.Number(), height: Type.Number() }, { description: "Browser viewport size (default: 1920x1080)" })),
|
|
1068
|
+
localStorage: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "localStorage key-value pairs injected before page load" })),
|
|
1069
|
+
exclude: Type.Optional(Type.Array(Type.String(), { description: "Glob patterns to exclude from tarball. Default: ['.git', '*.log']" })),
|
|
1070
|
+
audit: Type.Optional(Type.Boolean({ description: "Run security audit scan on submitted code. Returns dependency list, security findings, code summary, and risk flags." }))
|
|
1071
|
+
}),
|
|
1072
|
+
async execute(_id, params) {
|
|
1073
|
+
const { apiKey, baseUrl } = getCfg(api);
|
|
1074
|
+
if (!apiKey) {
|
|
1075
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
|
|
1076
|
+
}
|
|
1077
|
+
assertAllowedBaseUrl(baseUrl);
|
|
1078
|
+
const dir = params.directory;
|
|
1079
|
+
if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
|
|
1080
|
+
try {
|
|
1081
|
+
const st = await stat(dir);
|
|
1082
|
+
if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
|
|
1083
|
+
} catch (e) {
|
|
1084
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
await stat(`${dir}/Dockerfile`);
|
|
1088
|
+
} catch {
|
|
1089
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `No Dockerfile found at ${dir}/Dockerfile. riddle_build_preview requires a Dockerfile at the root of the directory.` }, null, 2) }] };
|
|
1090
|
+
}
|
|
1091
|
+
const endpoint = baseUrl.replace(/\/$/, "");
|
|
1092
|
+
let envRef = null;
|
|
1093
|
+
const hasSensitiveEnv = params.sensitive_env && Object.keys(params.sensitive_env).length > 0;
|
|
1094
|
+
const hasLocalStorage = params.localStorage && Object.keys(params.localStorage).length > 0;
|
|
1095
|
+
if (hasSensitiveEnv || hasLocalStorage) {
|
|
1096
|
+
const envBody = {};
|
|
1097
|
+
if (hasSensitiveEnv) envBody.env = params.sensitive_env;
|
|
1098
|
+
if (hasLocalStorage) envBody.localStorage = params.localStorage;
|
|
1099
|
+
const envRes = await fetch(`${endpoint}/v1/build-preview/env`, {
|
|
1100
|
+
method: "POST",
|
|
1101
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1102
|
+
body: JSON.stringify(envBody)
|
|
1103
|
+
});
|
|
1104
|
+
if (!envRes.ok) {
|
|
1105
|
+
const err = await envRes.text();
|
|
1106
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
|
|
1107
|
+
}
|
|
1108
|
+
const envData = await envRes.json();
|
|
1109
|
+
envRef = envData.env_ref;
|
|
1110
|
+
}
|
|
1111
|
+
const createBody = {
|
|
1112
|
+
command: params.command,
|
|
1113
|
+
port: params.port
|
|
1114
|
+
};
|
|
1115
|
+
if (params.path) createBody.path = params.path;
|
|
1116
|
+
if (params.env) createBody.env = params.env;
|
|
1117
|
+
if (envRef) createBody.env_ref = envRef;
|
|
1118
|
+
if (params.build_args) createBody.build_args = params.build_args;
|
|
1119
|
+
if (params.keep_image_minutes !== void 0) createBody.keep_image_minutes = params.keep_image_minutes;
|
|
1120
|
+
if (params.timeout) createBody.timeout = params.timeout;
|
|
1121
|
+
if (params.readiness_path) createBody.readiness_path = params.readiness_path;
|
|
1122
|
+
if (params.readiness_timeout) createBody.readiness_timeout = params.readiness_timeout;
|
|
1123
|
+
if (params.script) createBody.script = params.script;
|
|
1124
|
+
if (params.steps) createBody.steps = params.steps;
|
|
1125
|
+
if (params.wait_until) createBody.wait_until = params.wait_until;
|
|
1126
|
+
if (params.wait_for_selector) createBody.wait_for_selector = params.wait_for_selector;
|
|
1127
|
+
if (params.navigation_timeout) createBody.navigation_timeout = params.navigation_timeout;
|
|
1128
|
+
if (params.color_scheme) createBody.color_scheme = params.color_scheme;
|
|
1129
|
+
if (params.viewport) createBody.viewport = params.viewport;
|
|
1130
|
+
if (params.audit) createBody.audit = true;
|
|
1131
|
+
const createRes = await fetch(`${endpoint}/v1/build-preview`, {
|
|
1132
|
+
method: "POST",
|
|
1133
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1134
|
+
body: JSON.stringify(createBody)
|
|
1135
|
+
});
|
|
1136
|
+
if (!createRes.ok) {
|
|
1137
|
+
const err = await createRes.text();
|
|
1138
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
|
|
1139
|
+
}
|
|
1140
|
+
const created = await createRes.json();
|
|
1141
|
+
const tarball = `/tmp/riddle-bp-${created.job_id}.tar.gz`;
|
|
1142
|
+
try {
|
|
1143
|
+
const excludes = params.exclude || [".git", "*.log"];
|
|
1144
|
+
const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
|
|
1145
|
+
await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout: 12e4 });
|
|
1146
|
+
const tarData = await readFile(tarball);
|
|
1147
|
+
const uploadRes = await fetch(created.upload_url, {
|
|
1148
|
+
method: "PUT",
|
|
1149
|
+
headers: { "Content-Type": "application/gzip" },
|
|
1150
|
+
body: tarData
|
|
1151
|
+
});
|
|
1152
|
+
if (!uploadRes.ok) {
|
|
1153
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
|
|
1154
|
+
}
|
|
1155
|
+
} finally {
|
|
1156
|
+
try {
|
|
1157
|
+
await rm(tarball, { force: true });
|
|
1158
|
+
} catch {
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
const startRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
|
|
1162
|
+
method: "POST",
|
|
1163
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1164
|
+
});
|
|
1165
|
+
if (!startRes.ok) {
|
|
1166
|
+
const err = await startRes.text();
|
|
1167
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
|
|
1168
|
+
}
|
|
1169
|
+
const timeoutMs = ((params.timeout || 180) + 120) * 1e3;
|
|
1170
|
+
const pollStart = Date.now();
|
|
1171
|
+
const POLL_INTERVAL = 3e3;
|
|
1172
|
+
while (Date.now() - pollStart < timeoutMs) {
|
|
1173
|
+
const statusRes = await fetch(`${endpoint}/v1/build-preview/${created.job_id}`, {
|
|
1174
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1175
|
+
});
|
|
1176
|
+
if (!statusRes.ok) {
|
|
1177
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
|
|
1178
|
+
}
|
|
1179
|
+
const statusData = await statusRes.json();
|
|
1180
|
+
if (statusData.status === "complete" || statusData.status === "completed" || statusData.status === "failed") {
|
|
1181
|
+
const result = {
|
|
1182
|
+
ok: statusData.status === "complete" || statusData.status === "completed",
|
|
1183
|
+
job_id: created.job_id,
|
|
1184
|
+
status: statusData.status,
|
|
1185
|
+
outputs: statusData.outputs || [],
|
|
1186
|
+
compute_seconds: statusData.compute_seconds,
|
|
1187
|
+
build_duration_ms: statusData.build_duration_ms,
|
|
1188
|
+
egress_bytes: statusData.egress_bytes
|
|
1189
|
+
};
|
|
1190
|
+
if (statusData.error) result.error = statusData.error;
|
|
1191
|
+
if (statusData.build_log) result.build_log = statusData.build_log;
|
|
1192
|
+
if (statusData.container_log) result.container_log = statusData.container_log;
|
|
1193
|
+
if (statusData.audit) result.audit = statusData.audit;
|
|
1194
|
+
const workspace = getWorkspacePath(api);
|
|
1195
|
+
for (const output of result.outputs) {
|
|
1196
|
+
if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
|
|
1197
|
+
try {
|
|
1198
|
+
const imgRes = await fetch(output.url);
|
|
1199
|
+
if (imgRes.ok) {
|
|
1200
|
+
const buf = await imgRes.arrayBuffer();
|
|
1201
|
+
const base64 = Buffer.from(buf).toString("base64");
|
|
1202
|
+
const ref = await writeArtifactBinary(workspace, "screenshots", `${created.job_id}-${output.name}`, base64);
|
|
1203
|
+
output.saved = ref.path;
|
|
1204
|
+
output.sizeBytes = ref.sizeBytes;
|
|
1205
|
+
}
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
result.screenshots = result.outputs.filter((o) => /\.(png|jpg|jpeg)$/i.test(o.name));
|
|
1211
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1212
|
+
}
|
|
1213
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
1214
|
+
}
|
|
1215
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Job did not complete within ${timeoutMs / 1e3}s` }, null, 2) }] };
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
{ optional: true }
|
|
1219
|
+
);
|
|
1220
|
+
async function riddleApiFetch(api2, method, path, body) {
|
|
1221
|
+
const { apiKey, baseUrl } = getCfg(api2);
|
|
1222
|
+
if (!apiKey) throw new Error("Missing Riddle API key. Set RIDDLE_API_KEY or configure in plugin settings.");
|
|
1223
|
+
assertAllowedBaseUrl(baseUrl);
|
|
1224
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
1225
|
+
const res = await fetch(url, {
|
|
1226
|
+
method,
|
|
1227
|
+
headers: {
|
|
1228
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1229
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
1230
|
+
},
|
|
1231
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
1232
|
+
});
|
|
1233
|
+
const data = await res.json();
|
|
1234
|
+
if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`);
|
|
1235
|
+
return data;
|
|
1236
|
+
}
|
|
1237
|
+
api.registerTool(
|
|
1238
|
+
{
|
|
1239
|
+
name: "riddle_session_create",
|
|
1240
|
+
description: "Create a persistent browser session. Sessions maintain cookies, localStorage, and auth state across multiple riddle_session_run calls. Use for multi-step auth flows (2FA, OAuth) and authenticated agent workflows. Sessions are billed per-second while warm.",
|
|
1241
|
+
parameters: Type.Object({
|
|
1242
|
+
name: Type.String({ description: "Human-readable session name (unique per API key)" }),
|
|
1243
|
+
ttl_sec: Type.Optional(Type.Number({ description: "Max session lifetime in seconds (default: 3600, max: 86400)" })),
|
|
1244
|
+
idle_timeout_sec: Type.Optional(Type.Number({ description: "Max idle time between uses in seconds (default: 600, max: 3600)" }))
|
|
1245
|
+
}),
|
|
1246
|
+
async execute(_id, params) {
|
|
1247
|
+
const data = await riddleApiFetch(api, "POST", "/v1/sessions", {
|
|
1248
|
+
name: params.name,
|
|
1249
|
+
ttl_sec: params.ttl_sec,
|
|
1250
|
+
idle_timeout_sec: params.idle_timeout_sec
|
|
1251
|
+
});
|
|
1252
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
{ optional: true }
|
|
1256
|
+
);
|
|
1257
|
+
api.registerTool(
|
|
1258
|
+
{
|
|
1259
|
+
name: "riddle_session_list",
|
|
1260
|
+
description: "List all active persistent browser sessions for the current API key.",
|
|
1261
|
+
parameters: Type.Object({}),
|
|
1262
|
+
async execute() {
|
|
1263
|
+
const data = await riddleApiFetch(api, "GET", "/v1/sessions");
|
|
1264
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
1267
|
+
{ optional: true }
|
|
1268
|
+
);
|
|
1269
|
+
api.registerTool(
|
|
1270
|
+
{
|
|
1271
|
+
name: "riddle_session_run",
|
|
1272
|
+
description: "Run a Playwright script or steps in a persistent session. The browser context (cookies, localStorage, auth tokens) persists from previous calls to the same session. Use after riddle_session_create. Available helpers in script: saveScreenshot(label), saveJson(name, data), saveFile(name, buffer), scrape(), map(), crawl().",
|
|
1273
|
+
parameters: Type.Object({
|
|
1274
|
+
session_id: Type.String({ description: "Session ID from riddle_session_create" }),
|
|
1275
|
+
url: Type.Optional(Type.String({ description: "URL to navigate to" })),
|
|
1276
|
+
script: Type.Optional(Type.String({ description: "Playwright script to execute" })),
|
|
1277
|
+
steps: Type.Optional(Type.Array(Type.Any(), { description: "Declarative steps (alternative to script)" })),
|
|
1278
|
+
timeout_sec: Type.Optional(Type.Number({ description: "Max execution time in seconds (default: 60)" }))
|
|
1279
|
+
}),
|
|
1280
|
+
async execute(_id, params) {
|
|
1281
|
+
const { apiKey, baseUrl } = getCfg(api);
|
|
1282
|
+
if (!apiKey) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
|
|
1283
|
+
assertAllowedBaseUrl(baseUrl);
|
|
1284
|
+
const payload = { timeout_sec: params.timeout_sec || 60 };
|
|
1285
|
+
if (params.script) {
|
|
1286
|
+
payload.script = params.script;
|
|
1287
|
+
payload.url = params.url;
|
|
1288
|
+
} else if (params.steps) {
|
|
1289
|
+
payload.steps = params.steps;
|
|
1290
|
+
} else if (params.url) {
|
|
1291
|
+
payload.url = params.url;
|
|
1292
|
+
payload.script = `await page.goto('${params.url.replace(/'/g, "\\'")}'); await saveScreenshot('page');`;
|
|
1293
|
+
} else return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Provide script, steps, or url" }, null, 2) }] };
|
|
1294
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/sessions/${params.session_id}/run`, {
|
|
1295
|
+
method: "POST",
|
|
1296
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
1297
|
+
body: JSON.stringify(payload)
|
|
1298
|
+
});
|
|
1299
|
+
const data = await res.json();
|
|
1300
|
+
if (!res.ok) return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: data.error?.message || data.error || `HTTP ${res.status}` }, null, 2) }] };
|
|
1301
|
+
if (data.job_id) {
|
|
1302
|
+
const timeoutMs = (params.timeout_sec || 60) * 1e3 + 15e3;
|
|
1303
|
+
const POLL_INTERVAL = 2e3;
|
|
1304
|
+
const started = Date.now();
|
|
1305
|
+
while (Date.now() - started < timeoutMs) {
|
|
1306
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
1307
|
+
const statusRes = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${data.job_id}`, {
|
|
1308
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1309
|
+
});
|
|
1310
|
+
if (!statusRes.ok) continue;
|
|
1311
|
+
const statusData = await statusRes.json();
|
|
1312
|
+
if (statusData.status === "completed" || statusData.status === "failed" || statusData.status === "completed_error" || statusData.status === "completed_timeout") {
|
|
1313
|
+
const result = {
|
|
1314
|
+
ok: statusData.status === "completed",
|
|
1315
|
+
job_id: data.job_id,
|
|
1316
|
+
session_id: params.session_id,
|
|
1317
|
+
status: statusData.status,
|
|
1318
|
+
outputs: statusData.outputs || [],
|
|
1319
|
+
duration_ms: statusData.duration_ms
|
|
1320
|
+
};
|
|
1321
|
+
if (statusData.error) result.error = statusData.error;
|
|
1322
|
+
const workspace = getWorkspacePath(api);
|
|
1323
|
+
for (const output of result.outputs) {
|
|
1324
|
+
if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
|
|
1325
|
+
try {
|
|
1326
|
+
const imgRes = await fetch(output.url);
|
|
1327
|
+
if (imgRes.ok) {
|
|
1328
|
+
const buf = await imgRes.arrayBuffer();
|
|
1329
|
+
const base64 = Buffer.from(buf).toString("base64");
|
|
1330
|
+
const ref = await writeArtifactBinary(workspace, "screenshots", `${data.job_id}-${output.name}`, base64);
|
|
1331
|
+
output.saved = ref.path;
|
|
1332
|
+
output.sizeBytes = ref.sizeBytes;
|
|
1333
|
+
}
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
result.screenshots = result.outputs.filter((o) => /\.(png|jpg|jpeg)$/i.test(o.name));
|
|
1339
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: data.job_id, session_id: params.session_id, error: "Job did not complete in time" }, null, 2) }] };
|
|
1343
|
+
}
|
|
1344
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, ...data }, null, 2) }] };
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
{ optional: true }
|
|
1348
|
+
);
|
|
1349
|
+
api.registerTool(
|
|
1350
|
+
{
|
|
1351
|
+
name: "riddle_session_destroy",
|
|
1352
|
+
description: "Destroy a persistent browser session, closing the browser context and cleaning up all stored state. Stops warm-time billing.",
|
|
1353
|
+
parameters: Type.Object({
|
|
1354
|
+
session_id: Type.String({ description: "Session ID to destroy" })
|
|
1355
|
+
}),
|
|
1356
|
+
async execute(_id, params) {
|
|
1357
|
+
const data = await riddleApiFetch(api, "DELETE", `/v1/sessions/${params.session_id}`);
|
|
1358
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
{ optional: true }
|
|
1362
|
+
);
|
|
979
1363
|
}
|
|
980
1364
|
export {
|
|
981
1365
|
register as default
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
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.8.0",
|
|
6
|
+
"notes": "0.8.0: Added riddle_build_preview for Dockerfile-based builds with image caching.",
|
|
7
7
|
"type": "plugin",
|
|
8
8
|
"bundledSkills": [],
|
|
9
9
|
"capabilities": {
|
|
@@ -41,7 +41,12 @@
|
|
|
41
41
|
"riddle_visual_diff",
|
|
42
42
|
"riddle_preview",
|
|
43
43
|
"riddle_preview_delete",
|
|
44
|
-
"riddle_server_preview"
|
|
44
|
+
"riddle_server_preview",
|
|
45
|
+
"riddle_build_preview",
|
|
46
|
+
"riddle_session_create",
|
|
47
|
+
"riddle_session_list",
|
|
48
|
+
"riddle_session_run",
|
|
49
|
+
"riddle_session_destroy"
|
|
45
50
|
],
|
|
46
51
|
"invokes": [],
|
|
47
52
|
"note": "Provides tools for agent use; does not invoke other agent tools"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riddledc/openclaw-riddledc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "OpenClaw integration package for RiddleDC (no secrets).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "RiddleDC",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"openclaw": {
|
|
35
35
|
"extensions": [
|
|
36
|
-
"."
|
|
36
|
+
"./dist/index.js"
|
|
37
37
|
]
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|