clawmoney 0.17.7 → 0.17.9

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.
@@ -1 +1,3 @@
1
- export declare function marketSetupCommand(): Promise<void>;
1
+ export declare function marketSetupCommand(opts?: {
2
+ nested?: boolean;
3
+ }): Promise<void>;
@@ -1,22 +1,22 @@
1
- import { intro, outro, select, text, confirm, spinner, isCancel, cancel, log, note, } from "@clack/prompts";
1
+ import { intro, outro, multiselect, text, confirm, spinner, isCancel, cancel, log, note, } from "@clack/prompts";
2
2
  import chalk from "chalk";
3
3
  import { apiPost } from "../utils/api.js";
4
4
  import { loadConfig } from "../utils/config.js";
5
5
  import { setupCommand } from "./setup.js";
6
6
  const CATEGORIES = [
7
- { value: "generation/image", routing: "instant", timeoutS: 120, suggestedPrice: 0.02, priceRange: [0.01, 0.50] },
8
- { value: "generation/video", routing: "instant", timeoutS: 300, suggestedPrice: 0.10, priceRange: [0.05, 1.00] },
9
- { value: "generation/video_long", routing: "escrow", timeoutS: null, suggestedPrice: 5.00, priceRange: [1.00, 50.00] },
10
- { value: "generation/text", routing: "instant", timeoutS: 120, suggestedPrice: 0.01, priceRange: [0.005, 0.20] },
11
- { value: "generation/audio", routing: "instant", timeoutS: 180, suggestedPrice: 0.05, priceRange: [0.02, 0.50] },
12
- { value: "transformation/translate", routing: "instant", timeoutS: 60, suggestedPrice: 0.01, priceRange: [0.005, 0.10] },
13
- { value: "transformation/tts", routing: "instant", timeoutS: 120, suggestedPrice: 0.02, priceRange: [0.01, 0.20] },
14
- { value: "transformation/stt", routing: "instant", timeoutS: 120, suggestedPrice: 0.02, priceRange: [0.01, 0.20] },
15
- { value: "search/web", routing: "instant", timeoutS: 60, suggestedPrice: 0.01, priceRange: [0.005, 0.10] },
16
- { value: "analysis/data", routing: "instant", timeoutS: 180, suggestedPrice: 0.05, priceRange: [0.02, 0.50] },
17
- { value: "coding/generation", routing: "instant", timeoutS: 240, suggestedPrice: 0.05, priceRange: [0.02, 0.50] },
18
- { value: "coding/review", routing: "instant", timeoutS: 180, suggestedPrice: 0.05, priceRange: [0.02, 0.50] },
19
- { value: "other", routing: "auto", timeoutS: null, suggestedPrice: 0.02, priceRange: [0.01, 1.00] },
7
+ { value: "generation/image", routing: "instant", timeoutS: 120, suggestedPrice: 0.02, priceRange: [0.01, 0.50], defaultName: "gen-image", placeholderDesc: "Generate a 1024x1024 image from a text prompt" },
8
+ { value: "generation/video", routing: "instant", timeoutS: 300, suggestedPrice: 0.10, priceRange: [0.05, 1.00], defaultName: "gen-video", placeholderDesc: "Generate a short AI video clip from a text prompt" },
9
+ { value: "generation/video_long", routing: "escrow", timeoutS: null, suggestedPrice: 5.00, priceRange: [1.00, 50.00], defaultName: "gen-video-long", placeholderDesc: "Generate long-form narrated video (escrow)" },
10
+ { value: "generation/text", routing: "instant", timeoutS: 120, suggestedPrice: 0.01, priceRange: [0.005, 0.20], defaultName: "gen-text", placeholderDesc: "Generate text from a prompt" },
11
+ { value: "generation/audio", routing: "instant", timeoutS: 180, suggestedPrice: 0.05, priceRange: [0.02, 0.50], defaultName: "gen-audio", placeholderDesc: "Generate music or sound effects from a prompt" },
12
+ { value: "transformation/translate", routing: "instant", timeoutS: 60, suggestedPrice: 0.01, priceRange: [0.005, 0.10], defaultName: "translate", placeholderDesc: "Translate text between languages" },
13
+ { value: "transformation/tts", routing: "instant", timeoutS: 120, suggestedPrice: 0.02, priceRange: [0.01, 0.20], defaultName: "tts", placeholderDesc: "Convert text to natural-sounding speech" },
14
+ { value: "transformation/stt", routing: "instant", timeoutS: 120, suggestedPrice: 0.02, priceRange: [0.01, 0.20], defaultName: "stt", placeholderDesc: "Transcribe speech to text" },
15
+ { value: "search/web", routing: "instant", timeoutS: 60, suggestedPrice: 0.01, priceRange: [0.005, 0.10], defaultName: "web-search", placeholderDesc: "Search the web and return relevant results" },
16
+ { value: "analysis/data", routing: "instant", timeoutS: 180, suggestedPrice: 0.05, priceRange: [0.02, 0.50], defaultName: "data-analysis", placeholderDesc: "Analyze a dataset and return insights" },
17
+ { value: "coding/generation", routing: "instant", timeoutS: 240, suggestedPrice: 0.05, priceRange: [0.02, 0.50], defaultName: "code-gen", placeholderDesc: "Generate code from a natural-language spec" },
18
+ { value: "coding/review", routing: "instant", timeoutS: 180, suggestedPrice: 0.05, priceRange: [0.02, 0.50], defaultName: "code-review", placeholderDesc: "Review a diff or PR for bugs and style issues" },
19
+ { value: "other", routing: "auto", timeoutS: null, suggestedPrice: 0.02, priceRange: [0.01, 1.00], defaultName: "", placeholderDesc: "Describe what this skill does" },
20
20
  ];
21
21
  const PRICE_THRESHOLD_FOR_ESCROW = 1.0; // mirrors backend constant
22
22
  function formatHint(row) {
@@ -88,11 +88,11 @@ function validatePrice(value) {
88
88
  return "Price looks unreasonable (> $10,000)";
89
89
  return undefined;
90
90
  }
91
- // ── Main wizard ──
92
- export async function marketSetupCommand() {
91
+ export async function marketSetupCommand(opts = {}) {
93
92
  // Step 0: ensure the agent is logged in. Mirrors relaySetupCommand's
94
93
  // handoff to setupCommand so first-time users get a clean flow instead
95
- // of "No config found" mid-wizard.
94
+ // of "No config found" mid-wizard. Skipped when nested under
95
+ // `clawmoney setup` since that command already guarantees a config.
96
96
  let existing = loadConfig();
97
97
  if (!existing) {
98
98
  await setupCommand();
@@ -104,106 +104,166 @@ export async function marketSetupCommand() {
104
104
  console.log("");
105
105
  }
106
106
  const config = existing;
107
- intro(chalk.cyan(" ClawMoney Market Setup "));
108
- log.message("Register a skill on the Market so other agents can call (and pay) you.");
109
- // ── Step 1: category ──
110
- const category = await select({
111
- message: "Pick the skill category:",
112
- options: CATEGORIES.map((row) => ({
113
- value: row.value,
114
- label: row.value,
115
- hint: formatHint(row),
116
- })),
117
- initialValue: "generation/image",
118
- });
119
- if (isCancel(category)) {
120
- cancel("Setup cancelled");
121
- process.exit(0);
107
+ if (!opts.nested) {
108
+ intro(chalk.cyan(" ClawMoney Market Setup "));
122
109
  }
123
- const categoryStr = category;
124
- const categoryRow = CATEGORIES.find((c) => c.value === categoryStr);
125
- // ── Step 2: skill name ──
126
- const skillName = await text({
127
- message: "Skill name (used in URLs, e.g. gen-image):",
128
- placeholder: "gen-image",
129
- validate: validateSkillName,
110
+ log.message("Register one or more skills on the Market so other agents can call (and pay) you.");
111
+ // ── Step 1: multiselect categories. Grouped visually: Instant first,
112
+ // then Escrow, then Auto. clack's multiselect has no native separators,
113
+ // so we fake them with disabled-looking header rows whose value is a
114
+ // sentinel selecting one is a no-op (filtered out downstream). ──
115
+ const INSTANT_HEADER = "__hdr_instant__";
116
+ const ESCROW_HEADER = "__hdr_escrow__";
117
+ const AUTO_HEADER = "__hdr_auto__";
118
+ const HEADERS = new Set([INSTANT_HEADER, ESCROW_HEADER, AUTO_HEADER]);
119
+ const instantRows = CATEGORIES.filter((c) => c.routing === "instant");
120
+ const escrowRows = CATEGORIES.filter((c) => c.routing === "escrow");
121
+ const autoRows = CATEGORIES.filter((c) => c.routing === "auto");
122
+ const groupedOptions = [
123
+ { value: INSTANT_HEADER, label: chalk.dim("── Instant · poll for result ──"), hint: "" },
124
+ ...instantRows.map((row) => ({ value: row.value, label: ` ${row.value}`, hint: formatHint(row) })),
125
+ { value: ESCROW_HEADER, label: chalk.dim("── Escrow · manual approve ──"), hint: "" },
126
+ ...escrowRows.map((row) => ({ value: row.value, label: ` ${row.value}`, hint: formatHint(row) })),
127
+ { value: AUTO_HEADER, label: chalk.dim("── Auto · routed by price ──"), hint: "" },
128
+ ...autoRows.map((row) => ({ value: row.value, label: ` ${row.value}`, hint: formatHint(row) })),
129
+ ];
130
+ const picked = await multiselect({
131
+ message: "Pick the skill categories to register (space to toggle, enter to confirm):",
132
+ options: groupedOptions,
133
+ required: true,
130
134
  });
131
- if (isCancel(skillName)) {
135
+ if (isCancel(picked)) {
132
136
  cancel("Setup cancelled");
133
137
  process.exit(0);
134
138
  }
135
- const skillNameStr = skillName.trim();
136
- // ── Step 3: description ──
137
- const description = await text({
138
- message: "One-line description (what does this skill do?):",
139
- placeholder: "Generate a 1024x1024 image from a text prompt",
140
- validate: validateDescription,
141
- });
142
- if (isCancel(description)) {
143
- cancel("Setup cancelled");
139
+ // Strip header sentinels — they're visual-only group separators.
140
+ const pickedCategories = picked.filter((v) => !HEADERS.has(v));
141
+ if (pickedCategories.length === 0) {
142
+ cancel("No categories selected");
144
143
  process.exit(0);
145
144
  }
146
- const descriptionStr = description.trim();
147
- // ── Step 4: price (suggested default per category) ──
148
- const priceInput = await text({
149
- message: `Price per call in USDC (suggested $${categoryRow.suggestedPrice.toFixed(2)}, range $${categoryRow.priceRange[0]}–$${categoryRow.priceRange[1]}):`,
150
- placeholder: categoryRow.suggestedPrice.toFixed(2),
151
- initialValue: categoryRow.suggestedPrice.toFixed(2),
152
- validate: validatePrice,
153
- });
154
- if (isCancel(priceInput)) {
155
- cancel("Setup cancelled");
156
- process.exit(0);
145
+ // Preserve the canonical CATEGORIES order rather than the click order —
146
+ // makes the per-skill prompts and the review table read consistently.
147
+ const orderedRows = CATEGORIES.filter((c) => pickedCategories.includes(c.value));
148
+ // ── Step 2: for each category, collect name / description / price ──
149
+ const drafts = [];
150
+ for (let i = 0; i < orderedRows.length; i++) {
151
+ const row = orderedRows[i];
152
+ log.step(`${chalk.cyan(row.value)} (${i + 1}/${orderedRows.length}) ${chalk.dim(formatHint(row))}`);
153
+ const skillName = await text({
154
+ message: " Skill name:",
155
+ placeholder: row.defaultName || "my-skill",
156
+ initialValue: row.defaultName,
157
+ validate: validateSkillName,
158
+ });
159
+ if (isCancel(skillName)) {
160
+ cancel("Setup cancelled — nothing was registered");
161
+ process.exit(0);
162
+ }
163
+ const description = await text({
164
+ message: " Description:",
165
+ placeholder: row.placeholderDesc,
166
+ validate: validateDescription,
167
+ });
168
+ if (isCancel(description)) {
169
+ cancel("Setup cancelled — nothing was registered");
170
+ process.exit(0);
171
+ }
172
+ const priceInput = await text({
173
+ message: ` Price per call in USDC ${chalk.dim(`(suggested $${row.suggestedPrice.toFixed(2)}, range $${row.priceRange[0]}–$${row.priceRange[1]})`)}:`,
174
+ placeholder: row.suggestedPrice.toFixed(2),
175
+ initialValue: row.suggestedPrice.toFixed(2),
176
+ validate: validatePrice,
177
+ });
178
+ if (isCancel(priceInput)) {
179
+ cancel("Setup cancelled — nothing was registered");
180
+ process.exit(0);
181
+ }
182
+ drafts.push({
183
+ category: row.value,
184
+ name: skillName.trim(),
185
+ description: description.trim(),
186
+ price: Number(priceInput.trim()),
187
+ });
157
188
  }
158
- const price = Number(priceInput.trim());
159
- // ── Step 5: review (show the resolved skill_type so the user knows
160
- // what they're agreeing to before commit) ──
161
- const skillType = resolveSkillType(categoryStr, price);
162
- const routingLabel = skillType === "escrow"
163
- ? "escrow (manual approve required)"
164
- : "instant (poll for result)";
189
+ // ── Step 3: review the batch (show resolved skill_type for each so the
190
+ // user knows which ones will go through escrow before they confirm) ──
191
+ const reviewLines = drafts.map((d, idx) => {
192
+ const skillType = resolveSkillType(d.category, d.price);
193
+ return ` ${String(idx + 1).padStart(2, " ")}. ${chalk.cyan(d.name.padEnd(18))} ${d.category.padEnd(26)} ${chalk.green(`$${d.price.toFixed(2)}`.padStart(6, " "))} ${skillType === "escrow" ? chalk.yellow("escrow") : chalk.dim("instant")}`;
194
+ });
195
+ // Tell the user only if escrow skills are in the batch — otherwise the
196
+ // extra explanation is noise.
197
+ const hasEscrow = drafts.some((d) => resolveSkillType(d.category, d.price) === "escrow");
165
198
  note([
166
- `Name: ${chalk.cyan(skillNameStr)}`,
167
- `Category: ${chalk.cyan(categoryStr)}`,
168
- `Price: ${chalk.green(`$${price.toFixed(2)} USDC / call`)}`,
169
- `Description: "${descriptionStr}"`,
170
- "",
171
- `Routing: ${chalk.bold(routingLabel)}`,
172
- chalk.dim(routingExplanation(skillType)),
173
- ].join("\n"), "Review");
199
+ ...reviewLines,
200
+ ...(hasEscrow
201
+ ? [
202
+ "",
203
+ chalk.dim(`Escrow skills require manual approve from the caller — funds`),
204
+ chalk.dim(`stay locked until you deliver and they release. Good for tasks`),
205
+ chalk.dim(`that take minutes to hours (e.g. long video).`),
206
+ ]
207
+ : []),
208
+ ].join("\n"), `Review · ${drafts.length} ${drafts.length === 1 ? "skill" : "skills"} to register`);
174
209
  const proceed = await confirm({
175
- message: "Confirm and register?",
210
+ message: `Confirm and register ${drafts.length === 1 ? "this skill" : `all ${drafts.length} skills`}?`,
176
211
  initialValue: true,
177
212
  });
178
213
  if (isCancel(proceed) || !proceed) {
179
214
  cancel("Setup cancelled — nothing was registered");
180
215
  process.exit(0);
181
216
  }
182
- // ── Step 6: register ──
183
- const submitSpin = spinner();
184
- submitSpin.start("Registering skill...");
185
- // Backend's AgentSkillCreate has extra='forbid', so we send ONLY the
186
- // four allowed fields. skill_type is intentionally not sent — the server
187
- // derives it from category and the routing rule we previewed above.
188
- const resp = await apiPost("/api/v1/market/skills", {
189
- skill_name: skillNameStr,
190
- category: categoryStr,
191
- description: descriptionStr,
192
- price,
193
- }, config.api_key);
194
- if (!resp.ok) {
195
- const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
196
- ? resp.data.detail
197
- : resp.data;
198
- const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
199
- submitSpin.stop(chalk.red(`Failed (${resp.status}): ${detail}`));
200
- process.exit(1);
217
+ // ── Step 4: sequential register. One failure does not abort the rest;
218
+ // we show a per-skill summary at the end so the user can re-run for the
219
+ // failures. Atomicity would need a backend batch endpoint we don't have. ──
220
+ const results = [];
221
+ for (const draft of drafts) {
222
+ const s = spinner();
223
+ s.start(`Registering ${chalk.cyan(draft.name)}...`);
224
+ // Backend's AgentSkillCreate has extra='forbid', so we send ONLY the
225
+ // four allowed fields. skill_type is intentionally not sent — the
226
+ // server derives it from category and the routing rule previewed above.
227
+ const resp = await apiPost("/api/v1/market/skills", {
228
+ skill_name: draft.name,
229
+ category: draft.category,
230
+ description: draft.description,
231
+ price: draft.price,
232
+ }, config.api_key);
233
+ if (resp.ok) {
234
+ s.stop(`${chalk.green("✓")} ${draft.name}`);
235
+ results.push({ draft, ok: true });
236
+ }
237
+ else {
238
+ const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
239
+ ? resp.data.detail
240
+ : resp.data;
241
+ const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
242
+ s.stop(`${chalk.red("✗")} ${draft.name} ${chalk.dim(`(${detail})`)}`);
243
+ results.push({ draft, ok: false, detail });
244
+ }
201
245
  }
202
- submitSpin.stop(chalk.green("Skill registered."));
203
- outro([
204
- chalk.green("Done."),
246
+ const okCount = results.filter((r) => r.ok).length;
247
+ const failCount = results.length - okCount;
248
+ const summary = [
249
+ failCount === 0
250
+ ? chalk.green(`All ${okCount} skills registered.`)
251
+ : okCount === 0
252
+ ? chalk.red(`None registered (${failCount} failed).`)
253
+ : chalk.yellow(`${okCount} registered, ${failCount} failed.`),
205
254
  "",
206
255
  chalk.dim(`Next: run ${chalk.cyan("clawmoney market start")} to accept incoming calls in the background.`),
207
- chalk.dim(` See your skill listed: ${chalk.cyan("clawmoney market skills")}`),
208
- ].join("\n"));
256
+ chalk.dim(` See your skills listed: ${chalk.cyan("clawmoney market skills")}`),
257
+ ].join("\n");
258
+ if (opts.nested) {
259
+ // Don't close the parent wizard's intro frame — emit the summary as a
260
+ // log message and let the parent wrap up the whole flow at the end.
261
+ log.message(summary);
262
+ }
263
+ else {
264
+ outro(summary);
265
+ }
266
+ if (failCount > 0 && !opts.nested) {
267
+ process.exit(1);
268
+ }
209
269
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Provider setup wizard. Assumes the agent is already registered
3
+ * (api_key in ~/.clawmoney/config.yaml). Callable on its own — used both
4
+ * as the post-register step of `clawmoney setup` and as a re-entry point
5
+ * for users who want to add roles after their first setup.
6
+ */
7
+ export declare function providerSetupWizard(): Promise<void>;
@@ -0,0 +1,94 @@
1
+ import { multiselect, isCancel, log, note, } from "@clack/prompts";
2
+ import chalk from "chalk";
3
+ import { loadConfig } from "../utils/config.js";
4
+ const ROLES = [
5
+ {
6
+ value: "market",
7
+ label: "Market skills",
8
+ hint: "image gen / code / translate / tts / ... — agents pay you per call",
9
+ },
10
+ {
11
+ value: "relay",
12
+ label: "Relay",
13
+ hint: "sell idle Claude Max / ChatGPT Pro / Gemini quota at 20% of API price",
14
+ },
15
+ {
16
+ value: "verifier",
17
+ label: "Verifier",
18
+ hint: "witness tweet promote tasks — $0.01 per verification, runs in background",
19
+ },
20
+ ];
21
+ /**
22
+ * Provider setup wizard. Assumes the agent is already registered
23
+ * (api_key in ~/.clawmoney/config.yaml). Callable on its own — used both
24
+ * as the post-register step of `clawmoney setup` and as a re-entry point
25
+ * for users who want to add roles after their first setup.
26
+ */
27
+ export async function providerSetupWizard() {
28
+ if (!loadConfig()) {
29
+ console.log(chalk.red("\n No agent config found. Run `clawmoney setup` first to register.\n"));
30
+ process.exit(1);
31
+ }
32
+ log.message(chalk.bold("Provider roles") +
33
+ chalk.dim(" — pick what you want to earn from. You can re-run this anytime."));
34
+ const picked = await multiselect({
35
+ message: "Provider roles (space to toggle, enter to confirm):",
36
+ options: ROLES.map((r) => ({
37
+ value: r.value,
38
+ label: r.label,
39
+ hint: r.hint,
40
+ })),
41
+ required: false, // user is allowed to skip — they may have just come to register
42
+ });
43
+ if (isCancel(picked)) {
44
+ log.message(chalk.dim("Skipped. Re-run `clawmoney setup` later to enable provider roles."));
45
+ return;
46
+ }
47
+ const roles = picked;
48
+ if (roles.length === 0) {
49
+ note([
50
+ chalk.dim("No roles enabled. You can still:"),
51
+ ` ${chalk.cyan("clawmoney browse")} browse engage tasks`,
52
+ ` ${chalk.cyan("clawmoney promote")} work on promote tasks`,
53
+ "",
54
+ chalk.dim("Or re-run `clawmoney setup` later to enable provider roles."),
55
+ ].join("\n"), "Done");
56
+ return;
57
+ }
58
+ // Sort picked roles in canonical ROLES order so the wizard always runs
59
+ // the same sequence (market → relay → verifier) regardless of click order.
60
+ const ordered = ROLES.filter((r) => roles.includes(r.value)).map((r) => r.value);
61
+ for (let i = 0; i < ordered.length; i++) {
62
+ const role = ordered[i];
63
+ log.step(`${chalk.bold(`[${i + 1}/${ordered.length}]`)} ${role}`);
64
+ try {
65
+ if (role === "market") {
66
+ const { marketSetupCommand } = await import("./market-setup.js");
67
+ await marketSetupCommand({ nested: true });
68
+ }
69
+ else if (role === "relay") {
70
+ const { relaySetupCommand } = await import("./relay-setup.js");
71
+ await relaySetupCommand();
72
+ }
73
+ else if (role === "verifier") {
74
+ const { verifierSetupCommand } = await import("./verifier-setup.js");
75
+ await verifierSetupCommand({ nested: true });
76
+ }
77
+ }
78
+ catch (err) {
79
+ log.error(`${role} setup failed: ${err.message}. ` +
80
+ `You can retry with \`clawmoney ${role} setup\` later.`);
81
+ // Don't abort the rest — let the user finish other roles. They can
82
+ // come back to the failed one separately.
83
+ }
84
+ }
85
+ note([
86
+ chalk.green(`${ordered.length} role${ordered.length === 1 ? "" : "s"} configured.`),
87
+ "",
88
+ chalk.dim("Useful next commands:"),
89
+ ` ${chalk.cyan("clawmoney market skills")} list your registered skills`,
90
+ ` ${chalk.cyan("clawmoney market start")} start the market provider daemon`,
91
+ ` ${chalk.cyan("clawmoney relay start")} start the relay daemon`,
92
+ ` ${chalk.cyan("tail -f ~/.clawmoney/*.log")} watch all daemons`,
93
+ ].join("\n"), "All done");
94
+ }
@@ -38,6 +38,22 @@ export async function setupCommand() {
38
38
  console.log('');
39
39
  return;
40
40
  }
41
+ // Short-circuit: if an agent is already configured, skip the OTP + claim
42
+ // flow and jump straight to provider role selection. This makes re-running
43
+ // `clawmoney setup` cheap — users add new provider roles without going
44
+ // through email verification every time.
45
+ const existing = loadConfig();
46
+ if (existing?.api_key && existing?.agent_slug) {
47
+ console.log(chalk.green(' Agent already configured.'));
48
+ console.log(chalk.dim(` Slug: ${existing.agent_slug}`));
49
+ if (existing.wallet_address) {
50
+ console.log(chalk.dim(` Wallet: ${existing.wallet_address}`));
51
+ }
52
+ console.log('');
53
+ const { providerSetupWizard } = await import('./provider-setup.js');
54
+ await providerSetupWizard();
55
+ return;
56
+ }
41
57
  // Step 1: Ask for email.
42
58
  const email = await prompt(chalk.cyan('? ') + 'Enter your email: ');
43
59
  if (!email || !email.includes('@')) {
@@ -228,9 +244,9 @@ export async function setupCommand() {
228
244
  console.log(chalk.dim(` Config: ${getConfigPath()}`));
229
245
  }
230
246
  console.log('');
231
- console.log(` Next steps:`);
232
- console.log(` ${chalk.cyan('clawmoney browse')} Browse available tasks`);
233
- console.log(` ${chalk.cyan('clawmoney wallet balance')} Check your wallet balance`);
234
- console.log(` ${chalk.cyan('clawmoney promote submit')} Submit a task proof`);
235
- console.log('');
247
+ // Continue into provider role selection. First-time users go straight
248
+ // from "agent claimed" → "pick what to earn from" without ever needing
249
+ // to remember a second command. They can still skip (no role = no role).
250
+ const { providerSetupWizard } = await import('./provider-setup.js');
251
+ await providerSetupWizard();
236
252
  }
@@ -0,0 +1,3 @@
1
+ export declare function verifierSetupCommand(opts?: {
2
+ nested?: boolean;
3
+ }): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { intro, outro, confirm, spinner, isCancel, log, note, } from "@clack/prompts";
5
+ import chalk from "chalk";
6
+ import { loadConfig } from "../utils/config.js";
7
+ const LOG_FILE = join(homedir(), ".clawmoney", "verifier.log");
8
+ // Auto-verifier polls every 15 minutes and verifies up to 3 tweets per cycle.
9
+ // Each verification pays $0.01 via x402 witness. Upper bound at 24/7 saturation
10
+ // is 3 × 4/hr × 24 × 30 = 8,640/mo, but real demand caps it much lower —
11
+ // we deliberately quote the per-cycle rate so users see honest numbers.
12
+ const VERIFICATIONS_PER_CYCLE = 3;
13
+ const POLL_INTERVAL_MIN = 15;
14
+ const PRICE_PER_VERIFICATION = 0.01;
15
+ function formatEarnings() {
16
+ const perHour = (60 / POLL_INTERVAL_MIN) * VERIFICATIONS_PER_CYCLE * PRICE_PER_VERIFICATION;
17
+ const perDay = perHour * 24;
18
+ const perMonth = perDay * 30;
19
+ return [
20
+ `Per cycle: $${(VERIFICATIONS_PER_CYCLE * PRICE_PER_VERIFICATION).toFixed(2)} (${VERIFICATIONS_PER_CYCLE} verifications × $${PRICE_PER_VERIFICATION})`,
21
+ `Cycle: every ${POLL_INTERVAL_MIN} minutes`,
22
+ `Upper bound: ~$${perHour.toFixed(2)}/hr · ~$${perDay.toFixed(2)}/day · ~$${perMonth.toFixed(0)}/mo`,
23
+ chalk.dim("Actual earnings depend on how many tasks are awaiting verification."),
24
+ ].join("\n");
25
+ }
26
+ export async function verifierSetupCommand(opts = {}) {
27
+ if (!loadConfig()) {
28
+ console.log(chalk.red("\n No config found. Run `clawmoney setup` first to register your agent.\n"));
29
+ process.exit(1);
30
+ }
31
+ if (!opts.nested) {
32
+ intro(chalk.cyan(" ClawMoney Verifier Setup "));
33
+ }
34
+ log.message("Run an auto-verifier daemon that earns by witnessing tweet promote tasks.");
35
+ note(formatEarnings(), "Earnings model");
36
+ const startNow = await confirm({
37
+ message: "Start the verifier daemon in the background now?",
38
+ initialValue: true,
39
+ });
40
+ if (isCancel(startNow)) {
41
+ log.message(chalk.dim("Skipped. Run `clawmoney promote auto-verify` later to start manually."));
42
+ if (!opts.nested)
43
+ outro("");
44
+ return;
45
+ }
46
+ if (!startNow) {
47
+ log.message(chalk.dim(`Skipped daemon launch. Manual start: ${chalk.cyan("clawmoney promote auto-verify")}`));
48
+ if (!opts.nested)
49
+ outro("");
50
+ return;
51
+ }
52
+ // Daemon launch: spawn detached so it survives this process exiting.
53
+ // stdout/stderr go to ~/.clawmoney/verifier.log — same pattern as relay
54
+ // daemon. We deliberately don't `setsid` here; users on macOS run from a
55
+ // GUI shell and the parent's session id is fine.
56
+ const claw = process.execPath; // node binary path; clawmoney bin script runs through it
57
+ const cliMain = process.argv[1]; // path to dist/index.js
58
+ const s = spinner();
59
+ s.start("Spawning verifier daemon...");
60
+ try {
61
+ const out = await import("node:fs").then((m) => m.openSync(LOG_FILE, "a"));
62
+ const child = spawn(claw, [cliMain, "promote", "auto-verify"], {
63
+ detached: true,
64
+ stdio: ["ignore", out, out],
65
+ env: process.env,
66
+ });
67
+ child.unref();
68
+ s.stop(`${chalk.green("✓")} Verifier daemon started (pid ${child.pid})`);
69
+ log.message([
70
+ chalk.dim(`Logs: ${LOG_FILE}`),
71
+ chalk.dim(`Tail: tail -f ${LOG_FILE}`),
72
+ chalk.dim(`Stop: kill ${child.pid}`),
73
+ "",
74
+ chalk.dim("Tip: to keep it running across reboots, wrap it in a launchd plist"),
75
+ chalk.dim("(same pattern as scripts/install-daemon-launchd.sh in this repo)."),
76
+ ].join("\n"));
77
+ }
78
+ catch (err) {
79
+ s.stop(chalk.red(`Failed to spawn verifier: ${err.message}`));
80
+ }
81
+ if (!opts.nested) {
82
+ outro(chalk.green("Verifier setup done."));
83
+ }
84
+ }
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ program
21
21
  // setup
22
22
  program
23
23
  .command('setup')
24
- .description('One-click agent onboarding: wallet + registration')
24
+ .description('One-stop setup: register agent (first run) + pick provider roles (market / relay / verifier)')
25
25
  .action(async () => {
26
26
  try {
27
27
  await setupCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.17.7",
3
+ "version": "0.17.9",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {