run402 1.3.0 → 1.5.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/cli.mjs +12 -0
- package/lib/agent.mjs +68 -0
- package/lib/apps.mjs +57 -4
- package/lib/config.mjs +7 -4
- package/lib/deploy.mjs +1 -1
- package/lib/functions.mjs +4 -4
- package/lib/image.mjs +1 -1
- package/lib/message.mjs +56 -0
- package/lib/projects.mjs +8 -8
- package/lib/secrets.mjs +3 -3
- package/lib/sites.mjs +28 -8
- package/lib/subdomains.mjs +3 -3
- package/lib/wallet.mjs +103 -19
- package/package.json +10 -4
package/cli.mjs
CHANGED
|
@@ -23,6 +23,8 @@ Commands:
|
|
|
23
23
|
subdomains Manage custom subdomains (claim, list, delete)
|
|
24
24
|
apps Browse and manage the app marketplace
|
|
25
25
|
image Generate AI images via x402 micropayments
|
|
26
|
+
message Send messages to Run402 developers
|
|
27
|
+
agent Manage agent identity (contact info)
|
|
26
28
|
|
|
27
29
|
Run 'run402 <command> --help' for detailed usage of each command.
|
|
28
30
|
|
|
@@ -98,6 +100,16 @@ switch (cmd) {
|
|
|
98
100
|
await run(sub, rest);
|
|
99
101
|
break;
|
|
100
102
|
}
|
|
103
|
+
case "message": {
|
|
104
|
+
const { run } = await import("./lib/message.mjs");
|
|
105
|
+
await run(sub, rest);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "agent": {
|
|
109
|
+
const { run } = await import("./lib/agent.mjs");
|
|
110
|
+
await run(sub, rest);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
101
113
|
default:
|
|
102
114
|
console.error(`Unknown command: ${cmd}\n`);
|
|
103
115
|
console.log(HELP);
|
package/lib/agent.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readWallet, API, WALLET_FILE } from "./config.mjs";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 agent — Manage agent identity
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 agent contact --name <name> [--email <email>] [--webhook <url>]
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
- Costs $0.001 USDC via x402
|
|
11
|
+
- Registers contact info so Run402 can reach your agent
|
|
12
|
+
- Only name is required; email and webhook are optional
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
run402 agent contact --name my-agent
|
|
16
|
+
run402 agent contact --name my-agent --email ops@example.com --webhook https://example.com/hook
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
async function contact(args) {
|
|
20
|
+
let name = null, email = null, webhook = null;
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
if (args[i] === "--name" && args[i + 1]) name = args[++i];
|
|
23
|
+
if (args[i] === "--email" && args[i + 1]) email = args[++i];
|
|
24
|
+
if (args[i] === "--webhook" && args[i + 1]) webhook = args[++i];
|
|
25
|
+
}
|
|
26
|
+
if (!name) { console.error(JSON.stringify({ status: "error", message: "Missing --name <name>" })); process.exit(1); }
|
|
27
|
+
if (!existsSync(WALLET_FILE)) {
|
|
28
|
+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const wallet = readWallet();
|
|
33
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
34
|
+
const { createPublicClient, http } = await import("viem");
|
|
35
|
+
const { baseSepolia } = await import("viem/chains");
|
|
36
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
37
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
38
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
39
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
40
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
41
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
42
|
+
const client = new x402Client();
|
|
43
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
44
|
+
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
45
|
+
|
|
46
|
+
const body = { name };
|
|
47
|
+
if (email) body.email = email;
|
|
48
|
+
if (webhook) body.webhook = webhook;
|
|
49
|
+
|
|
50
|
+
const res = await fetchPaid(`${API}/agent/v1/contact`, {
|
|
51
|
+
method: "PUT",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify(body),
|
|
54
|
+
});
|
|
55
|
+
const data = await res.json();
|
|
56
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
57
|
+
console.log(JSON.stringify(data, null, 2));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function run(sub, args) {
|
|
61
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
62
|
+
if (sub !== "contact") {
|
|
63
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
64
|
+
console.log(HELP);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
await contact(args);
|
|
68
|
+
}
|
package/lib/apps.mjs
CHANGED
|
@@ -14,6 +14,10 @@ Subcommands:
|
|
|
14
14
|
publish <id> [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed]
|
|
15
15
|
Publish a project as an app
|
|
16
16
|
versions <id> List published versions of a project
|
|
17
|
+
inspect <version_id> Inspect a published app version
|
|
18
|
+
update <project_id> <version_id> [--description <desc>] [--tags <t1,t2>] [--visibility <v>] [--fork-allowed] [--no-fork]
|
|
19
|
+
Update a published version
|
|
20
|
+
delete <project_id> <version_id> Delete a published version
|
|
17
21
|
|
|
18
22
|
Examples:
|
|
19
23
|
run402 apps browse
|
|
@@ -21,10 +25,13 @@ Examples:
|
|
|
21
25
|
run402 apps fork ver_abc123 my-todo --tier prototype
|
|
22
26
|
run402 apps publish proj123 --description "Todo app" --tags todo,auth --visibility public --fork-allowed
|
|
23
27
|
run402 apps versions proj123
|
|
28
|
+
run402 apps inspect ver_abc123
|
|
29
|
+
run402 apps update proj123 ver_abc123 --description "Updated" --tags todo
|
|
30
|
+
run402 apps delete proj123 ver_abc123
|
|
24
31
|
`;
|
|
25
32
|
|
|
26
33
|
async function browse(args) {
|
|
27
|
-
let url = `${API}/v1
|
|
34
|
+
let url = `${API}/apps/v1`;
|
|
28
35
|
const tags = [];
|
|
29
36
|
for (let i = 0; i < args.length; i++) {
|
|
30
37
|
if (args[i] === "--tag" && args[i + 1]) tags.push(args[++i]);
|
|
@@ -64,7 +71,7 @@ async function fork(versionId, name, args) {
|
|
|
64
71
|
const body = { version_id: versionId, name };
|
|
65
72
|
if (opts.subdomain) body.subdomain = opts.subdomain;
|
|
66
73
|
|
|
67
|
-
const res = await fetchPaid(`${API}/v1
|
|
74
|
+
const res = await fetchPaid(`${API}/fork/v1/${opts.tier}`, {
|
|
68
75
|
method: "POST",
|
|
69
76
|
headers: { "Content-Type": "application/json" },
|
|
70
77
|
body: JSON.stringify(body),
|
|
@@ -102,7 +109,7 @@ async function publish(projectId, args) {
|
|
|
102
109
|
if (opts.visibility) body.visibility = opts.visibility;
|
|
103
110
|
if (opts.forkAllowed !== undefined) body.fork_allowed = opts.forkAllowed;
|
|
104
111
|
|
|
105
|
-
const res = await fetch(`${API}/
|
|
112
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/publish`, {
|
|
106
113
|
method: "POST",
|
|
107
114
|
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
108
115
|
body: JSON.stringify(body),
|
|
@@ -114,7 +121,7 @@ async function publish(projectId, args) {
|
|
|
114
121
|
|
|
115
122
|
async function versions(projectId) {
|
|
116
123
|
const p = findProject(projectId);
|
|
117
|
-
const res = await fetch(`${API}/
|
|
124
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/versions`, {
|
|
118
125
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
119
126
|
});
|
|
120
127
|
const data = await res.json();
|
|
@@ -122,6 +129,49 @@ async function versions(projectId) {
|
|
|
122
129
|
console.log(JSON.stringify(data, null, 2));
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
async function inspect(versionId) {
|
|
133
|
+
if (!versionId) { console.error(JSON.stringify({ status: "error", message: "Missing version ID" })); process.exit(1); }
|
|
134
|
+
const res = await fetch(`${API}/apps/v1/${versionId}`);
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
137
|
+
console.log(JSON.stringify(data, null, 2));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function update(projectId, versionId, args) {
|
|
141
|
+
const p = findProject(projectId);
|
|
142
|
+
const body = {};
|
|
143
|
+
for (let i = 0; i < args.length; i++) {
|
|
144
|
+
if (args[i] === "--description" && args[i + 1]) body.description = args[++i];
|
|
145
|
+
if (args[i] === "--tags" && args[i + 1]) body.tags = args[++i].split(",");
|
|
146
|
+
if (args[i] === "--visibility" && args[i + 1]) body.visibility = args[++i];
|
|
147
|
+
if (args[i] === "--fork-allowed") body.fork_allowed = true;
|
|
148
|
+
if (args[i] === "--no-fork") body.fork_allowed = false;
|
|
149
|
+
}
|
|
150
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/versions/${versionId}`, {
|
|
151
|
+
method: "PATCH",
|
|
152
|
+
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
153
|
+
body: JSON.stringify(body),
|
|
154
|
+
});
|
|
155
|
+
const data = await res.json();
|
|
156
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
157
|
+
console.log(JSON.stringify(data, null, 2));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function deleteVersion(projectId, versionId) {
|
|
161
|
+
const p = findProject(projectId);
|
|
162
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/versions/${versionId}`, {
|
|
163
|
+
method: "DELETE",
|
|
164
|
+
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
165
|
+
});
|
|
166
|
+
if (res.status === 204 || res.ok) {
|
|
167
|
+
console.log(JSON.stringify({ status: "ok", message: `Version ${versionId} deleted.` }));
|
|
168
|
+
} else {
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
console.error(JSON.stringify({ status: "error", http: res.status, ...data }));
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
125
175
|
export async function run(sub, args) {
|
|
126
176
|
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
127
177
|
switch (sub) {
|
|
@@ -129,6 +179,9 @@ export async function run(sub, args) {
|
|
|
129
179
|
case "fork": await fork(args[0], args[1], args.slice(2)); break;
|
|
130
180
|
case "publish": await publish(args[0], args.slice(1)); break;
|
|
131
181
|
case "versions": await versions(args[0]); break;
|
|
182
|
+
case "inspect": await inspect(args[0]); break;
|
|
183
|
+
case "update": await update(args[0], args[1], args.slice(2)); break;
|
|
184
|
+
case "delete": await deleteVersion(args[0], args[1]); break;
|
|
132
185
|
default:
|
|
133
186
|
console.error(`Unknown subcommand: ${sub}\n`);
|
|
134
187
|
console.log(HELP);
|
package/lib/config.mjs
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Kept in a separate module so credential reads stay isolated.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
7
|
-
import { join } from "path";
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, renameSync } from "fs";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
8
|
import { homedir } from "os";
|
|
9
|
+
import { randomBytes } from "crypto";
|
|
9
10
|
|
|
10
11
|
export const CONFIG_DIR = join(homedir(), ".config", "run402");
|
|
11
12
|
export const WALLET_FILE = join(CONFIG_DIR, "wallet.json");
|
|
@@ -19,8 +20,10 @@ export function readWallet() {
|
|
|
19
20
|
|
|
20
21
|
export function saveWallet(data) {
|
|
21
22
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const tmp = join(CONFIG_DIR, `.wallet.${randomBytes(4).toString("hex")}.tmp`);
|
|
24
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
25
|
+
renameSync(tmp, WALLET_FILE);
|
|
26
|
+
chmodSync(WALLET_FILE, 0o600);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function loadProjects() {
|
package/lib/deploy.mjs
CHANGED
|
@@ -84,7 +84,7 @@ export async function run(args) {
|
|
|
84
84
|
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
85
85
|
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
86
86
|
|
|
87
|
-
const res = await fetchPaid(`${API}/v1
|
|
87
|
+
const res = await fetchPaid(`${API}/deploy/v1/${opts.tier}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(manifest) });
|
|
88
88
|
const result = await res.json();
|
|
89
89
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...result })); process.exit(1); }
|
|
90
90
|
saveProject(result);
|
package/lib/functions.mjs
CHANGED
|
@@ -65,7 +65,7 @@ async function deploy(projectId, name, args) {
|
|
|
65
65
|
if (opts.deps) body.deps = opts.deps;
|
|
66
66
|
|
|
67
67
|
const fetchPaid = await setupPaidFetch();
|
|
68
|
-
const res = await fetchPaid(`${API}/
|
|
68
|
+
const res = await fetchPaid(`${API}/projects/v1/admin/${projectId}/functions`, {
|
|
69
69
|
method: "POST",
|
|
70
70
|
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
71
71
|
body: JSON.stringify(body),
|
|
@@ -102,7 +102,7 @@ async function logs(projectId, name, args) {
|
|
|
102
102
|
for (let i = 0; i < args.length; i++) {
|
|
103
103
|
if (args[i] === "--tail" && args[i + 1]) tail = parseInt(args[++i]);
|
|
104
104
|
}
|
|
105
|
-
const res = await fetch(`${API}/
|
|
105
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/functions/${encodeURIComponent(name)}/logs?tail=${tail}`, {
|
|
106
106
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
107
107
|
});
|
|
108
108
|
const data = await res.json();
|
|
@@ -112,7 +112,7 @@ async function logs(projectId, name, args) {
|
|
|
112
112
|
|
|
113
113
|
async function list(projectId) {
|
|
114
114
|
const p = findProject(projectId);
|
|
115
|
-
const res = await fetch(`${API}/
|
|
115
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/functions`, {
|
|
116
116
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
117
117
|
});
|
|
118
118
|
const data = await res.json();
|
|
@@ -122,7 +122,7 @@ async function list(projectId) {
|
|
|
122
122
|
|
|
123
123
|
async function deleteFunction(projectId, name) {
|
|
124
124
|
const p = findProject(projectId);
|
|
125
|
-
const res = await fetch(`${API}/
|
|
125
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/functions/${encodeURIComponent(name)}`, {
|
|
126
126
|
method: "DELETE",
|
|
127
127
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
128
128
|
});
|
package/lib/image.mjs
CHANGED
|
@@ -66,7 +66,7 @@ export async function run(sub, args) {
|
|
|
66
66
|
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
67
67
|
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
68
68
|
|
|
69
|
-
const res = await fetchPaid(`${API}/
|
|
69
|
+
const res = await fetchPaid(`${API}/generate-image/v1`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompt: opts.prompt, aspect: opts.aspect }) });
|
|
70
70
|
const data = await res.json();
|
|
71
71
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
72
72
|
|
package/lib/message.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readWallet, API, WALLET_FILE } from "./config.mjs";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
|
|
4
|
+
const HELP = `run402 message — Send messages to Run402 developers
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
run402 message send <text>
|
|
8
|
+
|
|
9
|
+
Notes:
|
|
10
|
+
- Costs $0.01 USDC via x402
|
|
11
|
+
- Requires a funded wallet
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
run402 message send "Hello from my agent!"
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
async function send(text) {
|
|
18
|
+
if (!text) { console.error(JSON.stringify({ status: "error", message: "Missing message text" })); process.exit(1); }
|
|
19
|
+
if (!existsSync(WALLET_FILE)) {
|
|
20
|
+
console.error(JSON.stringify({ status: "error", message: "No wallet found. Run: run402 wallet create && run402 wallet fund" }));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const wallet = readWallet();
|
|
25
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
26
|
+
const { createPublicClient, http } = await import("viem");
|
|
27
|
+
const { baseSepolia } = await import("viem/chains");
|
|
28
|
+
const { x402Client, wrapFetchWithPayment } = await import("@x402/fetch");
|
|
29
|
+
const { ExactEvmScheme } = await import("@x402/evm/exact/client");
|
|
30
|
+
const { toClientEvmSigner } = await import("@x402/evm");
|
|
31
|
+
const account = privateKeyToAccount(wallet.privateKey);
|
|
32
|
+
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
33
|
+
const signer = toClientEvmSigner(account, publicClient);
|
|
34
|
+
const client = new x402Client();
|
|
35
|
+
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
36
|
+
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
37
|
+
|
|
38
|
+
const res = await fetchPaid(`${API}/message/v1`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify({ message: text }),
|
|
42
|
+
});
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
45
|
+
console.log(JSON.stringify(data, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function run(sub, args) {
|
|
49
|
+
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
50
|
+
if (sub !== "send") {
|
|
51
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
52
|
+
console.log(HELP);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
await send(args.join(" "));
|
|
56
|
+
}
|
package/lib/projects.mjs
CHANGED
|
@@ -59,7 +59,7 @@ async function setupPaidFetch() {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
async function quote() {
|
|
62
|
-
const res = await fetch(`${API}/v1
|
|
62
|
+
const res = await fetch(`${API}/projects/v1`);
|
|
63
63
|
const data = await res.json();
|
|
64
64
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
65
65
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -74,7 +74,7 @@ async function provision(args) {
|
|
|
74
74
|
const fetchPaid = await setupPaidFetch();
|
|
75
75
|
const body = { tier: opts.tier };
|
|
76
76
|
if (opts.name) body.name = opts.name;
|
|
77
|
-
const res = await fetchPaid(`${API}/v1
|
|
77
|
+
const res = await fetchPaid(`${API}/projects/v1`, {
|
|
78
78
|
method: "POST",
|
|
79
79
|
headers: { "Content-Type": "application/json" },
|
|
80
80
|
body: JSON.stringify(body),
|
|
@@ -98,7 +98,7 @@ async function provision(args) {
|
|
|
98
98
|
async function rls(projectId, template, tablesJson) {
|
|
99
99
|
const p = findProject(projectId);
|
|
100
100
|
const tables = JSON.parse(tablesJson);
|
|
101
|
-
const res = await fetch(`${API}/
|
|
101
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/rls`, {
|
|
102
102
|
method: "POST",
|
|
103
103
|
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
104
104
|
body: JSON.stringify({ template, tables }),
|
|
@@ -116,7 +116,7 @@ async function list() {
|
|
|
116
116
|
|
|
117
117
|
async function sqlCmd(projectId, query) {
|
|
118
118
|
const p = findProject(projectId);
|
|
119
|
-
const res = await fetch(`${API}/
|
|
119
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/sql`, { method: "POST", headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "text/plain" }, body: query });
|
|
120
120
|
console.log(JSON.stringify(await res.json(), null, 2));
|
|
121
121
|
}
|
|
122
122
|
|
|
@@ -128,7 +128,7 @@ async function rest(projectId, table, queryParams) {
|
|
|
128
128
|
|
|
129
129
|
async function usage(projectId) {
|
|
130
130
|
const p = findProject(projectId);
|
|
131
|
-
const res = await fetch(`${API}/
|
|
131
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/usage`, { headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
132
132
|
const data = await res.json();
|
|
133
133
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
134
134
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -136,7 +136,7 @@ async function usage(projectId) {
|
|
|
136
136
|
|
|
137
137
|
async function schema(projectId) {
|
|
138
138
|
const p = findProject(projectId);
|
|
139
|
-
const res = await fetch(`${API}/
|
|
139
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/schema`, { headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
140
140
|
const data = await res.json();
|
|
141
141
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
142
142
|
console.log(JSON.stringify(data, null, 2));
|
|
@@ -144,7 +144,7 @@ async function schema(projectId) {
|
|
|
144
144
|
|
|
145
145
|
async function renew(projectId) {
|
|
146
146
|
const fetchPaid = await setupPaidFetch();
|
|
147
|
-
const res = await fetchPaid(`${API}/v1
|
|
147
|
+
const res = await fetchPaid(`${API}/projects/v1/${projectId}/renew`, { method: "POST", headers: { "Content-Type": "application/json" } });
|
|
148
148
|
const data = await res.json();
|
|
149
149
|
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
150
150
|
const projects = loadProjects();
|
|
@@ -155,7 +155,7 @@ async function renew(projectId) {
|
|
|
155
155
|
|
|
156
156
|
async function deleteProject(projectId) {
|
|
157
157
|
const p = findProject(projectId);
|
|
158
|
-
const res = await fetch(`${API}/v1
|
|
158
|
+
const res = await fetch(`${API}/projects/v1/${projectId}`, { method: "DELETE", headers: { "Authorization": `Bearer ${p.service_key}` } });
|
|
159
159
|
if (res.status === 204 || res.ok) {
|
|
160
160
|
saveProjects(loadProjects().filter(pr => pr.project_id !== projectId));
|
|
161
161
|
console.log(JSON.stringify({ status: "ok", message: `Project ${projectId} deleted.` }));
|
package/lib/secrets.mjs
CHANGED
|
@@ -22,7 +22,7 @@ Notes:
|
|
|
22
22
|
|
|
23
23
|
async function set(projectId, key, value) {
|
|
24
24
|
const p = findProject(projectId);
|
|
25
|
-
const res = await fetch(`${API}/
|
|
25
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/secrets`, {
|
|
26
26
|
method: "POST",
|
|
27
27
|
headers: { "Authorization": `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
28
28
|
body: JSON.stringify({ key, value }),
|
|
@@ -34,7 +34,7 @@ async function set(projectId, key, value) {
|
|
|
34
34
|
|
|
35
35
|
async function list(projectId) {
|
|
36
36
|
const p = findProject(projectId);
|
|
37
|
-
const res = await fetch(`${API}/
|
|
37
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/secrets`, {
|
|
38
38
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
39
39
|
});
|
|
40
40
|
const data = await res.json();
|
|
@@ -44,7 +44,7 @@ async function list(projectId) {
|
|
|
44
44
|
|
|
45
45
|
async function deleteSecret(projectId, key) {
|
|
46
46
|
const p = findProject(projectId);
|
|
47
|
-
const res = await fetch(`${API}/
|
|
47
|
+
const res = await fetch(`${API}/projects/v1/admin/${projectId}/secrets/${encodeURIComponent(key)}`, {
|
|
48
48
|
method: "DELETE",
|
|
49
49
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
50
50
|
});
|
package/lib/sites.mjs
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "fs";
|
|
2
2
|
import { readWallet, API, WALLET_FILE } from "./config.mjs";
|
|
3
3
|
|
|
4
|
-
const HELP = `run402 sites — Deploy static sites
|
|
4
|
+
const HELP = `run402 sites — Deploy and manage static sites
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
run402 sites deploy --name <name> --manifest <file> [--project <id>] [--target <target>]
|
|
8
|
+
run402 sites status <deployment_id>
|
|
8
9
|
cat manifest.json | run402 sites deploy --name <name>
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
Subcommands:
|
|
12
|
+
deploy Deploy a static site
|
|
13
|
+
status Check the status of a deployment
|
|
14
|
+
|
|
15
|
+
Options (deploy):
|
|
11
16
|
--name <name> Site name (e.g. 'portfolio', 'family-todo')
|
|
12
17
|
--manifest <file> Path to manifest JSON file (or read from stdin)
|
|
13
18
|
--project <id> Optional project ID to link this deployment to
|
|
@@ -24,6 +29,7 @@ Manifest format (JSON):
|
|
|
24
29
|
|
|
25
30
|
Examples:
|
|
26
31
|
run402 sites deploy --name my-site --manifest site.json
|
|
32
|
+
run402 sites status dep_abc123
|
|
27
33
|
cat site.json | run402 sites deploy --name my-site
|
|
28
34
|
|
|
29
35
|
Notes:
|
|
@@ -71,7 +77,7 @@ async function deploy(args) {
|
|
|
71
77
|
client.register("eip155:84532", new ExactEvmScheme(signer));
|
|
72
78
|
const fetchPaid = wrapFetchWithPayment(fetch, client);
|
|
73
79
|
|
|
74
|
-
const res = await fetchPaid(`${API}/v1
|
|
80
|
+
const res = await fetchPaid(`${API}/deployments/v1`, {
|
|
75
81
|
method: "POST",
|
|
76
82
|
headers: { "Content-Type": "application/json" },
|
|
77
83
|
body: JSON.stringify(body),
|
|
@@ -81,12 +87,26 @@ async function deploy(args) {
|
|
|
81
87
|
console.log(JSON.stringify(data, null, 2));
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
async function status(args) {
|
|
91
|
+
let deploymentId = null;
|
|
92
|
+
for (let i = 0; i < args.length; i++) {
|
|
93
|
+
if (!args[i].startsWith("-")) { deploymentId = args[i]; break; }
|
|
94
|
+
}
|
|
95
|
+
if (!deploymentId) { console.error(JSON.stringify({ status: "error", message: "Missing deployment ID" })); process.exit(1); }
|
|
96
|
+
const res = await fetch(`${API}/deployments/v1/${deploymentId}`);
|
|
97
|
+
const data = await res.json();
|
|
98
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
99
|
+
console.log(JSON.stringify(data, null, 2));
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
export async function run(sub, args) {
|
|
85
103
|
if (!sub || sub === '--help' || sub === '-h') { console.log(HELP); process.exit(0); }
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
104
|
+
switch (sub) {
|
|
105
|
+
case "deploy": await deploy(args); break;
|
|
106
|
+
case "status": await status(args); break;
|
|
107
|
+
default:
|
|
108
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
109
|
+
console.log(HELP);
|
|
110
|
+
process.exit(1);
|
|
90
111
|
}
|
|
91
|
-
await deploy(args);
|
|
92
112
|
}
|
package/lib/subdomains.mjs
CHANGED
|
@@ -31,7 +31,7 @@ async function claim(deploymentId, name, args) {
|
|
|
31
31
|
const p = findProject(opts.project);
|
|
32
32
|
headers["Authorization"] = `Bearer ${p.service_key}`;
|
|
33
33
|
}
|
|
34
|
-
const res = await fetch(`${API}/v1
|
|
34
|
+
const res = await fetch(`${API}/subdomains/v1`, {
|
|
35
35
|
method: "POST",
|
|
36
36
|
headers,
|
|
37
37
|
body: JSON.stringify({ name, deployment_id: deploymentId }),
|
|
@@ -51,7 +51,7 @@ async function deleteSubdomain(name, args) {
|
|
|
51
51
|
const p = findProject(opts.project);
|
|
52
52
|
headers["Authorization"] = `Bearer ${p.service_key}`;
|
|
53
53
|
}
|
|
54
|
-
const res = await fetch(`${API}/v1
|
|
54
|
+
const res = await fetch(`${API}/subdomains/v1/${encodeURIComponent(name)}`, {
|
|
55
55
|
method: "DELETE",
|
|
56
56
|
headers,
|
|
57
57
|
});
|
|
@@ -65,7 +65,7 @@ async function deleteSubdomain(name, args) {
|
|
|
65
65
|
|
|
66
66
|
async function list(projectId) {
|
|
67
67
|
const p = findProject(projectId);
|
|
68
|
-
const res = await fetch(`${API}/v1
|
|
68
|
+
const res = await fetch(`${API}/subdomains/v1`, {
|
|
69
69
|
headers: { "Authorization": `Bearer ${p.service_key}` },
|
|
70
70
|
});
|
|
71
71
|
const data = await res.json();
|
package/lib/wallet.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readWallet, saveWallet, API } from "./config.mjs";
|
|
1
|
+
import { readWallet, saveWallet, WALLET_FILE, API } from "./config.mjs";
|
|
2
2
|
|
|
3
3
|
const HELP = `run402 wallet — Manage your x402 wallet
|
|
4
4
|
|
|
@@ -9,26 +9,34 @@ Subcommands:
|
|
|
9
9
|
status Show wallet address, network, and funding status
|
|
10
10
|
create Generate a new wallet and save it locally
|
|
11
11
|
fund Request test USDC from the Run402 faucet (Base Sepolia)
|
|
12
|
-
balance
|
|
12
|
+
balance Show on-chain USDC (mainnet + testnet) and Run402 billing balance
|
|
13
13
|
export Print the wallet address (useful for scripting)
|
|
14
|
+
checkout Create a billing checkout session (--amount <usd_micros>)
|
|
15
|
+
history View billing transaction history (--limit <n>)
|
|
14
16
|
|
|
15
17
|
Notes:
|
|
16
18
|
- Wallet is stored locally at ~/.run402/wallet.json
|
|
17
|
-
-
|
|
18
|
-
- You need to create and fund a wallet before
|
|
19
|
+
- The wallet works on any EVM chain (currently Run402 uses Base Mainnet and Sepolia for testnet)
|
|
20
|
+
- You need to create and fund a wallet before any x402 transaction with Run402
|
|
19
21
|
|
|
20
22
|
Examples:
|
|
21
23
|
run402 wallet create
|
|
22
24
|
run402 wallet status
|
|
23
25
|
run402 wallet fund
|
|
24
26
|
run402 wallet export
|
|
27
|
+
run402 wallet checkout --amount 5000000
|
|
28
|
+
run402 wallet history --limit 10
|
|
25
29
|
`;
|
|
26
30
|
|
|
31
|
+
const USDC_ABI = [{ name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }] }];
|
|
32
|
+
const USDC_MAINNET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
33
|
+
const USDC_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
34
|
+
|
|
27
35
|
async function loadDeps() {
|
|
28
36
|
const { generatePrivateKey, privateKeyToAccount } = await import("viem/accounts");
|
|
29
37
|
const { createPublicClient, http } = await import("viem");
|
|
30
|
-
const { baseSepolia } = await import("viem/chains");
|
|
31
|
-
return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, baseSepolia };
|
|
38
|
+
const { base, baseSepolia } = await import("viem/chains");
|
|
39
|
+
return { generatePrivateKey, privateKeyToAccount, createPublicClient, http, base, baseSepolia };
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
async function status() {
|
|
@@ -37,7 +45,7 @@ async function status() {
|
|
|
37
45
|
console.log(JSON.stringify({ status: "no_wallet", message: "No wallet found. Run: run402 wallet create" }));
|
|
38
46
|
return;
|
|
39
47
|
}
|
|
40
|
-
console.log(JSON.stringify({ status: "ok", address: w.address,
|
|
48
|
+
console.log(JSON.stringify({ status: "ok", address: w.address, created: w.created, funded: w.funded || false, path: WALLET_FILE }));
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
async function create() {
|
|
@@ -48,31 +56,74 @@ async function create() {
|
|
|
48
56
|
const { generatePrivateKey, privateKeyToAccount } = await loadDeps();
|
|
49
57
|
const privateKey = generatePrivateKey();
|
|
50
58
|
const account = privateKeyToAccount(privateKey);
|
|
51
|
-
saveWallet({ address: account.address, privateKey,
|
|
52
|
-
console.log(JSON.stringify({ status: "ok", address: account.address, message:
|
|
59
|
+
saveWallet({ address: account.address, privateKey, created: new Date().toISOString(), funded: false });
|
|
60
|
+
console.log(JSON.stringify({ status: "ok", address: account.address, message: `Wallet created. Stored locally at ${WALLET_FILE}` }));
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
async function fund() {
|
|
56
64
|
const w = readWallet();
|
|
57
65
|
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
|
|
58
|
-
|
|
66
|
+
|
|
67
|
+
const { createPublicClient, http, baseSepolia } = await loadDeps();
|
|
68
|
+
const client = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
69
|
+
const before = await readUsdcBalance(client, USDC_SEPOLIA, w.address).catch(() => 0);
|
|
70
|
+
|
|
71
|
+
const res = await fetch(`${API}/faucet/v1`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address: w.address }) });
|
|
59
72
|
const data = await res.json();
|
|
60
|
-
if (res.ok) {
|
|
61
|
-
saveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
|
|
62
|
-
console.log(JSON.stringify({ status: "ok", ...data }));
|
|
63
|
-
} else {
|
|
73
|
+
if (!res.ok) {
|
|
64
74
|
console.log(JSON.stringify({ status: "error", ...data }));
|
|
65
75
|
process.exit(1);
|
|
66
76
|
}
|
|
77
|
+
|
|
78
|
+
const MAX_WAIT = 30;
|
|
79
|
+
for (let i = 0; i < MAX_WAIT; i++) {
|
|
80
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
81
|
+
const now = await readUsdcBalance(client, USDC_SEPOLIA, w.address).catch(() => before);
|
|
82
|
+
if (now > before) {
|
|
83
|
+
saveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
|
|
84
|
+
console.log(JSON.stringify({
|
|
85
|
+
address: w.address,
|
|
86
|
+
onchain: {
|
|
87
|
+
"base-sepolia_usd_micros": now,
|
|
88
|
+
},
|
|
89
|
+
}, null, 2));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
saveWallet({ ...w, funded: true, lastFaucet: new Date().toISOString() });
|
|
95
|
+
console.log(JSON.stringify({ status: "ok", message: "Faucet request sent but balance not yet confirmed", ...data }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readUsdcBalance(client, usdc, address) {
|
|
99
|
+
const raw = await client.readContract({ address: usdc, abi: USDC_ABI, functionName: "balanceOf", args: [address] });
|
|
100
|
+
return Number(raw);
|
|
67
101
|
}
|
|
68
102
|
|
|
69
103
|
async function balance() {
|
|
70
104
|
const w = readWallet();
|
|
71
105
|
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
|
|
107
|
+
const { createPublicClient, http, base, baseSepolia } = await loadDeps();
|
|
108
|
+
const mainnetClient = createPublicClient({ chain: base, transport: http() });
|
|
109
|
+
const sepoliaClient = createPublicClient({ chain: baseSepolia, transport: http() });
|
|
110
|
+
|
|
111
|
+
const [mainnetUsdc, sepoliaUsdc, billingRes] = await Promise.all([
|
|
112
|
+
readUsdcBalance(mainnetClient, USDC_MAINNET, w.address).catch(() => null),
|
|
113
|
+
readUsdcBalance(sepoliaClient, USDC_SEPOLIA, w.address).catch(() => null),
|
|
114
|
+
fetch(`${API}/billing/v1/accounts/${w.address.toLowerCase()}`),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const billing = billingRes.ok ? await billingRes.json() : null;
|
|
118
|
+
|
|
119
|
+
console.log(JSON.stringify({
|
|
120
|
+
address: w.address,
|
|
121
|
+
onchain: {
|
|
122
|
+
"base-mainnet_usd_micros": mainnetUsdc,
|
|
123
|
+
"base-sepolia_usd_micros": sepoliaUsdc,
|
|
124
|
+
},
|
|
125
|
+
run402: billing ? { balance_usd_micros: billing.available_usd_micros } : "no billing account",
|
|
126
|
+
}, null, 2));
|
|
76
127
|
}
|
|
77
128
|
|
|
78
129
|
async function exportAddr() {
|
|
@@ -81,6 +132,37 @@ async function exportAddr() {
|
|
|
81
132
|
console.log(w.address);
|
|
82
133
|
}
|
|
83
134
|
|
|
135
|
+
async function checkout(args) {
|
|
136
|
+
const w = readWallet();
|
|
137
|
+
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
|
|
138
|
+
let amount = null;
|
|
139
|
+
for (let i = 0; i < args.length; i++) {
|
|
140
|
+
if (args[i] === "--amount" && args[i + 1]) amount = parseInt(args[++i], 10);
|
|
141
|
+
}
|
|
142
|
+
if (!amount) { console.error(JSON.stringify({ status: "error", message: "Missing --amount <usd_micros> (e.g. --amount 5000000 for $5)" })); process.exit(1); }
|
|
143
|
+
const res = await fetch(`${API}/billing/v1/checkouts`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
body: JSON.stringify({ wallet: w.address.toLowerCase(), amount_usd_micros: amount }),
|
|
147
|
+
});
|
|
148
|
+
const data = await res.json();
|
|
149
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
150
|
+
console.log(JSON.stringify(data, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function history(args) {
|
|
154
|
+
const w = readWallet();
|
|
155
|
+
if (!w) { console.log(JSON.stringify({ status: "error", message: "No wallet. Run: run402 wallet create" })); process.exit(1); }
|
|
156
|
+
let limit = 20;
|
|
157
|
+
for (let i = 0; i < args.length; i++) {
|
|
158
|
+
if (args[i] === "--limit" && args[i + 1]) limit = parseInt(args[++i], 10);
|
|
159
|
+
}
|
|
160
|
+
const res = await fetch(`${API}/billing/v1/accounts/${w.address.toLowerCase()}/history?limit=${limit}`);
|
|
161
|
+
const data = await res.json();
|
|
162
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
163
|
+
console.log(JSON.stringify(data, null, 2));
|
|
164
|
+
}
|
|
165
|
+
|
|
84
166
|
export async function run(sub, args) {
|
|
85
167
|
if (!sub || sub === '--help' || sub === '-h') {
|
|
86
168
|
console.log(HELP);
|
|
@@ -91,7 +173,9 @@ export async function run(sub, args) {
|
|
|
91
173
|
case "create": await create(); break;
|
|
92
174
|
case "fund": await fund(); break;
|
|
93
175
|
case "balance": await balance(); break;
|
|
94
|
-
case "export":
|
|
176
|
+
case "export": await exportAddr(); break;
|
|
177
|
+
case "checkout": await checkout(args); break;
|
|
178
|
+
case "history": await history(args); break;
|
|
95
179
|
default:
|
|
96
180
|
console.error(`Unknown subcommand: ${sub}\n`);
|
|
97
181
|
console.log(HELP);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "run402",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "CLI for Run402 — provision Postgres databases, deploy static sites, generate images, and manage wallets via x402 micropayments.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"run402": "
|
|
7
|
+
"run402": "cli.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
@@ -22,8 +22,14 @@
|
|
|
22
22
|
"homepage": "https://run402.com",
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "https://github.com/kychee-com/run402.git",
|
|
25
|
+
"url": "git+https://github.com/kychee-com/run402.git",
|
|
26
26
|
"directory": "cli"
|
|
27
27
|
},
|
|
28
|
-
"keywords": [
|
|
28
|
+
"keywords": [
|
|
29
|
+
"run402",
|
|
30
|
+
"x402",
|
|
31
|
+
"postgres",
|
|
32
|
+
"ai-agent",
|
|
33
|
+
"openclaw"
|
|
34
|
+
]
|
|
29
35
|
}
|