agentspend 0.1.9 → 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.
@@ -1,227 +1,110 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerPayCommand = registerPayCommand;
4
- const promises_1 = require("node:fs/promises");
5
- const node_os_1 = require("node:os");
6
- const node_path_1 = require("node:path");
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
- async function readWalletConfig() {
24
- try {
25
- const data = JSON.parse(await (0, promises_1.readFile)(WALLET_FILE, "utf-8"));
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 parseHeaders(headerArgs) {
36
- const headers = {};
37
- for (const h of headerArgs) {
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
- async function payWithCard(url, cardConfig, body, extraHeaders) {
47
- const headers = {
48
- ...extraHeaders,
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
- // 402 with agentspend.service_id — need to bind
73
- if (res.status === 402) {
74
- const errorBody = await res.json().catch(() => ({}));
75
- const agentspend = errorBody?.agentspend;
76
- const serviceId = agentspend?.service_id;
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
- // Other error
111
- console.error(`Request failed (${res.status}): ${await res.text().catch(() => "")}`);
112
- process.exit(1);
26
+ return undefined;
113
27
  }
114
- async function payWithCrypto(url, walletConfig, body, extraHeaders) {
115
- const headers = { ...extraHeaders };
116
- if (body) {
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
- // Import x402 and viem dynamically
142
- const { x402Client } = await import("@x402/core/client");
143
- const { x402HTTPClient } = await import("@x402/core/http");
144
- const { registerExactEvmScheme } = await import("@x402/evm/exact/client");
145
- const { privateKeyToAccount } = await import("viem/accounts");
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
- console.log("Payment successful (crypto)");
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
- console.log(JSON.stringify(JSON.parse(data), null, 2));
169
- }
170
- catch {
171
- console.log(data);
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
- function registerPayCommand(program) {
175
- program
176
- .command("pay")
177
- .description("Pay a paywall-protected endpoint using card or crypto wallet")
178
- .argument("<url>", "URL of the paywall-protected endpoint")
179
- .option("--method <method>", "Payment method: card or crypto (default: auto-detect)")
180
- .option("--body <json>", "Request body JSON")
181
- .option("--header <key:value>", "Extra headers (repeatable)", (val, prev) => {
182
- prev.push(val);
183
- return prev;
184
- }, [])
185
- .action(async (url, opts) => {
186
- try {
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 (opts.method === "crypto") {
198
- const wallet = await readWalletConfig();
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
- // Auto-detect: try card first, then crypto
207
- const card = await readCardConfig();
208
- if (card) {
209
- await payWithCard(url, card, opts.body, extraHeaders);
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
- const wallet = await readWalletConfig();
213
- if (wallet) {
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
- catch (err) {
223
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
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,7 @@
1
+ import { requireApiKey } from "../lib/credentials.js";
2
+ import { printStatus } from "../lib/output.js";
3
+ export async function runStatus(apiClient) {
4
+ const apiKey = await requireApiKey();
5
+ const status = await apiClient.status(apiKey);
6
+ printStatus(status);
7
+ }
@@ -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
- "use strict";
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- const commander_1 = require("commander");
5
- const card_js_1 = require("./commands/card.js");
6
- const wallet_js_1 = require("./commands/wallet.js");
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
+ });
@@ -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
+ }