agentbundle-cli 0.2.1 → 0.2.3

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 CHANGED
@@ -10,17 +10,21 @@ npx agentbundle-cli
10
10
 
11
11
  ## What it does
12
12
 
13
- It asks for **one email up front** and reuses it for all three sign-ups (the Agentcard magic
14
- link and the AgentMail + AgentPhone verification codes), then runs a guided, three-step onboarding:
13
+ It asks for **one email up front** and reuses it for all three sign-ups (each product emails a
14
+ verification code), then runs a guided, four-step onboarding:
15
15
 
16
- 1. **Agentcard** — installs the `agent-cards` CLI, signs you up (one magic-link email click),
17
- registers + authenticates the MCP server via `agent-cards setup-mcp` (hosted HTTP), and offers
18
- to switch to production (real cards).
16
+ 1. **Agentcard** — signs you up (you type in the emailed verification code), registers the
17
+ hosted HTTP MCP server (OAuth sign-in on first tool use), and offers to switch to
18
+ production (real cards).
19
19
  2. **AgentMail** — fully automated: signs you up via the API (no console visit), provisions an
20
20
  inbox, you type in the verification code, and it registers the hosted HTTP MCP server
21
21
  (`mcp.agentmail.to`). Sign-ups are tagged with the `agentbundle` referrer.
22
22
  3. **AgentPhone** — fully automated: requests a verification code by email, you type it in, it
23
23
  provisions a real US number, and registers the stdio MCP server (`npx -y agentphone-mcp`). No browser.
24
+ 4. **Agent Bundle subscription** — checks whether your email already has the $20/mo bundle and,
25
+ if not, prints a Stripe checkout link for you to open. Nothing is charged by the CLI, and
26
+ nothing is charged until you confirm on that page. Until you do, the three products bill
27
+ separately. Skipped with `--only` and in `--sandbox`/`--dry-run`.
24
28
 
25
29
  All MCP servers are registered at **user scope** (available in every project), via `claude mcp add`.
26
30
 
@@ -8,6 +8,7 @@ const ac = require('../lib/agentcard-api');
8
8
  const agentcard = require('../lib/steps/agentcard');
9
9
  const agentmail = require('../lib/steps/agentmail');
10
10
  const agentphone = require('../lib/steps/agentphone');
11
+ const bundle = require('../lib/steps/bundle');
11
12
 
12
13
  const MODULES = {
13
14
  agentcard,
@@ -59,7 +60,7 @@ function icon(kind) {
59
60
  return kind === 'ok' ? ui.c.green('✓') : kind === 'warn' ? ui.c.yellow('!') : ui.c.red('✗');
60
61
  }
61
62
 
62
- // One email drives all three sign-ups (Agentcard magic link, AgentMail + AgentPhone OTP). Collect
63
+ // One email drives all three sign-ups (Agentcard, AgentMail + AgentPhone verification codes). Collect
63
64
  // it once up front so each product reuses it instead of asking three times. Stores it on ctx.email.
64
65
  async function collectEmail(ctx) {
65
66
  if (ctx.email && ac.isValidEmail(ctx.email)) return ctx.email; // from --email / AGENT_BUNDLE_EMAIL
@@ -86,7 +87,7 @@ async function main() {
86
87
  if (ctx.sandbox) ui.warn('SANDBOX — stub signups, throwaway config, no real accounts or charges.');
87
88
 
88
89
  // Preflight
89
- ui.heading(0, 3, 'Preflight');
90
+ ui.heading(0, 4, 'Preflight');
90
91
  ui.info('Node ' + process.version);
91
92
  ui.info('MCP config target: ' + ui.c.bold(mcp.describeBackend()));
92
93
  ui.dim('Each product loads as MCP tools after you restart your agent session at the end.');
@@ -101,7 +102,7 @@ async function main() {
101
102
  }
102
103
 
103
104
  // Step one: collect the single email FIRST — before the confirm or any product — so every
104
- // sign-up reuses it (Agentcard magic link, AgentMail + AgentPhone codes) and no step ever asks
105
+ // sign-up reuses it (Agentcard + AgentMail + AgentPhone verification codes) and no step ever asks
105
106
  // for it again. A pre-set --email / AGENT_BUNDLE_EMAIL skips the prompt entirely.
106
107
  // Only prompt when a selected product actually needs signup; if everything is already
107
108
  // configured the prompt is skipped and the run stays a clean no-op.
@@ -138,6 +139,18 @@ async function main() {
138
139
  }
139
140
  }
140
141
 
142
+ // The bundle subscription itself. Without this the three products are just three separate
143
+ // sign-ups billed separately — the customer finishes the flow believing they're on the
144
+ // bundle. Skipped for --only runs (one product isn't a bundle) and never fatal.
145
+ if (!ctx.only) {
146
+ try {
147
+ results.push(await bundle.step(ctx));
148
+ } catch (e) {
149
+ ui.fail(`bundle crashed: ${e.message}`);
150
+ results.push({ product: 'bundle', icon: 'fail', note: e.message });
151
+ }
152
+ }
153
+
141
154
  // Summary
142
155
  ui.blank();
143
156
  ui.info(ui.c.bold('── Summary ──────────────────────────────────────────'));
@@ -146,7 +159,8 @@ async function main() {
146
159
  }
147
160
  ui.blank();
148
161
 
149
- const anyConfigured = results.some((r) => r.icon === 'ok');
162
+ // Only the three products register MCP servers — a bundle 'ok' must not imply a restart is due.
163
+ const anyConfigured = results.some((r) => r.icon === 'ok' && r.product !== 'bundle');
150
164
  if (anyConfigured && !ctx.dryRun) {
151
165
  ui.info(ui.c.bold('Next:'));
152
166
  ui.step('Restart your agent session so the new MCP tools load.');
@@ -1,11 +1,15 @@
1
1
  'use strict';
2
2
  // Minimal Agentcard auth client used to complete signup from inside agent-bundle, so the whole
3
- // flow (ask email → magic link → verify) happens here AND the signup is tagged with our source.
3
+ // flow (ask email → enter the emailed code → verify) happens here AND the signup is tagged with
4
+ // our source.
4
5
  //
5
- // Mirrors the agent-cards CLI: same API base, same ~/.agent-cards/config.json ({email,jwt,mode},
6
- // mode 0600), same endpoints (POST /auth/signup, poll GET /auth/me with X-Magic-Token). The one
7
- // difference: we send `x-platform: agentbundle` so the backend records these signups as coming
8
- // from the bundle.
6
+ // Mirrors the agent-cards CLI: same API base, same ~/.agent-cards/config.json (mode 0600), same
7
+ // endpoints. Current backends answer POST /auth/signup with { mode: 'workos' } and email a
8
+ // numeric code, completed via POST /auth/verify-code → { accessToken, refreshToken, email }.
9
+ // Older backends return { sessionId } and email a magic link, completed by polling GET /auth/me
10
+ // with X-Magic-Token — both flows are supported. The one difference from the agent-cards CLI:
11
+ // we send `x-platform: agentbundle` so the backend records these signups as coming from the
12
+ // bundle.
9
13
 
10
14
  const fs = require('node:fs');
11
15
  const path = require('node:path');
@@ -76,16 +80,36 @@ async function getJson(pathname, extraHeaders) {
76
80
  }
77
81
  }
78
82
 
79
- // Request a magic link. Returns { sessionId } or throws with a readable message.
80
- async function requestMagicLink(email) {
83
+ // Start a signup/login. Returns { mode: 'workos' } (a numeric code was emailed; finish with
84
+ // verifyCode) or { mode: 'legacy', sessionId } (a magic link was emailed; finish with
85
+ // waitForVerification). Throws with a readable message on failure.
86
+ async function startSignup(email) {
81
87
  const r = await postJson('/auth/signup', { email });
82
- if (!r.ok || !r.data.sessionId) {
88
+ if (!r.ok) {
83
89
  throw new Error(r.data.error || r.data.message || `signup failed (HTTP ${r.status})`);
84
90
  }
85
- return { sessionId: r.data.sessionId };
91
+ if (r.data.mode === 'workos' || !r.data.sessionId) {
92
+ return { mode: 'workos' };
93
+ }
94
+ if (r.data._emailError) {
95
+ throw new Error('Email delivery failed on our side. Try again in a minute. If it keeps happening, email support@agentcard.com.');
96
+ }
97
+ return { mode: 'legacy', sessionId: r.data.sessionId };
86
98
  }
87
99
 
88
- // Poll until the user clicks the magic link. Returns { jwt, email, mode }.
100
+ // Complete a code-based (WorkOS) login. Returns { accessToken, refreshToken, email }.
101
+ // Throws with err.status set (401 = wrong/expired code → worth re-prompting, 429 = throttled).
102
+ async function verifyCode(email, code) {
103
+ const r = await postJson('/auth/verify-code', { email, code });
104
+ if (r.ok && r.data && (r.data.accessToken || r.data.jwt)) {
105
+ return r.data;
106
+ }
107
+ const err = new Error(r.data.error || r.data.message || `verification failed (HTTP ${r.status})`);
108
+ err.status = r.status;
109
+ throw err;
110
+ }
111
+
112
+ // Legacy flow only: poll until the user clicks the magic link. Returns { jwt, email, mode }.
89
113
  async function waitForVerification(sessionId, { intervalMs = 2500, timeoutMs = 10 * 60 * 1000 } = {}) {
90
114
  const deadline = Date.now() + timeoutMs;
91
115
  // Date.now is fine at runtime; this module is never run inside the workflow sandbox.
@@ -103,12 +127,21 @@ function isValidEmail(s) {
103
127
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
104
128
  }
105
129
 
130
+ // A stored session in ~/.agent-cards/config.json: either a WorkOS token pair
131
+ // (current) or a legacy magic-link JWT.
132
+ function hasSession(cfg) {
133
+ const c = cfg || readConfig();
134
+ return !!(c.accessToken || c.jwt);
135
+ }
136
+
106
137
  module.exports = {
107
138
  SOURCE,
108
139
  API_URL,
109
140
  readConfig,
110
141
  writeConfig,
111
- requestMagicLink,
142
+ hasSession,
143
+ startSignup,
144
+ verifyCode,
112
145
  waitForVerification,
113
146
  isValidEmail,
114
147
  };
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
- // [1/3] Agentcard — virtual cards. agent-bundle drives the full signup (ask email → magic link →
3
- // verify), tagging the signup source as `agentbundle`, then registers the MCP server with the JWT.
2
+ // [1/3] Agentcard — virtual cards. agent-bundle drives the full signup (ask email → enter the
3
+ // emailed code → verify), tagging the signup source as `agentbundle`, then registers the MCP
4
+ // server. Registration uses OAuth (no static Authorization header): Claude opens a browser
5
+ // sign-in the first time a tool runs. A baked bearer would be a short-lived WorkOS access token
6
+ // that silently starts 401ing within hours — same reason `agent-cards setup-mcp` dropped it.
4
7
 
5
8
  const { spawnSync } = require('node:child_process');
6
9
  const ui = require('../ui');
@@ -21,42 +24,78 @@ function run(cmd, args) {
21
24
  return !r.error && r.status === 0;
22
25
  }
23
26
 
24
- // Send the magic link and wait for the click. Reuses the shared email collected up front
25
- // (ctx.email); only asks if one wasn't provided. Returns the JWT, or null.
27
+ const CODE_MAX_ENTRIES = 3;
28
+
29
+ // Sign up (or in): current backends email a numeric code we verify; legacy backends email a
30
+ // magic link we poll for. Reuses the shared email collected up front (ctx.email); only asks if
31
+ // one wasn't provided. Returns true when a session is stored in ~/.agent-cards/config.json.
26
32
  async function completeSignup(ctx) {
27
33
  let email = ctx && ctx.email && ac.isValidEmail(ctx.email) ? ctx.email : '';
28
34
  for (let tries = 0; tries < 3 && !email; tries++) {
29
35
  const entered = await ui.ask('What email should your Agentcard account use?');
30
- if (!entered) return null; // blank = skip
36
+ if (!entered) return false; // blank = skip
31
37
  if (ac.isValidEmail(entered)) email = entered;
32
38
  else ui.warn('That doesn\'t look like an email — try again.');
33
39
  }
34
- if (!email) return null;
40
+ if (!email) return false;
35
41
 
36
- let sessionId;
42
+ let start;
37
43
  try {
38
- ui.step(`Sending a magic link to ${email}…`);
39
- ({ sessionId } = await ac.requestMagicLink(email));
44
+ ui.step(`Starting Agentcard sign-in for ${email}…`);
45
+ start = await ac.startSignup(email);
40
46
  } catch (e) {
41
47
  ui.fail('Could not start signup: ' + e.message);
42
- return null;
48
+ return false;
49
+ }
50
+
51
+ if (start.mode === 'workos') {
52
+ ui.ok(`We emailed a sign-in code to ${ui.c.bold(email)}.`);
53
+ for (let attempt = 1; attempt <= CODE_MAX_ENTRIES; attempt++) {
54
+ const code = (await ui.ask('Enter the code from your email:')).trim();
55
+ if (!code) {
56
+ ui.warn('No code entered — skipping Agentcard signup.');
57
+ return false;
58
+ }
59
+ try {
60
+ const verified = await ac.verifyCode(email, code);
61
+ // Replace the whole session: a stale legacy jwt left in place would shadow
62
+ // the fresh token pair for anything that still reads it.
63
+ ac.writeConfig({
64
+ email: verified.email || email,
65
+ accessToken: verified.accessToken,
66
+ refreshToken: verified.refreshToken,
67
+ jwt: undefined,
68
+ });
69
+ ui.ok(`Signed in as ${ui.c.bold(verified.email || email)}`);
70
+ return true;
71
+ } catch (e) {
72
+ if (e.status === 401 && attempt < CODE_MAX_ENTRIES) {
73
+ ui.warn(`That code didn't work (attempt ${attempt}/${CODE_MAX_ENTRIES}) — check the newest email to ${email}.`);
74
+ continue;
75
+ }
76
+ ui.fail('Signup not completed: ' + e.message);
77
+ return false;
78
+ }
79
+ }
80
+ return false;
43
81
  }
44
82
 
83
+ // Legacy magic-link backend.
45
84
  ui.ok(`Magic link sent to ${ui.c.bold(email)}.`);
46
85
  ui.info('Open your inbox and click the link — ' + ui.c.dim('waiting for you…'));
47
86
  try {
48
- const { jwt, email: verifiedEmail, mode } = await ac.waitForVerification(sessionId);
49
- ac.writeConfig({ email: verifiedEmail || email, jwt, mode });
87
+ const { jwt, email: verifiedEmail, mode } = await ac.waitForVerification(start.sessionId);
88
+ ac.writeConfig({ email: verifiedEmail || email, jwt, mode, accessToken: undefined, refreshToken: undefined });
50
89
  ui.ok(`Signed in as ${ui.c.bold(verifiedEmail || email)}` + (mode ? ` (${mode} mode)` : ''));
51
- return jwt;
90
+ return true;
52
91
  } catch (e) {
53
92
  ui.fail('Signup not completed: ' + e.message);
54
- return null;
93
+ return false;
55
94
  }
56
95
  }
57
96
 
58
97
  async function step(ctx) {
59
- ui.heading(1, 3, 'Agentcard — a virtual card for your agent');
98
+ ui.heading(1, 4, 'Agentcard — a virtual card for your agent');
60
99
  ui.dim('Prepaid virtual cards with per-task budgets, controlled through MCP tools.');
61
100
 
62
101
  if (mcp.exists(MCP_NAME)) {
@@ -64,35 +103,34 @@ async function step(ctx) {
64
103
  return { product: 'Agentcard', icon: 'ok', note: 'already configured' };
65
104
  }
66
105
 
67
- // 1. Get an authenticated session (JWT).
68
- let jwt;
106
+ // 1. Get an authenticated session (so the signup is attributed to the bundle and
107
+ // go-live has an account to work with). The MCP server itself authenticates via
108
+ // OAuth, so no token is threaded into the registration below.
69
109
  if (ctx.dryRun) {
70
- ui.step('would use the shared email, POST /auth/signup (x-platform: agentbundle), and wait for the magic link');
71
- ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL} --header "Authorization: Bearer <jwt>"`);
110
+ ui.step('would use the shared email, POST /auth/signup (x-platform: agentbundle), and verify the emailed code');
111
+ ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL} (OAuth — no static header)`);
72
112
  return { product: 'Agentcard', icon: 'ok', note: 'configured (dry-run)' };
73
113
  } else if (ctx.sandbox) {
74
- ui.step('[sandbox] simulating email → magic link → verified');
75
- jwt = 'sandbox_placeholder_jwt';
76
- } else if (ac.readConfig().jwt) {
114
+ ui.step('[sandbox] simulating email → code → verified');
115
+ } else if (ac.hasSession()) {
77
116
  const cfg = ac.readConfig();
78
117
  ui.ok(`Already signed in to Agentcard${cfg.email ? ' as ' + ui.c.bold(cfg.email) : ''} — skipping signup.`);
79
- jwt = cfg.jwt;
80
118
  } else if (!process.stdin.isTTY) {
81
119
  ui.warn('Not signed in to Agentcard, and no interactive terminal to complete signup.');
82
- ui.info('Re-run this in a terminal so it can collect your email and the magic link.');
120
+ ui.info('Re-run this in a terminal so it can collect your email and the emailed sign-in code.');
83
121
  return { product: 'Agentcard', icon: 'warn', note: 'needs interactive signup' };
84
122
  } else {
85
- jwt = await completeSignup(ctx);
86
- if (!jwt) {
123
+ const signedIn = await completeSignup(ctx);
124
+ if (!signedIn) {
87
125
  ui.warn('Skipped Agentcard signup — re-run with --only agentcard when ready.');
88
126
  return { product: 'Agentcard', icon: 'warn', note: 'signup not completed' };
89
127
  }
90
128
  }
91
129
 
92
- // 2. Register the MCP server with the JWT (matches the authenticated hosted-HTTP setup).
130
+ // 2. Register the MCP server (OAuth: a browser sign-in appears the first time a tool runs).
93
131
  ui.step('Registering the Agentcard MCP server…');
94
132
  const res = mcp.addServer(
95
- { name: MCP_NAME, transport: 'http', url: MCP_URL, headers: { Authorization: `Bearer ${jwt}` } },
133
+ { name: MCP_NAME, transport: 'http', url: MCP_URL },
96
134
  { dryRun: ctx.dryRun }
97
135
  );
98
136
  if (res.error) {
@@ -126,7 +164,7 @@ async function step(ctx) {
126
164
  // Whether a real run would need the shared email for this product (i.e. it isn't already set up).
127
165
  // Lets the launcher skip the up-front email prompt when nothing needs signup.
128
166
  function needsSetup() {
129
- return !mcp.exists(MCP_NAME) && !ac.readConfig().jwt;
167
+ return !mcp.exists(MCP_NAME) && !ac.hasSession();
130
168
  }
131
169
 
132
170
  module.exports = { step, MCP_NAME, needsSetup };
@@ -63,7 +63,7 @@ function register(key, ctx, extraNote) {
63
63
  }
64
64
 
65
65
  async function step(ctx) {
66
- ui.heading(2, 3, 'AgentMail — an email inbox for your agent');
66
+ ui.heading(2, 4, 'AgentMail — an email inbox for your agent');
67
67
  ui.dim('Real inboxes the agent owns — for sign-ups, verification emails, and outreach.');
68
68
 
69
69
  if (mcp.exists(MCP_NAME)) {
@@ -47,7 +47,7 @@ function register(key, ctx, extraNote) {
47
47
  }
48
48
 
49
49
  async function step(ctx) {
50
- ui.heading(3, 3, 'AgentPhone — a real phone number for your agent');
50
+ ui.heading(3, 4, 'AgentPhone — a real phone number for your agent');
51
51
  ui.dim('A live US number for SMS + voice. This step is fully automated — no browser needed.');
52
52
 
53
53
  if (mcp.exists(MCP_NAME)) {
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+ // [4/4] Agent Bundle subscription — the one thing that makes the three products a *bundle*.
3
+ //
4
+ // The three product steps each sign the customer up separately; none of them starts the
5
+ // $20/mo bundle subscription. That subscription lives on the bundle site (Stripe, keyed by
6
+ // email) — `apps/agent-bundle/src/app/api/checkout/route.ts` is written for this CLI to call
7
+ // ("the `agent-bundle` CLI calls this after installing the three MCP servers") but nothing
8
+ // ever did, so every run finished with the customer unsubscribed and no way to tell.
9
+ //
10
+ // We never charge here. We check status, and when there's no subscription we hand back the
11
+ // Stripe hosted checkout URL — the customer enters a card there or ignores it. A missing or
12
+ // unreachable bundle site is a warning, never a failure: the three products are already set up.
13
+
14
+ const ui = require('../ui');
15
+ const ac = require('../agentcard-api');
16
+
17
+ const SITE_URL = (process.env.AGENT_BUNDLE_SITE_URL || 'https://theagentbundle.com').replace(/\/+$/, '');
18
+ const PRICE = '$20/mo';
19
+
20
+ async function getJson(url, init) {
21
+ const ctrl = new AbortController();
22
+ const t = setTimeout(() => ctrl.abort(), 20000);
23
+ try {
24
+ const res = await fetch(url, { ...init, signal: ctrl.signal });
25
+ let body = null;
26
+ try {
27
+ body = await res.json();
28
+ } catch {
29
+ /* non-JSON body — treated as an empty response below */
30
+ }
31
+ return { status: res.status, body: body || {} };
32
+ } finally {
33
+ clearTimeout(t);
34
+ }
35
+ }
36
+
37
+ // True when this run should not touch the bundle site at all.
38
+ function needsSetup(ctx) {
39
+ return !ctx.dryRun && !ctx.sandbox;
40
+ }
41
+
42
+ async function step(ctx) {
43
+ const product = 'bundle';
44
+ ui.heading(4, 4, `Agent Bundle subscription (${PRICE})`);
45
+
46
+ if (ctx.dryRun) {
47
+ ui.step(`would GET ${SITE_URL}/api/status?email=… and, if not subscribed, POST /api/checkout for a ${PRICE} Stripe link`);
48
+ return { product, icon: 'ok', note: 'dry run — no subscription started' };
49
+ }
50
+ if (ctx.sandbox) {
51
+ ui.dim('sandbox — skipping the subscription (no money, no Stripe).');
52
+ return { product, icon: 'ok', note: 'skipped in sandbox' };
53
+ }
54
+ // On a re-run where all three products are already configured the CLI never asks for an email,
55
+ // so fall back to the one the Agentcard sign-up stored. Without this, exactly the customer who
56
+ // most needs the subscription check — the one who already finished the flow — never gets it.
57
+ const email = ctx.email || (ac.readConfig().email || '').trim();
58
+ if (!email) {
59
+ ui.warn('No email set, so I can\'t look up or start the bundle subscription.');
60
+ ui.dim(`Re-run with --email you@example.com, or subscribe at ${SITE_URL}.`);
61
+ return { product, icon: 'warn', note: 'no email — subscribe at ' + SITE_URL };
62
+ }
63
+
64
+ let status;
65
+ try {
66
+ status = await getJson(`${SITE_URL}/api/status?email=${encodeURIComponent(email)}`);
67
+ } catch (e) {
68
+ ui.warn(`Couldn't reach the bundle site (${e.message}).`);
69
+ ui.dim(`Your three products are set up. Subscribe when you can at ${SITE_URL}.`);
70
+ return { product, icon: 'warn', note: 'site unreachable — subscribe at ' + SITE_URL };
71
+ }
72
+
73
+ if (status.status !== 200 || status.body.configured !== true) {
74
+ ui.warn('The Agent Bundle subscription isn\'t open yet.');
75
+ ui.dim('Your three products work regardless — each is billed on its own for now.');
76
+ return { product, icon: 'warn', note: 'bundle not available yet' };
77
+ }
78
+ if (status.body.active === true) {
79
+ ui.ok(`Bundle already active for ${ui.c.bold(email)} — nothing to pay.`);
80
+ return { product, icon: 'ok', note: 'already subscribed' };
81
+ }
82
+
83
+ let checkout;
84
+ try {
85
+ checkout = await getJson(`${SITE_URL}/api/checkout`, {
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json' },
88
+ body: JSON.stringify({ email }),
89
+ });
90
+ } catch (e) {
91
+ ui.warn(`Couldn't start the subscription (${e.message}).`);
92
+ ui.dim(`Subscribe at ${SITE_URL} whenever you're ready.`);
93
+ return { product, icon: 'warn', note: 'checkout failed — subscribe at ' + SITE_URL };
94
+ }
95
+
96
+ // 409 = this email already has an active bundle; the status call raced us. Not an error.
97
+ if (checkout.status === 409) {
98
+ ui.ok(`Bundle already active for ${ui.c.bold(email)} — nothing to pay.`);
99
+ return { product, icon: 'ok', note: 'already subscribed' };
100
+ }
101
+ if (checkout.status !== 200 || !checkout.body.checkoutUrl) {
102
+ ui.warn(`Couldn't start the subscription${checkout.body.error ? ': ' + checkout.body.error : '.'}`);
103
+ ui.dim(`Subscribe at ${SITE_URL} whenever you're ready.`);
104
+ return { product, icon: 'warn', note: 'checkout failed — subscribe at ' + SITE_URL };
105
+ }
106
+
107
+ ui.blank();
108
+ ui.info(`Last step — open this to start the ${ui.c.bold(PRICE)} bundle and enter a card:`);
109
+ ui.info(' ' + ui.c.cyan(checkout.body.checkoutUrl));
110
+ ui.dim('Nothing is charged until you confirm on that page. Until then the three products bill separately.');
111
+ return { product, icon: 'warn', note: `open the checkout link to start the ${PRICE} bundle` };
112
+ }
113
+
114
+ module.exports = { step, needsSetup, SITE_URL, PRICE };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentbundle-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "One command to give your AI agent a card, an inbox, and a phone — installs Agentcard, AgentMail, and AgentPhone via a guided onboarding flow.",
5
5
  "bin": {
6
6
  "agentbundle-cli": "bin/agent-bundle.js",
@@ -10,6 +10,9 @@
10
10
  "engines": {
11
11
  "node": ">=18"
12
12
  },
13
+ "scripts": {
14
+ "test": "node --test test/*.test.js"
15
+ },
13
16
  "files": [
14
17
  "bin",
15
18
  "lib",