clawmoney 0.17.20 → 0.17.22

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.
@@ -120,10 +120,10 @@ export async function setupCommand() {
120
120
  wallet_address: loginData.agent?.wallet_address ?? undefined,
121
121
  });
122
122
  // Always hit /me/wallet/balance to (a) let the backend reconcile
123
- // legacy awal addresses to the canonical CDP address (the /login/verify
124
- // response may still carry the stale awal value from DB), and (b) cache
125
- // the authoritative address back to config. The returned `address` is
126
- // the CDP account address after _ensure_agent_wallet has run.
123
+ // any pre-CDP wallet address still on the agent row to the canonical
124
+ // Coinbase server wallet, and (b) cache the authoritative address
125
+ // back to config. The returned `address` is the CDP server-wallet
126
+ // account address after _ensure_agent_wallet has run.
127
127
  let walletAddress = '';
128
128
  const walletSpinner = ora('Reconciling CDP wallet...').start();
129
129
  try {
@@ -134,7 +134,7 @@ export async function setupCommand() {
134
134
  const prior = loginData.agent?.wallet_address ?? '';
135
135
  if (prior && prior.toLowerCase() !== walletAddress.toLowerCase()) {
136
136
  walletSpinner.succeed(`Wallet migrated: ${prior} → ${walletAddress}`);
137
- console.log(chalk.yellow(` (Your old awal address ${prior} is no longer`));
137
+ console.log(chalk.yellow(` (Your previous address ${prior} is no longer`));
138
138
  console.log(chalk.yellow(` used by this CLI. Transfer any remaining funds manually.)`));
139
139
  }
140
140
  else {
@@ -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,
@@ -174,36 +254,57 @@ function runCli(command, prompt, timeoutMs, orderId) {
174
254
  let stdout = "";
175
255
  let stderr = "";
176
256
  let earlyResolved = false;
177
- const tryEarlyExit = () => {
257
+ let pollHandle = null;
258
+ const finishWithEarlyImage = async (imagePath) => {
178
259
  if (earlyResolved)
179
260
  return;
180
- // Only attempt early exit for codex others either don't stream
181
- // events with this shape, or already finish promptly after their
182
- // last output. Codex on xhigh/high reasoning_effort wastes 60–90s
183
- // doing "final reflection" reasoning even after the agent_message
184
- // is already emitted with the image_path. Once we see that
185
- // message in the JSONL stream the deliverable is fully on disk
186
- // and we can ship the order — kill the child to avoid burning
187
- // buyer wall-time on reasoning we won't read.
188
- if (command !== "codex")
189
- return;
190
- if (!hasCodexDeliverable(stdout))
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)
191
266
  return;
192
267
  earlyResolved = true;
193
- // SIGTERM is enough; codex closes its writer and we'll get a
194
- // 'close' event shortly. Don't await — let the resolve below
195
- // return the snapshot we already have.
268
+ if (pollHandle) {
269
+ clearInterval(pollHandle);
270
+ pollHandle = null;
271
+ }
196
272
  try {
197
273
  child.kill("SIGTERM");
198
274
  }
199
275
  catch {
200
276
  // pid may already be gone
201
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";
202
290
  resolve({ stdout, stderr, exitCode: 0 });
203
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
+ }
204
306
  child.stdout.on("data", (chunk) => {
205
307
  stdout += chunk.toString();
206
- tryEarlyExit();
207
308
  });
208
309
  child.stderr.on("data", (chunk) => {
209
310
  stderr += chunk.toString();
@@ -211,62 +312,24 @@ function runCli(command, prompt, timeoutMs, orderId) {
211
312
  child.on("close", (code) => {
212
313
  if (earlyResolved)
213
314
  return;
315
+ if (pollHandle) {
316
+ clearInterval(pollHandle);
317
+ pollHandle = null;
318
+ }
214
319
  resolve({ stdout, stderr, exitCode: code });
215
320
  });
216
321
  child.on("error", (err) => {
217
322
  if (earlyResolved)
218
323
  return;
324
+ if (pollHandle) {
325
+ clearInterval(pollHandle);
326
+ pollHandle = null;
327
+ }
219
328
  stderr += err.message;
220
329
  resolve({ stdout, stderr, exitCode: null });
221
330
  });
222
331
  });
223
332
  }
224
- /**
225
- * Detect whether codex's JSONL stream already contains a final
226
- * agent_message event whose text parses to JSON with an `image_path`.
227
- * That's the signal that the image is on disk and the agent has
228
- * acknowledged it — everything codex does after this point is its
229
- * own final reasoning loop, which the buyer never sees.
230
- *
231
- * Scans newest-to-oldest so we exit as soon as we find the marker.
232
- */
233
- function hasCodexDeliverable(streamSoFar) {
234
- const lines = streamSoFar.split("\n");
235
- for (let i = lines.length - 1; i >= 0; i--) {
236
- const line = lines[i].trim();
237
- if (!line.startsWith("{"))
238
- continue;
239
- let event;
240
- try {
241
- event = JSON.parse(line);
242
- }
243
- catch {
244
- continue;
245
- }
246
- if (event.type !== "item.completed")
247
- continue;
248
- const item = event.item;
249
- if (item?.type !== "agent_message")
250
- continue;
251
- const text = item.text;
252
- if (typeof text !== "string")
253
- continue;
254
- // Cheap pre-check before JSON.parse: must mention image_path.
255
- if (!text.includes("image_path"))
256
- continue;
257
- try {
258
- const parsed = JSON.parse(text);
259
- const path = parsed.image_path;
260
- if (typeof path === "string" && path.length > 0)
261
- return true;
262
- }
263
- catch {
264
- // text wasn't JSON; agent might be talking about image_path
265
- // in prose. Keep looking — don't early-exit on prose.
266
- }
267
- }
268
- return false;
269
- }
270
333
  // ── JSON parser ──
271
334
  function parseJsonOutput(raw) {
272
335
  try {
@@ -587,7 +650,11 @@ export class Executor {
587
650
  const timeoutS = Math.max(call.timeout - TIMEOUT_BUFFER_S, 30);
588
651
  const command = this.config.provider.cli_command;
589
652
  logger.info(`Executing: ${command} for skill="${call.skill}" order=${call.order_id} (timeout=${timeoutS}s)`);
590
- 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"));
591
658
  if (exitCode !== 0) {
592
659
  const errMsg = stderr.trim() || `CLI exited with code ${exitCode}`;
593
660
  logger.error(`CLI failed (code=${exitCode}):`, errMsg);
package/dist/index.js CHANGED
@@ -160,7 +160,7 @@ promote
160
160
  }
161
161
  });
162
162
  // wallet
163
- const wallet = program.command('wallet').description('Wallet commands (via awal)');
163
+ const wallet = program.command('wallet').description('Wallet commands (Coinbase server wallet)');
164
164
  wallet
165
165
  .command('status')
166
166
  .description('Show wallet authentication status')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.17.20",
3
+ "version": "0.17.22",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {