@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 CHANGED
@@ -1,4 +1,4 @@
1
- 921867120c7149d34d965a56ab0b727b2ecd2f0abf4ea9980171aefd4e11c39f dist/index.cjs
1
+ 73579dcc6d9a149e5af0393db55cc09ea8f577a7902ab5271d30d1fb5606990e dist/index.cjs
2
2
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
3
3
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
4
- 06f7ee46883239b2b9e1180a0f82d70b62f03fb2b73c032ff4ae7dafb65ab8e1 dist/index.js
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: `Optional Playwright script to run after server is ready (e.g. 'await page.click("button")')` }))
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
- if (params.sensitive_env && Object.keys(params.sensitive_env).length > 0) {
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({ env: params.sensitive_env })
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
- await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 12e4 });
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: `Optional Playwright script to run after server is ready (e.g. 'await page.click("button")')` }))
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
- if (params.sensitive_env && Object.keys(params.sensitive_env).length > 0) {
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({ env: params.sensitive_env })
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
- await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 12e4 });
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
@@ -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.7.0",
6
- "notes": "0.7.0: Added riddle_server_preview tool for running server-side apps in isolated Docker containers.",
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.7.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": {