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.
- package/dist/commands/relay-setup.js +67 -68
- package/dist/commands/relay.js +47 -18
- package/package.json +1 -1
|
@@ -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,
|
|
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:
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
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%
|
|
282
|
-
30: "~50% (Balanced)",
|
|
283
|
-
45: "~75% (Heavy)",
|
|
284
|
-
60: "~100% (Full)",
|
|
285
|
+
15: "~25%", 30: "~50%", 45: "~75%", 60: "~100%",
|
|
285
286
|
};
|
|
286
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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.
|
|
347
|
+
log.warn(`${f.cli}/${f.model}: ${chalk.dim(f.error.slice(0, 120))}`);
|
|
352
348
|
}
|
|
353
349
|
}
|
|
354
|
-
// ── Step
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
log.message(
|
|
377
|
-
|
|
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(" + ")}`;
|
package/dist/commands/relay.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
console.log(
|
|
236
|
-
console.log(
|
|
237
|
-
|
|
238
|
-
|
|
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"));
|