@riddledc/openclaw-riddledc 0.5.6 → 0.7.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
- 041dfffc9dfaaa099cb6243c3512574254b91a80d19e64a68b078d3fc97bd4c8 dist/index.cjs
1
+ 921867120c7149d34d965a56ab0b727b2ecd2f0abf4ea9980171aefd4e11c39f dist/index.cjs
2
2
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
3
3
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
4
- b4d87078938b29cd38097688ac435c4fe328a561963212b53c678a2525c8b86a dist/index.js
4
+ 06f7ee46883239b2b9e1180a0f82d70b62f03fb2b73c032ff4ae7dafb65ab8e1 dist/index.js
package/dist/index.cjs CHANGED
@@ -26,6 +26,9 @@ module.exports = __toCommonJS(index_exports);
26
26
  var import_typebox = require("@sinclair/typebox");
27
27
  var import_promises = require("fs/promises");
28
28
  var import_node_path = require("path");
29
+ var import_node_child_process = require("child_process");
30
+ var import_node_util = require("util");
31
+ var execFile = (0, import_node_util.promisify)(import_node_child_process.execFile);
29
32
  var INLINE_CAP = 50 * 1024;
30
33
  function getCfg(api) {
31
34
  const cfg = api?.config ?? {};
@@ -753,4 +756,248 @@ function register(api) {
753
756
  },
754
757
  { optional: true }
755
758
  );
759
+ api.registerTool(
760
+ {
761
+ name: "riddle_preview",
762
+ description: "Deploy a local build directory as an ephemeral preview site. Tars the directory, uploads to Riddle, and returns a live URL at preview.riddledc.com that can be screenshotted with other riddle_* tools. Previews auto-expire after 24 hours.",
763
+ parameters: import_typebox.Type.Object({
764
+ directory: import_typebox.Type.String({ description: "Absolute path to the build output directory (e.g. /path/to/build or /path/to/dist)" }),
765
+ framework: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Framework hint: 'spa' (default) or 'static'" }))
766
+ }),
767
+ async execute(_id, params) {
768
+ const { apiKey, baseUrl } = getCfg(api);
769
+ if (!apiKey) {
770
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
771
+ }
772
+ assertAllowedBaseUrl(baseUrl);
773
+ const dir = params.directory;
774
+ if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
775
+ try {
776
+ const st = await (0, import_promises.stat)(dir);
777
+ if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
778
+ } catch (e) {
779
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
780
+ }
781
+ const endpoint = baseUrl.replace(/\/$/, "");
782
+ const createRes = await fetch(`${endpoint}/v1/preview`, {
783
+ method: "POST",
784
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
785
+ body: JSON.stringify({ framework: params.framework || "spa" })
786
+ });
787
+ if (!createRes.ok) {
788
+ const err = await createRes.text();
789
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
790
+ }
791
+ const created = await createRes.json();
792
+ const tarball = `/tmp/riddle-preview-${created.id}.tar.gz`;
793
+ try {
794
+ await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 6e4 });
795
+ const tarData = await (0, import_promises.readFile)(tarball);
796
+ const uploadRes = await fetch(created.upload_url, {
797
+ method: "PUT",
798
+ headers: { "Content-Type": "application/gzip" },
799
+ body: tarData
800
+ });
801
+ if (!uploadRes.ok) {
802
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
803
+ }
804
+ } finally {
805
+ try {
806
+ await (0, import_promises.rm)(tarball, { force: true });
807
+ } catch {
808
+ }
809
+ }
810
+ const publishRes = await fetch(`${endpoint}/v1/preview/${created.id}/publish`, {
811
+ method: "POST",
812
+ headers: { Authorization: `Bearer ${apiKey}` }
813
+ });
814
+ if (!publishRes.ok) {
815
+ const err = await publishRes.text();
816
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` }, null, 2) }] };
817
+ }
818
+ const published = await publishRes.json();
819
+ return {
820
+ content: [{
821
+ type: "text",
822
+ text: JSON.stringify({
823
+ ok: true,
824
+ id: published.id,
825
+ preview_url: published.preview_url,
826
+ file_count: published.file_count,
827
+ total_bytes: published.total_bytes,
828
+ expires_at: created.expires_at
829
+ }, null, 2)
830
+ }]
831
+ };
832
+ }
833
+ },
834
+ { optional: true }
835
+ );
836
+ api.registerTool(
837
+ {
838
+ name: "riddle_preview_delete",
839
+ description: "Delete an ephemeral preview site created by riddle_preview. Removes all files and frees the preview ID immediately instead of waiting for auto-expiry.",
840
+ parameters: import_typebox.Type.Object({
841
+ id: import_typebox.Type.String({ description: "Preview ID (e.g. pv_a1b2c3d4)" })
842
+ }),
843
+ async execute(_id, params) {
844
+ const { apiKey, baseUrl } = getCfg(api);
845
+ if (!apiKey) {
846
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
847
+ }
848
+ assertAllowedBaseUrl(baseUrl);
849
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
850
+ method: "DELETE",
851
+ headers: { Authorization: `Bearer ${apiKey}` }
852
+ });
853
+ if (!res.ok) {
854
+ const err = await res.text();
855
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: HTTP ${res.status} ${err}` }, null, 2) }] };
856
+ }
857
+ const data = await res.json();
858
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted: true, files_removed: data.files_removed }, null, 2) }] };
859
+ }
860
+ },
861
+ { optional: true }
862
+ );
863
+ api.registerTool(
864
+ {
865
+ name: "riddle_server_preview",
866
+ 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).",
867
+ parameters: import_typebox.Type.Object({
868
+ directory: import_typebox.Type.String({ description: "Absolute path to the project/build directory to deploy into the container" }),
869
+ image: import_typebox.Type.String({ description: "Docker image to run (e.g. 'node:20-slim', 'python:3.12-slim')" }),
870
+ 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')" }),
871
+ port: import_typebox.Type.Number({ description: "Port the server listens on inside the container" }),
872
+ path: import_typebox.Type.Optional(import_typebox.Type.String({ description: "URL path to screenshot (default: '/')" })),
873
+ env: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), import_typebox.Type.String(), { description: "Non-sensitive environment variables" })),
874
+ 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." })),
875
+ timeout: import_typebox.Type.Optional(import_typebox.Type.Number({ description: "Max execution time in seconds (default: 120, max: 600)" })),
876
+ readiness_path: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Path to poll for readiness (default: same as path)" })),
877
+ 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")')` }))
879
+ }),
880
+ async execute(_id, params) {
881
+ const { apiKey, baseUrl } = getCfg(api);
882
+ if (!apiKey) {
883
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
884
+ }
885
+ assertAllowedBaseUrl(baseUrl);
886
+ const dir = params.directory;
887
+ if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
888
+ try {
889
+ const st = await (0, import_promises.stat)(dir);
890
+ if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
891
+ } catch (e) {
892
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
893
+ }
894
+ const endpoint = baseUrl.replace(/\/$/, "");
895
+ let envRef = null;
896
+ if (params.sensitive_env && Object.keys(params.sensitive_env).length > 0) {
897
+ const envRes = await fetch(`${endpoint}/v1/server-preview/env`, {
898
+ method: "POST",
899
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
900
+ body: JSON.stringify({ env: params.sensitive_env })
901
+ });
902
+ if (!envRes.ok) {
903
+ const err = await envRes.text();
904
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
905
+ }
906
+ const envData = await envRes.json();
907
+ envRef = envData.env_ref;
908
+ }
909
+ const createBody = {
910
+ image: params.image,
911
+ command: params.command,
912
+ port: params.port
913
+ };
914
+ if (params.path) createBody.path = params.path;
915
+ if (params.env) createBody.env = params.env;
916
+ if (envRef) createBody.env_ref = envRef;
917
+ if (params.timeout) createBody.timeout = params.timeout;
918
+ if (params.readiness_path) createBody.readiness_path = params.readiness_path;
919
+ if (params.readiness_timeout) createBody.readiness_timeout = params.readiness_timeout;
920
+ if (params.script) createBody.script = params.script;
921
+ const createRes = await fetch(`${endpoint}/v1/server-preview`, {
922
+ method: "POST",
923
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
924
+ body: JSON.stringify(createBody)
925
+ });
926
+ if (!createRes.ok) {
927
+ const err = await createRes.text();
928
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
929
+ }
930
+ const created = await createRes.json();
931
+ const tarball = `/tmp/riddle-sp-${created.job_id}.tar.gz`;
932
+ try {
933
+ await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 12e4 });
934
+ const tarData = await (0, import_promises.readFile)(tarball);
935
+ const uploadRes = await fetch(created.upload_url, {
936
+ method: "PUT",
937
+ headers: { "Content-Type": "application/gzip" },
938
+ body: tarData
939
+ });
940
+ if (!uploadRes.ok) {
941
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
942
+ }
943
+ } finally {
944
+ try {
945
+ await (0, import_promises.rm)(tarball, { force: true });
946
+ } catch {
947
+ }
948
+ }
949
+ const startRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
950
+ method: "POST",
951
+ headers: { Authorization: `Bearer ${apiKey}` }
952
+ });
953
+ if (!startRes.ok) {
954
+ const err = await startRes.text();
955
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
956
+ }
957
+ const timeoutMs = ((params.timeout || 120) + 60) * 1e3;
958
+ const pollStart = Date.now();
959
+ const POLL_INTERVAL = 3e3;
960
+ while (Date.now() - pollStart < timeoutMs) {
961
+ const statusRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}`, {
962
+ headers: { Authorization: `Bearer ${apiKey}` }
963
+ });
964
+ if (!statusRes.ok) {
965
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
966
+ }
967
+ const statusData = await statusRes.json();
968
+ if (statusData.status === "completed" || statusData.status === "failed") {
969
+ const result = {
970
+ ok: statusData.status === "completed",
971
+ job_id: created.job_id,
972
+ status: statusData.status,
973
+ outputs: statusData.outputs || [],
974
+ compute_seconds: statusData.compute_seconds,
975
+ egress_bytes: statusData.egress_bytes
976
+ };
977
+ if (statusData.error) result.error = statusData.error;
978
+ const workspace = getWorkspacePath(api);
979
+ for (const output of result.outputs) {
980
+ if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
981
+ try {
982
+ const imgRes = await fetch(output.url);
983
+ if (imgRes.ok) {
984
+ const buf = await imgRes.arrayBuffer();
985
+ const base64 = Buffer.from(buf).toString("base64");
986
+ const ref = await writeArtifactBinary(workspace, "screenshots", `${created.job_id}-${output.name}`, base64);
987
+ output.saved = ref.path;
988
+ output.sizeBytes = ref.sizeBytes;
989
+ }
990
+ } catch {
991
+ }
992
+ }
993
+ }
994
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
995
+ }
996
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
997
+ }
998
+ 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) }] };
999
+ }
1000
+ },
1001
+ { optional: true }
1002
+ );
756
1003
  }
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  // src/index.ts
2
2
  import { Type } from "@sinclair/typebox";
3
- import { writeFile, mkdir } from "fs/promises";
3
+ import { writeFile, mkdir, readFile, stat, rm } from "fs/promises";
4
4
  import { join } from "path";
5
+ import { execFile as execFileCb } from "child_process";
6
+ import { promisify } from "util";
7
+ var execFile = promisify(execFileCb);
5
8
  var INLINE_CAP = 50 * 1024;
6
9
  function getCfg(api) {
7
10
  const cfg = api?.config ?? {};
@@ -729,6 +732,250 @@ function register(api) {
729
732
  },
730
733
  { optional: true }
731
734
  );
735
+ api.registerTool(
736
+ {
737
+ name: "riddle_preview",
738
+ description: "Deploy a local build directory as an ephemeral preview site. Tars the directory, uploads to Riddle, and returns a live URL at preview.riddledc.com that can be screenshotted with other riddle_* tools. Previews auto-expire after 24 hours.",
739
+ parameters: Type.Object({
740
+ directory: Type.String({ description: "Absolute path to the build output directory (e.g. /path/to/build or /path/to/dist)" }),
741
+ framework: Type.Optional(Type.String({ description: "Framework hint: 'spa' (default) or 'static'" }))
742
+ }),
743
+ async execute(_id, params) {
744
+ const { apiKey, baseUrl } = getCfg(api);
745
+ if (!apiKey) {
746
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
747
+ }
748
+ assertAllowedBaseUrl(baseUrl);
749
+ const dir = params.directory;
750
+ if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
751
+ try {
752
+ const st = await stat(dir);
753
+ if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
754
+ } catch (e) {
755
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
756
+ }
757
+ const endpoint = baseUrl.replace(/\/$/, "");
758
+ const createRes = await fetch(`${endpoint}/v1/preview`, {
759
+ method: "POST",
760
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
761
+ body: JSON.stringify({ framework: params.framework || "spa" })
762
+ });
763
+ if (!createRes.ok) {
764
+ const err = await createRes.text();
765
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
766
+ }
767
+ const created = await createRes.json();
768
+ const tarball = `/tmp/riddle-preview-${created.id}.tar.gz`;
769
+ try {
770
+ await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 6e4 });
771
+ const tarData = await readFile(tarball);
772
+ const uploadRes = await fetch(created.upload_url, {
773
+ method: "PUT",
774
+ headers: { "Content-Type": "application/gzip" },
775
+ body: tarData
776
+ });
777
+ if (!uploadRes.ok) {
778
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
779
+ }
780
+ } finally {
781
+ try {
782
+ await rm(tarball, { force: true });
783
+ } catch {
784
+ }
785
+ }
786
+ const publishRes = await fetch(`${endpoint}/v1/preview/${created.id}/publish`, {
787
+ method: "POST",
788
+ headers: { Authorization: `Bearer ${apiKey}` }
789
+ });
790
+ if (!publishRes.ok) {
791
+ const err = await publishRes.text();
792
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` }, null, 2) }] };
793
+ }
794
+ const published = await publishRes.json();
795
+ return {
796
+ content: [{
797
+ type: "text",
798
+ text: JSON.stringify({
799
+ ok: true,
800
+ id: published.id,
801
+ preview_url: published.preview_url,
802
+ file_count: published.file_count,
803
+ total_bytes: published.total_bytes,
804
+ expires_at: created.expires_at
805
+ }, null, 2)
806
+ }]
807
+ };
808
+ }
809
+ },
810
+ { optional: true }
811
+ );
812
+ api.registerTool(
813
+ {
814
+ name: "riddle_preview_delete",
815
+ description: "Delete an ephemeral preview site created by riddle_preview. Removes all files and frees the preview ID immediately instead of waiting for auto-expiry.",
816
+ parameters: Type.Object({
817
+ id: Type.String({ description: "Preview ID (e.g. pv_a1b2c3d4)" })
818
+ }),
819
+ async execute(_id, params) {
820
+ const { apiKey, baseUrl } = getCfg(api);
821
+ if (!apiKey) {
822
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
823
+ }
824
+ assertAllowedBaseUrl(baseUrl);
825
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/preview/${params.id}`, {
826
+ method: "DELETE",
827
+ headers: { Authorization: `Bearer ${apiKey}` }
828
+ });
829
+ if (!res.ok) {
830
+ const err = await res.text();
831
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Delete failed: HTTP ${res.status} ${err}` }, null, 2) }] };
832
+ }
833
+ const data = await res.json();
834
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted: true, files_removed: data.files_removed }, null, 2) }] };
835
+ }
836
+ },
837
+ { optional: true }
838
+ );
839
+ api.registerTool(
840
+ {
841
+ name: "riddle_server_preview",
842
+ 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).",
843
+ parameters: Type.Object({
844
+ directory: Type.String({ description: "Absolute path to the project/build directory to deploy into the container" }),
845
+ image: Type.String({ description: "Docker image to run (e.g. 'node:20-slim', 'python:3.12-slim')" }),
846
+ command: Type.String({ description: "Command to start the server inside the container (e.g. 'npm start', 'python manage.py runserver 0.0.0.0:3000')" }),
847
+ port: Type.Number({ description: "Port the server listens on inside the container" }),
848
+ path: Type.Optional(Type.String({ description: "URL path to screenshot (default: '/')" })),
849
+ env: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Non-sensitive environment variables" })),
850
+ sensitive_env: Type.Optional(Type.Record(Type.String(), Type.String(), { description: "Sensitive environment variables (API keys, DB passwords). Stored securely and deleted after use." })),
851
+ timeout: Type.Optional(Type.Number({ description: "Max execution time in seconds (default: 120, max: 600)" })),
852
+ readiness_path: Type.Optional(Type.String({ description: "Path to poll for readiness (default: same as path)" })),
853
+ 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")')` }))
855
+ }),
856
+ async execute(_id, params) {
857
+ const { apiKey, baseUrl } = getCfg(api);
858
+ if (!apiKey) {
859
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing Riddle API key." }, null, 2) }] };
860
+ }
861
+ assertAllowedBaseUrl(baseUrl);
862
+ const dir = params.directory;
863
+ if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
864
+ try {
865
+ const st = await stat(dir);
866
+ if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
867
+ } catch (e) {
868
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Cannot access directory: ${e.message}` }, null, 2) }] };
869
+ }
870
+ const endpoint = baseUrl.replace(/\/$/, "");
871
+ let envRef = null;
872
+ if (params.sensitive_env && Object.keys(params.sensitive_env).length > 0) {
873
+ const envRes = await fetch(`${endpoint}/v1/server-preview/env`, {
874
+ method: "POST",
875
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
876
+ body: JSON.stringify({ env: params.sensitive_env })
877
+ });
878
+ if (!envRes.ok) {
879
+ const err = await envRes.text();
880
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` }, null, 2) }] };
881
+ }
882
+ const envData = await envRes.json();
883
+ envRef = envData.env_ref;
884
+ }
885
+ const createBody = {
886
+ image: params.image,
887
+ command: params.command,
888
+ port: params.port
889
+ };
890
+ if (params.path) createBody.path = params.path;
891
+ if (params.env) createBody.env = params.env;
892
+ if (envRef) createBody.env_ref = envRef;
893
+ if (params.timeout) createBody.timeout = params.timeout;
894
+ if (params.readiness_path) createBody.readiness_path = params.readiness_path;
895
+ if (params.readiness_timeout) createBody.readiness_timeout = params.readiness_timeout;
896
+ if (params.script) createBody.script = params.script;
897
+ const createRes = await fetch(`${endpoint}/v1/server-preview`, {
898
+ method: "POST",
899
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
900
+ body: JSON.stringify(createBody)
901
+ });
902
+ if (!createRes.ok) {
903
+ const err = await createRes.text();
904
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` }, null, 2) }] };
905
+ }
906
+ const created = await createRes.json();
907
+ const tarball = `/tmp/riddle-sp-${created.job_id}.tar.gz`;
908
+ try {
909
+ await execFile("tar", ["czf", tarball, "-C", dir, "."], { timeout: 12e4 });
910
+ const tarData = await readFile(tarball);
911
+ const uploadRes = await fetch(created.upload_url, {
912
+ method: "PUT",
913
+ headers: { "Content-Type": "application/gzip" },
914
+ body: tarData
915
+ });
916
+ if (!uploadRes.ok) {
917
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` }, null, 2) }] };
918
+ }
919
+ } finally {
920
+ try {
921
+ await rm(tarball, { force: true });
922
+ } catch {
923
+ }
924
+ }
925
+ const startRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
926
+ method: "POST",
927
+ headers: { Authorization: `Bearer ${apiKey}` }
928
+ });
929
+ if (!startRes.ok) {
930
+ const err = await startRes.text();
931
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` }, null, 2) }] };
932
+ }
933
+ const timeoutMs = ((params.timeout || 120) + 60) * 1e3;
934
+ const pollStart = Date.now();
935
+ const POLL_INTERVAL = 3e3;
936
+ while (Date.now() - pollStart < timeoutMs) {
937
+ const statusRes = await fetch(`${endpoint}/v1/server-preview/${created.job_id}`, {
938
+ headers: { Authorization: `Bearer ${apiKey}` }
939
+ });
940
+ if (!statusRes.ok) {
941
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, job_id: created.job_id, error: `Poll failed: HTTP ${statusRes.status}` }, null, 2) }] };
942
+ }
943
+ const statusData = await statusRes.json();
944
+ if (statusData.status === "completed" || statusData.status === "failed") {
945
+ const result = {
946
+ ok: statusData.status === "completed",
947
+ job_id: created.job_id,
948
+ status: statusData.status,
949
+ outputs: statusData.outputs || [],
950
+ compute_seconds: statusData.compute_seconds,
951
+ egress_bytes: statusData.egress_bytes
952
+ };
953
+ if (statusData.error) result.error = statusData.error;
954
+ const workspace = getWorkspacePath(api);
955
+ for (const output of result.outputs) {
956
+ if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
957
+ try {
958
+ const imgRes = await fetch(output.url);
959
+ if (imgRes.ok) {
960
+ const buf = await imgRes.arrayBuffer();
961
+ const base64 = Buffer.from(buf).toString("base64");
962
+ const ref = await writeArtifactBinary(workspace, "screenshots", `${created.job_id}-${output.name}`, base64);
963
+ output.saved = ref.path;
964
+ output.sizeBytes = ref.sizeBytes;
965
+ }
966
+ } catch {
967
+ }
968
+ }
969
+ }
970
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
971
+ }
972
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
973
+ }
974
+ 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) }] };
975
+ }
976
+ },
977
+ { optional: true }
978
+ );
732
979
  }
733
980
  export {
734
981
  register as default
@@ -2,14 +2,15 @@
2
2
  "id": "openclaw-riddledc",
3
3
  "name": "Riddle",
4
4
  "description": "Riddle (riddledc.com) hosted browser API tools for OpenClaw agents.",
5
- "version": "0.5.6",
6
- "notes": "0.4.0: Added riddle_scrape, riddle_map, riddle_crawl convenience tools. Updated riddle_steps and riddle_script descriptions with data extraction capabilities.",
5
+ "version": "0.7.0",
6
+ "notes": "0.7.0: Added riddle_server_preview tool for running server-side apps in isolated Docker containers.",
7
7
  "type": "plugin",
8
8
  "bundledSkills": [],
9
9
  "capabilities": {
10
10
  "network": {
11
11
  "egress": [
12
- "api.riddledc.com"
12
+ "api.riddledc.com",
13
+ "preview.riddledc.com"
13
14
  ],
14
15
  "enforced": true,
15
16
  "note": "Hardcoded allowlist in assertAllowedBaseUrl() - cannot be overridden by config"
@@ -37,7 +38,10 @@
37
38
  "riddle_scrape",
38
39
  "riddle_map",
39
40
  "riddle_crawl",
40
- "riddle_visual_diff"
41
+ "riddle_visual_diff",
42
+ "riddle_preview",
43
+ "riddle_preview_delete",
44
+ "riddle_server_preview"
41
45
  ],
42
46
  "invokes": [],
43
47
  "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.5.6",
3
+ "version": "0.7.0",
4
4
  "description": "OpenClaw integration package for RiddleDC (no secrets).",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",