clawmoney 0.17.19 → 0.17.21

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,5 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
- import { existsSync, statSync } from "node:fs";
2
+ import { existsSync, statSync, readdirSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
3
5
  import { isProcessed, markProcessed } from "./dedup.js";
4
6
  import { replaceLocalPaths, uploadFile } from "./media.js";
5
7
  import { logger } from "./logger.js";
@@ -123,8 +125,83 @@ function validateImageDelivery(output) {
123
125
  return "Image upload to R2 failed.";
124
126
  }
125
127
  }
128
+ // ── Codex generated-image directory watcher ──
129
+ //
130
+ // codex saves all image_gen output under $CODEX_HOME/generated_images/
131
+ // in a per-thread subdir. Once the .png lands on disk and reaches a stable
132
+ // size, the deliverable is ready — everything codex does after (cp,
133
+ // verification, final reasoning) is overhead the buyer doesn't see. We
134
+ // snapshot the directory before spawning codex, then poll for any new
135
+ // thread dir that contains a stable image file. When we find one we
136
+ // fabricate a synthetic agent_message into the stdout stream so the
137
+ // downstream parser picks up the path naturally, then SIGTERM codex.
138
+ const CODEX_HOME = process.env.CODEX_HOME || join(homedir(), ".codex");
139
+ const CODEX_GEN_DIR = join(CODEX_HOME, "generated_images");
140
+ const FILE_STABLE_MS = 1500;
141
+ const POLL_INTERVAL_MS = 400;
142
+ function snapshotCodexThreads() {
143
+ try {
144
+ return new Set(readdirSync(CODEX_GEN_DIR));
145
+ }
146
+ catch {
147
+ // Directory may not exist yet on a fresh install — that's fine,
148
+ // codex will create it the first time it generates anything.
149
+ return new Set();
150
+ }
151
+ }
152
+ function findNewCodexImage(baseline) {
153
+ let entries;
154
+ try {
155
+ entries = readdirSync(CODEX_GEN_DIR);
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ for (const entry of entries) {
161
+ if (baseline.has(entry))
162
+ continue;
163
+ // A new per-thread directory has appeared. Look inside for the
164
+ // first image file the model dropped there.
165
+ const dir = join(CODEX_GEN_DIR, entry);
166
+ let files;
167
+ try {
168
+ files = readdirSync(dir);
169
+ }
170
+ catch {
171
+ continue;
172
+ }
173
+ for (const f of files) {
174
+ if (IMAGE_EXT_RE.test(f))
175
+ return join(dir, f);
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+ async function waitForStableSize(filePath) {
181
+ let lastSize = -1;
182
+ let stableSince = 0;
183
+ // Cap at 20s in case the file truly never settles — let the rest of
184
+ // the flow handle that as a delivery failure rather than hanging.
185
+ const deadline = Date.now() + 20_000;
186
+ while (Date.now() < deadline) {
187
+ try {
188
+ const size = statSync(filePath).size;
189
+ if (size !== lastSize) {
190
+ lastSize = size;
191
+ stableSince = Date.now();
192
+ }
193
+ else if (size > 0 && Date.now() - stableSince >= FILE_STABLE_MS) {
194
+ return;
195
+ }
196
+ }
197
+ catch {
198
+ // file may have moved between stat() calls — try again
199
+ }
200
+ await new Promise((r) => setTimeout(r, 200));
201
+ }
202
+ }
126
203
  // ── CLI execution (openclaw agent / claude -p) ──
127
- function runCli(command, prompt, timeoutMs, orderId) {
204
+ function runCli(command, prompt, timeoutMs, orderId, watchForImage) {
128
205
  return new Promise((resolve) => {
129
206
  // Build args based on command
130
207
  let args;
@@ -166,6 +243,9 @@ function runCli(command, prompt, timeoutMs, orderId) {
166
243
  // claude -p "..." --output-format json --dangerously-skip-permissions
167
244
  args = ["-p", prompt, "--output-format", "json", "--dangerously-skip-permissions"];
168
245
  }
246
+ // For codex generation/image calls, snapshot the generated-images dir
247
+ // BEFORE spawning so we can identify the new thread dir later.
248
+ const dirBaseline = watchForImage && command === "codex" ? snapshotCodexThreads() : null;
169
249
  const child = spawn(command, args, {
170
250
  stdio: ["ignore", "pipe", "pipe"],
171
251
  timeout: timeoutMs,
@@ -173,6 +253,56 @@ function runCli(command, prompt, timeoutMs, orderId) {
173
253
  });
174
254
  let stdout = "";
175
255
  let stderr = "";
256
+ let earlyResolved = false;
257
+ let pollHandle = null;
258
+ const finishWithEarlyImage = async (imagePath) => {
259
+ if (earlyResolved)
260
+ return;
261
+ // Wait until the file size stops growing before we tell the rest
262
+ // of the flow it's ready — otherwise we might upload a partial PNG
263
+ // and confuse R2 / the buyer's image viewer.
264
+ await waitForStableSize(imagePath);
265
+ if (earlyResolved)
266
+ return;
267
+ earlyResolved = true;
268
+ if (pollHandle) {
269
+ clearInterval(pollHandle);
270
+ pollHandle = null;
271
+ }
272
+ try {
273
+ child.kill("SIGTERM");
274
+ }
275
+ catch {
276
+ // pid may already be gone
277
+ }
278
+ // Inject a synthetic agent_message into the stream so the codex
279
+ // JSONL parser downstream picks up the image_path naturally,
280
+ // without us needing to plumb a separate "early path" through
281
+ // the rest of executeTask.
282
+ const synthetic = JSON.stringify({
283
+ type: "item.completed",
284
+ item: {
285
+ type: "agent_message",
286
+ text: JSON.stringify({ image_path: imagePath }),
287
+ },
288
+ });
289
+ stdout += "\n" + synthetic + "\n";
290
+ resolve({ stdout, stderr, exitCode: 0 });
291
+ };
292
+ if (dirBaseline) {
293
+ pollHandle = setInterval(() => {
294
+ if (earlyResolved)
295
+ return;
296
+ const newImage = findNewCodexImage(dirBaseline);
297
+ if (newImage) {
298
+ if (pollHandle) {
299
+ clearInterval(pollHandle);
300
+ pollHandle = null;
301
+ }
302
+ void finishWithEarlyImage(newImage);
303
+ }
304
+ }, POLL_INTERVAL_MS);
305
+ }
176
306
  child.stdout.on("data", (chunk) => {
177
307
  stdout += chunk.toString();
178
308
  });
@@ -180,9 +310,21 @@ function runCli(command, prompt, timeoutMs, orderId) {
180
310
  stderr += chunk.toString();
181
311
  });
182
312
  child.on("close", (code) => {
313
+ if (earlyResolved)
314
+ return;
315
+ if (pollHandle) {
316
+ clearInterval(pollHandle);
317
+ pollHandle = null;
318
+ }
183
319
  resolve({ stdout, stderr, exitCode: code });
184
320
  });
185
321
  child.on("error", (err) => {
322
+ if (earlyResolved)
323
+ return;
324
+ if (pollHandle) {
325
+ clearInterval(pollHandle);
326
+ pollHandle = null;
327
+ }
186
328
  stderr += err.message;
187
329
  resolve({ stdout, stderr, exitCode: null });
188
330
  });
@@ -508,7 +650,11 @@ export class Executor {
508
650
  const timeoutS = Math.max(call.timeout - TIMEOUT_BUFFER_S, 30);
509
651
  const command = this.config.provider.cli_command;
510
652
  logger.info(`Executing: ${command} for skill="${call.skill}" order=${call.order_id} (timeout=${timeoutS}s)`);
511
- const { stdout, stderr, exitCode } = await runCli(command, prompt, timeoutS * 1000, call.order_id);
653
+ const { stdout, stderr, exitCode } = await runCli(command, prompt, timeoutS * 1000, call.order_id,
654
+ // Watch the codex image dir for generation/image so we can
655
+ // ship the moment the file lands, skipping codex's final
656
+ // reasoning tail (~60–90s of pure overhead).
657
+ call.category?.startsWith("generation/image"));
512
658
  if (exitCode !== 0) {
513
659
  const errMsg = stderr.trim() || `CLI exited with code ${exitCode}`;
514
660
  logger.error(`CLI failed (code=${exitCode}):`, errMsg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.17.19",
3
+ "version": "0.17.21",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {