@caravo/cli 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Caravo AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Caravo CLI
2
+
3
+ Command-line interface for [Caravo](https://caravo.ai) — search, execute, and review marketplace tools with API key or x402 USDC payments.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @caravo/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Search for tools
15
+ caravo search "image generation" --per-page 5
16
+
17
+ # Get tool details
18
+ caravo info fal-ai/flux/schnell
19
+
20
+ # Execute a tool
21
+ caravo exec fal-ai/flux/schnell -d '{"prompt": "a sunset over mountains"}'
22
+
23
+ # Preview cost without paying
24
+ caravo dry-run fal-ai/flux/schnell -d '{"prompt": "test"}'
25
+
26
+ # Submit a review
27
+ caravo review EXECUTION_ID --rating 5 --comment "Great quality"
28
+
29
+ # Upvote an existing review
30
+ caravo upvote REVIEW_ID --exec EXECUTION_ID
31
+
32
+ # Manage favorites (requires API key)
33
+ caravo fav list
34
+ caravo fav add fal-ai/flux/schnell
35
+ caravo fav rm fal-ai/flux/schnell
36
+
37
+ # Check wallet
38
+ caravo wallet
39
+
40
+ # Raw x402 HTTP
41
+ caravo fetch https://example.com/api
42
+ caravo fetch POST https://example.com/api -d '{"key": "value"}'
43
+ ```
44
+
45
+ ## Payment
46
+
47
+ Payment is transparent — the same commands work in either mode:
48
+
49
+ - **API key mode**: Set `CARAVO_API_KEY` — balance is deducted per call
50
+ - **x402 USDC mode**: No API key needed. The CLI auto-manages a wallet and signs USDC payments on Base
51
+
52
+ ## Commands
53
+
54
+ | Command | Description |
55
+ |---------|-------------|
56
+ | `search` | Search tools by query, tag, or provider |
57
+ | `info` | Get tool details, pricing, and reviews |
58
+ | `exec` | Execute a tool |
59
+ | `dry-run` | Preview execution cost |
60
+ | `review` | Submit a review |
61
+ | `upvote` | Upvote an existing review |
62
+ | `fav` | Manage favorites (list, add, rm) |
63
+ | `tags` | List all categories |
64
+ | `providers` | List all providers |
65
+ | `requests` | List tool requests |
66
+ | `request` | Submit a tool request |
67
+ | `request-upvote` | Upvote a tool request |
68
+ | `wallet` | Show wallet address and USDC balance |
69
+ | `fetch` | Raw x402-protected HTTP requests |
70
+
71
+ ## Development
72
+
73
+ ```bash
74
+ npm install
75
+ npm run build
76
+ npm link # makes `caravo` available globally
77
+ ```
78
+
79
+ ## License
80
+
81
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ import { resolveAuth } from "./lib/auth.js";
3
+ import { log } from "./lib/output.js";
4
+ const HELP = `caravo — Caravo CLI
5
+
6
+ Usage:
7
+ caravo <command> [args] [options]
8
+
9
+ Commands:
10
+ search [query] Search tools by keyword
11
+ tags List all tags/categories
12
+ providers List all providers
13
+ info <tool-id> Get tool details + reviews
14
+ exec <tool-id> -d <json> Execute a tool
15
+ dry-run <tool-id> -d <json> Preview execution cost
16
+ review <exec-id> --rating <1-5> --comment <text>
17
+ Submit a review
18
+ upvote <review-id> --exec <exec-id>
19
+ Upvote a review
20
+ fav list|add|rm [tool-id] Manage favorites (API key required)
21
+ requests List tool requests
22
+ request --title <t> --desc <d>
23
+ Submit a tool request
24
+ request-upvote <req-id> Upvote a tool request
25
+ wallet Show wallet + balance info
26
+ fetch [METHOD] <url> Raw x402 HTTP request
27
+
28
+ Options:
29
+ --tag <name|slug> Filter by tag name or slug (search)
30
+ --provider <name|slug> Filter by provider name or slug (search)
31
+ --page <n> Page number
32
+ --per-page <n> Results per page
33
+ --status <s> Filter requests by status (open|fulfilled|closed)
34
+ --rating <1-5> Review rating
35
+ --comment <text> Review comment
36
+ --exec <id> Execution ID (for upvote/request)
37
+ --title <text> Tool request title
38
+ --desc <text> Tool request description
39
+ --use-case <text> Tool request use case
40
+ --agent-id <id> Agent identifier
41
+ --api-key <key> API key (default: $CARAVO_API_KEY)
42
+ --base-url <url> API base URL
43
+ --compact Compact JSON output (single line)
44
+ -d, --data <json> Request body (JSON string)
45
+ -H, --header <k:v> Additional header (fetch command, repeatable)
46
+ -o, --output <file> Write response to file (fetch command)
47
+ -w, --wallet <path> Custom wallet path
48
+ -h, --help Show this help
49
+ -v, --version Show version
50
+ `;
51
+ function parseArgs(argv) {
52
+ const args = {
53
+ subcommand: "",
54
+ positional: [],
55
+ data: null,
56
+ headers: {},
57
+ output: null,
58
+ walletPath: undefined,
59
+ apiKey: undefined,
60
+ baseUrl: undefined,
61
+ compact: false,
62
+ dryRun: false,
63
+ help: false,
64
+ version: false,
65
+ tag: undefined,
66
+ provider: undefined,
67
+ page: undefined,
68
+ perPage: undefined,
69
+ rating: undefined,
70
+ comment: undefined,
71
+ exec: undefined,
72
+ title: undefined,
73
+ desc: undefined,
74
+ useCase: undefined,
75
+ status: undefined,
76
+ agentId: undefined,
77
+ };
78
+ let i = 0;
79
+ // First non-flag arg is the subcommand
80
+ while (i < argv.length && argv[i].startsWith("-")) {
81
+ // Handle global flags before subcommand
82
+ if (argv[i] === "-h" || argv[i] === "--help") {
83
+ args.help = true;
84
+ }
85
+ else if (argv[i] === "-v" || argv[i] === "--version") {
86
+ args.version = true;
87
+ }
88
+ i++;
89
+ }
90
+ if (i < argv.length && !argv[i].startsWith("-")) {
91
+ args.subcommand = argv[i];
92
+ i++;
93
+ }
94
+ // Parse remaining args
95
+ while (i < argv.length) {
96
+ const arg = argv[i];
97
+ if (arg === "-h" || arg === "--help") {
98
+ args.help = true;
99
+ }
100
+ else if (arg === "-v" || arg === "--version") {
101
+ args.version = true;
102
+ }
103
+ else if (arg === "--compact") {
104
+ args.compact = true;
105
+ }
106
+ else if (arg === "--dry-run") {
107
+ args.dryRun = true;
108
+ }
109
+ else if (arg === "-d" || arg === "--data") {
110
+ args.data = argv[++i];
111
+ }
112
+ else if (arg === "-H" || arg === "--header") {
113
+ const val = argv[++i];
114
+ if (val) {
115
+ const colonIdx = val.indexOf(":");
116
+ if (colonIdx > 0) {
117
+ args.headers[val.slice(0, colonIdx).trim()] = val.slice(colonIdx + 1).trim();
118
+ }
119
+ }
120
+ }
121
+ else if (arg === "-o" || arg === "--output") {
122
+ args.output = argv[++i];
123
+ }
124
+ else if (arg === "-w" || arg === "--wallet") {
125
+ args.walletPath = argv[++i];
126
+ }
127
+ else if (arg === "--api-key") {
128
+ args.apiKey = argv[++i];
129
+ }
130
+ else if (arg === "--base-url") {
131
+ args.baseUrl = argv[++i];
132
+ }
133
+ else if (arg === "--tag") {
134
+ args.tag = argv[++i];
135
+ }
136
+ else if (arg === "--provider") {
137
+ args.provider = argv[++i];
138
+ }
139
+ else if (arg === "--page") {
140
+ args.page = argv[++i];
141
+ }
142
+ else if (arg === "--per-page") {
143
+ args.perPage = argv[++i];
144
+ }
145
+ else if (arg === "--rating") {
146
+ args.rating = argv[++i];
147
+ }
148
+ else if (arg === "--comment") {
149
+ args.comment = argv[++i];
150
+ }
151
+ else if (arg === "--exec") {
152
+ args.exec = argv[++i];
153
+ }
154
+ else if (arg === "--title") {
155
+ args.title = argv[++i];
156
+ }
157
+ else if (arg === "--desc") {
158
+ args.desc = argv[++i];
159
+ }
160
+ else if (arg === "--use-case") {
161
+ args.useCase = argv[++i];
162
+ }
163
+ else if (arg === "--status") {
164
+ args.status = argv[++i];
165
+ }
166
+ else if (arg === "--agent-id") {
167
+ args.agentId = argv[++i];
168
+ }
169
+ else if (!arg.startsWith("-")) {
170
+ args.positional.push(arg);
171
+ }
172
+ i++;
173
+ }
174
+ return args;
175
+ }
176
+ const VERSION = "2.0.0";
177
+ async function main() {
178
+ const args = parseArgs(process.argv.slice(2));
179
+ if (args.version) {
180
+ process.stdout.write(`falm ${VERSION}\n`);
181
+ process.exit(0);
182
+ }
183
+ if (args.help || !args.subcommand) {
184
+ process.stdout.write(HELP);
185
+ process.exit(args.help ? 0 : 1);
186
+ }
187
+ const auth = resolveAuth({
188
+ apiKey: args.apiKey,
189
+ baseUrl: args.baseUrl,
190
+ walletPath: args.walletPath,
191
+ });
192
+ switch (args.subcommand) {
193
+ case "search": {
194
+ const { runSearch } = await import("./commands/search.js");
195
+ // Join all positional args so "falm search image generation" works like "image generation"
196
+ const query = args.positional.length > 0 ? args.positional.join(" ") : undefined;
197
+ await runSearch(query, {
198
+ tag: args.tag,
199
+ provider: args.provider,
200
+ page: args.page,
201
+ perPage: args.perPage,
202
+ }, auth, args.compact);
203
+ break;
204
+ }
205
+ case "tags": {
206
+ const { runTags } = await import("./commands/search.js");
207
+ await runTags(auth, args.compact);
208
+ break;
209
+ }
210
+ case "providers": {
211
+ const { runProviders } = await import("./commands/search.js");
212
+ await runProviders(auth, args.compact);
213
+ break;
214
+ }
215
+ case "info": {
216
+ const { run } = await import("./commands/info.js");
217
+ await run(args.positional[0], auth, args.compact);
218
+ break;
219
+ }
220
+ case "exec": {
221
+ const { run } = await import("./commands/exec.js");
222
+ await run(args.positional[0], args.data, auth, args.compact);
223
+ break;
224
+ }
225
+ case "dry-run": {
226
+ const { runDryRun } = await import("./commands/exec.js");
227
+ await runDryRun(args.positional[0], args.data, auth, args.compact);
228
+ break;
229
+ }
230
+ case "review": {
231
+ const { runReview } = await import("./commands/review.js");
232
+ await runReview(args.positional[0], {
233
+ rating: args.rating,
234
+ comment: args.comment,
235
+ agentId: args.agentId,
236
+ }, auth, args.compact);
237
+ break;
238
+ }
239
+ case "upvote": {
240
+ const { runUpvote } = await import("./commands/review.js");
241
+ await runUpvote(args.positional[0], args.exec, auth, args.compact);
242
+ break;
243
+ }
244
+ case "fav": {
245
+ const { run } = await import("./commands/fav.js");
246
+ await run(args.positional[0], args.positional[1], auth, args.compact);
247
+ break;
248
+ }
249
+ case "requests": {
250
+ const { runList } = await import("./commands/requests.js");
251
+ await runList({
252
+ status: args.status,
253
+ page: args.page,
254
+ perPage: args.perPage,
255
+ }, auth, args.compact);
256
+ break;
257
+ }
258
+ case "request": {
259
+ const { runRequest } = await import("./commands/requests.js");
260
+ await runRequest({
261
+ title: args.title,
262
+ desc: args.desc,
263
+ useCase: args.useCase,
264
+ exec: args.exec,
265
+ agentId: args.agentId,
266
+ }, auth, args.compact);
267
+ break;
268
+ }
269
+ case "request-upvote": {
270
+ const { runUpvote: runReqUpvote } = await import("./commands/requests.js");
271
+ await runReqUpvote(args.positional[0], args.exec, auth, args.compact);
272
+ break;
273
+ }
274
+ case "wallet": {
275
+ const { run } = await import("./commands/wallet-cmd.js");
276
+ await run(auth, args.compact);
277
+ break;
278
+ }
279
+ case "fetch": {
280
+ const { run } = await import("./commands/fetch.js");
281
+ await run(args.positional, {
282
+ data: args.data,
283
+ headers: args.headers,
284
+ output: args.output,
285
+ dryRun: args.dryRun,
286
+ compact: args.compact,
287
+ }, auth);
288
+ break;
289
+ }
290
+ default:
291
+ log(`Unknown command: ${args.subcommand}`);
292
+ process.stdout.write(HELP);
293
+ process.exit(1);
294
+ }
295
+ }
296
+ main().catch((err) => {
297
+ log(`error: ${err instanceof Error ? err.message : err}`);
298
+ process.exit(1);
299
+ });
@@ -0,0 +1,72 @@
1
+ import { apiGet, apiPost, validateToolId, normalizeToolId } from "../lib/api.js";
2
+ import { outputJson, log } from "../lib/output.js";
3
+ import { parsePaymentPreview } from "../x402.js";
4
+ export async function run(toolId, data, auth, compact) {
5
+ if (!toolId) {
6
+ log("Usage: caravo exec <tool-id> -d '<json>'");
7
+ process.exit(1);
8
+ }
9
+ const err = validateToolId(toolId);
10
+ if (err) {
11
+ log(err);
12
+ process.exit(1);
13
+ }
14
+ const normalized = normalizeToolId(toolId);
15
+ let input = {};
16
+ if (data) {
17
+ try {
18
+ input = JSON.parse(data);
19
+ }
20
+ catch {
21
+ log("Invalid JSON in -d/--data");
22
+ process.exit(1);
23
+ }
24
+ }
25
+ const result = await apiPost(`/api/tools/${normalized}/execute`, input, auth);
26
+ outputJson(result.data, compact);
27
+ }
28
+ export async function runDryRun(toolId, data, auth, compact) {
29
+ if (!toolId) {
30
+ log("Usage: caravo dry-run <tool-id> [-d '<json>']");
31
+ process.exit(1);
32
+ }
33
+ const err = validateToolId(toolId);
34
+ if (err) {
35
+ log(err);
36
+ process.exit(1);
37
+ }
38
+ const normalized = normalizeToolId(toolId);
39
+ if (auth.mode === "x402") {
40
+ // Probe execute endpoint for exact x402 cost
41
+ const url = `${auth.baseUrl}/api/tools/${normalized}/execute`;
42
+ const resp = await fetch(url, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: data || "{}",
46
+ });
47
+ if (resp.status === 402) {
48
+ const preview = parsePaymentPreview(resp);
49
+ if (preview) {
50
+ outputJson({
51
+ tool_id: toolId,
52
+ cost: `$${preview.amount}`,
53
+ asset: preview.asset,
54
+ pay_to: preview.payTo,
55
+ wallet: auth.wallet.address,
56
+ mode: "x402",
57
+ }, compact);
58
+ return;
59
+ }
60
+ }
61
+ outputJson({
62
+ tool_id: toolId,
63
+ status: resp.status,
64
+ note: "Endpoint did not return 402 payment requirements",
65
+ }, compact);
66
+ return;
67
+ }
68
+ // API key mode: fetch tool info for pricing
69
+ const info = await apiGet(`/api/tools/${normalized}`, auth);
70
+ const pricing = info?.pricing;
71
+ outputJson({ tool_id: normalized, pricing, mode: "apikey" }, compact);
72
+ }
@@ -0,0 +1,46 @@
1
+ import { apiGet, apiPost, apiDelete, validateToolId } from "../lib/api.js";
2
+ import { outputJson, log } from "../lib/output.js";
3
+ export async function run(sub, toolId, auth, compact) {
4
+ if (auth.mode !== "apikey") {
5
+ log("Favorites require an API key. Set $CARAVO_API_KEY or use --api-key.");
6
+ process.exit(1);
7
+ }
8
+ switch (sub) {
9
+ case "list": {
10
+ const data = await apiGet("/api/favorites", auth);
11
+ outputJson(data, compact);
12
+ break;
13
+ }
14
+ case "add": {
15
+ if (!toolId) {
16
+ log("Usage: caravo fav add <tool-id>");
17
+ process.exit(1);
18
+ }
19
+ const err = validateToolId(toolId);
20
+ if (err) {
21
+ log(err);
22
+ process.exit(1);
23
+ }
24
+ const { data } = await apiPost("/api/favorites", { tool_id: toolId }, auth);
25
+ outputJson(data, compact);
26
+ break;
27
+ }
28
+ case "rm": {
29
+ if (!toolId) {
30
+ log("Usage: caravo fav rm <tool-id>");
31
+ process.exit(1);
32
+ }
33
+ const err = validateToolId(toolId);
34
+ if (err) {
35
+ log(err);
36
+ process.exit(1);
37
+ }
38
+ const data = await apiDelete("/api/favorites", { tool_id: toolId }, auth);
39
+ outputJson(data, compact);
40
+ break;
41
+ }
42
+ default:
43
+ log("Usage: caravo fav <list|add|rm> [tool-id]");
44
+ process.exit(1);
45
+ }
46
+ }
@@ -0,0 +1,71 @@
1
+ import { writeFileSync } from "fs";
2
+ import { fetchWithX402, parsePaymentPreview } from "../x402.js";
3
+ import { outputJson, log } from "../lib/output.js";
4
+ export async function run(positional, opts, auth) {
5
+ let method;
6
+ let url;
7
+ if (positional.length >= 2) {
8
+ method = positional[0].toUpperCase();
9
+ url = positional[1];
10
+ }
11
+ else if (positional.length === 1) {
12
+ method = "GET";
13
+ url = positional[0];
14
+ }
15
+ else {
16
+ log("Usage: caravo fetch [METHOD] <url> [-d '<json>']");
17
+ process.exit(1);
18
+ }
19
+ const headers = { ...opts.headers };
20
+ if (opts.data && !headers["Content-Type"] && !headers["content-type"]) {
21
+ headers["Content-Type"] = "application/json";
22
+ }
23
+ const fetchOpts = {
24
+ method,
25
+ headers,
26
+ ...(opts.data ? { body: opts.data } : {}),
27
+ };
28
+ // --dry-run: probe endpoint for cost without paying
29
+ if (opts.dryRun) {
30
+ const resp = await fetch(url, fetchOpts);
31
+ if (resp.status === 402) {
32
+ const preview = parsePaymentPreview(resp);
33
+ if (preview) {
34
+ outputJson({
35
+ dry_run: true,
36
+ cost: `$${preview.amount}`,
37
+ pay_to: preview.payTo,
38
+ wallet: auth.wallet.address,
39
+ }, opts.compact);
40
+ }
41
+ else {
42
+ const body = await resp.text();
43
+ outputJson({ dry_run: true, status: 402, body }, opts.compact);
44
+ }
45
+ }
46
+ else {
47
+ outputJson({
48
+ dry_run: true,
49
+ status: resp.status,
50
+ note: "Endpoint did not return 402",
51
+ }, opts.compact);
52
+ }
53
+ return;
54
+ }
55
+ // Make request with automatic x402 payment
56
+ const { response, paid, cost } = await fetchWithX402(url, fetchOpts, auth.wallet);
57
+ const body = await response.text();
58
+ if (paid)
59
+ log(`paid $${cost} via x402`);
60
+ if (opts.output) {
61
+ writeFileSync(opts.output, body);
62
+ log(`response written to ${opts.output}`);
63
+ }
64
+ else {
65
+ process.stdout.write(body);
66
+ if (!body.endsWith("\n"))
67
+ process.stdout.write("\n");
68
+ }
69
+ if (!response.ok)
70
+ process.exit(1);
71
+ }
@@ -0,0 +1,16 @@
1
+ import { apiGet, validateToolId, normalizeToolId } from "../lib/api.js";
2
+ import { outputJson, log } from "../lib/output.js";
3
+ export async function run(toolId, auth, compact) {
4
+ if (!toolId) {
5
+ log("Usage: caravo info <tool-id>");
6
+ process.exit(1);
7
+ }
8
+ const err = validateToolId(toolId);
9
+ if (err) {
10
+ log(err);
11
+ process.exit(1);
12
+ }
13
+ const normalized = normalizeToolId(toolId);
14
+ const data = await apiGet(`/api/tools/${normalized}`, auth);
15
+ outputJson(data, compact);
16
+ }
@@ -0,0 +1,51 @@
1
+ import { apiGet, apiPost, parsePositiveInt } from "../lib/api.js";
2
+ import { outputJson, log } from "../lib/output.js";
3
+ export async function runList(opts, auth, compact) {
4
+ const params = new URLSearchParams();
5
+ if (opts.status)
6
+ params.set("status", opts.status);
7
+ if (opts.page) {
8
+ const p = parsePositiveInt(opts.page, "page");
9
+ if (p === null)
10
+ process.exit(1);
11
+ params.set("page", String(p));
12
+ }
13
+ if (opts.perPage) {
14
+ const pp = parsePositiveInt(opts.perPage, "per-page");
15
+ if (pp === null)
16
+ process.exit(1);
17
+ params.set("per_page", String(pp));
18
+ }
19
+ const qs = params.toString();
20
+ const data = await apiGet(`/api/tool-requests${qs ? `?${qs}` : ""}`, auth);
21
+ outputJson(data, compact);
22
+ }
23
+ export async function runRequest(opts, auth, compact) {
24
+ if (!opts.title || !opts.desc) {
25
+ log("Usage: caravo request --title <title> --desc <description>");
26
+ process.exit(1);
27
+ }
28
+ const body = {
29
+ title: opts.title,
30
+ description: opts.desc,
31
+ };
32
+ if (opts.useCase)
33
+ body.use_case = opts.useCase;
34
+ if (opts.exec)
35
+ body.execution_id = opts.exec;
36
+ if (opts.agentId)
37
+ body.agent_id = opts.agentId;
38
+ const { data } = await apiPost("/api/tool-requests", body, auth);
39
+ outputJson(data, compact);
40
+ }
41
+ export async function runUpvote(reqId, execId, auth, compact) {
42
+ if (!reqId) {
43
+ log("Usage: caravo request-upvote <request-id> [--exec <execution-id>]");
44
+ process.exit(1);
45
+ }
46
+ const body = {};
47
+ if (execId)
48
+ body.execution_id = execId;
49
+ const { data } = await apiPost(`/api/tool-requests/${reqId}`, body, auth);
50
+ outputJson(data, compact);
51
+ }
@@ -0,0 +1,43 @@
1
+ import { apiPost } from "../lib/api.js";
2
+ import { outputJson, log } from "../lib/output.js";
3
+ export async function runReview(execId, opts, auth, compact) {
4
+ if (!execId) {
5
+ log("Usage: caravo review <execution-id> --rating <1-5> --comment <text>");
6
+ process.exit(1);
7
+ }
8
+ if (!opts.rating || !opts.comment) {
9
+ log("--rating and --comment are required");
10
+ process.exit(1);
11
+ }
12
+ // Reject floats like "3.5" — parseInt would silently truncate to 3
13
+ if (opts.rating !== String(parseInt(opts.rating, 10))) {
14
+ log("--rating must be an integer 1-5");
15
+ process.exit(1);
16
+ }
17
+ const rating = parseInt(opts.rating, 10);
18
+ if (isNaN(rating) || rating < 1 || rating > 5) {
19
+ log("--rating must be an integer 1-5");
20
+ process.exit(1);
21
+ }
22
+ const body = {
23
+ execution_id: execId,
24
+ rating,
25
+ comment: opts.comment,
26
+ };
27
+ if (opts.agentId)
28
+ body.agent_id = opts.agentId;
29
+ const { data } = await apiPost("/api/reviews", body, auth);
30
+ outputJson(data, compact);
31
+ }
32
+ export async function runUpvote(reviewId, execId, auth, compact) {
33
+ if (!reviewId || !execId) {
34
+ log("Usage: caravo upvote <review-id> --exec <execution-id>");
35
+ process.exit(1);
36
+ }
37
+ const body = {
38
+ review_id: reviewId,
39
+ execution_id: execId,
40
+ };
41
+ const { data } = await apiPost("/api/reviews/upvote", body, auth);
42
+ outputJson(data, compact);
43
+ }
@@ -0,0 +1,34 @@
1
+ import { apiGet, parsePositiveInt } from "../lib/api.js";
2
+ import { outputJson } from "../lib/output.js";
3
+ export async function runSearch(query, opts, auth, compact) {
4
+ const params = new URLSearchParams();
5
+ if (query)
6
+ params.set("query", query);
7
+ if (opts.tag)
8
+ params.set("tag", opts.tag);
9
+ if (opts.provider)
10
+ params.set("provider", opts.provider);
11
+ if (opts.page) {
12
+ const p = parsePositiveInt(opts.page, "page");
13
+ if (p === null)
14
+ process.exit(1);
15
+ params.set("page", String(p));
16
+ }
17
+ if (opts.perPage) {
18
+ const pp = parsePositiveInt(opts.perPage, "per-page");
19
+ if (pp === null)
20
+ process.exit(1);
21
+ params.set("per_page", String(pp));
22
+ }
23
+ const qs = params.toString();
24
+ const data = await apiGet(`/api/tools${qs ? `?${qs}` : ""}`, auth);
25
+ outputJson(data, compact);
26
+ }
27
+ export async function runTags(auth, compact) {
28
+ const data = await apiGet("/api/tags", auth);
29
+ outputJson(data, compact);
30
+ }
31
+ export async function runProviders(auth, compact) {
32
+ const data = await apiGet("/api/providers", auth);
33
+ outputJson(data, compact);
34
+ }
@@ -0,0 +1,48 @@
1
+ import { createPublicClient, http } from "viem";
2
+ import { base } from "viem/chains";
3
+ import { apiGet } from "../lib/api.js";
4
+ import { outputJson } from "../lib/output.js";
5
+ const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
6
+ const USDC_ABI = [
7
+ {
8
+ inputs: [{ name: "account", type: "address" }],
9
+ name: "balanceOf",
10
+ outputs: [{ name: "", type: "uint256" }],
11
+ stateMutability: "view",
12
+ type: "function",
13
+ },
14
+ ];
15
+ async function getUsdcBalance(address) {
16
+ const client = createPublicClient({ chain: base, transport: http() });
17
+ const balance = await client.readContract({
18
+ address: USDC_ADDRESS,
19
+ abi: USDC_ABI,
20
+ functionName: "balanceOf",
21
+ args: [address],
22
+ });
23
+ return (Number(balance) / 1_000_000).toFixed(6);
24
+ }
25
+ export async function run(auth, compact) {
26
+ const info = {
27
+ mode: auth.mode,
28
+ wallet_address: auth.wallet.address,
29
+ };
30
+ // Check on-chain USDC balance
31
+ try {
32
+ info.usdc_balance = `$${await getUsdcBalance(auth.wallet.address)}`;
33
+ }
34
+ catch {
35
+ info.usdc_balance = "unavailable (RPC error)";
36
+ }
37
+ // If API key mode, also show platform balance
38
+ if (auth.mode === "apikey") {
39
+ try {
40
+ const profile = (await apiGet("/api/profile", auth));
41
+ info.api_balance = profile?.balance;
42
+ }
43
+ catch {
44
+ info.api_balance = "unavailable";
45
+ }
46
+ }
47
+ outputJson(info, compact);
48
+ }
@@ -0,0 +1,81 @@
1
+ import { fetchWithX402 } from "../x402.js";
2
+ import { log } from "./output.js";
3
+ /** Normalize a tool ID: trim whitespace, strip trailing slashes, lowercase. */
4
+ export function normalizeToolId(toolId) {
5
+ return toolId.trim().replace(/\/+$/, "").toLowerCase();
6
+ }
7
+ /** Validate tool_id format: only allow safe chars, no path traversal. */
8
+ export function validateToolId(toolId) {
9
+ const trimmed = normalizeToolId(toolId);
10
+ if (!trimmed)
11
+ return "tool_id must not be empty";
12
+ if (trimmed.includes(".."))
13
+ return "Invalid tool_id: path traversal not allowed";
14
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_./-]*$/.test(trimmed)) {
15
+ return "Invalid tool_id format: must start with alphanumeric and contain only letters, numbers, hyphens, underscores, dots, and slashes";
16
+ }
17
+ if (trimmed.length > 200)
18
+ return "tool_id too long";
19
+ return null;
20
+ }
21
+ /** Check if API response is an error and set process exit code accordingly. */
22
+ export function checkResponseError(data, httpStatus) {
23
+ if (httpStatus >= 400) {
24
+ process.exitCode = 1;
25
+ return true;
26
+ }
27
+ if (data && typeof data === "object" && "error" in data) {
28
+ process.exitCode = 1;
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ export async function apiGet(path, auth) {
34
+ const r = await fetch(`${auth.baseUrl}${path}`, { headers: auth.headers() });
35
+ const data = await r.json();
36
+ checkResponseError(data, r.status);
37
+ return data;
38
+ }
39
+ export async function apiPost(path, body, auth) {
40
+ const url = `${auth.baseUrl}${path}`;
41
+ const opts = {
42
+ method: "POST",
43
+ headers: auth.headers(),
44
+ body: JSON.stringify(body),
45
+ };
46
+ if (auth.mode === "x402") {
47
+ const { response, paid, cost } = await fetchWithX402(url, opts, auth.wallet);
48
+ if (paid)
49
+ log(`paid $${cost} via x402`);
50
+ const data = await response.json();
51
+ checkResponseError(data, response.status);
52
+ return { data, paid, cost };
53
+ }
54
+ const r = await fetch(url, opts);
55
+ const data = await r.json();
56
+ checkResponseError(data, r.status);
57
+ return { data, paid: false, cost: null };
58
+ }
59
+ export async function apiDelete(path, body, auth) {
60
+ const r = await fetch(`${auth.baseUrl}${path}`, {
61
+ method: "DELETE",
62
+ headers: auth.headers(),
63
+ body: JSON.stringify(body),
64
+ });
65
+ const data = await r.json();
66
+ checkResponseError(data, r.status);
67
+ return data;
68
+ }
69
+ /** Validate a string is a positive integer. Returns the parsed int or null. */
70
+ export function parsePositiveInt(value, name) {
71
+ if (value !== String(parseInt(value, 10))) {
72
+ log(`--${name} must be a positive integer`);
73
+ return null;
74
+ }
75
+ const n = parseInt(value, 10);
76
+ if (isNaN(n) || n < 1) {
77
+ log(`--${name} must be a positive integer`);
78
+ return null;
79
+ }
80
+ return n;
81
+ }
@@ -0,0 +1,23 @@
1
+ import { loadOrCreateWallet } from "../wallet.js";
2
+ const DEFAULT_BASE_URL = "https://caravo.ai";
3
+ export function resolveAuth(args) {
4
+ const apiKey = args.apiKey || process.env.CARAVO_API_KEY;
5
+ const baseUrl = args.baseUrl || process.env.CARAVO_URL || DEFAULT_BASE_URL;
6
+ let cached;
7
+ return {
8
+ mode: apiKey ? "apikey" : "x402",
9
+ apiKey,
10
+ baseUrl,
11
+ get wallet() {
12
+ if (!cached)
13
+ cached = loadOrCreateWallet(args.walletPath);
14
+ return cached;
15
+ },
16
+ headers() {
17
+ const h = { "Content-Type": "application/json" };
18
+ if (apiKey)
19
+ h["Authorization"] = `Bearer ${apiKey}`;
20
+ return h;
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,7 @@
1
+ export function outputJson(data, compact = false) {
2
+ const json = compact ? JSON.stringify(data) : JSON.stringify(data, null, 2);
3
+ process.stdout.write(json + "\n");
4
+ }
5
+ export function log(msg) {
6
+ process.stderr.write(`[caravo] ${msg}\n`);
7
+ }
package/dist/wallet.js ADDED
@@ -0,0 +1,80 @@
1
+ import { randomBytes } from "crypto";
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { privateKeyToAccount } from "viem/accounts";
6
+ const WALLET_DIR = join(homedir(), ".caravo");
7
+ const WALLET_FILE = join(WALLET_DIR, "wallet.json");
8
+ /**
9
+ * Known wallet paths from other MCP servers and web3 services.
10
+ * On startup we check these in order — if any exist, we reuse that wallet
11
+ * instead of creating a new one. This avoids fragmenting USDC across
12
+ * multiple addresses.
13
+ */
14
+ const KNOWN_WALLET_PATHS = [
15
+ // Legacy wallet path (pre-rename)
16
+ join(homedir(), ".fal-marketplace-mcp", "wallet.json"),
17
+ join(homedir(), ".x402scan-mcp", "wallet.json"),
18
+ join(homedir(), ".payments-mcp", "wallet.json"),
19
+ ];
20
+ function tryLoadWallet(path) {
21
+ try {
22
+ if (!existsSync(path))
23
+ return null;
24
+ const data = JSON.parse(readFileSync(path, "utf-8"));
25
+ if (typeof data.privateKey === "string" &&
26
+ data.privateKey.startsWith("0x") &&
27
+ typeof data.address === "string" &&
28
+ data.address.startsWith("0x")) {
29
+ return { privateKey: data.privateKey, address: data.address };
30
+ }
31
+ return null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** Check if a file exists but is not a valid wallet (corrupted/malformed). */
38
+ function isCorruptedWallet(path) {
39
+ if (!existsSync(path))
40
+ return false;
41
+ return tryLoadWallet(path) === null;
42
+ }
43
+ export function loadOrCreateWallet(customPath) {
44
+ // 0. Custom wallet path takes priority
45
+ if (customPath) {
46
+ const custom = tryLoadWallet(customPath);
47
+ if (custom)
48
+ return custom;
49
+ // Distinguish between missing and corrupted
50
+ if (isCorruptedWallet(customPath)) {
51
+ process.stderr.write(`[caravo] invalid wallet file at ${customPath} (corrupted or malformed)\n`);
52
+ }
53
+ else {
54
+ process.stderr.write(`[caravo] wallet not found at ${customPath}\n`);
55
+ }
56
+ process.exit(1);
57
+ }
58
+ // 1. Check our own wallet first
59
+ const own = tryLoadWallet(WALLET_FILE);
60
+ if (own)
61
+ return own;
62
+ // 2. Check wallets from other known MCPs
63
+ for (const path of KNOWN_WALLET_PATHS) {
64
+ const existing = tryLoadWallet(path);
65
+ if (existing) {
66
+ mkdirSync(WALLET_DIR, { recursive: true });
67
+ writeFileSync(WALLET_FILE, JSON.stringify(existing, null, 2), { mode: 0o600 });
68
+ process.stderr.write(`[caravo] reusing existing wallet from ${path}\n`);
69
+ return existing;
70
+ }
71
+ }
72
+ // 3. No existing wallet found — generate new
73
+ const privateKey = ("0x" + randomBytes(32).toString("hex"));
74
+ const account = privateKeyToAccount(privateKey);
75
+ const wallet = { privateKey, address: account.address };
76
+ mkdirSync(WALLET_DIR, { recursive: true });
77
+ writeFileSync(WALLET_FILE, JSON.stringify(wallet, null, 2), { mode: 0o600 });
78
+ process.stderr.write(`[caravo] created new wallet: ${account.address}\n`);
79
+ return wallet;
80
+ }
package/dist/x402.js ADDED
@@ -0,0 +1,119 @@
1
+ import { randomBytes } from "crypto";
2
+ import { getAddress } from "viem";
3
+ import { signTypedData } from "viem/actions";
4
+ import { createWalletClient, http } from "viem";
5
+ import { base } from "viem/chains";
6
+ import { privateKeyToAccount } from "viem/accounts";
7
+ const authorizationTypes = {
8
+ TransferWithAuthorization: [
9
+ { name: "from", type: "address" },
10
+ { name: "to", type: "address" },
11
+ { name: "value", type: "uint256" },
12
+ { name: "validAfter", type: "uint256" },
13
+ { name: "validBefore", type: "uint256" },
14
+ { name: "nonce", type: "bytes32" },
15
+ ],
16
+ };
17
+ async function signPayment(requirements, wallet) {
18
+ const account = privateKeyToAccount(wallet.privateKey);
19
+ const client = createWalletClient({ account, chain: base, transport: http() });
20
+ const now = Math.floor(Date.now() / 1000);
21
+ const chainId = parseInt(requirements.network.split(":")[1]);
22
+ const nonce = ("0x" + randomBytes(32).toString("hex"));
23
+ const authorization = {
24
+ from: getAddress(account.address),
25
+ to: getAddress(requirements.payTo),
26
+ value: BigInt(requirements.amount),
27
+ validAfter: BigInt(now - 60),
28
+ validBefore: BigInt(now + requirements.maxTimeoutSeconds),
29
+ nonce,
30
+ };
31
+ const signature = await signTypedData(client, {
32
+ domain: {
33
+ name: requirements.extra?.name ?? "USD Coin",
34
+ version: requirements.extra?.version ?? "2",
35
+ chainId,
36
+ verifyingContract: getAddress(requirements.asset),
37
+ },
38
+ types: authorizationTypes,
39
+ primaryType: "TransferWithAuthorization",
40
+ message: authorization,
41
+ });
42
+ return {
43
+ x402Version: 2,
44
+ resource: undefined,
45
+ accepted: requirements,
46
+ payload: {
47
+ authorization: {
48
+ from: authorization.from,
49
+ to: authorization.to,
50
+ value: authorization.value.toString(),
51
+ validAfter: authorization.validAfter.toString(),
52
+ validBefore: authorization.validBefore.toString(),
53
+ nonce,
54
+ },
55
+ signature,
56
+ },
57
+ };
58
+ }
59
+ export async function fetchWithX402(url, options, wallet) {
60
+ const resp = await fetch(url, options);
61
+ if (resp.status !== 402) {
62
+ return { response: resp, paid: false, cost: null };
63
+ }
64
+ // Parse payment requirements from header or body
65
+ let paymentRequired = null;
66
+ const header = resp.headers.get("payment-required");
67
+ if (header) {
68
+ try {
69
+ paymentRequired = JSON.parse(atob(header));
70
+ }
71
+ catch {
72
+ paymentRequired = null;
73
+ }
74
+ }
75
+ if (!paymentRequired) {
76
+ try {
77
+ paymentRequired = await resp.json();
78
+ }
79
+ catch {
80
+ return { response: resp, paid: false, cost: null };
81
+ }
82
+ }
83
+ const requirements = paymentRequired?.accepts?.[0];
84
+ if (!requirements) {
85
+ return { response: resp, paid: false, cost: null };
86
+ }
87
+ // Sign and retry
88
+ const paymentPayload = await signPayment(requirements, wallet);
89
+ const paymentHeader = btoa(JSON.stringify(paymentPayload));
90
+ const paidResp = await fetch(url, {
91
+ ...options,
92
+ headers: {
93
+ ...options.headers,
94
+ "X-PAYMENT": paymentHeader,
95
+ },
96
+ });
97
+ // Cost in human-readable dollars (amount is in USDC micro-units, 1e6 = $1)
98
+ const costDollars = (parseInt(requirements.amount) / 1_000_000).toFixed(6);
99
+ return { response: paidResp, paid: true, cost: costDollars };
100
+ }
101
+ export function parsePaymentPreview(resp) {
102
+ const header = resp.headers.get("payment-required");
103
+ if (!header)
104
+ return null;
105
+ try {
106
+ const pr = JSON.parse(atob(header));
107
+ const req = pr.accepts?.[0];
108
+ if (!req)
109
+ return null;
110
+ return {
111
+ amount: (parseInt(req.amount) / 1_000_000).toFixed(6),
112
+ asset: req.asset,
113
+ payTo: req.payTo,
114
+ };
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@caravo/cli",
3
+ "version": "0.1.0",
4
+ "description": "Caravo CLI — search, execute, and review tools with API key or x402 USDC payments",
5
+ "type": "module",
6
+ "bin": {
7
+ "caravo": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "node --experimental-strip-types src/cli.ts"
12
+ },
13
+ "dependencies": {
14
+ "viem": "^2.28.0"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5.8.2",
18
+ "@types/node": "^22"
19
+ },
20
+ "keywords": ["caravo", "marketplace", "cli", "x402", "usdc", "base", "agent", "mcp"],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/Caravo-AI/Caravo-CLI"
24
+ },
25
+ "homepage": "https://caravo.ai",
26
+ "license": "MIT",
27
+ "files": [
28
+ "dist/"
29
+ ]
30
+ }