clawmoney 0.15.30 → 0.15.32

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.
@@ -195,7 +195,7 @@ export async function relaySetupCommand() {
195
195
  log.warn(`${cli}: no recommended models found — skipping`);
196
196
  continue;
197
197
  }
198
- log.step(`${chalk.bold(cli)}: ${recommended.length} models ${chalk.dim("— " + recommended.join(", "))}`);
198
+ log.success(`${chalk.bold(cli)}: ${recommended.length} models ${chalk.dim("— " + recommended.join(", "))}`);
199
199
  for (const model of recommended) {
200
200
  const p = API_PRICES[model];
201
201
  registrations.push({
@@ -285,47 +285,50 @@ export async function relaySetupCommand() {
285
285
  15: "~25%", 30: "~50%", 45: "~75%", 60: "~100%",
286
286
  };
287
287
  const earnPct = Math.round((1 - PLATFORM_FEE) * 100);
288
+ // Single batch POST — one round-trip, one DB session, no
289
+ // client-side fan-out. The earlier sequential loop paid 7× the
290
+ // TLS/bcrypt/CF overhead, and parallelizing with Promise.all
291
+ // tripped over client- or proxy-level concurrency limits on some
292
+ // machines. Batch endpoint is the right architecture.
288
293
  let succeeded = 0;
289
294
  let failed = 0;
290
295
  const failures = [];
291
296
  const regSpin = spinner();
292
297
  regSpin.start(`Registering ${registrations.length} providers...`);
293
- for (const r of registrations) {
294
- try {
295
- const body = {
296
- cli_type: r.cli,
297
- model: r.model,
298
- mode: "chat",
299
- concurrency,
300
- daily_limit_usd: dailyLimit,
301
- price_input_per_m: r.input,
302
- price_output_per_m: r.output,
303
- };
304
- const resp = await apiPost("/api/v1/relay/providers", body, config.api_key);
305
- if (resp.ok) {
306
- succeeded++;
307
- }
308
- else {
309
- const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
310
- ? resp.data.detail
311
- : resp.data;
312
- const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
313
- // Already-registered is a soft success — idempotent re-run.
314
- if (detail.includes("Already registered")) {
315
- succeeded++;
316
- }
317
- else {
318
- failed++;
319
- failures.push({ cli: r.cli, model: r.model, error: detail });
320
- }
321
- }
298
+ const batchBody = {
299
+ providers: registrations.map((r) => ({
300
+ cli_type: r.cli,
301
+ model: r.model,
302
+ mode: "chat",
303
+ concurrency,
304
+ daily_limit_usd: dailyLimit,
305
+ price_input_per_m: r.input,
306
+ price_output_per_m: r.output,
307
+ })),
308
+ };
309
+ try {
310
+ const resp = await apiPost("/api/v1/relay/providers/batch", batchBody, config.api_key);
311
+ if (!resp.ok) {
312
+ const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
313
+ ? resp.data.detail
314
+ : resp.data;
315
+ const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
316
+ regSpin.stop(chalk.red(`✗ Batch registration failed: ${detail}`));
317
+ cancel("Setup aborted");
318
+ process.exit(1);
322
319
  }
323
- catch (err) {
324
- const msg = err.message;
325
- failed++;
326
- failures.push({ cli: r.cli, model: r.model, error: msg });
320
+ // Per-row counts from the batch result.
321
+ succeeded = (resp.data.created?.length ?? 0) + (resp.data.skipped?.length ?? 0);
322
+ failed = resp.data.failed?.length ?? 0;
323
+ for (const f of resp.data.failed ?? []) {
324
+ failures.push({ cli: f.cli_type, model: f.model, error: f.error });
327
325
  }
328
326
  }
327
+ catch (err) {
328
+ regSpin.stop(chalk.red(`✗ Batch registration failed: ${err.message}`));
329
+ cancel("Setup aborted");
330
+ process.exit(1);
331
+ }
329
332
  if (failed === 0) {
330
333
  regSpin.stop(`${chalk.green(`✓ ${succeeded} providers registered`)} ` +
331
334
  chalk.dim(`(${limitLabel[dailyLimit] ?? `$${dailyLimit}`} quota share · you earn ~${earnPct}%)`));
@@ -362,11 +365,14 @@ export async function relaySetupCommand() {
362
365
  outro(chalk.yellow("Setup complete (daemon not started)"));
363
366
  return;
364
367
  }
365
- log.message("");
366
- log.message(chalk.dim("Useful follow-up commands:"));
367
- log.message(` ${chalk.cyan("clawmoney relay status")} # check daemon health + provider list`);
368
- log.message(` ${chalk.cyan("clawmoney relay credits")} # check earnings + payout balance`);
369
- log.message(` ${chalk.cyan("clawmoney relay stop")} # stop the daemon`);
368
+ // One multi-line log.message renders each line with a `│` prefix
369
+ // but without clack's inter-call gap — 3 bullets fit in 3 lines
370
+ // instead of 6.
371
+ log.message(chalk.dim("Next:") +
372
+ "\n" +
373
+ ` ${chalk.cyan("clawmoney relay status")} daemon health + providers\n` +
374
+ ` ${chalk.cyan("clawmoney relay credits")} earnings + balance\n` +
375
+ ` ${chalk.cyan("clawmoney relay stop")} stop daemon`);
370
376
  const cliLabel = uniqueClis.length === 1
371
377
  ? `${uniqueClis[0]} daemon running`
372
378
  : `daemon serving ${uniqueClis.join(" + ")}`;
@@ -12,6 +12,10 @@ export declare function relayStartCommand(options: {
12
12
  cli?: string;
13
13
  }): Promise<void>;
14
14
  export declare function relayStopCommand(): Promise<void>;
15
+ export declare function relayLogsCommand(options: {
16
+ follow?: boolean;
17
+ lines?: string;
18
+ }): Promise<void>;
15
19
  export declare function relayStatusCommand(): Promise<void>;
16
20
  export declare function relayModelsCommand(): Promise<void>;
17
21
  export declare function relayCreditsCommand(): Promise<void>;
@@ -192,6 +192,32 @@ export async function relayStopCommand() {
192
192
  await new Promise((resolve) => setTimeout(resolve, 500));
193
193
  removeRelayPid();
194
194
  }
195
+ // ── relay logs ──
196
+ export async function relayLogsCommand(options) {
197
+ const { existsSync } = await import("node:fs");
198
+ const { spawn } = await import("node:child_process");
199
+ if (!existsSync(LOG_FILE)) {
200
+ console.log(chalk.dim(`No log file yet at ${LOG_FILE}`));
201
+ console.log(chalk.dim("Start the daemon first: clawmoney relay start"));
202
+ return;
203
+ }
204
+ // Default: tail -f the last 50 lines. Use system `tail` rather than
205
+ // reimplementing it in Node — it's just a debug helper, not worth
206
+ // a pure-JS reinvention.
207
+ const nLines = options.lines ?? "50";
208
+ const args = ["-n", nLines];
209
+ if (options.follow !== false) {
210
+ args.push("-f");
211
+ }
212
+ args.push(LOG_FILE);
213
+ const child = spawn("tail", args, { stdio: "inherit" });
214
+ child.on("exit", (code) => {
215
+ process.exit(code ?? 0);
216
+ });
217
+ // Ctrl-C inside `tail -f` kills tail but returns us here; propagate
218
+ // the exit code so the wrapper looks transparent.
219
+ process.on("SIGINT", () => child.kill("SIGINT"));
220
+ }
195
221
  export async function relayStatusCommand() {
196
222
  const config = requireConfig();
197
223
  // Local process status
@@ -206,36 +232,78 @@ export async function relayStatusCommand() {
206
232
  else {
207
233
  console.log(chalk.dim(" Local process: not running"));
208
234
  }
209
- // Remote status
235
+ // Remote status. /api/v1/relay/providers/me returns a LIST of
236
+ // RelayProviderPublic (one row per registered model), so we can't
237
+ // treat the body as a single object. For multi-cli providers the
238
+ // list can easily be 7-10 rows — render them as a table with one
239
+ // line per row instead of 14 labeled lines for a single picked row.
210
240
  const spinner = ora("Fetching relay provider status...").start();
211
241
  try {
212
242
  const resp = await apiGet("/api/v1/relay/providers/me", config.api_key);
213
243
  if (!resp.ok) {
214
244
  if (resp.status === 404) {
215
245
  spinner.info("Not registered as relay provider yet.");
216
- console.log(chalk.dim(` Run "clawmoney relay register" to get started.`));
246
+ console.log(chalk.dim(` Run "clawmoney relay setup" to get started.`));
217
247
  return;
218
248
  }
219
- const detail = resp.data?.detail ?? resp.status;
249
+ const detail = resp.data?.detail ?? String(resp.status);
220
250
  spinner.fail(chalk.red(`Failed to fetch status: ${detail}`));
221
251
  process.exit(1);
222
252
  }
223
- const data = resp.data;
224
- const statusColor = data.status === "online" ? chalk.green : data.status === "offline" ? chalk.dim : chalk.yellow;
225
- spinner.succeed("Relay Provider Status");
253
+ // Normalize: backend currently returns a list, but guard against
254
+ // a single-object shape in case someone points the CLI at an older
255
+ // Hub build.
256
+ const providers = Array.isArray(resp.data)
257
+ ? resp.data
258
+ : resp.data
259
+ ? [resp.data]
260
+ : [];
261
+ if (providers.length === 0) {
262
+ spinner.info("No providers registered yet.");
263
+ console.log(chalk.dim(` Run "clawmoney relay setup" to get started.`));
264
+ return;
265
+ }
266
+ // Group rows by cli_type so `claude-*` lines stay together, then
267
+ // `codex-*`, then `gemini-*`, then `antigravity-*`. Within a family
268
+ // we sort by model name so the ordering is stable across calls.
269
+ const CLI_ORDER = ["claude", "codex", "gemini", "antigravity"];
270
+ providers.sort((a, b) => {
271
+ const ai = CLI_ORDER.indexOf(a.cli_type ?? "");
272
+ const bi = CLI_ORDER.indexOf(b.cli_type ?? "");
273
+ const aRank = ai === -1 ? CLI_ORDER.length : ai;
274
+ const bRank = bi === -1 ? CLI_ORDER.length : bi;
275
+ if (aRank !== bRank)
276
+ return aRank - bRank;
277
+ return (a.model ?? "").localeCompare(b.model ?? "");
278
+ });
279
+ spinner.succeed(`Relay Providers (${providers.length})`);
226
280
  console.log("");
227
- console.log(` ${chalk.bold("Provider ID:")} ${data.id ?? data.provider_id ?? "-"}`);
228
- console.log(` ${chalk.bold("Status:")} ${statusColor(data.status ?? "-")}`);
229
- console.log(` ${chalk.bold("CLI:")} ${data.cli_type ?? "-"}`);
230
- console.log(` ${chalk.bold("Model:")} ${data.model ?? "-"}`);
231
- console.log(` ${chalk.bold("Mode:")} ${data.mode ?? "-"}`);
232
- console.log(` ${chalk.bold("Concurrency:")} ${data.concurrency ?? "-"}`);
233
- console.log(` ${chalk.bold("Current Load:")} ${data.current_load ?? 0}`);
234
- console.log(` ${chalk.bold("Daily Spent:")} $${(data.daily_spent_usd ?? 0).toFixed(2)} / $${(data.daily_limit_usd ?? 0).toFixed(2)}`);
235
- console.log(` ${chalk.bold("Total Earned:")} $${(data.total_earned_usd ?? 0).toFixed(2)}`);
236
- console.log(` ${chalk.bold("Total Requests:")} ${data.total_requests ?? 0}`);
237
- console.log(` ${chalk.bold("Input Price:")} $${data.price_input_per_m ?? "-"}/1M tokens`);
238
- console.log(` ${chalk.bold("Output Price:")} $${data.price_output_per_m ?? "-"}/1M tokens`);
281
+ // Aggregate stats across all rows since users think of earnings /
282
+ // spend as account-level, not per-model.
283
+ const totalEarned = providers.reduce((s, p) => s + (p.total_earned_usd ?? 0), 0);
284
+ const totalRequests = providers.reduce((s, p) => s + (p.total_requests ?? 0), 0);
285
+ const totalDailySpent = providers.reduce((s, p) => s + (p.daily_spent_usd ?? 0), 0);
286
+ const totalDailyLimit = providers.reduce((s, p) => s + (p.daily_limit_usd ?? 0), 0);
287
+ // Per-provider rows — compact table with status/cli/model/load.
288
+ const header = ` ${"STATUS".padEnd(9)} ${"CLI".padEnd(12)} ${"MODEL".padEnd(30)} ${"LOAD".padEnd(8)} ${"EARNED".padEnd(10)}`;
289
+ console.log(chalk.bold(header));
290
+ console.log(chalk.dim(" " + "─".repeat(75)));
291
+ for (const p of providers) {
292
+ const statusRaw = (p.status ?? "-").padEnd(9);
293
+ const statusColored = p.status === "online"
294
+ ? chalk.green(statusRaw)
295
+ : p.status === "offline"
296
+ ? chalk.dim(statusRaw)
297
+ : chalk.yellow(statusRaw);
298
+ const cli = (p.cli_type ?? "-").padEnd(12);
299
+ const model = (p.model ?? "-").padEnd(30);
300
+ const load = `${p.current_load ?? 0}/${p.concurrency ?? "-"}`.padEnd(8);
301
+ const earned = `$${(p.total_earned_usd ?? 0).toFixed(2)}`.padEnd(10);
302
+ console.log(` ${statusColored} ${cli} ${model} ${load} ${earned}`);
303
+ }
304
+ console.log("");
305
+ console.log(` ${chalk.bold("Daily quota:")} $${totalDailySpent.toFixed(2)} / $${totalDailyLimit.toFixed(2)}`);
306
+ console.log(` ${chalk.bold("Total earned:")} $${totalEarned.toFixed(2)} (${totalRequests} requests)`);
239
307
  }
240
308
  catch (err) {
241
309
  spinner.fail(chalk.red("Failed to fetch status"));
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { walletStatusCommand, walletBalanceCommand, walletAddressCommand, wallet
8
8
  import { tweetCommand } from './commands/tweet.js';
9
9
  import { gigCreateCommand, gigBrowseCommand, gigDetailCommand, gigAcceptCommand, gigDeliverCommand, gigApproveCommand, gigDisputeCommand, } from './commands/gig.js';
10
10
  import { hubStartCommand, hubStopCommand, hubStatusCommand, hubSearchCommand, hubCallCommand, hubRegisterCommand, hubSkillsCommand, hubOrderCommand, hubHistoryCommand, } from './commands/hub.js';
11
- import { relayRegisterCommand, relayStartCommand, relayStopCommand, relayStatusCommand, relayModelsCommand, relayCreditsCommand, } from './commands/relay.js';
11
+ import { relayRegisterCommand, relayStartCommand, relayStopCommand, relayStatusCommand, relayModelsCommand, relayCreditsCommand, relayLogsCommand, } from './commands/relay.js';
12
12
  import { antigravityLoginCommand, antigravityStatusCommand, } from './commands/antigravity.js';
13
13
  import { createRequire } from 'node:module';
14
14
  const require = createRequire(import.meta.url);
@@ -527,6 +527,20 @@ relay
527
527
  process.exit(1);
528
528
  }
529
529
  });
530
+ relay
531
+ .command('logs')
532
+ .description('Tail the daemon log in real time (like `tail -f ~/.clawmoney/relay.log`)')
533
+ .option('-n, --lines <n>', 'Lines of history to show before following', '50')
534
+ .option('--no-follow', "Print and exit instead of following")
535
+ .action(async (options) => {
536
+ try {
537
+ await relayLogsCommand(options);
538
+ }
539
+ catch (err) {
540
+ console.error(err.message);
541
+ process.exit(1);
542
+ }
543
+ });
530
544
  relay
531
545
  .command('models')
532
546
  .description('List available relay models')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.30",
3
+ "version": "0.15.32",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {