clawmoney 0.17.6 → 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.d.ts +1 -0
- package/dist/commands/market-setup.js +240 -0
- package/dist/index.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function marketSetupCommand(): Promise<void>;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { intro, outro, multiselect, text, confirm, spinner, isCancel, cancel, log, note, } from "@clack/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { apiPost } from "../utils/api.js";
|
|
4
|
+
import { loadConfig } from "../utils/config.js";
|
|
5
|
+
import { setupCommand } from "./setup.js";
|
|
6
|
+
const CATEGORIES = [
|
|
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
|
+
];
|
|
21
|
+
const PRICE_THRESHOLD_FOR_ESCROW = 1.0; // mirrors backend constant
|
|
22
|
+
function formatHint(row) {
|
|
23
|
+
if (row.routing === "escrow")
|
|
24
|
+
return "escrow · manual approve";
|
|
25
|
+
if (row.routing === "auto")
|
|
26
|
+
return "auto · by price";
|
|
27
|
+
return `instant · ${String(row.timeoutS).padStart(3, " ")}s timeout`;
|
|
28
|
+
}
|
|
29
|
+
// What skill_type will the backend resolve this to? Used for the review
|
|
30
|
+
// screen — the user gets to see and confirm the routing decision before
|
|
31
|
+
// commit. Keep this in sync with app/core/market_skill_routing.py.
|
|
32
|
+
function resolveSkillType(category, price) {
|
|
33
|
+
const row = CATEGORIES.find((c) => c.value === category);
|
|
34
|
+
if (!row)
|
|
35
|
+
return "instant";
|
|
36
|
+
if (row.routing === "instant")
|
|
37
|
+
return "instant";
|
|
38
|
+
if (row.routing === "escrow")
|
|
39
|
+
return "escrow";
|
|
40
|
+
return price > PRICE_THRESHOLD_FOR_ESCROW ? "escrow" : "instant";
|
|
41
|
+
}
|
|
42
|
+
function routingExplanation(skillType) {
|
|
43
|
+
if (skillType === "escrow") {
|
|
44
|
+
return [
|
|
45
|
+
"Callers fund the task up front, you accept, deliver,",
|
|
46
|
+
"and they approve to release funds. Good for tasks that",
|
|
47
|
+
"take minutes to hours.",
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
return [
|
|
51
|
+
"Callers invoke with x402 payment, you respond via WebSocket,",
|
|
52
|
+
"they poll for the result. Good for tasks that finish in",
|
|
53
|
+
"seconds to a few minutes.",
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
// ── Validators ──
|
|
57
|
+
// Skill names live in URLs (market/<agent_slug>/<skill_name>) and config
|
|
58
|
+
// files, so we keep them URL-safe and short. Same regex as backend slugs.
|
|
59
|
+
function validateSkillName(value) {
|
|
60
|
+
const v = value.trim();
|
|
61
|
+
if (!v)
|
|
62
|
+
return "Skill name is required";
|
|
63
|
+
if (v.length > 100)
|
|
64
|
+
return "Skill name must be 100 characters or fewer";
|
|
65
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(v)) {
|
|
66
|
+
return "Use lowercase letters, digits, and hyphens only (e.g. gen-image)";
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function validateDescription(value) {
|
|
71
|
+
const v = value.trim();
|
|
72
|
+
if (!v)
|
|
73
|
+
return "Description is required";
|
|
74
|
+
if (v.length > 1000)
|
|
75
|
+
return "Description must be 1000 characters or fewer";
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
function validatePrice(value) {
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
if (!trimmed)
|
|
81
|
+
return "Price is required";
|
|
82
|
+
const n = Number(trimmed);
|
|
83
|
+
if (!Number.isFinite(n))
|
|
84
|
+
return "Price must be a number";
|
|
85
|
+
if (n < 0)
|
|
86
|
+
return "Price cannot be negative";
|
|
87
|
+
if (n > 10_000)
|
|
88
|
+
return "Price looks unreasonable (> $10,000)";
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
export async function marketSetupCommand() {
|
|
92
|
+
// Step 0: ensure the agent is logged in. Mirrors relaySetupCommand's
|
|
93
|
+
// handoff to setupCommand so first-time users get a clean flow instead
|
|
94
|
+
// of "No config found" mid-wizard.
|
|
95
|
+
let existing = loadConfig();
|
|
96
|
+
if (!existing) {
|
|
97
|
+
await setupCommand();
|
|
98
|
+
existing = loadConfig();
|
|
99
|
+
if (!existing) {
|
|
100
|
+
console.log(chalk.red("\n Login did not complete. Run `clawmoney setup` manually, then re-run `clawmoney market setup`.\n"));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
console.log("");
|
|
104
|
+
}
|
|
105
|
+
const config = existing;
|
|
106
|
+
intro(chalk.cyan(" ClawMoney Market Setup "));
|
|
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
|
+
options: CATEGORIES.map((row) => ({
|
|
113
|
+
value: row.value,
|
|
114
|
+
label: row.value,
|
|
115
|
+
hint: formatHint(row),
|
|
116
|
+
})),
|
|
117
|
+
required: true,
|
|
118
|
+
});
|
|
119
|
+
if (isCancel(picked)) {
|
|
120
|
+
cancel("Setup cancelled");
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
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
|
+
});
|
|
167
|
+
}
|
|
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")}`;
|
|
173
|
+
});
|
|
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");
|
|
177
|
+
note([
|
|
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`);
|
|
188
|
+
const proceed = await confirm({
|
|
189
|
+
message: `Confirm and register ${drafts.length === 1 ? "this skill" : `all ${drafts.length} skills`}?`,
|
|
190
|
+
initialValue: true,
|
|
191
|
+
});
|
|
192
|
+
if (isCancel(proceed) || !proceed) {
|
|
193
|
+
cancel("Setup cancelled — nothing was registered");
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
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
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const okCount = results.filter((r) => r.ok).length;
|
|
226
|
+
const failCount = results.length - okCount;
|
|
227
|
+
outro([
|
|
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.`),
|
|
233
|
+
"",
|
|
234
|
+
chalk.dim(`Next: run ${chalk.cyan("clawmoney market start")} to accept incoming calls in the background.`),
|
|
235
|
+
chalk.dim(` See your skills listed: ${chalk.cyan("clawmoney market skills")}`),
|
|
236
|
+
].join("\n"));
|
|
237
|
+
if (failCount > 0) {
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -228,6 +228,19 @@ program
|
|
|
228
228
|
const market = program
|
|
229
229
|
.command('market')
|
|
230
230
|
.description('Agent Market: provide services, register skills');
|
|
231
|
+
market
|
|
232
|
+
.command('setup')
|
|
233
|
+
.description('Interactive: register a skill on the Market with a guided wizard (recommended for first-time setup)')
|
|
234
|
+
.action(async () => {
|
|
235
|
+
try {
|
|
236
|
+
const { marketSetupCommand } = await import('./commands/market-setup.js');
|
|
237
|
+
await marketSetupCommand();
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
console.error(err.message);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
231
244
|
market
|
|
232
245
|
.command('start')
|
|
233
246
|
.description('Start Market Provider (background process)')
|