clawmoney 0.15.10 → 0.15.12

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.
@@ -0,0 +1 @@
1
+ export declare function relaySetupCommand(): Promise<void>;
@@ -0,0 +1,344 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { intro, outro, multiselect, confirm, text, spinner, isCancel, cancel, log, } from "@clack/prompts";
6
+ import chalk from "chalk";
7
+ import { apiPost } from "../utils/api.js";
8
+ import { requireConfig } from "../utils/config.js";
9
+ import { API_PRICES, RELAY_DISCOUNT, PLATFORM_FEE } from "../relay/pricing.js";
10
+ // ── Per-cli_type model catalogs ──
11
+ //
12
+ // `RECOMMENDED_MODELS` is what gets registered when the user picks "all
13
+ // recommended" — it's a curated subset of API_PRICES that mirrors what
14
+ // each CLI's NATIVE /model picker exposes by default. Cross-checked
15
+ // against:
16
+ // - Claude Code 2.1.x /model menu (4 entries → 3 unique IDs;
17
+ // Sonnet 1M is the same model + context-1m beta header)
18
+ // - Codex CLI 0.117.x /model menu (4 entries: gpt-5.4 current,
19
+ // gpt-5.4-mini, gpt-5.3-codex, gpt-5.2)
20
+ // - sub2api backend/internal/pkg/gemini/models.go DefaultModels()
21
+ // for the Gemini CLI catalog
22
+ // - sub2api backend/internal/pkg/antigravity/claude_types.go
23
+ // claudeModels + geminiModels for the Antigravity catalog
24
+ //
25
+ // The "manual select" path (when the user says no to recommended)
26
+ // falls through to modelsForCli(cli) which returns EVERY priced
27
+ // model in that family.
28
+ const RECOMMENDED_MODELS = {
29
+ // Claude Code /model menu: Default(Sonnet 4.6) / Sonnet(1M) / Opus(1M) / Haiku
30
+ // → 3 unique model IDs (Sonnet 1M = same model + context-1m beta)
31
+ claude: ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5"],
32
+ // Codex CLI /model menu: gpt-5.4 (current frontier) / gpt-5.4-mini /
33
+ // gpt-5.3-codex (Codex-optimized) / gpt-5.2 (long-running pro)
34
+ codex: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.2"],
35
+ // Gemini CLI exposes a long list; mainstream picks are the production-
36
+ // stable 2.5 line (pro + flash) and the latest 3.x preview (pro + flash).
37
+ // Image / thinking variants and lite/customtools are intentionally
38
+ // skipped from the recommended set — users can pick them manually.
39
+ gemini: [
40
+ "gemini-2.5-pro",
41
+ "gemini-2.5-flash",
42
+ "gemini-3-pro-preview",
43
+ "gemini-3-flash-preview",
44
+ ],
45
+ // Antigravity exposes Claude Opus/Sonnet AND Gemini Pro/Flash via the
46
+ // SAME Google Antigravity OAuth token (one of its main selling points
47
+ // for providers). Recommended set spans both halves so a single
48
+ // antigravity provider serves both Anthropic and Google buyers.
49
+ antigravity: [
50
+ "antigravity-claude-sonnet-4-6",
51
+ "antigravity-claude-opus-4-6",
52
+ "antigravity-gemini-3-pro",
53
+ "antigravity-gemini-3-flash",
54
+ "antigravity-gemini-2.5-pro",
55
+ ],
56
+ };
57
+ function modelsForCli(cli) {
58
+ const all = Object.keys(API_PRICES);
59
+ if (cli === "claude") {
60
+ return all.filter((m) => m.startsWith("claude-"));
61
+ }
62
+ if (cli === "codex") {
63
+ // Only gpt-5.x family — o3/o4 reasoning models are public Responses
64
+ // API only, NOT served by Codex CLI's internal ChatGPT WS path.
65
+ // They're in API_PRICES for OpenAI SDK callers via /v1/chat/completions
66
+ // but a `codex` daemon can't actually serve them.
67
+ return all.filter((m) => m.startsWith("gpt-"));
68
+ }
69
+ if (cli === "antigravity") {
70
+ return all.filter((m) => m.startsWith("antigravity-"));
71
+ }
72
+ if (cli === "gemini") {
73
+ // Exclude antigravity-prefixed gemini variants — those are served by
74
+ // the antigravity cli_type, not the standalone gemini cli_type.
75
+ return all.filter((m) => m.startsWith("gemini-") && !m.startsWith("antigravity-"));
76
+ }
77
+ return [];
78
+ }
79
+ function detectInstalledClis() {
80
+ const results = [];
81
+ // Binary-based CLIs: probe `which`. We intentionally don't try to
82
+ // validate OAuth state here — the daemon's preflight does that on
83
+ // first start, and probing OAuth from a sync setup wizard would be
84
+ // brittle (keychain prompts, refresh-token races, etc).
85
+ const binaries = [
86
+ { cli: "claude", bin: "claude" },
87
+ { cli: "codex", bin: "codex" },
88
+ { cli: "gemini", bin: "gemini" },
89
+ ];
90
+ for (const { cli, bin } of binaries) {
91
+ let installed = false;
92
+ try {
93
+ execSync(`command -v ${bin}`, { stdio: "pipe" });
94
+ installed = true;
95
+ }
96
+ catch {
97
+ installed = false;
98
+ }
99
+ results.push({
100
+ cli,
101
+ available: installed,
102
+ hint: installed
103
+ ? "binary in PATH (login state will be validated when daemon starts)"
104
+ : `${bin} not found in PATH`,
105
+ });
106
+ }
107
+ // Antigravity is OAuth-file based — there's no `antigravity` binary
108
+ // installed locally. We check for the OAuth credentials file that
109
+ // `clawmoney antigravity login` writes.
110
+ const antigravityFile = join(homedir(), ".clawmoney", "antigravity-accounts.json");
111
+ const antigravityAvailable = existsSync(antigravityFile);
112
+ results.push({
113
+ cli: "antigravity",
114
+ available: antigravityAvailable,
115
+ hint: antigravityAvailable
116
+ ? "Google OAuth file present"
117
+ : "no Google OAuth linked (run `clawmoney antigravity login` first)",
118
+ });
119
+ return results;
120
+ }
121
+ // ── Helpers ──
122
+ function formatPrice(input, output) {
123
+ return `$${input}/$${output} per 1M`;
124
+ }
125
+ function formatBuyerPrice(input, output) {
126
+ const buyerInput = (input * RELAY_DISCOUNT).toFixed(3);
127
+ const buyerOutput = (output * RELAY_DISCOUNT).toFixed(3);
128
+ return `$${buyerInput}/$${buyerOutput} per 1M (after ${Math.round((1 - RELAY_DISCOUNT) * 100)}% relay discount)`;
129
+ }
130
+ // ── Main command ──
131
+ export async function relaySetupCommand() {
132
+ const config = requireConfig();
133
+ intro(chalk.cyan(" ClawMoney Relay Setup "));
134
+ log.message("Sell your idle Claude Max / ChatGPT Pro / Google subscription capacity to other AI agents.");
135
+ // ── Step 1: detect installed CLIs ──
136
+ const detectSpin = spinner();
137
+ detectSpin.start("Scanning for installed CLI clients...");
138
+ const detected = detectInstalledClis();
139
+ detectSpin.stop("Scan complete");
140
+ for (const d of detected) {
141
+ if (d.available) {
142
+ log.success(`${chalk.bold(d.cli.padEnd(12))} ${chalk.dim(d.hint)}`);
143
+ }
144
+ else {
145
+ log.warn(`${chalk.bold(d.cli.padEnd(12))} ${chalk.dim(d.hint)}`);
146
+ }
147
+ }
148
+ const available = detected.filter((d) => d.available);
149
+ if (available.length === 0) {
150
+ log.error("No supported CLI clients found locally. Install at least one of: " +
151
+ chalk.cyan("claude, codex, gemini") +
152
+ " — or run `clawmoney antigravity login` to link a Google account.");
153
+ cancel("Setup aborted");
154
+ process.exit(1);
155
+ }
156
+ // ── Step 2: pick which families to register ──
157
+ const familyChoice = await multiselect({
158
+ message: "Which CLI families do you want to provide? (space to toggle, enter to confirm)",
159
+ options: available.map((d) => ({
160
+ value: d.cli,
161
+ label: d.cli,
162
+ hint: d.hint,
163
+ })),
164
+ initialValues: available.map((d) => d.cli),
165
+ required: true,
166
+ });
167
+ if (isCancel(familyChoice)) {
168
+ cancel("Setup cancelled");
169
+ process.exit(0);
170
+ }
171
+ const selectedClis = familyChoice;
172
+ const registrations = [];
173
+ for (const cli of selectedClis) {
174
+ const allModels = modelsForCli(cli);
175
+ const recommended = (RECOMMENDED_MODELS[cli] ?? []).filter((m) => allModels.includes(m));
176
+ if (allModels.length === 0) {
177
+ log.warn(`${cli}: no models found in pricing table — skipping`);
178
+ continue;
179
+ }
180
+ log.step(`${chalk.bold(cli)}: choose models`);
181
+ const useRecommended = await confirm({
182
+ message: `Register the ${recommended.length} recommended ${cli} models? (${recommended.join(", ")})`,
183
+ initialValue: true,
184
+ });
185
+ if (isCancel(useRecommended)) {
186
+ cancel("Setup cancelled");
187
+ process.exit(0);
188
+ }
189
+ let chosen;
190
+ if (useRecommended) {
191
+ chosen = recommended;
192
+ }
193
+ else {
194
+ const picked = await multiselect({
195
+ message: `Pick ${cli} models to register:`,
196
+ options: allModels.map((m) => {
197
+ const p = API_PRICES[m];
198
+ return {
199
+ value: m,
200
+ label: m,
201
+ hint: formatPrice(p.input, p.output),
202
+ };
203
+ }),
204
+ initialValues: recommended,
205
+ required: true,
206
+ });
207
+ if (isCancel(picked)) {
208
+ cancel("Setup cancelled");
209
+ process.exit(0);
210
+ }
211
+ chosen = picked;
212
+ }
213
+ for (const model of chosen) {
214
+ const p = API_PRICES[model];
215
+ registrations.push({
216
+ cli,
217
+ model,
218
+ input: p.input,
219
+ output: p.output,
220
+ });
221
+ }
222
+ }
223
+ if (registrations.length === 0) {
224
+ cancel("No models selected — nothing to register");
225
+ process.exit(0);
226
+ }
227
+ // ── Step 4: global concurrency + daily budget ──
228
+ const concurrencyAns = await text({
229
+ message: "Concurrency cap per provider? (1-5 recommended; higher looks less like a single power user to upstream fingerprint detection)",
230
+ placeholder: "5",
231
+ defaultValue: "5",
232
+ validate: (v) => {
233
+ const n = parseInt(v || "5", 10);
234
+ if (Number.isNaN(n) || n < 1 || n > 20) {
235
+ return "Must be a number between 1 and 20";
236
+ }
237
+ return undefined;
238
+ },
239
+ });
240
+ if (isCancel(concurrencyAns)) {
241
+ cancel("Setup cancelled");
242
+ process.exit(0);
243
+ }
244
+ const dailyLimitAns = await text({
245
+ message: "Daily API spend cap per provider in USD? (the daemon stops accepting requests when exceeded)",
246
+ placeholder: "15",
247
+ defaultValue: "15",
248
+ validate: (v) => {
249
+ const n = parseFloat(v || "15");
250
+ if (Number.isNaN(n) || n < 0) {
251
+ return "Must be a non-negative number";
252
+ }
253
+ return undefined;
254
+ },
255
+ });
256
+ if (isCancel(dailyLimitAns)) {
257
+ cancel("Setup cancelled");
258
+ process.exit(0);
259
+ }
260
+ const concurrency = parseInt(concurrencyAns, 10);
261
+ const dailyLimit = parseFloat(dailyLimitAns);
262
+ // ── Step 5: confirmation summary ──
263
+ log.step(chalk.bold("Summary"));
264
+ for (const r of registrations) {
265
+ log.message(` ${chalk.cyan(r.cli + "/" + r.model).padEnd(50)} ${chalk.dim(formatBuyerPrice(r.input, r.output))}`);
266
+ }
267
+ log.message(chalk.dim(` ${registrations.length} providers · concurrency=${concurrency} · daily_limit=$${dailyLimit}`));
268
+ log.message(chalk.dim(` You earn ~${Math.round((1 - PLATFORM_FEE) * 100)}% of what buyers pay (after platform fee)`));
269
+ const proceed = await confirm({
270
+ message: `Register all ${registrations.length} providers now?`,
271
+ initialValue: true,
272
+ });
273
+ if (isCancel(proceed) || !proceed) {
274
+ cancel("Setup cancelled");
275
+ process.exit(0);
276
+ }
277
+ // ── Step 6: register each (idempotent — "already registered" counts as success) ──
278
+ let succeeded = 0;
279
+ let failed = 0;
280
+ const failures = [];
281
+ for (const r of registrations) {
282
+ const regSpin = spinner();
283
+ regSpin.start(`Registering ${r.cli}/${r.model}...`);
284
+ try {
285
+ const body = {
286
+ cli_type: r.cli,
287
+ model: r.model,
288
+ mode: "chat",
289
+ concurrency,
290
+ daily_limit_usd: dailyLimit,
291
+ price_input_per_m: r.input,
292
+ price_output_per_m: r.output,
293
+ };
294
+ const resp = await apiPost("/api/v1/relay/providers", body, config.api_key);
295
+ if (resp.ok) {
296
+ regSpin.stop(`${chalk.green("✓")} ${r.cli}/${r.model}`);
297
+ succeeded++;
298
+ }
299
+ else {
300
+ const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
301
+ ? resp.data.detail
302
+ : resp.data;
303
+ const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
304
+ // Already-registered is a soft success — idempotent re-run.
305
+ if (detail.includes("Already registered")) {
306
+ regSpin.stop(`${chalk.yellow("~")} ${r.cli}/${r.model} ${chalk.dim("(already registered, no change)")}`);
307
+ succeeded++;
308
+ }
309
+ else {
310
+ regSpin.stop(`${chalk.red("✗")} ${r.cli}/${r.model} ${chalk.dim("(" + detail.slice(0, 80) + ")")}`);
311
+ failed++;
312
+ failures.push({ cli: r.cli, model: r.model, error: detail });
313
+ }
314
+ }
315
+ }
316
+ catch (err) {
317
+ const msg = err.message;
318
+ regSpin.stop(`${chalk.red("✗")} ${r.cli}/${r.model} ${chalk.dim("(" + msg + ")")}`);
319
+ failed++;
320
+ failures.push({ cli: r.cli, model: r.model, error: msg });
321
+ }
322
+ }
323
+ // ── Step 7: next steps ──
324
+ log.step(chalk.bold("Done"));
325
+ log.message(`${chalk.green(succeeded.toString())} providers registered`);
326
+ if (failed > 0) {
327
+ log.warn(`${failed} registrations failed`);
328
+ for (const f of failures) {
329
+ log.message(chalk.dim(` ${f.cli}/${f.model}: ${f.error.slice(0, 120)}`));
330
+ }
331
+ }
332
+ log.message("");
333
+ log.message(chalk.bold("Next steps"));
334
+ log.message(" Start the daemon for each cli_type you registered:");
335
+ for (const cli of selectedClis) {
336
+ log.message(` ${chalk.cyan(`clawmoney relay start --cli ${cli}`)}`);
337
+ }
338
+ log.message("");
339
+ log.message(" Useful follow-up commands:");
340
+ log.message(` ${chalk.cyan("clawmoney relay status")} # check daemon health + provider list`);
341
+ log.message(` ${chalk.cyan("clawmoney relay credits")} # check earnings + payout balance`);
342
+ log.message(` ${chalk.cyan("clawmoney relay stop")} # stop the daemon`);
343
+ outro(chalk.green("Setup complete"));
344
+ }
package/dist/index.js CHANGED
@@ -458,6 +458,19 @@ gig
458
458
  const relay = program
459
459
  .command('relay')
460
460
  .description('Relay marketplace: sell idle AI subscription capacity');
461
+ relay
462
+ .command('setup')
463
+ .description('Interactive: detect installed CLIs, pick models, register all in one go (recommended for first-time setup)')
464
+ .action(async () => {
465
+ try {
466
+ const { relaySetupCommand } = await import('./commands/relay-setup.js');
467
+ await relaySetupCommand();
468
+ }
469
+ catch (err) {
470
+ console.error(err.message);
471
+ process.exit(1);
472
+ }
473
+ });
461
474
  relay
462
475
  .command('register')
463
476
  .description('Register as a relay provider')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.15.10",
3
+ "version": "0.15.12",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "dependencies": {
21
21
  "awal": "^2.2.0",
22
22
  "@bnbot/cli": "^0.3.0",
23
+ "@clack/prompts": "^0.7.0",
23
24
  "chalk": "^5.3.0",
24
25
  "commander": "^12.0.0",
25
26
  "ora": "^8.0.0",