clawmoney 0.15.29 → 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.
@@ -7,7 +7,7 @@ import chalk from "chalk";
7
7
  import { apiPost } from "../utils/api.js";
8
8
  import { loadConfig, requireConfig } from "../utils/config.js";
9
9
  import { setupCommand } from "./setup.js";
10
- import { API_PRICES, RELAY_DISCOUNT, PLATFORM_FEE } from "../relay/pricing.js";
10
+ import { API_PRICES, PLATFORM_FEE } from "../relay/pricing.js";
11
11
  // ── Per-cli_type model catalogs ──
12
12
  //
13
13
  // `RECOMMENDED_MODELS` is what gets registered when the user picks "all
@@ -119,12 +119,6 @@ function detectInstalledClis() {
119
119
  });
120
120
  return results;
121
121
  }
122
- // ── Helpers ──
123
- function formatBuyerPrice(input, output) {
124
- const buyerInput = (input * RELAY_DISCOUNT).toFixed(3);
125
- const buyerOutput = (output * RELAY_DISCOUNT).toFixed(3);
126
- return `$${buyerInput}/$${buyerOutput} per 1M (after ${Math.round((1 - RELAY_DISCOUNT) * 100)}% relay discount)`;
127
- }
128
122
  // ── Main command ──
129
123
  export async function relaySetupCommand() {
130
124
  // ── Step 0: ensure the agent is logged in ──
@@ -273,85 +267,87 @@ export async function relaySetupCommand() {
273
267
  process.exit(0);
274
268
  }
275
269
  const dailyLimit = dailyLimitChoice;
276
- // ── Step 5: confirmation summary ──
277
- // Translate the chosen daily-limit USD value back into the percentage
278
- // label the user picked, so what they see in the summary matches what
279
- // they answered in the prompt.
270
+ // ── Step 5: register everything under one spinner ──
271
+ //
272
+ // We deliberately skip the old per-model Summary block: pricing is on
273
+ // the website, and Step 3 already listed which models were queued per
274
+ // subscription. The remaining signal (quota share + earn %) goes into
275
+ // the spinner's final message so users see it exactly once.
276
+ //
277
+ // Also: one spinner for the whole batch, not N. Sequential per-model
278
+ // spinners produced 7+ rows of clack vertical whitespace for what's
279
+ // really a single bulk action.
280
+ //
281
+ // No "Register all N providers now?" confirm either — the user picked
282
+ // subscriptions + quota share above; Ctrl-C still aborts, and the
283
+ // backend is idempotent so mid-way aborts are safe to re-run.
280
284
  const limitLabel = {
281
- 15: "~25% (Light)",
282
- 30: "~50% (Balanced)",
283
- 45: "~75% (Heavy)",
284
- 60: "~100% (Full)",
285
+ 15: "~25%", 30: "~50%", 45: "~75%", 60: "~100%",
285
286
  };
286
- log.step(chalk.bold("Summary"));
287
- for (const r of registrations) {
288
- log.message(` ${chalk.cyan(r.cli + "/" + r.model).padEnd(50)} ${chalk.dim(formatBuyerPrice(r.input, r.output))}`);
289
- }
290
- log.message(chalk.dim(` ${registrations.length} provider(s) · ${limitLabel[dailyLimit] ?? `$${dailyLimit}/day cap`} per model`));
291
- log.message(chalk.dim(` You earn ~${Math.round((1 - PLATFORM_FEE) * 100)}% of what buyers pay (after platform fee)`));
292
- log.message(chalk.dim(` To customize: edit ~/.clawmoney/config.yaml after start`));
293
- // ── Step 6: register each (idempotent — "already registered" counts as success) ──
294
- //
295
- // No "Register all N providers now?" confirm — the user already
296
- // picked subscriptions + daily quota share. Seeing the summary and
297
- // immediately going into registration is the expected flow. Ctrl-C
298
- // still aborts, and registrations are idempotent so a mid-way abort
299
- // is recoverable by re-running.
287
+ const earnPct = Math.round((1 - PLATFORM_FEE) * 100);
300
288
  let succeeded = 0;
301
289
  let failed = 0;
302
290
  const failures = [];
303
- for (const r of registrations) {
304
- const regSpin = spinner();
305
- regSpin.start(`Registering ${r.cli}/${r.model}...`);
291
+ const regSpin = spinner();
292
+ regSpin.start(`Registering ${registrations.length} providers...`);
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
+ };
306
308
  try {
307
- const body = {
308
- cli_type: r.cli,
309
- model: r.model,
310
- mode: "chat",
311
- concurrency,
312
- daily_limit_usd: dailyLimit,
313
- price_input_per_m: r.input,
314
- price_output_per_m: r.output,
315
- };
316
309
  const resp = await apiPost("/api/v1/relay/providers", body, config.api_key);
317
310
  if (resp.ok) {
318
- regSpin.stop(`${chalk.green("✓")} ${r.cli}/${r.model}`);
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")) {
319
320
  succeeded++;
320
321
  }
321
322
  else {
322
- const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
323
- ? resp.data.detail
324
- : resp.data;
325
- const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
326
- // Already-registered is a soft success — idempotent re-run.
327
- if (detail.includes("Already registered")) {
328
- regSpin.stop(`${chalk.yellow("~")} ${r.cli}/${r.model} ${chalk.dim("(already registered, no change)")}`);
329
- succeeded++;
330
- }
331
- else {
332
- regSpin.stop(`${chalk.red("✗")} ${r.cli}/${r.model} ${chalk.dim("(" + detail.slice(0, 80) + ")")}`);
333
- failed++;
334
- failures.push({ cli: r.cli, model: r.model, error: detail });
335
- }
323
+ failed++;
324
+ failures.push({ cli: r.cli, model: r.model, error: detail });
336
325
  }
337
326
  }
338
327
  catch (err) {
339
328
  const msg = err.message;
340
- regSpin.stop(`${chalk.red("✗")} ${r.cli}/${r.model} ${chalk.dim("(" + msg + ")")}`);
341
329
  failed++;
342
330
  failures.push({ cli: r.cli, model: r.model, error: msg });
343
331
  }
332
+ }));
333
+ if (failed === 0) {
334
+ regSpin.stop(`${chalk.green(`✓ ${succeeded} providers registered`)} ` +
335
+ chalk.dim(`(${limitLabel[dailyLimit] ?? `$${dailyLimit}`} quota share · you earn ~${earnPct}%)`));
344
336
  }
345
- // ── Step 7: registration done, offer to auto-start ──
346
- log.step(chalk.bold("Registered"));
347
- log.message(`${chalk.green(succeeded.toString())} provider(s) registered`);
337
+ else {
338
+ regSpin.stop(`${chalk.yellow(`${succeeded} registered, ${failed} failed`)}`);
339
+ }
340
+ // ── Step 6: on failure, list which ones broke ──
341
+ //
342
+ // On success we say nothing — the spinner's final message is already
343
+ // the "registered" summary. On failure we dump a per-row detail line
344
+ // so the user can tell what to fix.
348
345
  if (failed > 0) {
349
- log.warn(`${failed} registrations failed`);
350
346
  for (const f of failures) {
351
- log.message(chalk.dim(` ${f.cli}/${f.model}: ${f.error.slice(0, 120)}`));
347
+ log.warn(`${f.cli}/${f.model}: ${chalk.dim(f.error.slice(0, 120))}`);
352
348
  }
353
349
  }
354
- // ── Step 8: auto-start the daemon ──
350
+ // ── Step 7: auto-start the daemon ──
355
351
  //
356
352
  // The daemon now runs in multi-cli auto mode by default: it fetches
357
353
  // every provider this agent has registered, preflights each distinct
@@ -370,11 +366,14 @@ export async function relaySetupCommand() {
370
366
  outro(chalk.yellow("Setup complete (daemon not started)"));
371
367
  return;
372
368
  }
373
- log.message("");
374
- log.message(chalk.dim("Useful follow-up commands:"));
375
- log.message(` ${chalk.cyan("clawmoney relay status")} # check daemon health + provider list`);
376
- log.message(` ${chalk.cyan("clawmoney relay credits")} # check earnings + payout balance`);
377
- 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`);
378
377
  const cliLabel = uniqueClis.length === 1
379
378
  ? `${uniqueClis[0]} daemon running`
380
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.29",
3
+ "version": "0.15.31",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {