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 +9 -5
- package/bin/agent-bundle.js +18 -4
- package/lib/agentcard-api.js +44 -11
- package/lib/steps/agentcard.js +67 -29
- package/lib/steps/agentmail.js +1 -1
- package/lib/steps/agentphone.js +1 -1
- package/lib/steps/bundle.js +114 -0
- package/package.json +4 -1
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 (
|
|
14
|
-
|
|
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** —
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
package/bin/agent-bundle.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.');
|
package/lib/agentcard-api.js
CHANGED
|
@@ -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 →
|
|
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 (
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
//
|
|
80
|
-
|
|
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
|
|
88
|
+
if (!r.ok) {
|
|
83
89
|
throw new Error(r.data.error || r.data.message || `signup failed (HTTP ${r.status})`);
|
|
84
90
|
}
|
|
85
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
142
|
+
hasSession,
|
|
143
|
+
startSignup,
|
|
144
|
+
verifyCode,
|
|
112
145
|
waitForVerification,
|
|
113
146
|
isValidEmail,
|
|
114
147
|
};
|
package/lib/steps/agentcard.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// [1/3] Agentcard — virtual cards. agent-bundle drives the full signup (ask email →
|
|
3
|
-
// verify), tagging the signup source as `agentbundle`, then registers the MCP
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
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
|
|
40
|
+
if (!email) return false;
|
|
35
41
|
|
|
36
|
-
let
|
|
42
|
+
let start;
|
|
37
43
|
try {
|
|
38
|
-
ui.step(`
|
|
39
|
-
|
|
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
|
|
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
|
|
90
|
+
return true;
|
|
52
91
|
} catch (e) {
|
|
53
92
|
ui.fail('Signup not completed: ' + e.message);
|
|
54
|
-
return
|
|
93
|
+
return false;
|
|
55
94
|
}
|
|
56
95
|
}
|
|
57
96
|
|
|
58
97
|
async function step(ctx) {
|
|
59
|
-
ui.heading(1,
|
|
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 (
|
|
68
|
-
|
|
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
|
|
71
|
-
ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}
|
|
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 →
|
|
75
|
-
|
|
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
|
|
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
|
-
|
|
86
|
-
if (!
|
|
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
|
|
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
|
|
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.
|
|
167
|
+
return !mcp.exists(MCP_NAME) && !ac.hasSession();
|
|
130
168
|
}
|
|
131
169
|
|
|
132
170
|
module.exports = { step, MCP_NAME, needsSetup };
|
package/lib/steps/agentmail.js
CHANGED
|
@@ -63,7 +63,7 @@ function register(key, ctx, extraNote) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
async function step(ctx) {
|
|
66
|
-
ui.heading(2,
|
|
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)) {
|
package/lib/steps/agentphone.js
CHANGED
|
@@ -47,7 +47,7 @@ function register(key, ctx, extraNote) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async function step(ctx) {
|
|
50
|
-
ui.heading(3,
|
|
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.
|
|
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",
|