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.
@@ -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 setup' first.");
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("Check the setup status of a pending card")
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("setup")
80
- .description("Set up a card and wait until setup is complete")
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 setup URL in browser...`);
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({ card_id: status.card_id }, null, 2));
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
@@ -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, cardId, body, extraHeaders) {
46
+ async function payWithCard(url, cardConfig, body, extraHeaders) {
46
47
  const headers = {
47
48
  ...extraHeaders,
48
- "x-card-id": cardId,
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
- const data = await res.text();
59
- if (!res.ok) {
60
- console.error(`Request failed (${res.status}): ${data}`);
61
- process.exit(1);
62
- }
63
- console.log("Payment successful (card)");
64
- try {
65
- console.log(JSON.stringify(JSON.parse(data), null, 2));
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
- catch {
68
- console.log(data);
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.card_id, opts.body, extraHeaders);
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.card_id, opts.body, extraHeaders);
209
+ await payWithCard(url, card, opts.body, extraHeaders);
167
210
  return;
168
211
  }
169
212
  const wallet = await readWalletConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentspend",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CLI for AgentSpend — manage cards and crypto wallets for AI agent payments",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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 setup' first.");
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("Check the setup status of a pending card")
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("setup")
105
- .description("Set up a card and wait until setup is complete")
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 setup URL in browser...`);
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({ card_id: status.card_id }, null, 2));
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
@@ -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
- cardId: string,
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": cardId,
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
- const data = await res.text();
77
- if (!res.ok) {
78
- console.error(`Request failed (${res.status}): ${data}`);
79
- process.exit(1);
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
- console.log("Payment successful (card)");
83
- try {
84
- console.log(JSON.stringify(JSON.parse(data), null, 2));
85
- } catch {
86
- console.log(data);
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.card_id, opts.body, extraHeaders);
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.card_id, opts.body, extraHeaders);
251
+ await payWithCard(url, card, opts.body, extraHeaders);
206
252
  return;
207
253
  }
208
254