agentbundle-cli 0.2.1 → 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
@@ -10,12 +10,12 @@ 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, three-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.
@@ -59,7 +59,7 @@ function icon(kind) {
59
59
  return kind === 'ok' ? ui.c.green('✓') : kind === 'warn' ? ui.c.yellow('!') : ui.c.red('✗');
60
60
  }
61
61
 
62
- // One email drives all three sign-ups (Agentcard magic link, AgentMail + AgentPhone OTP). Collect
62
+ // One email drives all three sign-ups (Agentcard, AgentMail + AgentPhone verification codes). Collect
63
63
  // it once up front so each product reuses it instead of asking three times. Stores it on ctx.email.
64
64
  async function collectEmail(ctx) {
65
65
  if (ctx.email && ac.isValidEmail(ctx.email)) return ctx.email; // from --email / AGENT_BUNDLE_EMAIL
@@ -101,7 +101,7 @@ async function main() {
101
101
  }
102
102
 
103
103
  // 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
104
+ // sign-up reuses it (Agentcard + AgentMail + AgentPhone verification codes) and no step ever asks
105
105
  // for it again. A pre-set --email / AGENT_BUNDLE_EMAIL skips the prompt entirely.
106
106
  // Only prompt when a selected product actually needs signup; if everything is already
107
107
  // configured the prompt is skipped and the run stays a clean no-op.
@@ -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.sh.');
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,37 +24,73 @@ 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
 
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentbundle-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",