agentbundle-cli 0.2.0 → 0.2.2

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
@@ -5,27 +5,22 @@ One command to give your AI agent the three things it needs to act online — a
5
5
  you through installing all three, one at a time, and registers each as an MCP server.
6
6
 
7
7
  ```bash
8
- npx agent-bundle
8
+ npx agentbundle-cli
9
9
  ```
10
10
 
11
11
  ## What it does
12
12
 
13
- 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, three-step onboarding:
14
15
 
15
- 1. **Agentcard** — installs the `agent-cards` CLI, signs you up (one magic-link email click),
16
- registers + authenticates the MCP server via `agent-cards setup-mcp` (hosted HTTP), and offers
17
- to switch to production (real cards).
18
- 2. **AgentMail** — takes your API key (or points you to the console to make one) and registers the
19
- hosted HTTP MCP server (`mcp.agentmail.to`).
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
+ 2. **AgentMail** — fully automated: signs you up via the API (no console visit), provisions an
20
+ inbox, you type in the verification code, and it registers the hosted HTTP MCP server
21
+ (`mcp.agentmail.to`). Sign-ups are tagged with the `agentbundle` referrer.
20
22
  3. **AgentPhone** — fully automated: requests a verification code by email, you type it in, it
21
23
  provisions a real US number, and registers the stdio MCP server (`npx -y agentphone-mcp`). No browser.
22
- 4. **Agent Bundle** — with all three installed, it offers the bundle subscription: **$20/mo** for
23
- Agentcard + AgentMail + AgentPhone together (about half of buying them standalone). It opens a
24
- Stripe-hosted checkout on the bundle server (theagentbundle.com), keyed by your email — no charge
25
- until you enter a card and confirm on Stripe's page. Re-run with `--only bundle` anytime.
26
-
27
- Every account is created in **production** — real inboxes, real numbers, and live Agentcard cards.
28
- There is no test or sandbox mode.
29
24
 
30
25
  All MCP servers are registered at **user scope** (available in every project), via `claude mcp add`.
31
26
 
@@ -35,7 +30,9 @@ It's **idempotent** — anything already configured is skipped, so it's safe to
35
30
 
36
31
  | Flag | Effect |
37
32
  |---|---|
38
- | `--only <agentcard\|agentmail\|agentphone\|bundle>` | Run just one product, or `bundle` for the $20/mo bundle checkout. |
33
+ | `--only <agentcard\|agentmail\|agentphone>` | Run just one product. |
34
+ | `--email <you@example.com>` | Email used for all three sign-ups (skips the up-front prompt). |
35
+ | `--sandbox` | Walk the whole flow with stub signups + a throwaway config — no real accounts, no charges. |
39
36
  | `--dry-run` | Print what it would do, change nothing. |
40
37
  | `--yes`, `-y` | Accept defaults, skip confirmations. |
41
38
  | `--no-color` | Plain output. |
@@ -44,17 +41,20 @@ It's **idempotent** — anything already configured is skipped, so it's safe to
44
41
  ## Skip prompts with env vars
45
42
 
46
43
  ```bash
47
- export AGENTMAIL_API_KEY=am_...
44
+ export AGENT_BUNDLE_EMAIL=you@example.com # used for all three sign-ups
45
+ export AGENTMAIL_API_KEY=am_... # already have a key? skip the AgentMail sign-up
48
46
  export AGENTPHONE_API_KEY=sk_live_...
49
- npx agent-bundle
47
+ npx agentbundle-cli
50
48
  ```
51
49
 
52
- ## Preview without changing anything
50
+ ## Test it safely
53
51
 
54
- Use `--dry-run` to print every step it would take without touching your config or creating accounts:
52
+ This writes the MCP config to a throwaway file and uses stub signups — your real `~/.claude`
53
+ config is never touched and no money is spent:
55
54
 
56
55
  ```bash
57
- npx agent-bundle --dry-run
56
+ AGENT_BUNDLE_MCP_CONFIG=/tmp/bundle-test.json npx . --sandbox
57
+ cat /tmp/bundle-test.json
58
58
  ```
59
59
 
60
60
  ## How config is written
@@ -4,30 +4,35 @@
4
4
 
5
5
  const ui = require('../lib/ui');
6
6
  const mcp = require('../lib/mcp');
7
+ const ac = require('../lib/agentcard-api');
7
8
  const agentcard = require('../lib/steps/agentcard');
8
9
  const agentmail = require('../lib/steps/agentmail');
9
10
  const agentphone = require('../lib/steps/agentphone');
10
- const bundle = require('../lib/steps/bundle');
11
11
 
12
+ const MODULES = {
13
+ agentcard,
14
+ agentmail,
15
+ agentphone,
16
+ };
12
17
  const STEPS = {
13
18
  agentcard: agentcard.step,
14
19
  agentmail: agentmail.step,
15
20
  agentphone: agentphone.step,
16
21
  };
17
22
 
18
- // The bundle subscription runs after the three products, not as one of them.
19
- const ONLY_TARGETS = new Set([...Object.keys(STEPS), 'bundle']);
20
-
21
23
  function parseArgs(argv) {
22
- const ctx = { dryRun: false, yes: false, only: null, help: false };
24
+ const ctx = { dryRun: false, sandbox: false, yes: false, only: null, help: false, email: (process.env.AGENT_BUNDLE_EMAIL || '').trim() };
23
25
  for (let i = 0; i < argv.length; i++) {
24
26
  const a = argv[i];
25
27
  if (a === '--dry-run') ctx.dryRun = true;
28
+ else if (a === '--sandbox') ctx.sandbox = true;
26
29
  else if (a === '--yes' || a === '-y') ctx.yes = true;
27
30
  else if (a === '--no-color') {/* handled in ui */}
28
31
  else if (a === '--help' || a === '-h') ctx.help = true;
29
32
  else if (a === '--only') ctx.only = (argv[++i] || '').toLowerCase();
30
33
  else if (a.startsWith('--only=')) ctx.only = a.slice(7).toLowerCase();
34
+ else if (a === '--email') ctx.email = (argv[++i] || '').trim();
35
+ else if (a.startsWith('--email=')) ctx.email = a.slice(8).trim();
31
36
  }
32
37
  return ctx;
33
38
  }
@@ -37,13 +42,16 @@ function help() {
37
42
  ui.info('Guided setup for your agent\'s card, inbox, and phone.\n');
38
43
  ui.info(ui.c.bold('Usage:') + ' npx agentbundle-cli [options]\n');
39
44
  ui.info(ui.c.bold('Options:'));
40
- ui.dim('--only <agentcard|agentmail|agentphone|bundle> run just one product (or the $20/mo bundle checkout)');
45
+ ui.dim('--only <agentcard|agentmail|agentphone> run just one product');
46
+ ui.dim('--email <you@example.com> email used for all three sign-ups (skips the prompt)');
47
+ ui.dim('--sandbox walk the whole flow with stub signups + a throwaway config (no money, no real accounts)');
41
48
  ui.dim('--dry-run print what it would do, change nothing');
42
49
  ui.dim('--yes, -y accept defaults, skip confirmations');
43
50
  ui.dim('--no-color disable colored output');
44
51
  ui.dim('--help, -h show this help');
45
52
  ui.blank();
46
- ui.info(ui.c.bold('Pre-set keys to skip prompts:') + ' AGENTMAIL_API_KEY, AGENTPHONE_API_KEY');
53
+ ui.info(ui.c.bold('Pre-set to skip prompts:') + ' AGENT_BUNDLE_EMAIL, AGENTMAIL_API_KEY, AGENTPHONE_API_KEY');
54
+ ui.info(ui.c.bold('Test safely:') + ' AGENT_BUNDLE_MCP_CONFIG=/tmp/x.json npx agentbundle-cli --sandbox');
47
55
  ui.blank();
48
56
  }
49
57
 
@@ -51,12 +59,31 @@ function icon(kind) {
51
59
  return kind === 'ok' ? ui.c.green('✓') : kind === 'warn' ? ui.c.yellow('!') : ui.c.red('✗');
52
60
  }
53
61
 
62
+ // One email drives all three sign-ups (Agentcard, AgentMail + AgentPhone verification codes). Collect
63
+ // it once up front so each product reuses it instead of asking three times. Stores it on ctx.email.
64
+ async function collectEmail(ctx) {
65
+ if (ctx.email && ac.isValidEmail(ctx.email)) return ctx.email; // from --email / AGENT_BUNDLE_EMAIL
66
+ if (ctx.sandbox) { ctx.email = 'agent@sandbox.local'; return ctx.email; }
67
+ // An invalid --email / AGENT_BUNDLE_EMAIL with no TTY to fix it must be cleared, otherwise the
68
+ // steps would post the bad value to the signup APIs and fail. Treat it as "no email set".
69
+ if (ctx.dryRun || !process.stdin.isTTY) { ctx.email = ''; return ''; }
70
+ for (let tries = 0; tries < 3; tries++) {
71
+ const entered = await ui.ask("What email should your agent's card, inbox, and phone use?");
72
+ if (!entered) break;
73
+ if (ac.isValidEmail(entered)) { ctx.email = entered; return entered; }
74
+ ui.warn("That doesn't look like an email — try again.");
75
+ }
76
+ ctx.email = '';
77
+ return '';
78
+ }
79
+
54
80
  async function main() {
55
81
  const ctx = parseArgs(process.argv.slice(2));
56
82
  if (ctx.help) return help();
57
83
 
58
84
  ui.banner();
59
85
  if (ctx.dryRun) ui.warn('DRY RUN — nothing will be changed.');
86
+ if (ctx.sandbox) ui.warn('SANDBOX — stub signups, throwaway config, no real accounts or charges.');
60
87
 
61
88
  // Preflight
62
89
  ui.heading(0, 3, 'Preflight');
@@ -66,15 +93,29 @@ async function main() {
66
93
 
67
94
  let order = ['agentcard', 'agentmail', 'agentphone'];
68
95
  if (ctx.only) {
69
- if (!ONLY_TARGETS.has(ctx.only)) {
70
- ui.fail(`Unknown --only target: ${ctx.only} (use agentcard | agentmail | agentphone | bundle)`);
96
+ if (!STEPS[ctx.only]) {
97
+ ui.fail(`Unknown --only target: ${ctx.only} (use agentcard | agentmail | agentphone)`);
71
98
  process.exit(2);
72
99
  }
73
- order = ctx.only === 'bundle' ? [] : [ctx.only];
100
+ order = [ctx.only];
74
101
  }
75
102
 
76
- // The $20/mo bundle is offered after a full run, or when explicitly requested.
77
- const runBundle = !ctx.only || ctx.only === 'bundle';
103
+ // Step one: collect the single email FIRST — before the confirm or any product — so every
104
+ // sign-up reuses it (Agentcard + AgentMail + AgentPhone verification codes) and no step ever asks
105
+ // for it again. A pre-set --email / AGENT_BUNDLE_EMAIL skips the prompt entirely.
106
+ // Only prompt when a selected product actually needs signup; if everything is already
107
+ // configured the prompt is skipped and the run stays a clean no-op.
108
+ if (!ctx.dryRun) {
109
+ const needEmail = order.some((key) => MODULES[key].needsSetup(ctx));
110
+ if (needEmail) {
111
+ ui.blank();
112
+ ui.info(ui.c.bold(ui.c.blue('Step one — your email')));
113
+ ui.dim('Asked once here, then reused for the sign-ups below — you won\'t be asked again.');
114
+ await collectEmail(ctx);
115
+ if (ctx.email) ui.ok('Using ' + ui.c.bold(ctx.email) + ' for the products that need it.');
116
+ else ui.warn('No email set up front — each product will ask only if it needs one.');
117
+ }
118
+ }
78
119
 
79
120
  if (!ctx.yes && !ctx.dryRun) {
80
121
  ui.blank();
@@ -97,17 +138,6 @@ async function main() {
97
138
  }
98
139
  }
99
140
 
100
- // Offer the $20/mo bundle once the products are in place.
101
- if (runBundle) {
102
- ui.blank();
103
- try {
104
- results.push(await bundle.step(ctx));
105
- } catch (e) {
106
- ui.fail(`bundle crashed: ${e.message}`);
107
- results.push({ product: 'Bundle', icon: 'fail', note: e.message });
108
- }
109
- }
110
-
111
141
  // Summary
112
142
  ui.blank();
113
143
  ui.info(ui.c.bold('── Summary ──────────────────────────────────────────'));
@@ -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,18 +80,39 @@ 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.sh.');
96
+ }
97
+ return { mode: 'legacy', sessionId: r.data.sessionId };
98
+ }
99
+
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;
86
110
  }
87
111
 
88
- // Poll until the user clicks the magic link. Returns { jwt, email, mode }.
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;
115
+ // Date.now is fine at runtime; this module is never run inside the workflow sandbox.
91
116
  for (;;) {
92
117
  const r = await getJson('/auth/me', { 'X-Magic-Token': sessionId });
93
118
  if (r.ok && r.data && r.data.status === 'verified' && r.data.jwt) {
@@ -102,14 +127,11 @@ function isValidEmail(s) {
102
127
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
103
128
  }
104
129
 
105
- // Put the signed-in account into production mode (real cards). New accounts are
106
- // created live, so we call this right after signup. Returns the resulting mode.
107
- async function setMode(jwt, mode) {
108
- const r = await postJson('/users/me/mode', { mode }, { Authorization: `Bearer ${jwt}` });
109
- if (!r.ok) throw new Error(r.data.error || r.data.message || `mode change failed (HTTP ${r.status})`);
110
- // Keep the local config in sync with the account's mode.
111
- writeConfig({ mode });
112
- return r.data.mode || mode;
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);
113
135
  }
114
136
 
115
137
  module.exports = {
@@ -117,8 +139,9 @@ module.exports = {
117
139
  API_URL,
118
140
  readConfig,
119
141
  writeConfig,
120
- requestMagicLink,
142
+ hasSession,
143
+ startSignup,
144
+ verifyCode,
121
145
  waitForVerification,
122
146
  isValidEmail,
123
- setMode,
124
147
  };
package/lib/mcp.js CHANGED
@@ -4,7 +4,8 @@
4
4
  // Two backends, auto-selected:
5
5
  // * "claude": uses the `claude` CLI (`claude mcp get` / `claude mcp add`). Default in real use.
6
6
  // * "file": merges into a JSON config file. Used when AGENT_BUNDLE_MCP_CONFIG is set, or when
7
- // `claude` isn't on PATH — writes to that file instead of the user's ~/.claude config.
7
+ // `claude` isn't on PATH. Perfect for isolated/sandbox testing — never touches the
8
+ // user's real ~/.claude config.
8
9
 
9
10
  const { spawnSync } = require('node:child_process');
10
11
  const fs = require('node:fs');
@@ -1,8 +1,11 @@
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
4
- // and puts the account into production (real cards) — there's no test mode in this flow.
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.
5
7
 
8
+ const { spawnSync } = require('node:child_process');
6
9
  const ui = require('../ui');
7
10
  const mcp = require('../mcp');
8
11
  const ac = require('../agentcard-api');
@@ -10,36 +13,84 @@ const ac = require('../agentcard-api');
10
13
  const MCP_NAME = 'agent-cards';
11
14
  const MCP_URL = 'https://mcp.agentcard.sh/mcp';
12
15
 
13
- // Ask for the email, send the magic link, and wait for the click. Returns the JWT, or null.
14
- async function completeSignup() {
15
- let email = '';
16
+ function have(cmd) {
17
+ const r = spawnSync(cmd, ['--version'], { stdio: 'ignore' });
18
+ return !r.error && r.status === 0;
19
+ }
20
+ function run(cmd, args) {
21
+ ui.pause();
22
+ const r = spawnSync(cmd, args, { stdio: 'inherit' });
23
+ ui.resume();
24
+ return !r.error && r.status === 0;
25
+ }
26
+
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.
32
+ async function completeSignup(ctx) {
33
+ let email = ctx && ctx.email && ac.isValidEmail(ctx.email) ? ctx.email : '';
16
34
  for (let tries = 0; tries < 3 && !email; tries++) {
17
35
  const entered = await ui.ask('What email should your Agentcard account use?');
18
- if (!entered) return null; // blank = skip
36
+ if (!entered) return false; // blank = skip
19
37
  if (ac.isValidEmail(entered)) email = entered;
20
38
  else ui.warn('That doesn\'t look like an email — try again.');
21
39
  }
22
- if (!email) return null;
40
+ if (!email) return false;
23
41
 
24
- let sessionId;
42
+ let start;
25
43
  try {
26
- ui.step(`Sending a magic link to ${email}…`);
27
- ({ sessionId } = await ac.requestMagicLink(email));
44
+ ui.step(`Starting Agentcard sign-in for ${email}…`);
45
+ start = await ac.startSignup(email);
28
46
  } catch (e) {
29
47
  ui.fail('Could not start signup: ' + e.message);
30
- 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;
31
81
  }
32
82
 
83
+ // Legacy magic-link backend.
33
84
  ui.ok(`Magic link sent to ${ui.c.bold(email)}.`);
34
85
  ui.info('Open your inbox and click the link — ' + ui.c.dim('waiting for you…'));
35
86
  try {
36
- const { jwt, email: verifiedEmail, mode } = await ac.waitForVerification(sessionId);
37
- ac.writeConfig({ email: verifiedEmail || email, jwt, mode });
38
- ui.ok(`Signed in as ${ui.c.bold(verifiedEmail || email)}`);
39
- return jwt;
87
+ const { jwt, email: verifiedEmail, mode } = await ac.waitForVerification(start.sessionId);
88
+ ac.writeConfig({ email: verifiedEmail || email, jwt, mode, accessToken: undefined, refreshToken: undefined });
89
+ ui.ok(`Signed in as ${ui.c.bold(verifiedEmail || email)}` + (mode ? ` (${mode} mode)` : ''));
90
+ return true;
40
91
  } catch (e) {
41
92
  ui.fail('Signup not completed: ' + e.message);
42
- return null;
93
+ return false;
43
94
  }
44
95
  }
45
96
 
@@ -52,33 +103,34 @@ async function step(ctx) {
52
103
  return { product: 'Agentcard', icon: 'ok', note: 'already configured' };
53
104
  }
54
105
 
55
- // 1. Get an authenticated session (JWT).
56
- 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.
57
109
  if (ctx.dryRun) {
58
- ui.step('would ask for your email, POST /auth/signup (x-platform: agentbundle), and wait for the magic link');
59
- ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL} --header "Authorization: Bearer <jwt>"`);
60
- ui.dim('would: POST /users/me/mode { mode: "prod" } to put the account into production');
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)`);
61
112
  return { product: 'Agentcard', icon: 'ok', note: 'configured (dry-run)' };
62
- } else if (ac.readConfig().jwt) {
113
+ } else if (ctx.sandbox) {
114
+ ui.step('[sandbox] simulating email → code → verified');
115
+ } else if (ac.hasSession()) {
63
116
  const cfg = ac.readConfig();
64
117
  ui.ok(`Already signed in to Agentcard${cfg.email ? ' as ' + ui.c.bold(cfg.email) : ''} — skipping signup.`);
65
- jwt = cfg.jwt;
66
118
  } else if (!process.stdin.isTTY) {
67
119
  ui.warn('Not signed in to Agentcard, and no interactive terminal to complete signup.');
68
- 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.');
69
121
  return { product: 'Agentcard', icon: 'warn', note: 'needs interactive signup' };
70
122
  } else {
71
- jwt = await completeSignup();
72
- if (!jwt) {
123
+ const signedIn = await completeSignup(ctx);
124
+ if (!signedIn) {
73
125
  ui.warn('Skipped Agentcard signup — re-run with --only agentcard when ready.');
74
126
  return { product: 'Agentcard', icon: 'warn', note: 'signup not completed' };
75
127
  }
76
128
  }
77
129
 
78
- // 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).
79
131
  ui.step('Registering the Agentcard MCP server…');
80
132
  const res = mcp.addServer(
81
- { name: MCP_NAME, transport: 'http', url: MCP_URL, headers: { Authorization: `Bearer ${jwt}` } },
133
+ { name: MCP_NAME, transport: 'http', url: MCP_URL },
82
134
  { dryRun: ctx.dryRun }
83
135
  );
84
136
  if (res.error) {
@@ -87,20 +139,32 @@ async function step(ctx) {
87
139
  }
88
140
  ui.ok('Agentcard MCP server registered.');
89
141
 
90
- // 3. Put the account into PRODUCTION (real cards). New accounts go live by default.
91
- let note = 'configured (production)';
92
- try {
93
- ui.step('Setting your account to production…');
94
- await ac.setMode(jwt, 'prod');
95
- ui.ok('Agentcard account is in ' + ui.c.bold('production') + ' mode.');
96
- ui.dim('Add a payment method to fund real cards: ' + ui.c.bold('agent-cards payment-method') + ' (or in the dashboard).');
97
- } catch (e) {
98
- ui.warn('Could not set production mode automatically: ' + e.message);
99
- ui.dim('Set it from the dashboard, or run ' + ui.c.bold('agent-cards mode prod') + '.');
100
- note = 'configured (set production in the dashboard)';
142
+ // 3. Production (go live) — needs payment method + identity; opt-in. Uses the CLI when present.
143
+ let note = 'configured (TEST mode)';
144
+ if (ctx.sandbox) {
145
+ ui.dim('TEST mode by default. Going live (real cards) is offered here in a real run.');
146
+ } else {
147
+ const live = ctx.yes ? false : await ui.confirm('Switch to PRODUCTION now (issue real cards)? Needs a payment method + a quick identity check.', false);
148
+ if (live) {
149
+ if (have('agent-cards') && run('agent-cards', ['mode', 'prod'])) {
150
+ ui.info('To fund real cards, run ' + ui.c.bold('agent-cards payment-method') + ' (one-time Stripe page) and finish the identity prompts.');
151
+ note = 'configured (PROD mode — finish payment method to issue real cards)';
152
+ } else {
153
+ ui.info('Install the CLI (' + ui.c.bold('npm i -g agent-cards') + ') then run ' + ui.c.bold('agent-cards mode prod') + ' to go live.');
154
+ note = 'configured (TEST mode — install CLI to go live)';
155
+ }
156
+ } else {
157
+ ui.info('Staying in ' + ui.c.bold('TEST') + ' mode (sandbox cards). Go live anytime with ' + ui.c.bold('agent-cards mode prod') + '.');
158
+ }
101
159
  }
102
160
 
103
161
  return { product: 'Agentcard', icon: 'ok', note };
104
162
  }
105
163
 
106
- module.exports = { step, MCP_NAME };
164
+ // Whether a real run would need the shared email for this product (i.e. it isn't already set up).
165
+ // Lets the launcher skip the up-front email prompt when nothing needs signup.
166
+ function needsSetup() {
167
+ return !mcp.exists(MCP_NAME) && !ac.hasSession();
168
+ }
169
+
170
+ module.exports = { step, MCP_NAME, needsSetup };
@@ -1,12 +1,66 @@
1
1
  'use strict';
2
- // [2/3] AgentMail — an email inbox. API-key driven (one console visit only if no key yet).
2
+ // [2/3] AgentMail — an email inbox. Fully API-driven: sign-up -> OTP -> verify (no console visit).
3
+ // Signs up via https://docs.agentmail.to/api-reference/agent/sign-up, tagging referrer=agentbundle,
4
+ // reusing the email collected up front. The sign-up response carries the api_key directly; the OTP
5
+ // verify step lifts the unverified-org send allowlist.
3
6
 
4
7
  const ui = require('../ui');
5
8
  const mcp = require('../mcp');
9
+ const ac = require('../agentcard-api'); // shared email validation (isValidEmail)
6
10
 
7
11
  const MCP_NAME = 'agentmail';
8
12
  const MCP_URL = 'https://mcp.agentmail.to/mcp';
13
+ const API = 'https://api.agentmail.to';
9
14
  const CONSOLE = 'https://console.agentmail.to';
15
+ const REFERRER = 'agentbundle'; // the channel we report this sign-up came from
16
+ const SOURCE = 'agentbundle-cli'; // platform identifier
17
+
18
+ async function apiPost(path, body, token) {
19
+ const ctrl = new AbortController();
20
+ const t = setTimeout(() => ctrl.abort(), 30000);
21
+ try {
22
+ const res = await fetch(API + path, {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
27
+ },
28
+ body: JSON.stringify(body),
29
+ signal: ctrl.signal,
30
+ });
31
+ const text = await res.text();
32
+ let data;
33
+ try { data = JSON.parse(text); } catch { data = { raw: text }; }
34
+ return { ok: res.ok, status: res.status, data };
35
+ } catch (e) {
36
+ return { ok: false, status: 0, data: { error: e.message } };
37
+ } finally {
38
+ clearTimeout(t);
39
+ }
40
+ }
41
+
42
+ // Derive an inbox username from the email local-part, plus a short suffix so the globally-unique
43
+ // inbox address (username@agentmail.to) is unlikely to collide with an existing one.
44
+ function usernameFor(email) {
45
+ const local = String(email).split('@')[0].toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
46
+ const base = local || 'agent';
47
+ const suffix = Math.random().toString(36).slice(2, 6);
48
+ return `${base}-${suffix}`;
49
+ }
50
+
51
+ function register(key, ctx, extraNote) {
52
+ ui.step('Registering the AgentMail MCP server…');
53
+ const res = mcp.addServer(
54
+ { name: MCP_NAME, transport: 'http', url: MCP_URL, headers: { 'x-api-key': key } },
55
+ { dryRun: ctx.dryRun }
56
+ );
57
+ if (res.error) {
58
+ ui.fail('Failed to register MCP server: ' + res.error);
59
+ return { product: 'AgentMail', icon: 'fail', note: res.error };
60
+ }
61
+ ui.ok('AgentMail MCP server registered.');
62
+ return { product: 'AgentMail', icon: 'ok', note: extraNote || 'configured' };
63
+ }
10
64
 
11
65
  async function step(ctx) {
12
66
  ui.heading(2, 3, 'AgentMail — an email inbox for your agent');
@@ -17,35 +71,94 @@ async function step(ctx) {
17
71
  return { product: 'AgentMail', icon: 'ok', note: 'already configured' };
18
72
  }
19
73
 
20
- // Get the API key: env first, else prompt (the one possible browser touch).
21
- let key = process.env.AGENTMAIL_API_KEY || '';
22
- if (key) {
23
- ui.ok(`Using AGENTMAIL_API_KEY from your environment (${ui.mask(key)}).`);
24
- } else if (ctx.dryRun) {
25
- ui.step('would prompt for an AgentMail API key (am_…)');
74
+ // A pre-set key short-circuits the whole sign-up. Trim it — a copied key often carries a
75
+ // trailing newline/space that would register a broken credential.
76
+ const envKey = (process.env.AGENTMAIL_API_KEY || '').trim();
77
+ if (envKey) {
78
+ ui.ok(`Using AGENTMAIL_API_KEY from your environment (${ui.mask(envKey)}).`);
79
+ return register(envKey, ctx);
80
+ }
81
+
82
+ if (ctx.dryRun) {
83
+ ui.step('would: POST /v0/agent/sign-up (referrer: agentbundle) → ask for OTP → POST /v0/agent/verify → get am_ key + inbox');
26
84
  ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL} --header "x-api-key: …"`);
27
85
  return { product: 'AgentMail', icon: 'ok', note: 'configured (dry-run)' };
28
- } else {
29
- ui.info('AgentMail needs an API key (starts with ' + ui.c.bold('am_') + ').');
30
- ui.info('Get one (free tier) at ' + ui.c.cyan(CONSOLE) + ' → Settings → API Keys.');
31
- key = await ui.ask('Paste your AgentMail API key (or leave blank to skip for now):');
32
- if (!key) {
33
- ui.warn('No key provided — skipping AgentMail. Re-run later with the key set.');
34
- return { product: 'AgentMail', icon: 'warn', note: 'needs key (set AGENTMAIL_API_KEY)' };
35
- }
36
86
  }
37
87
 
38
- ui.step('Registering the AgentMail MCP server…');
39
- const res = mcp.addServer(
40
- { name: MCP_NAME, url: MCP_URL, headers: { 'x-api-key': key } },
41
- { dryRun: ctx.dryRun }
42
- );
43
- if (res.error) {
44
- ui.fail('Failed to register MCP server: ' + res.error);
45
- return { product: 'AgentMail', icon: 'fail', note: res.error };
88
+ if (ctx.sandbox) {
89
+ ui.step('[sandbox] simulating sign-up → OTP → verify…');
90
+ ui.ok('[sandbox] provisioned inbox my-agent@agentmail.to');
91
+ return register('am_sandbox_placeholder_key', ctx, 'configured (sandbox inbox)');
46
92
  }
47
- ui.ok('AgentMail MCP server registered.');
48
- return { product: 'AgentMail', icon: 'ok', note: 'configured' };
93
+
94
+ // Real flow — reuse the email collected up front; only ask (with validation) if one wasn't provided.
95
+ let email = ctx.email && ac.isValidEmail(ctx.email) ? ctx.email : '';
96
+ for (let tries = 0; !email && tries < 3; tries++) {
97
+ const entered = await ui.ask('What email should AgentMail send your verification code to?');
98
+ if (!entered) break;
99
+ if (ac.isValidEmail(entered)) email = entered;
100
+ else ui.warn("That doesn't look like an email — try again.");
101
+ }
102
+ if (!email) {
103
+ ui.warn('No email — skipping AgentMail. Re-run later.');
104
+ return { product: 'AgentMail', icon: 'warn', note: 'skipped (no email)' };
105
+ }
106
+
107
+ // Sign-up is idempotent on human_email (re-signup just rotates the key + returns it), so an
108
+ // existing account isn't a 409 — it returns 200. A 409 is a username collision (the random
109
+ // suffix clashed with another inbox), so retry with a fresh username before giving up.
110
+ ui.step('Creating your AgentMail inbox…');
111
+ let signup;
112
+ for (let attempt = 0; attempt < 3; attempt++) {
113
+ signup = await apiPost('/v0/agent/sign-up', {
114
+ human_email: email,
115
+ username: usernameFor(email),
116
+ source: SOURCE,
117
+ referrer: REFERRER,
118
+ });
119
+ if (signup.status !== 409) break;
120
+ }
121
+
122
+ if (signup.status === 409) {
123
+ ui.warn('Could not auto-pick an available inbox name after a few tries.');
124
+ ui.info('Grab a key from ' + ui.c.cyan(CONSOLE) + ' → Settings → API Keys.');
125
+ const key = (await ui.ask('Paste your AgentMail API key (am_…), or blank to skip:')).trim();
126
+ if (!key) return { product: 'AgentMail', icon: 'warn', note: 'needs key (set AGENTMAIL_API_KEY)' };
127
+ return register(key, ctx);
128
+ }
129
+ if (!signup.ok || !signup.data.api_key) {
130
+ ui.fail('Sign-up failed: ' + (signup.data.error || signup.data.message || `HTTP ${signup.status}`));
131
+ return { product: 'AgentMail', icon: 'fail', note: 'sign-up failed' };
132
+ }
133
+
134
+ const key = signup.data.api_key;
135
+ const inbox = signup.data.inbox_id;
136
+ if (inbox) ui.ok('Inbox created: ' + ui.c.bold(inbox));
137
+
138
+ // Verify the OTP to lift the unverified-org send allowlist. The key already works for receiving
139
+ // without it, so a missing/wrong code degrades gracefully rather than failing the whole step.
140
+ ui.ok(`Verification code sent to ${email}. Check your inbox.`);
141
+ const otp = await ui.ask('Enter the 6-digit code (or blank to verify later):');
142
+ if (otp) {
143
+ ui.step('Verifying…');
144
+ const verify = await apiPost('/v0/agent/verify', { otp_code: otp }, key);
145
+ if (verify.ok && verify.data.verified) {
146
+ ui.ok('AgentMail account verified — sending unlocked.');
147
+ } else {
148
+ ui.warn('Could not verify the code now (' + (verify.data.error || verify.data.message || `HTTP ${verify.status}`) + ').');
149
+ ui.dim('Your inbox still works for receiving; verify later to lift send limits.');
150
+ }
151
+ } else {
152
+ ui.dim('Skipped verification — inbox works for receiving; verify later to send freely.');
153
+ }
154
+
155
+ ui.dim('API key stored in the MCP config (masked: ' + ui.mask(key) + '). Never share it.');
156
+ return register(key, ctx, inbox ? `inbox ${inbox}` : 'configured');
157
+ }
158
+
159
+ // Whether a real run would need the shared email for this product (i.e. it isn't already set up).
160
+ function needsSetup() {
161
+ return !mcp.exists(MCP_NAME) && !(process.env.AGENTMAIL_API_KEY || '').trim();
49
162
  }
50
163
 
51
- module.exports = { step, MCP_NAME };
164
+ module.exports = { step, MCP_NAME, needsSetup };
@@ -3,10 +3,12 @@
3
3
 
4
4
  const ui = require('../ui');
5
5
  const mcp = require('../mcp');
6
+ const ac = require('../agentcard-api'); // shared email validation (isValidEmail)
6
7
 
7
8
  const MCP_NAME = 'agentphone';
8
9
  const API = 'https://api.agentphone.ai';
9
10
  const SETTINGS = 'https://agentphone.ai/settings';
11
+ const SOURCE = 'agentbundle'; // sent on verify so AgentPhone attributes the signup to the bundle
10
12
 
11
13
  async function apiPost(path, body) {
12
14
  const ctrl = new AbortController();
@@ -53,20 +55,34 @@ async function step(ctx) {
53
55
  return { product: 'AgentPhone', icon: 'ok', note: 'already configured' };
54
56
  }
55
57
 
56
- // Existing key in env?
57
- if (process.env.AGENTPHONE_API_KEY) {
58
- ui.ok(`Using AGENTPHONE_API_KEY from your environment (${ui.mask(process.env.AGENTPHONE_API_KEY)}).`);
59
- return register(process.env.AGENTPHONE_API_KEY, ctx);
58
+ // Existing key in env? Trim it — a copied key often carries a trailing newline/space that
59
+ // would register a broken credential.
60
+ const envKey = (process.env.AGENTPHONE_API_KEY || '').trim();
61
+ if (envKey) {
62
+ ui.ok(`Using AGENTPHONE_API_KEY from your environment (${ui.mask(envKey)}).`);
63
+ return register(envKey, ctx);
60
64
  }
61
65
 
62
66
  if (ctx.dryRun) {
63
- ui.step('would: POST /v0/agent/sign-up → ask for OTP → POST /v0/agent/verify → get sk_live_ key + number');
67
+ ui.step('would: POST /v0/agent/sign-up → ask for OTP → POST /v0/agent/verify (source: agentbundle) → get sk_live_ key + number');
64
68
  ui.dim(`would: claude mcp add ${MCP_NAME} --env AGENTPHONE_API_KEY=… -- npx -y agentphone-mcp`);
65
69
  return { product: 'AgentPhone', icon: 'ok', note: 'configured (dry-run)' };
66
70
  }
67
71
 
68
- // Real flow.
69
- const email = await ui.ask('What email should AgentPhone send your verification code to?');
72
+ if (ctx.sandbox) {
73
+ ui.step('[sandbox] simulating sign-up → OTP → verify…');
74
+ ui.ok('[sandbox] provisioned number +1 (555) 010-0042');
75
+ return register('sk_live_sandbox_placeholder_key', ctx, 'configured (sandbox number)');
76
+ }
77
+
78
+ // Real flow — reuse the email collected up front; only ask (with validation) if one wasn't provided.
79
+ let email = ctx.email && ac.isValidEmail(ctx.email) ? ctx.email : '';
80
+ for (let tries = 0; !email && tries < 3; tries++) {
81
+ const entered = await ui.ask('What email should AgentPhone send your verification code to?');
82
+ if (!entered) break;
83
+ if (ac.isValidEmail(entered)) email = entered;
84
+ else ui.warn("That doesn't look like an email — try again.");
85
+ }
70
86
  if (!email) {
71
87
  ui.warn('No email — skipping AgentPhone. Re-run later.');
72
88
  return { product: 'AgentPhone', icon: 'warn', note: 'skipped (no email)' };
@@ -78,7 +94,7 @@ async function step(ctx) {
78
94
  if (signup.status === 409) {
79
95
  ui.warn('You already have an AgentPhone account for that email.');
80
96
  ui.info('Grab a key from ' + ui.c.cyan(SETTINGS) + ' → API Keys.');
81
- const key = await ui.ask('Paste your AgentPhone API key (sk_live_…), or blank to skip:');
97
+ const key = (await ui.ask('Paste your AgentPhone API key (sk_live_…), or blank to skip:')).trim();
82
98
  if (!key) return { product: 'AgentPhone', icon: 'warn', note: 'needs key (set AGENTPHONE_API_KEY)' };
83
99
  return register(key, ctx);
84
100
  }
@@ -95,7 +111,7 @@ async function step(ctx) {
95
111
  }
96
112
 
97
113
  ui.step('Verifying and provisioning your number…');
98
- const verify = await apiPost('/v0/agent/verify', { verification_id: signup.data.verification_id, otp_code: otp });
114
+ const verify = await apiPost('/v0/agent/verify', { verification_id: signup.data.verification_id, otp_code: otp, source: SOURCE });
99
115
  if (!verify.ok || !verify.data.api_key) {
100
116
  ui.fail('Verification failed: ' + (verify.data.error || verify.data.message || `HTTP ${verify.status}`));
101
117
  return { product: 'AgentPhone', icon: 'fail', note: 'verification failed' };
@@ -106,4 +122,9 @@ async function step(ctx) {
106
122
  return register(verify.data.api_key, ctx, `live number ${verify.data.phone_number}`);
107
123
  }
108
124
 
109
- module.exports = { step, MCP_NAME };
125
+ // Whether a real run would need the shared email for this product (i.e. it isn't already set up).
126
+ function needsSetup() {
127
+ return !mcp.exists(MCP_NAME) && !(process.env.AGENTPHONE_API_KEY || '').trim();
128
+ }
129
+
130
+ module.exports = { step, MCP_NAME, needsSetup };
package/lib/ui.js CHANGED
@@ -36,9 +36,7 @@ function banner() {
36
36
  }
37
37
 
38
38
  function heading(n, total, title) {
39
- // total falsy → a plain section heading with no [n/total] counter (e.g. the bundle step).
40
- const prefix = total ? `[${n}/${total}] ` : '';
41
- stdout.write('\n' + c.bold(c.blue(`${prefix}${title}`)) + '\n');
39
+ stdout.write('\n' + c.bold(c.blue(`[${n}/${total}] ${title}`)) + '\n');
42
40
  }
43
41
 
44
42
  const info = (m) => stdout.write(' ' + m + '\n');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentbundle-cli",
3
- "version": "0.2.0",
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.",
3
+ "version": "0.2.2",
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",
7
7
  "agentbundle": "bin/agent-bundle.js",
@@ -25,5 +25,13 @@
25
25
  "onboarding",
26
26
  "cli"
27
27
  ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/tiny-agent-company/tiny-agent-company.git",
34
+ "directory": "apps/agent-bundle-cli"
35
+ },
28
36
  "license": "MIT"
29
37
  }
@@ -1,136 +0,0 @@
1
- 'use strict';
2
- // Final step — the $20/mo bundle subscription. After all three MCP servers are
3
- // installed, this offers to subscribe to the Agent Bundle: one $20/mo charge for
4
- // Agentcard + AgentMail + AgentPhone (vs ~$43 buying them standalone).
5
- //
6
- // Billing runs on the Agent Bundle's OWN server (theagentbundle.com), on the
7
- // Agentcard Corporation Stripe account — separate from Agentcard's product
8
- // backend. The customer is keyed by email; we just open Stripe's hosted
9
- // checkout, so no charge happens until they enter a card and confirm there.
10
-
11
- const { spawn } = require('node:child_process');
12
- const ui = require('../ui');
13
- const ac = require('../agentcard-api');
14
-
15
- const PRICE_LABEL = '$20/mo';
16
- // The bundle server. Override for local/staging with AGENT_BUNDLE_API_URL.
17
- const BUNDLE_API_URL = (process.env.AGENT_BUNDLE_API_URL || 'https://theagentbundle.com').replace(/\/$/, '');
18
-
19
- async function fetchJson(method, path, body) {
20
- const ctrl = new AbortController();
21
- const t = setTimeout(() => ctrl.abort(), 30000);
22
- try {
23
- const res = await fetch(BUNDLE_API_URL + path, {
24
- method,
25
- headers: { 'Content-Type': 'application/json', 'x-source': 'agent-bundle-cli' },
26
- ...(body ? { body: JSON.stringify(body) } : {}),
27
- signal: ctrl.signal,
28
- });
29
- const text = await res.text();
30
- let data;
31
- try { data = JSON.parse(text); } catch { data = { raw: text }; }
32
- return { ok: res.ok, status: res.status, data };
33
- } finally {
34
- clearTimeout(t);
35
- }
36
- }
37
-
38
- // Open a URL in the user's default browser, best-effort and non-blocking.
39
- function openInBrowser(url) {
40
- const cmd = process.platform === 'darwin' ? 'open'
41
- : process.platform === 'win32' ? 'cmd'
42
- : 'xdg-open';
43
- const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
44
- try {
45
- const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
46
- child.on('error', () => {});
47
- child.unref();
48
- return true;
49
- } catch {
50
- return false;
51
- }
52
- }
53
-
54
- // Use the email from the Agentcard signup; fall back to asking.
55
- async function resolveEmail(ctx) {
56
- const saved = ac.readConfig().email;
57
- if (saved && ac.isValidEmail(saved)) return saved;
58
- if (ctx.yes || !process.stdin.isTTY) return null;
59
- for (let tries = 0; tries < 3; tries++) {
60
- const entered = await ui.ask('What email should your bundle subscription use?');
61
- if (!entered) return null;
62
- if (ac.isValidEmail(entered)) return entered;
63
- ui.warn('That doesn\'t look like an email — try again.');
64
- }
65
- return null;
66
- }
67
-
68
- async function step(ctx) {
69
- ui.heading(0, 0, 'Agent Bundle — one subscription for all three');
70
- ui.dim(`Agentcard + AgentMail + AgentPhone together for ${PRICE_LABEL} (about half of buying them separately).`);
71
-
72
- if (ctx.dryRun) {
73
- ui.step(`would: POST ${BUNDLE_API_URL}/api/checkout {email} → open Stripe checkout for ${PRICE_LABEL}`);
74
- return { product: 'Bundle', icon: 'ok', note: `offer ${PRICE_LABEL} (dry-run)` };
75
- }
76
- const email = await resolveEmail(ctx);
77
- if (!email) {
78
- ui.warn('No email for the bundle — skipping. Re-run with ' + ui.c.bold('--only bundle') + ' to subscribe.');
79
- return { product: 'Bundle', icon: 'warn', note: 'needs email' };
80
- }
81
-
82
- // Is the bundle live, and is this email already subscribed?
83
- try {
84
- const r = await fetchJson('GET', `/api/status?email=${encodeURIComponent(email)}`);
85
- if (r.ok && r.data && r.data.configured === false) {
86
- ui.info('The bundle isn\'t available yet — your three MCP servers are installed and ready.');
87
- return { product: 'Bundle', icon: 'ok', note: 'not offered (coming soon)' };
88
- }
89
- if (r.ok && r.data && r.data.active) {
90
- ui.ok('This email already has an active Agent Bundle — nothing to do.');
91
- return { product: 'Bundle', icon: 'ok', note: 'already subscribed' };
92
- }
93
- } catch {
94
- ui.dim('(could not check bundle status — offering anyway)');
95
- }
96
-
97
- if (!ctx.yes) {
98
- const go = await ui.confirm(`Subscribe to the Agent Bundle for ${PRICE_LABEL} now?`, true);
99
- if (!go) {
100
- ui.info('No problem — subscribe anytime by re-running with ' + ui.c.bold('--only bundle') + '.');
101
- return { product: 'Bundle', icon: 'warn', note: 'declined' };
102
- }
103
- }
104
-
105
- let checkoutUrl;
106
- try {
107
- ui.step('Creating your secure Stripe checkout…');
108
- const r = await fetchJson('POST', '/api/checkout', { email });
109
- if (r.status === 503) {
110
- ui.info('The bundle isn\'t available yet — your three MCP servers are installed and ready.');
111
- return { product: 'Bundle', icon: 'ok', note: 'not offered (coming soon)' };
112
- }
113
- if (r.status === 409) {
114
- ui.ok('This email already has an active Agent Bundle — nothing to do.');
115
- return { product: 'Bundle', icon: 'ok', note: 'already subscribed' };
116
- }
117
- if (!r.ok || !r.data || !r.data.checkoutUrl) {
118
- throw new Error((r.data && r.data.error) || `HTTP ${r.status}`);
119
- }
120
- checkoutUrl = r.data.checkoutUrl;
121
- } catch (e) {
122
- ui.fail('Could not start checkout: ' + e.message);
123
- return { product: 'Bundle', icon: 'fail', note: 'checkout failed' };
124
- }
125
-
126
- const opened = openInBrowser(checkoutUrl);
127
- if (opened) ui.ok('Opened Stripe checkout in your browser.');
128
- else ui.warn('Could not open your browser automatically.');
129
- ui.info('Complete the ' + ui.c.bold(PRICE_LABEL) + ' subscription here:');
130
- ui.info(' ' + ui.c.cyan(checkoutUrl));
131
- ui.dim('No charge happens until you enter a card and confirm on Stripe\'s page.');
132
-
133
- return { product: 'Bundle', icon: 'ok', note: `checkout opened (${PRICE_LABEL})` };
134
- }
135
-
136
- module.exports = { step };