clawmoney 0.15.20 → 0.15.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.
@@ -5,7 +5,8 @@ import { join } from "node:path";
5
5
  import { intro, outro, multiselect, confirm, select, spinner, isCancel, cancel, log, } from "@clack/prompts";
6
6
  import chalk from "chalk";
7
7
  import { apiPost } from "../utils/api.js";
8
- import { requireConfig } from "../utils/config.js";
8
+ import { loadConfig, requireConfig } from "../utils/config.js";
9
+ import { setupCommand } from "./setup.js";
9
10
  import { API_PRICES, RELAY_DISCOUNT, PLATFORM_FEE } from "../relay/pricing.js";
10
11
  // ── Per-cli_type model catalogs ──
11
12
  //
@@ -129,6 +130,27 @@ function formatBuyerPrice(input, output) {
129
130
  }
130
131
  // ── Main command ──
131
132
  export async function relaySetupCommand() {
133
+ // ── Step 0: ensure the agent is logged in ──
134
+ //
135
+ // Relay setup relies on an ACTIVE ClawMoney agent (api_key + agent_id
136
+ // in ~/.clawmoney/config.yaml) to register provider rows and to auth
137
+ // the daemon's WS connection. If the user runs `clawmoney relay setup`
138
+ // before ever running `clawmoney setup`, we inline the login flow
139
+ // here instead of throwing a raw "No config found" error. The nested
140
+ // setupCommand uses its own ora/prompt UI — visually different from
141
+ // the clack wizard below, but that's acceptable since it only runs on
142
+ // the first-time-user path.
143
+ let existing = loadConfig();
144
+ if (!existing) {
145
+ console.log(chalk.yellow("\n You're not logged in yet — running `clawmoney setup` first.\n"));
146
+ await setupCommand();
147
+ existing = loadConfig();
148
+ if (!existing) {
149
+ console.log(chalk.red("\n Login did not complete. Run `clawmoney setup` manually, then re-run `clawmoney relay setup`.\n"));
150
+ process.exit(1);
151
+ }
152
+ console.log("");
153
+ }
132
154
  const config = requireConfig();
133
155
  intro(chalk.cyan(" ClawMoney Relay Setup "));
134
156
  log.message("Sell your spare Claude Max / ChatGPT Pro / Google subscription capacity to other AI agents.");
@@ -363,54 +385,30 @@ export async function relaySetupCommand() {
363
385
  }
364
386
  // ── Step 8: auto-start the daemon ──
365
387
  //
366
- // Daemon limitation: a single clawmoney process can only serve ONE
367
- // cli_type today (single ~/.clawmoney/relay.pid file). When the user
368
- // registered providers across multiple cli_types we can still
369
- // auto-start ONE of them and tell them how to switch later. Tracked
370
- // separately as a daemon refactor task.
388
+ // The daemon now runs in multi-cli auto mode by default: it fetches
389
+ // every provider this agent has registered, preflights each distinct
390
+ // cli_type, and dispatches requests to the right upstream based on
391
+ // the `cli_type` field in each incoming relay_request. A single
392
+ // daemon process can serve Claude + Codex + Gemini + Antigravity
393
+ // simultaneously, so there's no need to pick one here.
371
394
  const uniqueClis = Array.from(new Set(selectedClis));
372
- if (uniqueClis.length === 1) {
373
- // Single cli_type — go straight into the daemon. The user already
374
- // confirmed "Register all N providers?" above, so asking a second
375
- // time just adds friction.
376
- const cli = uniqueClis[0];
377
- const { relayStartCommand } = await import("./relay.js");
378
- try {
379
- await relayStartCommand({ cli });
380
- }
381
- catch (err) {
382
- log.error(`Failed to start daemon: ${err.message}\n` +
383
- `Try manually: ${chalk.cyan(`clawmoney relay start --cli ${cli}`)}`);
384
- outro(chalk.yellow("Setup complete (daemon not started)"));
385
- return;
386
- }
387
- log.message("");
388
- log.message(chalk.dim("Useful follow-up commands:"));
389
- log.message(` ${chalk.cyan("clawmoney relay status")} # check daemon health + provider list`);
390
- log.message(` ${chalk.cyan("clawmoney relay credits")} # check earnings + payout balance`);
391
- log.message(` ${chalk.cyan("clawmoney relay stop")} # stop the daemon`);
392
- outro(chalk.green("Setup complete · daemon running"));
393
- return;
394
- }
395
- // Multi cli_type — daemon can only host one at a time today. Start
396
- // the first registered cli_type directly and tell the user how to
397
- // switch to the others later.
398
- const firstCli = uniqueClis[0];
399
- log.warn(`You registered providers across ${uniqueClis.length} CLI families ` +
400
- `(${uniqueClis.join(", ")}). The daemon currently serves ONE cli_type ` +
401
- `per process — starting ${chalk.cyan(firstCli)} first.`);
402
395
  const { relayStartCommand } = await import("./relay.js");
403
396
  try {
404
- await relayStartCommand({ cli: firstCli });
397
+ await relayStartCommand({});
405
398
  }
406
399
  catch (err) {
407
400
  log.error(`Failed to start daemon: ${err.message}\n` +
408
- `Try manually: ${chalk.cyan(`clawmoney relay start --cli ${firstCli}`)}`);
401
+ `Try manually: ${chalk.cyan("clawmoney relay start")}`);
409
402
  outro(chalk.yellow("Setup complete (daemon not started)"));
410
403
  return;
411
404
  }
412
405
  log.message("");
413
- log.message(chalk.dim("To switch to a different cli_type later:"));
414
- log.message(chalk.dim(` ${chalk.cyan("clawmoney relay stop")} && ${chalk.cyan("clawmoney relay start --cli <other-cli>")}`));
415
- outro(chalk.green(`Setup complete · ${firstCli} daemon running`));
406
+ log.message(chalk.dim("Useful follow-up commands:"));
407
+ log.message(` ${chalk.cyan("clawmoney relay status")} # check daemon health + provider list`);
408
+ log.message(` ${chalk.cyan("clawmoney relay credits")} # check earnings + payout balance`);
409
+ log.message(` ${chalk.cyan("clawmoney relay stop")} # stop the daemon`);
410
+ const cliLabel = uniqueClis.length === 1
411
+ ? `${uniqueClis[0]} daemon running`
412
+ : `daemon serving ${uniqueClis.join(" + ")}`;
413
+ outro(chalk.green(`Setup complete · ${cliLabel}`));
416
414
  }
@@ -151,7 +151,12 @@ export async function relayStartCommand(options) {
151
151
  if (pid && isRelayPidAlive(pid)) {
152
152
  spinner.succeed(chalk.green(`Relay Provider started (PID ${pid})`));
153
153
  console.log(chalk.dim(` Log file: ${LOG_FILE}`));
154
- console.log(chalk.dim(` CLI: ${options.cli || "claude (default)"}`));
154
+ if (options.cli) {
155
+ console.log(chalk.dim(` CLI: ${options.cli} (single)`));
156
+ }
157
+ else {
158
+ console.log(chalk.dim(` CLI: auto (serving all registered cli_types for this agent)`));
159
+ }
155
160
  console.log(chalk.dim(` API key: ${config.api_key.slice(0, 8)}...`));
156
161
  }
157
162
  else {
@@ -7,6 +7,7 @@ import { callClaudeApi, callClaudeApiPassthrough, preflightClaudeApi, getRateGua
7
7
  import { callCodexApi, callCodexApiPassthrough, preflightCodexApi, getRateGuardSnapshot as getCodexRateGuardSnapshot, } from "./upstream/codex-api.js";
8
8
  import { callGeminiApi, preflightGeminiApi, getGeminiRateGuardSnapshot, } from "./upstream/gemini-api.js";
9
9
  import { callAntigravityApi, preflightAntigravityApi, getAntigravityRateGuardSnapshot, } from "./upstream/antigravity-api.js";
10
+ import { apiGet } from "../utils/api.js";
10
11
  /**
11
12
  * Pick the rate-guard snapshot matching this request's cli_type. Fixes a
12
13
  * pre-existing bug where gemini/codex responses were piggy-backing Claude's
@@ -305,6 +306,45 @@ async function executeRelayRequest(request, config, sendChunk) {
305
306
  };
306
307
  }
307
308
  }
309
+ // ── Preflight helpers ──
310
+ // Map a cli_type string to its preflight function. Returns null for
311
+ // unknown types so the caller can skip them gracefully instead of
312
+ // crashing the daemon.
313
+ function getPreflightFn(cliType) {
314
+ switch (cliType) {
315
+ case "claude":
316
+ return preflightClaudeApi;
317
+ case "codex":
318
+ return preflightCodexApi;
319
+ case "gemini":
320
+ return preflightGeminiApi;
321
+ case "antigravity":
322
+ return preflightAntigravityApi;
323
+ default:
324
+ return null;
325
+ }
326
+ }
327
+ // Query the Hub for the full set of cli_types the current agent has
328
+ // registered as relay providers. Used by the multi-cli daemon to decide
329
+ // which upstream preflights to run. Returns null on any network / auth
330
+ // failure — caller falls back to single-cli mode.
331
+ async function fetchRegisteredCliTypes(config) {
332
+ try {
333
+ const resp = await apiGet("/api/v1/relay/providers/me", config.api_key);
334
+ if (!resp.ok || !Array.isArray(resp.data)) {
335
+ return null;
336
+ }
337
+ const seen = new Set();
338
+ for (const p of resp.data) {
339
+ if (typeof p.cli_type === "string" && p.cli_type)
340
+ seen.add(p.cli_type);
341
+ }
342
+ return Array.from(seen);
343
+ }
344
+ catch {
345
+ return null;
346
+ }
347
+ }
308
348
  // ── Main daemon entry point ──
309
349
  export function runRelayProvider(cliOverride) {
310
350
  // Check for existing process
@@ -318,23 +358,76 @@ export function runRelayProvider(cliOverride) {
318
358
  // process.env.HTTPS_PROXY / http_proxy at init time. Must run BEFORE any
319
359
  // preflight call so the first outbound request already goes through it.
320
360
  applyProxyFromConfig(config);
321
- // Validate the OAuth token + fingerprint up-front so we fail fast instead
322
- // of on the first inbound request. Each cli_type has its own preflight
323
- // path (different credential file, different fingerprint schema, different
324
- // rate-guard instance).
325
- const preflightFn = config.relay.cli_type === "codex"
326
- ? preflightCodexApi
327
- : config.relay.cli_type === "gemini"
328
- ? preflightGeminiApi
329
- : config.relay.cli_type === "claude"
330
- ? preflightClaudeApi
331
- : config.relay.cli_type === "antigravity"
332
- ? preflightAntigravityApi
333
- : null;
334
- if (preflightFn) {
335
- preflightFn(config.relay.rate_guard).catch((err) => {
336
- logger.error(`${config.relay.cli_type} API preflight failed: ${err.message}`);
337
- process.exit(1);
361
+ // Validate OAuth tokens + fingerprints up front so we fail fast on the
362
+ // happy path instead of on the first inbound request.
363
+ //
364
+ // Two modes:
365
+ //
366
+ // 1. Single-cli legacy mode — `--cli X` passed explicitly. Run only
367
+ // that one preflight and fail-fast on error (preserves the old
368
+ // behavior when the user knows exactly what they're starting).
369
+ //
370
+ // 2. Multi-cli auto mode — no `--cli` override. Query the Hub for the
371
+ // full list of registered providers, collect their distinct
372
+ // cli_types, and run each preflight in parallel. Per-cli failures
373
+ // are tolerated (logged as a warning) so a broken codex OAuth
374
+ // doesn't take down the claude half of the daemon.
375
+ //
376
+ // The Hub already dispatches requests by agent_id and stamps each
377
+ // relay_request with its target `cli_type` — provider.ts's request
378
+ // handler (`executeRelayRequest`) routes to the right upstream module
379
+ // based on that field. So from the Hub's point of view, a daemon that
380
+ // has preflighted multiple cli_types is just "a more capable provider".
381
+ if (cliOverride) {
382
+ // Legacy single-cli path.
383
+ const preflightFn = getPreflightFn(cliOverride);
384
+ if (preflightFn) {
385
+ preflightFn(config.relay.rate_guard).catch((err) => {
386
+ logger.error(`${cliOverride} API preflight failed: ${err.message}`);
387
+ process.exit(1);
388
+ });
389
+ }
390
+ }
391
+ else {
392
+ // Multi-cli auto mode — fire-and-forget, don't block startup on the
393
+ // HTTP round-trip. The WS connection can come up in parallel; the
394
+ // preflights only gate the FIRST call of each family, not the WS
395
+ // handshake.
396
+ (async () => {
397
+ const registered = await fetchRegisteredCliTypes(config);
398
+ if (!registered || registered.length === 0) {
399
+ logger.warn("Could not determine registered cli_types from Hub. " +
400
+ "Falling back to config cli_type for preflight.");
401
+ const fallback = config.relay.cli_type;
402
+ const fn = getPreflightFn(fallback);
403
+ if (fn) {
404
+ try {
405
+ await fn(config.relay.rate_guard);
406
+ }
407
+ catch (err) {
408
+ logger.warn(`${fallback} preflight failed: ${err.message}`);
409
+ }
410
+ }
411
+ return;
412
+ }
413
+ logger.info(`Preflighting ${registered.length} registered cli_type(s): ${registered.join(", ")}`);
414
+ await Promise.all(registered.map(async (ct) => {
415
+ const fn = getPreflightFn(ct);
416
+ if (!fn) {
417
+ logger.warn(`Unknown cli_type ${ct} — skipping preflight`);
418
+ return;
419
+ }
420
+ try {
421
+ await fn(config.relay.rate_guard);
422
+ logger.info(` ${ct} preflight OK`);
423
+ }
424
+ catch (err) {
425
+ logger.warn(` ${ct} preflight failed: ${err.message} — ` +
426
+ `that family will NOT be able to serve requests until fixed`);
427
+ }
428
+ }));
429
+ })().catch((err) => {
430
+ logger.warn(`Multi-cli preflight driver error: ${err.message}`);
338
431
  });
339
432
  }
340
433
  const activeTasks = new Set();
@@ -419,5 +512,10 @@ export function runRelayProvider(cliOverride) {
419
512
  writeRelayPid();
420
513
  wsClient.start();
421
514
  logger.info("Relay Provider running. Listening for relay requests...");
422
- logger.info(`Config: cli=${config.relay.cli_type}, model=${config.relay.model}, mode=${config.relay.mode}, concurrency=${config.relay.concurrency}`);
515
+ if (cliOverride) {
516
+ logger.info(`Config: cli=${cliOverride} (single), mode=${config.relay.mode}, concurrency=${config.relay.concurrency}`);
517
+ }
518
+ else {
519
+ logger.info(`Config: cli=auto (multi), mode=${config.relay.mode}, concurrency=${config.relay.concurrency}`);
520
+ }
423
521
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.20",
3
+ "version": "0.15.22",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {