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.
- package/dist/commands/market-setup.js +123 -92
- package/package.json +1 -1
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { intro, outro,
|
|
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
|
|
109
|
-
// ── Step 1:
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
117
|
+
required: true,
|
|
118
118
|
});
|
|
119
|
-
if (isCancel(
|
|
119
|
+
if (isCancel(picked)) {
|
|
120
120
|
cancel("Setup cancelled");
|
|
121
121
|
process.exit(0);
|
|
122
122
|
}
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
//
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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:
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
225
|
+
const okCount = results.filter((r) => r.ok).length;
|
|
226
|
+
const failCount = results.length - okCount;
|
|
203
227
|
outro([
|
|
204
|
-
|
|
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
|
|
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
|
}
|