@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 +21 -0
- package/README.md +81 -0
- package/dist/cli.js +299 -0
- package/dist/commands/exec.js +72 -0
- package/dist/commands/fav.js +46 -0
- package/dist/commands/fetch.js +71 -0
- package/dist/commands/info.js +16 -0
- package/dist/commands/requests.js +51 -0
- package/dist/commands/review.js +43 -0
- package/dist/commands/search.js +34 -0
- package/dist/commands/wallet-cmd.js +48 -0
- package/dist/lib/api.js +81 -0
- package/dist/lib/auth.js +23 -0
- package/dist/lib/output.js +7 -0
- package/dist/wallet.js +80 -0
- package/dist/x402.js +119 -0
- package/package.json +30 -0
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
|
+
}
|
package/dist/lib/api.js
ADDED
|
@@ -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
|
+
}
|
package/dist/lib/auth.js
ADDED
|
@@ -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
|
+
}
|
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
|
+
}
|