@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/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
  }