clawmoney 0.15.30 → 0.15.31

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.
@@ -290,34 +290,38 @@ export async function relaySetupCommand() {
290
290
  const failures = [];
291
291
  const regSpin = spinner();
292
292
  regSpin.start(`Registering ${registrations.length} providers...`);
293
- for (const r of registrations) {
293
+ // Parallel registration each request creates a distinct RelayProvider
294
+ // row so there's no write contention, and the backend's insert path is
295
+ // idempotent on (agent_id, cli_type, model). Sequential registration
296
+ // was costing ~1 RTT per row, which on a high-latency link (e.g. from
297
+ // China) added up to 7-10s of visible wait for 7 providers.
298
+ await Promise.all(registrations.map(async (r) => {
299
+ const body = {
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
+ };
294
308
  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
309
  const resp = await apiPost("/api/v1/relay/providers", body, config.api_key);
305
310
  if (resp.ok) {
306
311
  succeeded++;
312
+ return;
313
+ }
314
+ const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
315
+ ? resp.data.detail
316
+ : resp.data;
317
+ const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
318
+ // Already-registered is a soft success — idempotent re-run.
319
+ if (detail.includes("Already registered")) {
320
+ succeeded++;
307
321
  }
308
322
  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
- }
323
+ failed++;
324
+ failures.push({ cli: r.cli, model: r.model, error: detail });
321
325
  }
322
326
  }
323
327
  catch (err) {
@@ -325,7 +329,7 @@ export async function relaySetupCommand() {
325
329
  failed++;
326
330
  failures.push({ cli: r.cli, model: r.model, error: msg });
327
331
  }
328
- }
332
+ }));
329
333
  if (failed === 0) {
330
334
  regSpin.stop(`${chalk.green(`✓ ${succeeded} providers registered`)} ` +
331
335
  chalk.dim(`(${limitLabel[dailyLimit] ?? `$${dailyLimit}`} quota share · you earn ~${earnPct}%)`));
@@ -362,11 +366,14 @@ export async function relaySetupCommand() {
362
366
  outro(chalk.yellow("Setup complete (daemon not started)"));
363
367
  return;
364
368
  }
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`);
369
+ // One multi-line log.message renders each line with a `│` prefix
370
+ // but without clack's inter-call gap — 3 bullets fit in 3 lines
371
+ // instead of 6.
372
+ log.message(chalk.dim("Next:") +
373
+ "\n" +
374
+ ` ${chalk.cyan("clawmoney relay status")} daemon health + providers\n` +
375
+ ` ${chalk.cyan("clawmoney relay credits")} earnings + balance\n` +
376
+ ` ${chalk.cyan("clawmoney relay stop")} stop daemon`);
370
377
  const cliLabel = uniqueClis.length === 1
371
378
  ? `${uniqueClis[0]} daemon running`
372
379
  : `daemon serving ${uniqueClis.join(" + ")}`;
@@ -206,36 +206,65 @@ export async function relayStatusCommand() {
206
206
  else {
207
207
  console.log(chalk.dim(" Local process: not running"));
208
208
  }
209
- // Remote status
209
+ // Remote status. /api/v1/relay/providers/me returns a LIST of
210
+ // RelayProviderPublic (one row per registered model), so we can't
211
+ // treat the body as a single object. For multi-cli providers the
212
+ // list can easily be 7-10 rows — render them as a table with one
213
+ // line per row instead of 14 labeled lines for a single picked row.
210
214
  const spinner = ora("Fetching relay provider status...").start();
211
215
  try {
212
216
  const resp = await apiGet("/api/v1/relay/providers/me", config.api_key);
213
217
  if (!resp.ok) {
214
218
  if (resp.status === 404) {
215
219
  spinner.info("Not registered as relay provider yet.");
216
- console.log(chalk.dim(` Run "clawmoney relay register" to get started.`));
220
+ console.log(chalk.dim(` Run "clawmoney relay setup" to get started.`));
217
221
  return;
218
222
  }
219
- const detail = resp.data?.detail ?? resp.status;
223
+ const detail = resp.data?.detail ?? String(resp.status);
220
224
  spinner.fail(chalk.red(`Failed to fetch status: ${detail}`));
221
225
  process.exit(1);
222
226
  }
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");
227
+ // Normalize: backend currently returns a list, but guard against
228
+ // a single-object shape in case someone points the CLI at an older
229
+ // Hub build.
230
+ const providers = Array.isArray(resp.data)
231
+ ? resp.data
232
+ : resp.data
233
+ ? [resp.data]
234
+ : [];
235
+ if (providers.length === 0) {
236
+ spinner.info("No providers registered yet.");
237
+ console.log(chalk.dim(` Run "clawmoney relay setup" to get started.`));
238
+ return;
239
+ }
240
+ spinner.succeed(`Relay Providers (${providers.length})`);
226
241
  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`);
242
+ // Aggregate stats across all rows since users think of earnings /
243
+ // spend as account-level, not per-model.
244
+ const totalEarned = providers.reduce((s, p) => s + (p.total_earned_usd ?? 0), 0);
245
+ const totalRequests = providers.reduce((s, p) => s + (p.total_requests ?? 0), 0);
246
+ const totalDailySpent = providers.reduce((s, p) => s + (p.daily_spent_usd ?? 0), 0);
247
+ const totalDailyLimit = providers.reduce((s, p) => s + (p.daily_limit_usd ?? 0), 0);
248
+ // Per-provider rows — compact table with status/cli/model/load.
249
+ const header = ` ${"STATUS".padEnd(9)} ${"CLI".padEnd(12)} ${"MODEL".padEnd(30)} ${"LOAD".padEnd(8)} ${"EARNED".padEnd(10)}`;
250
+ console.log(chalk.bold(header));
251
+ console.log(chalk.dim(" " + "─".repeat(75)));
252
+ for (const p of providers) {
253
+ const statusRaw = (p.status ?? "-").padEnd(9);
254
+ const statusColored = p.status === "online"
255
+ ? chalk.green(statusRaw)
256
+ : p.status === "offline"
257
+ ? chalk.dim(statusRaw)
258
+ : chalk.yellow(statusRaw);
259
+ const cli = (p.cli_type ?? "-").padEnd(12);
260
+ const model = (p.model ?? "-").padEnd(30);
261
+ const load = `${p.current_load ?? 0}/${p.concurrency ?? "-"}`.padEnd(8);
262
+ const earned = `$${(p.total_earned_usd ?? 0).toFixed(2)}`.padEnd(10);
263
+ console.log(` ${statusColored} ${cli} ${model} ${load} ${earned}`);
264
+ }
265
+ console.log("");
266
+ console.log(` ${chalk.bold("Daily quota:")} $${totalDailySpent.toFixed(2)} / $${totalDailyLimit.toFixed(2)}`);
267
+ console.log(` ${chalk.bold("Total earned:")} $${totalEarned.toFixed(2)} (${totalRequests} requests)`);
239
268
  }
240
269
  catch (err) {
241
270
  spinner.fail(chalk.red("Failed to fetch status"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.30",
3
+ "version": "0.15.31",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {