clawmoney 0.17.16 → 0.17.18

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.
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { existsSync, statSync } from "node:fs";
2
3
  import { isProcessed, markProcessed } from "./dedup.js";
3
4
  import { replaceLocalPaths, uploadFile } from "./media.js";
4
5
  import { logger } from "./logger.js";
@@ -20,13 +21,108 @@ function buildPrompt(call, config) {
20
21
  `Input: ${JSON.stringify(call.input, null, 2)}`,
21
22
  "",
22
23
  ];
23
- // Category-specific instructions
24
+ // Category-specific instructions. Skill names differ per CLI runtime —
25
+ // openclaw exposes nano-banana-pro, codex exposes built-in imagegen,
26
+ // claude/gemini surface image generation via their own tools. We pick
27
+ // the right hint so the model goes straight to the real tool instead
28
+ // of trying to "be helpful" by writing PIL code or hallucinating a path.
24
29
  if (call.category?.startsWith("generation/image")) {
25
- lines.push("IMPORTANT: Use the nano-banana-pro skill (or any image generation tool) to generate a real PNG/JPG image.", "Do NOT write SVG, HTML, or any code to fake an image.", "If no image generation tool is available, return {\"success\": false, \"error\": \"No image generation tool available\"}.", "Save the generated image and include the file path in your output.");
30
+ const cli = config.provider.cli_command;
31
+ const skillHint = cli === "codex"
32
+ ? "Use the imagegen skill (the built-in image_gen tool — do NOT use shell/python to draw an image)"
33
+ : cli === "openclaw"
34
+ ? "Use the nano-banana-pro skill"
35
+ : "Use your native image generation tool";
36
+ lines.push(`IMPORTANT: ${skillHint} to generate a real PNG/JPG image.`, "Do NOT write SVG, HTML, Python (PIL), or any code to fake an image.", "If no image generation tool is available in this environment, return {\"success\": false, \"error\": \"No image generation tool available\"}.", "Save the generated image and include the absolute file path in your JSON output as \"image_path\".");
26
37
  }
27
38
  lines.push("Execute this task and return the result as JSON.", "If you generate any files (images, videos, etc.), save them and include their file paths in the output.", "Return ONLY the JSON result, no other text.");
28
39
  return lines.join("\n");
29
40
  }
41
+ // ── Image-output validation ──
42
+ //
43
+ // Codex / Gemini / generic-claude paths don't have a structured response
44
+ // schema like OpenClaw, so providers using those CLIs can return
45
+ // `{"image_path":"/tmp/whatever.png"}` even when the model never actually
46
+ // generated an image — either by writing Python that fakes a bitmap, or
47
+ // (worse) by hallucinating the path entirely without spawning any tool.
48
+ //
49
+ // `extractClaimedPaths` walks an output object and collects every local
50
+ // file path the provider is claiming to have produced. Used so we can
51
+ // physically verify each file exists before we deliver to the buyer.
52
+ const FILE_PATH_KEYS = ["image_path", "video_path", "audio_path", "file_path", "primary_file"];
53
+ const IMAGE_EXT_RE = /\.(png|jpg|jpeg|webp|gif)$/i;
54
+ const CDN_URL_RE = /^https?:\/\//i;
55
+ function extractClaimedPaths(output) {
56
+ const out = [];
57
+ const visit = (node) => {
58
+ if (!node || typeof node !== "object")
59
+ return;
60
+ const obj = node;
61
+ for (const key of FILE_PATH_KEYS) {
62
+ const v = obj[key];
63
+ if (typeof v === "string" && v.startsWith("/"))
64
+ out.push(v);
65
+ }
66
+ const files = obj.files;
67
+ if (Array.isArray(files)) {
68
+ for (const f of files) {
69
+ if (typeof f === "string" && f.startsWith("/"))
70
+ out.push(f);
71
+ }
72
+ }
73
+ // Recurse into nested .result so providers wrapping output in
74
+ // { result: { image_path: ... } } don't bypass the check.
75
+ if (obj.result && typeof obj.result === "object")
76
+ visit(obj.result);
77
+ };
78
+ visit(output);
79
+ return Array.from(new Set(out));
80
+ }
81
+ /**
82
+ * After `replaceLocalPaths` has run, every successful image upload is
83
+ * promoted from `image_path` (local) to `image_url` (CDN). If the upload
84
+ * silently failed (file didn't exist), `image_path` stays in the output
85
+ * and we deliver a broken result — UNLESS we catch it here.
86
+ *
87
+ * Returns null when the delivery is OK for a `generation/image` call,
88
+ * otherwise an error string explaining why it isn't.
89
+ */
90
+ function validateImageDelivery(output) {
91
+ // CDN URL anywhere in the output? Then a real file got uploaded.
92
+ const top = output;
93
+ if (typeof top.image_url === "string" && CDN_URL_RE.test(top.image_url))
94
+ return null;
95
+ if (Array.isArray(top.files)) {
96
+ const hasCdnImage = top.files.some((f) => typeof f === "string" && CDN_URL_RE.test(f) && IMAGE_EXT_RE.test(f));
97
+ if (hasCdnImage)
98
+ return null;
99
+ }
100
+ // No real image URL delivered. Why?
101
+ const claimedPaths = extractClaimedPaths(output);
102
+ if (claimedPaths.length === 0) {
103
+ return "Provider returned no image (model likely lacks an image-generation tool).";
104
+ }
105
+ const missing = claimedPaths.filter((p) => !existsSync(p));
106
+ if (missing.length > 0) {
107
+ return `Provider claimed image at ${missing[0]} but the file does not exist (hallucinated path).`;
108
+ }
109
+ // Files exist but none are images, or upload failed for a different reason.
110
+ const hasImagePath = claimedPaths.some((p) => IMAGE_EXT_RE.test(p));
111
+ if (!hasImagePath) {
112
+ return "Provider produced non-image files only for a generation/image task.";
113
+ }
114
+ // File exists and is .png/.jpg/.webp/.gif but upload still failed.
115
+ // Most likely cause: file is empty or permission denied. Surface bytes.
116
+ try {
117
+ const stats = claimedPaths
118
+ .filter((p) => IMAGE_EXT_RE.test(p))
119
+ .map((p) => `${p} (${statSync(p).size}B)`);
120
+ return `Image file exists but R2 upload failed: ${stats.join(", ")}`;
121
+ }
122
+ catch {
123
+ return "Image upload to R2 failed.";
124
+ }
125
+ }
30
126
  // ── CLI execution (openclaw agent / claude -p) ──
31
127
  function runCli(command, prompt, timeoutMs, orderId) {
32
128
  return new Promise((resolve) => {
@@ -37,8 +133,15 @@ function runCli(command, prompt, timeoutMs, orderId) {
37
133
  args = ["agent", "--message", prompt, "--session-id", orderId || "hub-task", "--json"];
38
134
  }
39
135
  else if (command === "codex") {
40
- // codex exec "..." --json --skip-git-repo-check
41
- args = ["exec", prompt, "--json", "--skip-git-repo-check"];
136
+ // -s workspace-write is required so the built-in image_gen tool can
137
+ // write files under $CODEX_HOME/generated_images and so the model
138
+ // can mv/cp the result to the user-named path. With the default
139
+ // read-only sandbox, image_gen silently degrades — the model falls
140
+ // back to either drawing the image with Python in /tmp (slow, ugly)
141
+ // or hallucinating an image_path with no file behind it (worst
142
+ // case, since the buyer pays for a nonexistent file). Verified on
143
+ // codex 0.128.0 + gpt-5.5 xhigh, 2026-05-12.
144
+ args = ["exec", "-s", "workspace-write", prompt, "--json", "--skip-git-repo-check"];
42
145
  }
43
146
  else if (command === "gemini") {
44
147
  // gemini -p "..." -o json --yolo
@@ -501,6 +604,25 @@ export class Executor {
501
604
  // Upload local files via generic path replacement
502
605
  output = await replaceLocalPaths(output, this.config);
503
606
  }
607
+ // Image-output validation. The OpenClaw branch above does this inline
608
+ // (using its richer parseOpenClawResponse files list); for everything
609
+ // else we re-run the check on the post-upload output so the buyer
610
+ // never gets `{image_path: "/tmp/elon.png"}` when the file doesn't
611
+ // exist and uploadFile silently returned null. Skip the check for the
612
+ // OpenClaw branch — it already validated and may have set _meta etc.
613
+ if (command !== "openclaw" &&
614
+ call.category?.startsWith("generation/image")) {
615
+ const reason = validateImageDelivery(output);
616
+ if (reason) {
617
+ logger.error(`Image validation failed for order=${call.order_id} (${command}): ${reason}`);
618
+ this.send({
619
+ event: "deliver",
620
+ order_id: call.order_id,
621
+ error: reason,
622
+ });
623
+ return;
624
+ }
625
+ }
504
626
  const sent = this.send({
505
627
  event: "deliver",
506
628
  order_id: call.order_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.17.16",
3
+ "version": "0.17.18",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {