@riddledc/openclaw-riddledc 0.6.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 +532 -8
- package/dist/index.js +532 -8
- package/openclaw.plugin.json +9 -3
- package/package.json +2 -2
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
|
}
|
|
@@ -860,4 +904,484 @@ function register(api) {
|
|
|
860
904
|
},
|
|
861
905
|
{ optional: true }
|
|
862
906
|
);
|
|
907
|
+
api.registerTool(
|
|
908
|
+
{
|
|
909
|
+
name: "riddle_server_preview",
|
|
910
|
+
description: "Run a server-side app (Next.js, Express, Django, etc.) in an isolated Docker container and screenshot it. Tars the build directory, uploads to Riddle, starts the container with the specified image and command, waits for readiness, then takes a Playwright screenshot. Use for apps that need a running server process (not static sites \u2014 use riddle_preview for those).",
|
|
911
|
+
parameters: import_typebox.Type.Object({
|
|
912
|
+
directory: import_typebox.Type.String({ description: "Absolute path to the project/build directory to deploy into the container" }),
|
|
913
|
+
image: import_typebox.Type.String({ description: "Docker image to run (e.g. 'node:20-slim', 'python:3.12-slim')" }),
|
|
914
|
+
command: import_typebox.Type.String({ description: "Command to start the server inside the container (e.g. 'npm start', 'python manage.py runserver 0.0.0.0:3000')" }),
|
|
915
|
+
port: import_typebox.Type.Number({ description: "Port the server listens on inside the container" }),
|
|
916
|
+
path: import_typebox.Type.Optional(import_typebox.Type.String({ description: "URL path to screenshot (default: '/')" })),
|
|
917
|
+
env: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "Non-sensitive environment variables" })),
|
|
918
|
+
sensitive_env: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "Sensitive environment variables (API keys, DB passwords). Stored securely and deleted after use." })),
|
|
919
|
+
timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max execution time in seconds (default: 120, max: 600)" })),
|
|
920
|
+
readiness_path: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Path to poll for readiness (default: same as path)" })),
|
|
921
|
+
readiness_timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max seconds to wait for server readiness (default: 30)" })),
|
|
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)." }))
|
|
931
|
+
}),
|
|
932
|
+
async execute(_id, params) {
|
|
933
|
+
const { apiKey, baseUrl } = getCfg(api);
|
|
934
|
+
if (!apiKey) {
|
|
935
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
|
|
936
|
+
}
|
|
937
|
+
assertAllowedBaseUrl(baseUrl);
|
|
938
|
+
const dir = params.directory;
|
|
939
|
+
if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
|
|
940
|
+
try {
|
|
941
|
+
const st = await (0, import_promises.stat)(dir);
|
|
942
|
+
if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
|
|
943
|
+
} catch (e) {
|
|
944
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
|
|
945
|
+
}
|
|
946
|
+
const endpoint = baseUrl.replace(/\/$/, "");
|
|
947
|
+
let envRef = null;
|
|
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;
|
|
954
|
+
const envRes = await fetch(`${endpoint}/v1/server-preview/env`, {
|
|
955
|
+
method: "POST",
|
|
956
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
957
|
+
body: JSON.stringify(envBody)
|
|
958
|
+
});
|
|
959
|
+
if (!envRes.ok) {
|
|
960
|
+
const err = await envRes.text();
|
|
961
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
|
|
962
|
+
}
|
|
963
|
+
const envData = await envRes.json();
|
|
964
|
+
envRef = envData.env_ref;
|
|
965
|
+
}
|
|
966
|
+
const createBody = {
|
|
967
|
+
image: params.image,
|
|
968
|
+
command: params.command,
|
|
969
|
+
port: params.port
|
|
970
|
+
};
|
|
971
|
+
if (params.path) createBody.path = params.path;
|
|
972
|
+
if (params.env) createBody.env = params.env;
|
|
973
|
+
if (envRef) createBody.env_ref = envRef;
|
|
974
|
+
if (params.timeout) createBody.timeout = params.timeout;
|
|
975
|
+
if (params.readiness_path) createBody.readiness_path = params.readiness_path;
|
|
976
|
+
if (params.readiness_timeout) createBody.readiness_timeout = params.readiness_timeout;
|
|
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;
|
|
984
|
+
const createRes = await fetch(`${endpoint}/v1/server-preview`, {
|
|
985
|
+
method: "POST",
|
|
986
|
+
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
987
|
+
body: JSON.stringify(createBody)
|
|
988
|
+
});
|
|
989
|
+
if (!createRes.ok) {
|
|
990
|
+
const err = await createRes.text();
|
|
991
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
|
|
992
|
+
}
|
|
993
|
+
const created = await createRes.json();
|
|
994
|
+
const tarball = `/tmp/riddle-sp-${created.job_id}.tar.gz`;
|
|
995
|
+
try {
|
|
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 });
|
|
999
|
+
const tarData = await (0, import_promises.readFile)(tarball);
|
|
1000
|
+
const uploadRes = await fetch(created.upload_url, {
|
|
1001
|
+
method: "PUT",
|
|
1002
|
+
headers: { "Content-Type": "application/gzip" },
|
|
1003
|
+
body: tarData
|
|
1004
|
+
});
|
|
1005
|
+
if (!uploadRes.ok) {
|
|
1006
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
|
|
1007
|
+
}
|
|
1008
|
+
} finally {
|
|
1009
|
+
try {
|
|
1010
|
+
await (0, import_promises.rm)(tarball, { force: true });
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const startRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
|
|
1015
|
+
method: "POST",
|
|
1016
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1017
|
+
});
|
|
1018
|
+
if (!startRes.ok) {
|
|
1019
|
+
const err = await startRes.text();
|
|
1020
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
|
|
1021
|
+
}
|
|
1022
|
+
const timeoutMs = ((params.timeout || 120) + 60) * 1e3;
|
|
1023
|
+
const pollStart = Date.now();
|
|
1024
|
+
const POLL_INTERVAL = 3e3;
|
|
1025
|
+
while (Date.now() - pollStart < timeoutMs) {
|
|
1026
|
+
const statusRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}`, {
|
|
1027
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
1028
|
+
});
|
|
1029
|
+
if (!statusRes.ok) {
|
|
1030
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
|
|
1031
|
+
}
|
|
1032
|
+
const statusData = await statusRes.json();
|
|
1033
|
+
if (statusData.status === "complete" || statusData.status === "completed" || statusData.status === "failed") {
|
|
1034
|
+
const result = {
|
|
1035
|
+
ok: statusData.status === "complete" || statusData.status === "completed",
|
|
1036
|
+
job_id: created.job_id,
|
|
1037
|
+
status: statusData.status,
|
|
1038
|
+
outputs: statusData.outputs || [],
|
|
1039
|
+
compute_seconds: statusData.compute_seconds,
|
|
1040
|
+
egress_bytes: statusData.egress_bytes
|
|
1041
|
+
};
|
|
1042
|
+
if (statusData.error) result.error = statusData.error;
|
|
1043
|
+
const workspace = getWorkspacePath(api);
|
|
1044
|
+
for (const output of result.outputs) {
|
|
1045
|
+
if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
|
|
1046
|
+
try {
|
|
1047
|
+
const imgRes = await fetch(output.url);
|
|
1048
|
+
if (imgRes.ok) {
|
|
1049
|
+
const buf = await imgRes.arrayBuffer();
|
|
1050
|
+
const base64 = Buffer.from(buf).toString("base64");
|
|
1051
|
+
const ref = await writeArtifactBinary(workspace, "screenshots", `${created.job_id}-${output.name}`, base64);
|
|
1052
|
+
output.saved = ref.path;
|
|
1053
|
+
output.sizeBytes = ref.sizeBytes;
|
|
1054
|
+
}
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
result.screenshots = result.outputs.filter((o) => /\.(png|jpg|jpeg)$/i.test(o.name));
|
|
1060
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1061
|
+
}
|
|
1062
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
1063
|
+
}
|
|
1064
|
+
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) }] };
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
{ optional: true }
|
|
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
|
+
);
|
|
863
1387
|
}
|