agentspend 0.1.10 → 0.2.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/README.md +20 -25
- package/SKILL.md +118 -0
- package/dist/cli.js +63 -0
- package/dist/commands/check.js +40 -0
- package/dist/commands/configure.js +99 -0
- package/dist/commands/pay.js +89 -206
- package/dist/commands/search.js +16 -0
- package/dist/commands/setup.js +36 -0
- package/dist/commands/status.js +7 -0
- package/dist/dev-index.js +10 -0
- package/dist/index.js +5 -15
- package/dist/lib/api.js +98 -0
- package/dist/lib/credentials.js +65 -0
- package/dist/lib/output.js +76 -0
- package/dist/lib/request-options.js +35 -0
- package/dist/types.js +1 -0
- package/package.json +17 -18
- package/dist/commands/card.d.ts +0 -2
- package/dist/commands/card.js +0 -179
- package/dist/commands/crypto-wallet.d.ts +0 -2
- package/dist/commands/crypto-wallet.js +0 -113
- package/dist/commands/pay.d.ts +0 -2
- package/dist/commands/wallet.d.ts +0 -2
- package/dist/commands/wallet.js +0 -102
- package/dist/index.d.ts +0 -2
- package/src/commands/card.ts +0 -230
- package/src/commands/pay.ts +0 -270
- package/src/commands/wallet.ts +0 -118
- package/src/index.ts +0 -19
- package/tsconfig.json +0 -8
package/dist/commands/pay.js
CHANGED
|
@@ -1,227 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const CONFIG_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".agentspend");
|
|
8
|
-
const CARD_FILE = (0, node_path_1.join)(CONFIG_DIR, "card.json");
|
|
9
|
-
const WALLET_FILE = (0, node_path_1.join)(CONFIG_DIR, "wallet.json");
|
|
10
|
-
const API_BASE = process.env.AGENTSPEND_API_URL ?? "https://api.agentspend.co";
|
|
11
|
-
async function readCardConfig() {
|
|
12
|
-
try {
|
|
13
|
-
const data = JSON.parse(await (0, promises_1.readFile)(CARD_FILE, "utf-8"));
|
|
14
|
-
if (typeof data.card_id === "string" && data.card_id && typeof data.card_secret === "string" && data.card_secret) {
|
|
15
|
-
return data;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
// file doesn't exist or is invalid
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
1
|
+
import { ApiError } from "../lib/api.js";
|
|
2
|
+
import { requireApiKey } from "../lib/credentials.js";
|
|
3
|
+
import { formatJson, formatUsd, formatUsdEstimate, usd6ToUsd } from "../lib/output.js";
|
|
4
|
+
import { normalizeMethod, parseBody, parseHeaders } from "../lib/request-options.js";
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null;
|
|
22
7
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (typeof data.address === "string" && data.address) {
|
|
27
|
-
return data;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
31
|
-
// file doesn't exist or is invalid
|
|
32
|
-
}
|
|
33
|
-
return null;
|
|
8
|
+
function readNumber(record, key) {
|
|
9
|
+
const value = record[key];
|
|
10
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
34
11
|
}
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const idx = h.indexOf(":");
|
|
39
|
-
if (idx === -1) {
|
|
40
|
-
throw new Error(`Invalid header format: "${h}". Use key:value`);
|
|
41
|
-
}
|
|
42
|
-
headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
43
|
-
}
|
|
44
|
-
return headers;
|
|
12
|
+
function readString(record, key) {
|
|
13
|
+
const value = record[key];
|
|
14
|
+
return typeof value === "string" ? value : undefined;
|
|
45
15
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"x-card-id": cardConfig.card_id,
|
|
50
|
-
};
|
|
51
|
-
if (body) {
|
|
52
|
-
headers["content-type"] = "application/json";
|
|
53
|
-
}
|
|
54
|
-
// Try with x-card-id
|
|
55
|
-
const res = await fetch(url, {
|
|
56
|
-
method: "POST",
|
|
57
|
-
headers,
|
|
58
|
-
body: body ?? undefined,
|
|
59
|
-
});
|
|
60
|
-
// Success
|
|
61
|
-
if (res.ok) {
|
|
62
|
-
console.log("Payment successful (card)");
|
|
63
|
-
const data = await res.text();
|
|
64
|
-
try {
|
|
65
|
-
console.log(JSON.stringify(JSON.parse(data), null, 2));
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
console.log(data);
|
|
69
|
-
}
|
|
70
|
-
return;
|
|
16
|
+
function parsePayErrorCode(value) {
|
|
17
|
+
if (typeof value !== "string") {
|
|
18
|
+
return undefined;
|
|
71
19
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (serviceId) {
|
|
78
|
-
console.log(`Binding to service ${serviceId}...`);
|
|
79
|
-
const bindRes = await fetch(`${API_BASE}/v1/card/${encodeURIComponent(cardConfig.card_id)}/bind`, {
|
|
80
|
-
method: "POST",
|
|
81
|
-
headers: { "content-type": "application/json" },
|
|
82
|
-
body: JSON.stringify({
|
|
83
|
-
card_secret: cardConfig.card_secret,
|
|
84
|
-
service_id: serviceId,
|
|
85
|
-
}),
|
|
86
|
-
});
|
|
87
|
-
if (!bindRes.ok) {
|
|
88
|
-
const bindError = await bindRes.text();
|
|
89
|
-
console.error(`Failed to bind (${bindRes.status}): ${bindError}`);
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
console.log("Bound. Retrying payment...");
|
|
93
|
-
// Retry with x-card-id
|
|
94
|
-
const retryRes = await fetch(url, { method: "POST", headers, body: body ?? undefined });
|
|
95
|
-
if (!retryRes.ok) {
|
|
96
|
-
console.error(`Retry failed (${retryRes.status}): ${await retryRes.text()}`);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
console.log("Payment successful (card)");
|
|
100
|
-
const data = await retryRes.text();
|
|
101
|
-
try {
|
|
102
|
-
console.log(JSON.stringify(JSON.parse(data), null, 2));
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
console.log(data);
|
|
106
|
-
}
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
20
|
+
if (value === "PRICE_EXCEEDS_MAX" ||
|
|
21
|
+
value === "PRICE_NOT_CONVERTIBLE" ||
|
|
22
|
+
value === "WEEKLY_BUDGET_EXCEEDED" ||
|
|
23
|
+
value === "DOMAIN_NOT_ALLOWLISTED") {
|
|
24
|
+
return value;
|
|
109
25
|
}
|
|
110
|
-
|
|
111
|
-
console.error(`Request failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
112
|
-
process.exit(1);
|
|
26
|
+
return undefined;
|
|
113
27
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
headers["content-type"] = "application/json";
|
|
118
|
-
}
|
|
119
|
-
// First request — expect 402
|
|
120
|
-
const initialRes = await fetch(url, {
|
|
121
|
-
method: "POST",
|
|
122
|
-
headers,
|
|
123
|
-
body: body ?? undefined,
|
|
124
|
-
});
|
|
125
|
-
if (initialRes.status !== 402) {
|
|
126
|
-
if (initialRes.ok) {
|
|
127
|
-
console.log("Request succeeded without payment.");
|
|
128
|
-
const data = await initialRes.text();
|
|
129
|
-
try {
|
|
130
|
-
console.log(JSON.stringify(JSON.parse(data), null, 2));
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
console.log(data);
|
|
134
|
-
}
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const data = await initialRes.text();
|
|
138
|
-
console.error(`Expected 402 but got ${initialRes.status}: ${data}`);
|
|
139
|
-
process.exit(1);
|
|
28
|
+
function parsePayErrorBody(body) {
|
|
29
|
+
if (!isRecord(body)) {
|
|
30
|
+
return {};
|
|
140
31
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const account = privateKeyToAccount(walletConfig.private_key);
|
|
147
|
-
const coreClient = new x402Client();
|
|
148
|
-
registerExactEvmScheme(coreClient, { signer: account });
|
|
149
|
-
const httpClient = new x402HTTPClient(coreClient);
|
|
150
|
-
// Decode payment requirements from 402 response
|
|
151
|
-
const paymentRequired = httpClient.getPaymentRequiredResponse((name) => initialRes.headers.get(name), await initialRes.clone().json().catch(() => undefined));
|
|
152
|
-
// Sign payment
|
|
153
|
-
const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
|
|
154
|
-
const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
155
|
-
// Resend with payment header
|
|
156
|
-
const paidRes = await fetch(url, {
|
|
157
|
-
method: "POST",
|
|
158
|
-
headers: { ...headers, ...paymentHeaders },
|
|
159
|
-
body: body ?? undefined,
|
|
160
|
-
});
|
|
161
|
-
const data = await paidRes.text();
|
|
162
|
-
if (!paidRes.ok) {
|
|
163
|
-
console.error(`Paid request failed (${paidRes.status}): ${data}`);
|
|
164
|
-
process.exit(1);
|
|
32
|
+
const parsed = {
|
|
33
|
+
code: parsePayErrorCode(body.code),
|
|
34
|
+
};
|
|
35
|
+
if (!isRecord(body.details)) {
|
|
36
|
+
return parsed;
|
|
165
37
|
}
|
|
166
|
-
|
|
38
|
+
const detailsRecord = body.details;
|
|
39
|
+
parsed.details = {
|
|
40
|
+
offered_price_usd6: readNumber(detailsRecord, "offered_price_usd6"),
|
|
41
|
+
offered_price_usd: readNumber(detailsRecord, "offered_price_usd"),
|
|
42
|
+
max_cost_usd6: readNumber(detailsRecord, "max_cost_usd6"),
|
|
43
|
+
max_cost_usd: readNumber(detailsRecord, "max_cost_usd"),
|
|
44
|
+
weekly_limit_usd6: readNumber(detailsRecord, "weekly_limit_usd6"),
|
|
45
|
+
weekly_limit_usd: readNumber(detailsRecord, "weekly_limit_usd"),
|
|
46
|
+
spent_this_week_usd6: readNumber(detailsRecord, "spent_this_week_usd6"),
|
|
47
|
+
spent_this_week_usd: readNumber(detailsRecord, "spent_this_week_usd"),
|
|
48
|
+
attempted_charge_usd6: readNumber(detailsRecord, "attempted_charge_usd6"),
|
|
49
|
+
attempted_charge_usd: readNumber(detailsRecord, "attempted_charge_usd"),
|
|
50
|
+
estimated_usd: readNumber(detailsRecord, "estimated_usd"),
|
|
51
|
+
amount_display: readString(detailsRecord, "amount_display"),
|
|
52
|
+
currency: readString(detailsRecord, "currency"),
|
|
53
|
+
};
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
export async function runPay(apiClient, url, options) {
|
|
57
|
+
const apiKey = await requireApiKey();
|
|
58
|
+
const method = normalizeMethod(options.method);
|
|
167
59
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
60
|
+
const response = await apiClient.pay(apiKey, {
|
|
61
|
+
url,
|
|
62
|
+
method,
|
|
63
|
+
headers: parseHeaders(options.header),
|
|
64
|
+
body: parseBody(options.body),
|
|
65
|
+
max_cost_usd: options.maxCost,
|
|
66
|
+
});
|
|
67
|
+
console.log(formatJson(response.body));
|
|
68
|
+
if (response.payment) {
|
|
69
|
+
console.log(`\nCharged: ${formatUsd(response.payment.charged_usd)} | Remaining: ${formatUsd(response.payment.remaining_budget_usd)}`);
|
|
70
|
+
}
|
|
172
71
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const extraHeaders = parseHeaders(opts.header);
|
|
188
|
-
if (opts.method === "card") {
|
|
189
|
-
const card = await readCardConfig();
|
|
190
|
-
if (!card) {
|
|
191
|
-
console.error("No card configured. Run: agentspend card setup");
|
|
192
|
-
process.exit(1);
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof ApiError) {
|
|
74
|
+
const body = parsePayErrorBody(error.body);
|
|
75
|
+
if (error.status === 400 && body.code === "PRICE_EXCEEDS_MAX") {
|
|
76
|
+
const offered = body.details?.offered_price_usd ??
|
|
77
|
+
(typeof body.details?.offered_price_usd6 === "number" ? usd6ToUsd(body.details.offered_price_usd6) : 0);
|
|
78
|
+
const max = body.details?.max_cost_usd ??
|
|
79
|
+
(typeof body.details?.max_cost_usd6 === "number" ? usd6ToUsd(body.details.max_cost_usd6) : 0);
|
|
80
|
+
const estimatedUsd = body.details?.estimated_usd;
|
|
81
|
+
const amountDisplay = body.details?.amount_display;
|
|
82
|
+
const currency = body.details?.currency ?? "USDC";
|
|
83
|
+
console.error(`Price ${formatUsd(offered)} exceeds --max-cost ${formatUsd(max)}. Run without --max-cost or increase it.`);
|
|
84
|
+
if (amountDisplay) {
|
|
85
|
+
console.error(`Offered token amount: ${amountDisplay} ${currency} (~${formatUsdEstimate(estimatedUsd, offered)})`);
|
|
193
86
|
}
|
|
194
|
-
await payWithCard(url, card, opts.body, extraHeaders);
|
|
195
87
|
return;
|
|
196
88
|
}
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
if (!wallet) {
|
|
200
|
-
console.error("No wallet configured. Run: agentspend wallet create");
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
await payWithCrypto(url, wallet, opts.body, extraHeaders);
|
|
89
|
+
if (error.status === 400 && body.code === "PRICE_NOT_CONVERTIBLE") {
|
|
90
|
+
console.error("Price could not be converted to 6-decimal USD units for policy checks.");
|
|
204
91
|
return;
|
|
205
92
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
93
|
+
if (error.status === 402 && body.code === "WEEKLY_BUDGET_EXCEEDED") {
|
|
94
|
+
const weeklyLimit = body.details?.weekly_limit_usd ??
|
|
95
|
+
(typeof body.details?.weekly_limit_usd6 === "number" ? usd6ToUsd(body.details.weekly_limit_usd6) : 0);
|
|
96
|
+
const spent = body.details?.spent_this_week_usd ??
|
|
97
|
+
(typeof body.details?.spent_this_week_usd6 === "number" ? usd6ToUsd(body.details.spent_this_week_usd6) : 0);
|
|
98
|
+
const attempted = body.details?.attempted_charge_usd ??
|
|
99
|
+
(typeof body.details?.attempted_charge_usd6 === "number" ? usd6ToUsd(body.details.attempted_charge_usd6) : 0);
|
|
100
|
+
console.error(`Weekly budget exceeded. Limit ${formatUsd(weeklyLimit)}, spent ${formatUsd(spent)}, attempted ${formatUsd(attempted)}.`);
|
|
210
101
|
return;
|
|
211
102
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await payWithCrypto(url, wallet, opts.body, extraHeaders);
|
|
103
|
+
if (error.status === 403 && body.code === "DOMAIN_NOT_ALLOWLISTED") {
|
|
104
|
+
console.error("This domain is not in your AgentSpend allowlist.");
|
|
215
105
|
return;
|
|
216
106
|
}
|
|
217
|
-
console.error("No payment method configured.");
|
|
218
|
-
console.error("Set up a card: agentspend card setup");
|
|
219
|
-
console.error("Or create a wallet: agentspend wallet create");
|
|
220
|
-
process.exit(1);
|
|
221
107
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
process.exit(1);
|
|
225
|
-
}
|
|
226
|
-
});
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
227
110
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { requireApiKey } from "../lib/credentials.js";
|
|
2
|
+
export async function runSearch(apiClient, query) {
|
|
3
|
+
const apiKey = await requireApiKey();
|
|
4
|
+
const response = await apiClient.search(apiKey, query);
|
|
5
|
+
if (response.services.length === 0) {
|
|
6
|
+
console.log(`No services matched "${response.query}".`);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
for (const service of response.services) {
|
|
10
|
+
console.log(service.name);
|
|
11
|
+
console.log(`Description: ${service.description}`);
|
|
12
|
+
console.log(`Domain: ${service.domain}`);
|
|
13
|
+
console.log(`Skill URL: ${service.skill_url ?? "n/a"}`);
|
|
14
|
+
console.log("");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { saveCredentials } from "../lib/credentials.js";
|
|
4
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
5
|
+
const SETUP_TIMEOUT_MS = 10 * 60 * 1000;
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
function generateApiKey() {
|
|
10
|
+
return `sk_agent_${crypto.randomBytes(32).toString("hex")}`;
|
|
11
|
+
}
|
|
12
|
+
export async function runSetup(apiClient) {
|
|
13
|
+
const setup = await apiClient.createSetup();
|
|
14
|
+
console.log(`Open this URL to complete setup:\n${setup.setup_url}\n`);
|
|
15
|
+
console.log("Waiting for setup to complete...");
|
|
16
|
+
const started = Date.now();
|
|
17
|
+
while (Date.now() - started < SETUP_TIMEOUT_MS) {
|
|
18
|
+
const status = await apiClient.getSetupStatus(setup.setup_id);
|
|
19
|
+
if (status.status === "ready") {
|
|
20
|
+
const apiKey = generateApiKey();
|
|
21
|
+
const apiKeyHash = await bcrypt.hash(apiKey, 12);
|
|
22
|
+
await apiClient.claimSetup(setup.setup_id, apiKeyHash);
|
|
23
|
+
await saveCredentials(apiKey);
|
|
24
|
+
console.log("Ready! Your agent can now spend.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (status.status === "claimed") {
|
|
28
|
+
throw new Error("This setup session was already claimed. Run agentspend setup again.");
|
|
29
|
+
}
|
|
30
|
+
if (status.status === "expired") {
|
|
31
|
+
throw new Error("Setup timed out. Run agentspend setup again.");
|
|
32
|
+
}
|
|
33
|
+
await sleep(POLL_INTERVAL_MS);
|
|
34
|
+
}
|
|
35
|
+
throw new Error("Setup timed out. Run agentspend setup again.");
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from "./cli.js";
|
|
3
|
+
const LOCAL_API_BASE_URL = "http://127.0.0.1:8787";
|
|
4
|
+
runCli({
|
|
5
|
+
baseUrl: LOCAL_API_BASE_URL,
|
|
6
|
+
programName: "agentspend-dev",
|
|
7
|
+
}).catch((error) => {
|
|
8
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const pay_js_1 = require("./commands/pay.js");
|
|
8
|
-
const program = new commander_1.Command();
|
|
9
|
-
program
|
|
10
|
-
.name("agentspend")
|
|
11
|
-
.description("AgentSpend CLI — manage cards and billing")
|
|
12
|
-
.version("0.1.0");
|
|
13
|
-
(0, card_js_1.registerCardCommands)(program);
|
|
14
|
-
(0, wallet_js_1.registerWalletCommands)(program);
|
|
15
|
-
(0, pay_js_1.registerPayCommand)(program);
|
|
16
|
-
program.parse();
|
|
2
|
+
import { runCli } from "./cli.js";
|
|
3
|
+
runCli().catch((error) => {
|
|
4
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
5
|
+
process.exitCode = 1;
|
|
6
|
+
});
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const API_URL = "https://api.agentspend.co";
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(message, status, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ApiError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function parseBody(response) {
|
|
13
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
14
|
+
if (contentType.includes("application/json")) {
|
|
15
|
+
try {
|
|
16
|
+
return await response.json();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return response.text();
|
|
23
|
+
}
|
|
24
|
+
function errorMessageFromBody(body, fallback) {
|
|
25
|
+
if (body && typeof body === "object" && "error" in body && typeof body.error === "string") {
|
|
26
|
+
return body.error;
|
|
27
|
+
}
|
|
28
|
+
if (typeof body === "string" && body.length > 0) {
|
|
29
|
+
return body;
|
|
30
|
+
}
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
export class AgentspendApiClient {
|
|
34
|
+
baseUrl;
|
|
35
|
+
constructor(baseUrl = API_URL) {
|
|
36
|
+
this.baseUrl = baseUrl;
|
|
37
|
+
}
|
|
38
|
+
async request(path, init = {}, apiKey) {
|
|
39
|
+
const headers = {
|
|
40
|
+
...init.headers,
|
|
41
|
+
};
|
|
42
|
+
if (apiKey) {
|
|
43
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
44
|
+
}
|
|
45
|
+
if (init.body && !headers["content-type"] && !headers["Content-Type"]) {
|
|
46
|
+
headers["content-type"] = "application/json";
|
|
47
|
+
}
|
|
48
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
49
|
+
...init,
|
|
50
|
+
headers,
|
|
51
|
+
});
|
|
52
|
+
const body = await parseBody(response);
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new ApiError(errorMessageFromBody(body, `Request failed (${response.status})`), response.status, body);
|
|
55
|
+
}
|
|
56
|
+
return body;
|
|
57
|
+
}
|
|
58
|
+
pay(apiKey, payload) {
|
|
59
|
+
return this.request("/pay", {
|
|
60
|
+
method: "POST",
|
|
61
|
+
body: JSON.stringify(payload),
|
|
62
|
+
}, apiKey);
|
|
63
|
+
}
|
|
64
|
+
check(apiKey, payload) {
|
|
65
|
+
return this.request("/check", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: JSON.stringify(payload),
|
|
68
|
+
}, apiKey);
|
|
69
|
+
}
|
|
70
|
+
status(apiKey) {
|
|
71
|
+
return this.request("/status", {
|
|
72
|
+
method: "GET",
|
|
73
|
+
}, apiKey);
|
|
74
|
+
}
|
|
75
|
+
configure(payload, apiKey) {
|
|
76
|
+
return this.request("/configure", {
|
|
77
|
+
method: "POST",
|
|
78
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
79
|
+
}, apiKey);
|
|
80
|
+
}
|
|
81
|
+
configureStatus(token) {
|
|
82
|
+
return this.request(`/configure/${encodeURIComponent(token)}/status`, {
|
|
83
|
+
method: "GET",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
claimConfigure(token, apiKeyHash) {
|
|
87
|
+
return this.request(`/configure/${encodeURIComponent(token)}/claim`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
body: JSON.stringify({ api_key_hash: apiKeyHash }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
search(apiKey, query) {
|
|
93
|
+
const params = new URLSearchParams({ q: query });
|
|
94
|
+
return this.request(`/search?${params.toString()}`, {
|
|
95
|
+
method: "GET",
|
|
96
|
+
}, apiKey);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, chmod, unlink } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const CREDENTIALS_DIR = path.join(os.homedir(), ".agentspend");
|
|
5
|
+
const CREDENTIALS_FILE = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
6
|
+
const PENDING_CONFIGURE_FILE = path.join(CREDENTIALS_DIR, "pending-configure.json");
|
|
7
|
+
export async function readCredentials() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await readFile(CREDENTIALS_FILE, "utf-8");
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (!parsed.api_key || !parsed.api_key.startsWith("sk_agent_")) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return parsed;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export async function saveCredentials(apiKey) {
|
|
21
|
+
const payload = {
|
|
22
|
+
api_key: apiKey,
|
|
23
|
+
created_at: new Date().toISOString(),
|
|
24
|
+
};
|
|
25
|
+
await mkdir(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
26
|
+
await writeFile(CREDENTIALS_FILE, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
27
|
+
try {
|
|
28
|
+
await chmod(CREDENTIALS_FILE, 0o600);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// no-op when chmod is not available on the host filesystem
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function requireApiKey() {
|
|
35
|
+
const credentials = await readCredentials();
|
|
36
|
+
if (!credentials) {
|
|
37
|
+
throw new Error(`No API key found at ${CREDENTIALS_FILE}. Run \`agentspend configure\` first.`);
|
|
38
|
+
}
|
|
39
|
+
return credentials.api_key;
|
|
40
|
+
}
|
|
41
|
+
export async function readPendingConfigureToken() {
|
|
42
|
+
try {
|
|
43
|
+
const raw = await readFile(PENDING_CONFIGURE_FILE, "utf-8");
|
|
44
|
+
const parsed = JSON.parse(raw);
|
|
45
|
+
if (!parsed.token || !parsed.token.startsWith("cfs_")) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return parsed.token;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function savePendingConfigureToken(token) {
|
|
55
|
+
await mkdir(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
56
|
+
await writeFile(PENDING_CONFIGURE_FILE, `${JSON.stringify({ token, created_at: new Date().toISOString() }, null, 2)}\n`, "utf-8");
|
|
57
|
+
}
|
|
58
|
+
export async function clearPendingConfigureToken() {
|
|
59
|
+
try {
|
|
60
|
+
await unlink(PENDING_CONFIGURE_FILE);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// no-op
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const USD6_SCALE = 1_000_000;
|
|
2
|
+
function formatUsdNumber(value, minDecimals = 2, maxDecimals = 6) {
|
|
3
|
+
const fixed = value.toFixed(maxDecimals);
|
|
4
|
+
const trimmed = fixed.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
|
|
5
|
+
if (!trimmed.includes(".")) {
|
|
6
|
+
return minDecimals > 0 ? `${trimmed}.${"0".repeat(minDecimals)}` : trimmed;
|
|
7
|
+
}
|
|
8
|
+
const [whole, frac = ""] = trimmed.split(".");
|
|
9
|
+
if (frac.length >= minDecimals) {
|
|
10
|
+
return trimmed;
|
|
11
|
+
}
|
|
12
|
+
return `${whole}.${frac.padEnd(minDecimals, "0")}`;
|
|
13
|
+
}
|
|
14
|
+
export function usd6ToUsd(usd6) {
|
|
15
|
+
return usd6 / USD6_SCALE;
|
|
16
|
+
}
|
|
17
|
+
export function formatUsd(usd) {
|
|
18
|
+
if (!Number.isFinite(usd)) {
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
return `$${formatUsdNumber(usd, 2, 6)}`;
|
|
22
|
+
}
|
|
23
|
+
export function formatUsdEstimate(estimatedUsd, fallbackUsd) {
|
|
24
|
+
if (typeof estimatedUsd === "number") {
|
|
25
|
+
return formatUsd(estimatedUsd);
|
|
26
|
+
}
|
|
27
|
+
if (typeof fallbackUsd === "number") {
|
|
28
|
+
return formatUsd(fallbackUsd);
|
|
29
|
+
}
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
export function formatJson(value) {
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return JSON.stringify(value, null, 2);
|
|
37
|
+
}
|
|
38
|
+
export function relativeTime(iso) {
|
|
39
|
+
const timestamp = new Date(iso).getTime();
|
|
40
|
+
const diffMs = Date.now() - timestamp;
|
|
41
|
+
const abs = Math.max(0, Math.floor(diffMs / 1000));
|
|
42
|
+
if (abs < 60) {
|
|
43
|
+
return `${abs}s ago`;
|
|
44
|
+
}
|
|
45
|
+
const minutes = Math.floor(abs / 60);
|
|
46
|
+
if (minutes < 60) {
|
|
47
|
+
return `${minutes}m ago`;
|
|
48
|
+
}
|
|
49
|
+
const hours = Math.floor(minutes / 60);
|
|
50
|
+
if (hours < 24) {
|
|
51
|
+
return `${hours}h ago`;
|
|
52
|
+
}
|
|
53
|
+
const days = Math.floor(hours / 24);
|
|
54
|
+
return `${days}d ago`;
|
|
55
|
+
}
|
|
56
|
+
function formatCharge(charge) {
|
|
57
|
+
const amountUsd = charge.amount_usd ?? usd6ToUsd(charge.amount_usd6);
|
|
58
|
+
const amount = formatUsd(amountUsd).padEnd(10);
|
|
59
|
+
const domain = charge.target_domain.padEnd(24);
|
|
60
|
+
const when = relativeTime(charge.created_at);
|
|
61
|
+
return ` ${amount} ${domain} ${when}`;
|
|
62
|
+
}
|
|
63
|
+
export function printStatus(status) {
|
|
64
|
+
console.log(`Weekly budget: ${formatUsd(status.weekly_budget_usd)}`);
|
|
65
|
+
console.log(`Spent this week: ${formatUsd(status.spent_this_week_usd)}`);
|
|
66
|
+
console.log(`Remaining: ${formatUsd(status.remaining_budget_usd)}`);
|
|
67
|
+
console.log("");
|
|
68
|
+
if (status.recent_charges.length === 0) {
|
|
69
|
+
console.log("Recent charges: none");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.log("Recent charges:");
|
|
73
|
+
for (const charge of status.recent_charges) {
|
|
74
|
+
console.log(formatCharge(charge));
|
|
75
|
+
}
|
|
76
|
+
}
|