clawmoney 0.17.7 → 0.17.8

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,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,7 +88,6 @@ function validatePrice(value) {
88
88
  return "Price looks unreasonable (> $10,000)";
89
89
  return undefined;
90
90
  }
91
- // ── Main wizard ──
92
91
  export async function marketSetupCommand() {
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
@@ -105,105 +104,137 @@ export async function marketSetupCommand() {
105
104
  }
106
105
  const config = existing;
107
106
  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:",
107
+ log.message("Register one or more skills on the Market so other agents can call (and pay) you.");
108
+ // ── Step 1: multiselect categories (the big difference vs relay setup —
109
+ // each picked category becomes one skill, no dupes within this run) ──
110
+ const picked = await multiselect({
111
+ message: "Pick the skill categories to register (space to toggle, enter to confirm):",
112
112
  options: CATEGORIES.map((row) => ({
113
113
  value: row.value,
114
114
  label: row.value,
115
115
  hint: formatHint(row),
116
116
  })),
117
- initialValue: "generation/image",
117
+ required: true,
118
118
  });
119
- if (isCancel(category)) {
119
+ if (isCancel(picked)) {
120
120
  cancel("Setup cancelled");
121
121
  process.exit(0);
122
122
  }
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,
130
- });
131
- if (isCancel(skillName)) {
132
- cancel("Setup cancelled");
133
- process.exit(0);
134
- }
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");
144
- process.exit(0);
123
+ const pickedCategories = picked;
124
+ // Preserve the canonical CATEGORIES order rather than the click order —
125
+ // makes the per-skill prompts and the review table read consistently.
126
+ const orderedRows = CATEGORIES.filter((c) => pickedCategories.includes(c.value));
127
+ // ── Step 2: for each category, collect name / description / price ──
128
+ const drafts = [];
129
+ for (let i = 0; i < orderedRows.length; i++) {
130
+ const row = orderedRows[i];
131
+ log.step(`${chalk.cyan(row.value)} (${i + 1}/${orderedRows.length}) ${chalk.dim(formatHint(row))}`);
132
+ const skillName = await text({
133
+ message: " Skill name:",
134
+ placeholder: row.defaultName || "my-skill",
135
+ initialValue: row.defaultName,
136
+ validate: validateSkillName,
137
+ });
138
+ if (isCancel(skillName)) {
139
+ cancel("Setup cancelled nothing was registered");
140
+ process.exit(0);
141
+ }
142
+ const description = await text({
143
+ message: " Description:",
144
+ placeholder: row.placeholderDesc,
145
+ validate: validateDescription,
146
+ });
147
+ if (isCancel(description)) {
148
+ cancel("Setup cancelled — nothing was registered");
149
+ process.exit(0);
150
+ }
151
+ const priceInput = await text({
152
+ message: ` Price per call in USDC ${chalk.dim(`(suggested $${row.suggestedPrice.toFixed(2)}, range $${row.priceRange[0]}–$${row.priceRange[1]})`)}:`,
153
+ placeholder: row.suggestedPrice.toFixed(2),
154
+ initialValue: row.suggestedPrice.toFixed(2),
155
+ validate: validatePrice,
156
+ });
157
+ if (isCancel(priceInput)) {
158
+ cancel("Setup cancelled — nothing was registered");
159
+ process.exit(0);
160
+ }
161
+ drafts.push({
162
+ category: row.value,
163
+ name: skillName.trim(),
164
+ description: description.trim(),
165
+ price: Number(priceInput.trim()),
166
+ });
145
167
  }
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,
168
+ // ── Step 3: review the batch (show resolved skill_type for each so the
169
+ // user knows which ones will go through escrow before they confirm) ──
170
+ const reviewLines = drafts.map((d, idx) => {
171
+ const skillType = resolveSkillType(d.category, d.price);
172
+ 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")}`;
153
173
  });
154
- if (isCancel(priceInput)) {
155
- cancel("Setup cancelled");
156
- process.exit(0);
157
- }
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)";
174
+ // Tell the user only if escrow skills are in the batch — otherwise the
175
+ // extra explanation is noise.
176
+ const hasEscrow = drafts.some((d) => resolveSkillType(d.category, d.price) === "escrow");
165
177
  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");
178
+ ...reviewLines,
179
+ ...(hasEscrow
180
+ ? [
181
+ "",
182
+ chalk.dim(`Escrow skills require manual approve from the caller — funds`),
183
+ chalk.dim(`stay locked until you deliver and they release. Good for tasks`),
184
+ chalk.dim(`that take minutes to hours (e.g. long video).`),
185
+ ]
186
+ : []),
187
+ ].join("\n"), `Review · ${drafts.length} ${drafts.length === 1 ? "skill" : "skills"} to register`);
174
188
  const proceed = await confirm({
175
- message: "Confirm and register?",
189
+ message: `Confirm and register ${drafts.length === 1 ? "this skill" : `all ${drafts.length} skills`}?`,
176
190
  initialValue: true,
177
191
  });
178
192
  if (isCancel(proceed) || !proceed) {
179
193
  cancel("Setup cancelled — nothing was registered");
180
194
  process.exit(0);
181
195
  }
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);
196
+ // ── Step 4: sequential register. One failure does not abort the rest;
197
+ // we show a per-skill summary at the end so the user can re-run for the
198
+ // failures. Atomicity would need a backend batch endpoint we don't have. ──
199
+ const results = [];
200
+ for (const draft of drafts) {
201
+ const s = spinner();
202
+ s.start(`Registering ${chalk.cyan(draft.name)}...`);
203
+ // Backend's AgentSkillCreate has extra='forbid', so we send ONLY the
204
+ // four allowed fields. skill_type is intentionally not sent — the
205
+ // server derives it from category and the routing rule previewed above.
206
+ const resp = await apiPost("/api/v1/market/skills", {
207
+ skill_name: draft.name,
208
+ category: draft.category,
209
+ description: draft.description,
210
+ price: draft.price,
211
+ }, config.api_key);
212
+ if (resp.ok) {
213
+ s.stop(`${chalk.green("✓")} ${draft.name}`);
214
+ results.push({ draft, ok: true });
215
+ }
216
+ else {
217
+ const raw = resp.data && typeof resp.data === "object" && "detail" in resp.data
218
+ ? resp.data.detail
219
+ : resp.data;
220
+ const detail = typeof raw === "string" ? raw : JSON.stringify(raw);
221
+ s.stop(`${chalk.red("✗")} ${draft.name} ${chalk.dim(`(${detail})`)}`);
222
+ results.push({ draft, ok: false, detail });
223
+ }
201
224
  }
202
- submitSpin.stop(chalk.green("Skill registered."));
225
+ const okCount = results.filter((r) => r.ok).length;
226
+ const failCount = results.length - okCount;
203
227
  outro([
204
- chalk.green("Done."),
228
+ failCount === 0
229
+ ? chalk.green(`All ${okCount} skills registered.`)
230
+ : okCount === 0
231
+ ? chalk.red(`None registered (${failCount} failed).`)
232
+ : chalk.yellow(`${okCount} registered, ${failCount} failed.`),
205
233
  "",
206
234
  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")}`),
235
+ chalk.dim(` See your skills listed: ${chalk.cyan("clawmoney market skills")}`),
208
236
  ].join("\n"));
237
+ if (failCount > 0) {
238
+ process.exit(1);
239
+ }
209
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawmoney",
3
- "version": "0.17.7",
3
+ "version": "0.17.8",
4
4
  "description": "ClawMoney CLI -- Earn rewards with your AI agent",
5
5
  "type": "module",
6
6
  "bin": {