@riddledc/openclaw-riddledc 0.6.0 → 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
- 876e2b78ca5f38ca4da27097761bcdb99882a5724f025d5befd5ba78710c5832 dist/index.cjs
1
+ 921867120c7149d34d965a56ab0b727b2ecd2f0abf4ea9980171aefd4e11c39f dist/index.cjs
2
2
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.cts
3
3
  94ce04f0e2d84bf64dd68f0500dfdd2f951287a3deccec87f197261961927f6f dist/index.d.ts
4
- c481cef6e2fd6aca8d7aef042bc7b8cb5fc12274afe9c50ba10947e398c566ce dist/index.js
4
+ 06f7ee46883239b2b9e1180a0f82d70b62f03fb2b73c032ff4ae7dafb65ab8e1 dist/index.js
package/dist/index.cjs CHANGED
@@ -860,4 +860,144 @@ function register(api) {
860
860
  },
861
861
  { optional: true }
862
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
+ );
863
1003
  }
package/dist/index.js CHANGED
@@ -836,6 +836,146 @@ function register(api) {
836
836
  },
837
837
  { optional: true }
838
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
+ );
839
979
  }
840
980
  export {
841
981
  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.6.0",
6
- "notes": "0.6.0: Added riddle_preview and riddle_preview_delete tools for ephemeral preview hosting at preview.riddledc.com.",
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": {
@@ -40,7 +40,8 @@
40
40
  "riddle_crawl",
41
41
  "riddle_visual_diff",
42
42
  "riddle_preview",
43
- "riddle_preview_delete"
43
+ "riddle_preview_delete",
44
+ "riddle_server_preview"
44
45
  ],
45
46
  "invokes": [],
46
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.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "OpenClaw integration package for RiddleDC (no secrets).",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",