agentspend 0.1.5 → 0.1.7
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/dist/commands/card.js +85 -6
- package/dist/commands/pay.js +58 -15
- package/package.json +1 -1
- package/src/commands/card.ts +111 -6
- package/src/commands/pay.ts +60 -14
package/dist/commands/card.js
CHANGED
|
@@ -27,7 +27,19 @@ async function readSetupId() {
|
|
|
27
27
|
catch {
|
|
28
28
|
// file doesn't exist or is invalid
|
|
29
29
|
}
|
|
30
|
-
throw new Error("No setup_id found. Run 'agentspend card
|
|
30
|
+
throw new Error("No setup_id found. Run 'agentspend card configure' first.");
|
|
31
|
+
}
|
|
32
|
+
async function readCardFile() {
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(await (0, promises_1.readFile)(CARD_FILE, "utf-8"));
|
|
35
|
+
if (typeof data.card_id === "string" && typeof data.card_secret === "string") {
|
|
36
|
+
return { card_id: data.card_id, card_secret: data.card_secret };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// file doesn't exist or is invalid
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
31
43
|
}
|
|
32
44
|
async function createCard() {
|
|
33
45
|
const response = await fetch(`${API_BASE}/v1/card/create`, {
|
|
@@ -41,6 +53,18 @@ async function createCard() {
|
|
|
41
53
|
}
|
|
42
54
|
return (await response.json());
|
|
43
55
|
}
|
|
56
|
+
async function requestConfigure(cardId, cardSecret) {
|
|
57
|
+
const response = await fetch(`${API_BASE}/v1/card/configure`, {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { "content-type": "application/json" },
|
|
60
|
+
body: JSON.stringify({ card_id: cardId, card_secret: cardSecret })
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const body = await response.text();
|
|
64
|
+
throw new Error(`Failed to open configure page (${response.status}): ${body}`);
|
|
65
|
+
}
|
|
66
|
+
return (await response.json());
|
|
67
|
+
}
|
|
44
68
|
async function getSetupStatus(setupId) {
|
|
45
69
|
const response = await fetch(`${API_BASE}/v1/card/setup/${encodeURIComponent(setupId)}`);
|
|
46
70
|
if (!response.ok) {
|
|
@@ -49,18 +73,61 @@ async function getSetupStatus(setupId) {
|
|
|
49
73
|
}
|
|
50
74
|
return (await response.json());
|
|
51
75
|
}
|
|
76
|
+
async function getCardStatus(cardId, cardSecret) {
|
|
77
|
+
const response = await fetch(`${API_BASE}/v1/card/${encodeURIComponent(cardId)}/status`, {
|
|
78
|
+
headers: { "x-card-secret": cardSecret }
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const body = await response.text();
|
|
82
|
+
throw new Error(`Failed to get card status (${response.status}): ${body}`);
|
|
83
|
+
}
|
|
84
|
+
return (await response.json());
|
|
85
|
+
}
|
|
52
86
|
function sleep(ms) {
|
|
53
87
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
88
|
}
|
|
89
|
+
function formatCents(cents) {
|
|
90
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
91
|
+
}
|
|
55
92
|
function registerCardCommands(program) {
|
|
56
93
|
const card = program
|
|
57
94
|
.command("card")
|
|
58
95
|
.description("Manage AgentSpend cards");
|
|
59
96
|
card
|
|
60
97
|
.command("status")
|
|
61
|
-
.description("
|
|
98
|
+
.description("Show card dashboard: weekly budget, services, and recent charges")
|
|
62
99
|
.action(async () => {
|
|
63
100
|
try {
|
|
101
|
+
// If card.json exists, show full dashboard
|
|
102
|
+
const cardData = await readCardFile();
|
|
103
|
+
if (cardData) {
|
|
104
|
+
const status = await getCardStatus(cardData.card_id, cardData.card_secret);
|
|
105
|
+
console.log(`Card ID: ${status.card_id}`);
|
|
106
|
+
console.log(`Weekly budget: ${formatCents(status.weekly_spent_cents)} / ${formatCents(status.weekly_limit_cents)} used this week`);
|
|
107
|
+
console.log(`Remaining: ${formatCents(status.weekly_remaining_cents)}`);
|
|
108
|
+
console.log();
|
|
109
|
+
if (status.services.length > 0) {
|
|
110
|
+
console.log("Authorized services:");
|
|
111
|
+
for (const svc of status.services) {
|
|
112
|
+
console.log(` - ${svc.name} (${svc.status}, since ${new Date(svc.created_at).toLocaleDateString()})`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.log("Authorized services: No services authorized yet");
|
|
117
|
+
}
|
|
118
|
+
console.log();
|
|
119
|
+
if (status.recent_charges.length > 0) {
|
|
120
|
+
console.log("Recent charges:");
|
|
121
|
+
for (const ch of status.recent_charges) {
|
|
122
|
+
console.log(` - ${ch.service_name}: ${formatCents(ch.amount_cents)} on ${new Date(ch.created_at).toLocaleDateString()}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
console.log("Recent charges: No charges yet");
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Fall back to checking pending setup status
|
|
64
131
|
const setupId = await readSetupId();
|
|
65
132
|
const status = await getSetupStatus(setupId);
|
|
66
133
|
console.log(`Setup ID: ${status.setup_id}`);
|
|
@@ -76,13 +143,22 @@ function registerCardCommands(program) {
|
|
|
76
143
|
}
|
|
77
144
|
});
|
|
78
145
|
card
|
|
79
|
-
.command("
|
|
80
|
-
.description("Set up
|
|
146
|
+
.command("configure")
|
|
147
|
+
.description("Set up or reconfigure your card — opens the configuration page in your browser")
|
|
81
148
|
.action(async () => {
|
|
82
149
|
try {
|
|
150
|
+
// Check if card.json exists — reconfigure flow
|
|
151
|
+
const cardData = await readCardFile();
|
|
152
|
+
if (cardData) {
|
|
153
|
+
const result = await requestConfigure(cardData.card_id, cardData.card_secret);
|
|
154
|
+
console.log("Opened configuration page in browser.");
|
|
155
|
+
openUrl(result.configure_url);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// First-time setup flow
|
|
83
159
|
const result = await createCard();
|
|
84
160
|
console.log(`Setup ID: ${result.setup_id}`);
|
|
85
|
-
console.log(`Opening
|
|
161
|
+
console.log(`Opening configuration page in browser...`);
|
|
86
162
|
openUrl(result.setup_url);
|
|
87
163
|
await ensureConfigDir();
|
|
88
164
|
await (0, promises_1.writeFile)(SETUP_FILE, JSON.stringify({ setup_id: result.setup_id }, null, 2));
|
|
@@ -93,7 +169,10 @@ function registerCardCommands(program) {
|
|
|
93
169
|
if (status.status === "ready") {
|
|
94
170
|
console.log(`Card is ready!`);
|
|
95
171
|
if (status.card_id) {
|
|
96
|
-
await (0, promises_1.writeFile)(CARD_FILE, JSON.stringify({
|
|
172
|
+
await (0, promises_1.writeFile)(CARD_FILE, JSON.stringify({
|
|
173
|
+
card_id: status.card_id,
|
|
174
|
+
card_secret: status.card_secret,
|
|
175
|
+
}, null, 2));
|
|
97
176
|
console.log(`Card ID saved to ${CARD_FILE}`);
|
|
98
177
|
}
|
|
99
178
|
// Clean up setup file
|
package/dist/commands/pay.js
CHANGED
|
@@ -7,10 +7,11 @@ const node_path_1 = require("node:path");
|
|
|
7
7
|
const CONFIG_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), ".agentspend");
|
|
8
8
|
const CARD_FILE = (0, node_path_1.join)(CONFIG_DIR, "card.json");
|
|
9
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";
|
|
10
11
|
async function readCardConfig() {
|
|
11
12
|
try {
|
|
12
13
|
const data = JSON.parse(await (0, promises_1.readFile)(CARD_FILE, "utf-8"));
|
|
13
|
-
if (typeof data.card_id === "string" && data.card_id) {
|
|
14
|
+
if (typeof data.card_id === "string" && data.card_id && typeof data.card_secret === "string" && data.card_secret) {
|
|
14
15
|
return data;
|
|
15
16
|
}
|
|
16
17
|
}
|
|
@@ -42,31 +43,73 @@ function parseHeaders(headerArgs) {
|
|
|
42
43
|
}
|
|
43
44
|
return headers;
|
|
44
45
|
}
|
|
45
|
-
async function payWithCard(url,
|
|
46
|
+
async function payWithCard(url, cardConfig, body, extraHeaders) {
|
|
46
47
|
const headers = {
|
|
47
48
|
...extraHeaders,
|
|
48
|
-
"x-card-id":
|
|
49
|
+
"x-card-id": cardConfig.card_id,
|
|
49
50
|
};
|
|
50
51
|
if (body) {
|
|
51
52
|
headers["content-type"] = "application/json";
|
|
52
53
|
}
|
|
54
|
+
// Try with x-card-id
|
|
53
55
|
const res = await fetch(url, {
|
|
54
56
|
method: "POST",
|
|
55
57
|
headers,
|
|
56
58
|
body: body ?? undefined,
|
|
57
59
|
});
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
console.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
109
|
}
|
|
110
|
+
// Other error
|
|
111
|
+
console.error(`Request failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
112
|
+
process.exit(1);
|
|
70
113
|
}
|
|
71
114
|
async function payWithCrypto(url, walletConfig, body, extraHeaders) {
|
|
72
115
|
const headers = { ...extraHeaders };
|
|
@@ -148,7 +191,7 @@ function registerPayCommand(program) {
|
|
|
148
191
|
console.error("No card configured. Run: agentspend card setup");
|
|
149
192
|
process.exit(1);
|
|
150
193
|
}
|
|
151
|
-
await payWithCard(url, card
|
|
194
|
+
await payWithCard(url, card, opts.body, extraHeaders);
|
|
152
195
|
return;
|
|
153
196
|
}
|
|
154
197
|
if (opts.method === "crypto") {
|
|
@@ -163,7 +206,7 @@ function registerPayCommand(program) {
|
|
|
163
206
|
// Auto-detect: try card first, then crypto
|
|
164
207
|
const card = await readCardConfig();
|
|
165
208
|
if (card) {
|
|
166
|
-
await payWithCard(url, card
|
|
209
|
+
await payWithCard(url, card, opts.body, extraHeaders);
|
|
167
210
|
return;
|
|
168
211
|
}
|
|
169
212
|
const wallet = await readWalletConfig();
|
package/package.json
CHANGED
package/src/commands/card.ts
CHANGED
|
@@ -18,6 +18,21 @@ interface CardSetupStatusResponse {
|
|
|
18
18
|
status: CardSetupStatus;
|
|
19
19
|
expires_at: string;
|
|
20
20
|
card_id?: string;
|
|
21
|
+
card_secret?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CardConfigureResponse {
|
|
25
|
+
setup_id: string;
|
|
26
|
+
configure_url: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CardStatusResponse {
|
|
30
|
+
card_id: string;
|
|
31
|
+
weekly_limit_cents: number;
|
|
32
|
+
weekly_spent_cents: number;
|
|
33
|
+
weekly_remaining_cents: number;
|
|
34
|
+
services: { name: string; status: string; created_at: string }[];
|
|
35
|
+
recent_charges: { service_name: string; amount_cents: number; created_at: string }[];
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
const CONFIG_DIR = join(homedir(), ".agentspend");
|
|
@@ -43,7 +58,19 @@ async function readSetupId(): Promise<string> {
|
|
|
43
58
|
} catch {
|
|
44
59
|
// file doesn't exist or is invalid
|
|
45
60
|
}
|
|
46
|
-
throw new Error("No setup_id found. Run 'agentspend card
|
|
61
|
+
throw new Error("No setup_id found. Run 'agentspend card configure' first.");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readCardFile(): Promise<{ card_id: string; card_secret: string } | null> {
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse(await readFile(CARD_FILE, "utf-8"));
|
|
67
|
+
if (typeof data.card_id === "string" && typeof data.card_secret === "string") {
|
|
68
|
+
return { card_id: data.card_id, card_secret: data.card_secret };
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// file doesn't exist or is invalid
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
47
74
|
}
|
|
48
75
|
|
|
49
76
|
async function createCard(): Promise<CardCreateResponse> {
|
|
@@ -61,6 +88,21 @@ async function createCard(): Promise<CardCreateResponse> {
|
|
|
61
88
|
return (await response.json()) as CardCreateResponse;
|
|
62
89
|
}
|
|
63
90
|
|
|
91
|
+
async function requestConfigure(cardId: string, cardSecret: string): Promise<CardConfigureResponse> {
|
|
92
|
+
const response = await fetch(`${API_BASE}/v1/card/configure`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "content-type": "application/json" },
|
|
95
|
+
body: JSON.stringify({ card_id: cardId, card_secret: cardSecret })
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const body = await response.text();
|
|
100
|
+
throw new Error(`Failed to open configure page (${response.status}): ${body}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (await response.json()) as CardConfigureResponse;
|
|
104
|
+
}
|
|
105
|
+
|
|
64
106
|
async function getSetupStatus(setupId: string): Promise<CardSetupStatusResponse> {
|
|
65
107
|
const response = await fetch(`${API_BASE}/v1/card/setup/${encodeURIComponent(setupId)}`);
|
|
66
108
|
|
|
@@ -72,10 +114,27 @@ async function getSetupStatus(setupId: string): Promise<CardSetupStatusResponse>
|
|
|
72
114
|
return (await response.json()) as CardSetupStatusResponse;
|
|
73
115
|
}
|
|
74
116
|
|
|
117
|
+
async function getCardStatus(cardId: string, cardSecret: string): Promise<CardStatusResponse> {
|
|
118
|
+
const response = await fetch(`${API_BASE}/v1/card/${encodeURIComponent(cardId)}/status`, {
|
|
119
|
+
headers: { "x-card-secret": cardSecret }
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const body = await response.text();
|
|
124
|
+
throw new Error(`Failed to get card status (${response.status}): ${body}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return (await response.json()) as CardStatusResponse;
|
|
128
|
+
}
|
|
129
|
+
|
|
75
130
|
function sleep(ms: number): Promise<void> {
|
|
76
131
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
77
132
|
}
|
|
78
133
|
|
|
134
|
+
function formatCents(cents: number): string {
|
|
135
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
79
138
|
export function registerCardCommands(program: Command): void {
|
|
80
139
|
const card = program
|
|
81
140
|
.command("card")
|
|
@@ -83,9 +142,42 @@ export function registerCardCommands(program: Command): void {
|
|
|
83
142
|
|
|
84
143
|
card
|
|
85
144
|
.command("status")
|
|
86
|
-
.description("
|
|
145
|
+
.description("Show card dashboard: weekly budget, services, and recent charges")
|
|
87
146
|
.action(async () => {
|
|
88
147
|
try {
|
|
148
|
+
// If card.json exists, show full dashboard
|
|
149
|
+
const cardData = await readCardFile();
|
|
150
|
+
if (cardData) {
|
|
151
|
+
const status = await getCardStatus(cardData.card_id, cardData.card_secret);
|
|
152
|
+
|
|
153
|
+
console.log(`Card ID: ${status.card_id}`);
|
|
154
|
+
console.log(`Weekly budget: ${formatCents(status.weekly_spent_cents)} / ${formatCents(status.weekly_limit_cents)} used this week`);
|
|
155
|
+
console.log(`Remaining: ${formatCents(status.weekly_remaining_cents)}`);
|
|
156
|
+
|
|
157
|
+
console.log();
|
|
158
|
+
if (status.services.length > 0) {
|
|
159
|
+
console.log("Authorized services:");
|
|
160
|
+
for (const svc of status.services) {
|
|
161
|
+
console.log(` - ${svc.name} (${svc.status}, since ${new Date(svc.created_at).toLocaleDateString()})`);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
console.log("Authorized services: No services authorized yet");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log();
|
|
168
|
+
if (status.recent_charges.length > 0) {
|
|
169
|
+
console.log("Recent charges:");
|
|
170
|
+
for (const ch of status.recent_charges) {
|
|
171
|
+
console.log(` - ${ch.service_name}: ${formatCents(ch.amount_cents)} on ${new Date(ch.created_at).toLocaleDateString()}`);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
console.log("Recent charges: No charges yet");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Fall back to checking pending setup status
|
|
89
181
|
const setupId = await readSetupId();
|
|
90
182
|
const status = await getSetupStatus(setupId);
|
|
91
183
|
console.log(`Setup ID: ${status.setup_id}`);
|
|
@@ -101,13 +193,23 @@ export function registerCardCommands(program: Command): void {
|
|
|
101
193
|
});
|
|
102
194
|
|
|
103
195
|
card
|
|
104
|
-
.command("
|
|
105
|
-
.description("Set up
|
|
196
|
+
.command("configure")
|
|
197
|
+
.description("Set up or reconfigure your card — opens the configuration page in your browser")
|
|
106
198
|
.action(async () => {
|
|
107
199
|
try {
|
|
200
|
+
// Check if card.json exists — reconfigure flow
|
|
201
|
+
const cardData = await readCardFile();
|
|
202
|
+
if (cardData) {
|
|
203
|
+
const result = await requestConfigure(cardData.card_id, cardData.card_secret);
|
|
204
|
+
console.log("Opened configuration page in browser.");
|
|
205
|
+
openUrl(result.configure_url);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// First-time setup flow
|
|
108
210
|
const result = await createCard();
|
|
109
211
|
console.log(`Setup ID: ${result.setup_id}`);
|
|
110
|
-
console.log(`Opening
|
|
212
|
+
console.log(`Opening configuration page in browser...`);
|
|
111
213
|
openUrl(result.setup_url);
|
|
112
214
|
|
|
113
215
|
await ensureConfigDir();
|
|
@@ -121,7 +223,10 @@ export function registerCardCommands(program: Command): void {
|
|
|
121
223
|
if (status.status === "ready") {
|
|
122
224
|
console.log(`Card is ready!`);
|
|
123
225
|
if (status.card_id) {
|
|
124
|
-
await writeFile(CARD_FILE, JSON.stringify({
|
|
226
|
+
await writeFile(CARD_FILE, JSON.stringify({
|
|
227
|
+
card_id: status.card_id,
|
|
228
|
+
card_secret: status.card_secret,
|
|
229
|
+
}, null, 2));
|
|
125
230
|
console.log(`Card ID saved to ${CARD_FILE}`);
|
|
126
231
|
}
|
|
127
232
|
// Clean up setup file
|
package/src/commands/pay.ts
CHANGED
|
@@ -6,9 +6,11 @@ import { join } from "node:path";
|
|
|
6
6
|
const CONFIG_DIR = join(homedir(), ".agentspend");
|
|
7
7
|
const CARD_FILE = join(CONFIG_DIR, "card.json");
|
|
8
8
|
const WALLET_FILE = join(CONFIG_DIR, "wallet.json");
|
|
9
|
+
const API_BASE = process.env.AGENTSPEND_API_URL ?? "https://api.agentspend.co";
|
|
9
10
|
|
|
10
11
|
interface CardConfig {
|
|
11
12
|
card_id: string;
|
|
13
|
+
card_secret: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
interface WalletConfig {
|
|
@@ -20,7 +22,7 @@ interface WalletConfig {
|
|
|
20
22
|
async function readCardConfig(): Promise<CardConfig | null> {
|
|
21
23
|
try {
|
|
22
24
|
const data = JSON.parse(await readFile(CARD_FILE, "utf-8"));
|
|
23
|
-
if (typeof data.card_id === "string" && data.card_id) {
|
|
25
|
+
if (typeof data.card_id === "string" && data.card_id && typeof data.card_secret === "string" && data.card_secret) {
|
|
24
26
|
return data as CardConfig;
|
|
25
27
|
}
|
|
26
28
|
} catch {
|
|
@@ -55,36 +57,80 @@ function parseHeaders(headerArgs: string[]): Record<string, string> {
|
|
|
55
57
|
|
|
56
58
|
async function payWithCard(
|
|
57
59
|
url: string,
|
|
58
|
-
|
|
60
|
+
cardConfig: CardConfig,
|
|
59
61
|
body: string | undefined,
|
|
60
62
|
extraHeaders: Record<string, string>
|
|
61
63
|
): Promise<void> {
|
|
62
64
|
const headers: Record<string, string> = {
|
|
63
65
|
...extraHeaders,
|
|
64
|
-
"x-card-id":
|
|
66
|
+
"x-card-id": cardConfig.card_id,
|
|
65
67
|
};
|
|
66
68
|
if (body) {
|
|
67
69
|
headers["content-type"] = "application/json";
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
// Try with x-card-id
|
|
70
73
|
const res = await fetch(url, {
|
|
71
74
|
method: "POST",
|
|
72
75
|
headers,
|
|
73
76
|
body: body ?? undefined,
|
|
74
77
|
});
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
if (
|
|
78
|
-
console.
|
|
79
|
-
|
|
79
|
+
// Success
|
|
80
|
+
if (res.ok) {
|
|
81
|
+
console.log("Payment successful (card)");
|
|
82
|
+
const data = await res.text();
|
|
83
|
+
try { console.log(JSON.stringify(JSON.parse(data), null, 2)); }
|
|
84
|
+
catch { console.log(data); }
|
|
85
|
+
return;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
// 402 with agentspend.service_id — need to bind
|
|
89
|
+
if (res.status === 402) {
|
|
90
|
+
const errorBody = await res.json().catch(() => ({})) as Record<string, unknown>;
|
|
91
|
+
const agentspend = errorBody?.agentspend as Record<string, unknown> | undefined;
|
|
92
|
+
const serviceId = agentspend?.service_id as string | undefined;
|
|
93
|
+
|
|
94
|
+
if (serviceId) {
|
|
95
|
+
console.log(`Binding to service ${serviceId}...`);
|
|
96
|
+
const bindRes = await fetch(
|
|
97
|
+
`${API_BASE}/v1/card/${encodeURIComponent(cardConfig.card_id)}/bind`,
|
|
98
|
+
{
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: { "content-type": "application/json" },
|
|
101
|
+
body: JSON.stringify({
|
|
102
|
+
card_secret: cardConfig.card_secret,
|
|
103
|
+
service_id: serviceId,
|
|
104
|
+
}),
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (!bindRes.ok) {
|
|
109
|
+
const bindError = await bindRes.text();
|
|
110
|
+
console.error(`Failed to bind (${bindRes.status}): ${bindError}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log("Bound. Retrying payment...");
|
|
115
|
+
|
|
116
|
+
// Retry with x-card-id
|
|
117
|
+
const retryRes = await fetch(url, { method: "POST", headers, body: body ?? undefined });
|
|
118
|
+
if (!retryRes.ok) {
|
|
119
|
+
console.error(`Retry failed (${retryRes.status}): ${await retryRes.text()}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log("Payment successful (card)");
|
|
124
|
+
const data = await retryRes.text();
|
|
125
|
+
try { console.log(JSON.stringify(JSON.parse(data), null, 2)); }
|
|
126
|
+
catch { console.log(data); }
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
87
129
|
}
|
|
130
|
+
|
|
131
|
+
// Other error
|
|
132
|
+
console.error(`Request failed (${res.status}): ${await res.text().catch(() => "")}`);
|
|
133
|
+
process.exit(1);
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
async function payWithCrypto(
|
|
@@ -185,7 +231,7 @@ export function registerPayCommand(program: Command): void {
|
|
|
185
231
|
console.error("No card configured. Run: agentspend card setup");
|
|
186
232
|
process.exit(1);
|
|
187
233
|
}
|
|
188
|
-
await payWithCard(url, card
|
|
234
|
+
await payWithCard(url, card, opts.body, extraHeaders);
|
|
189
235
|
return;
|
|
190
236
|
}
|
|
191
237
|
|
|
@@ -202,7 +248,7 @@ export function registerPayCommand(program: Command): void {
|
|
|
202
248
|
// Auto-detect: try card first, then crypto
|
|
203
249
|
const card = await readCardConfig();
|
|
204
250
|
if (card) {
|
|
205
|
-
await payWithCard(url, card
|
|
251
|
+
await payWithCard(url, card, opts.body, extraHeaders);
|
|
206
252
|
return;
|
|
207
253
|
}
|
|
208
254
|
|