agentbundle-cli 0.1.1 → 0.1.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 +8 -4
- package/bin/agent-bundle.js +46 -2
- package/lib/steps/agentcard.js +13 -6
- package/lib/steps/agentmail.js +139 -29
- package/lib/steps/agentphone.js +25 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,13 +10,15 @@ npx agentbundle-cli
|
|
|
10
10
|
|
|
11
11
|
## What it does
|
|
12
12
|
|
|
13
|
-
|
|
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:
|
|
14
15
|
|
|
15
16
|
1. **Agentcard** — installs the `agent-cards` CLI, signs you up (one magic-link email click),
|
|
16
17
|
registers + authenticates the MCP server via `agent-cards setup-mcp` (hosted HTTP), and offers
|
|
17
18
|
to switch to production (real cards).
|
|
18
|
-
2. **AgentMail** —
|
|
19
|
-
hosted HTTP MCP server
|
|
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
24
|
|
|
@@ -29,6 +31,7 @@ It's **idempotent** — anything already configured is skipped, so it's safe to
|
|
|
29
31
|
| Flag | Effect |
|
|
30
32
|
|---|---|
|
|
31
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). |
|
|
32
35
|
| `--sandbox` | Walk the whole flow with stub signups + a throwaway config — no real accounts, no charges. |
|
|
33
36
|
| `--dry-run` | Print what it would do, change nothing. |
|
|
34
37
|
| `--yes`, `-y` | Accept defaults, skip confirmations. |
|
|
@@ -38,7 +41,8 @@ It's **idempotent** — anything already configured is skipped, so it's safe to
|
|
|
38
41
|
## Skip prompts with env vars
|
|
39
42
|
|
|
40
43
|
```bash
|
|
41
|
-
export
|
|
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
|
|
42
46
|
export AGENTPHONE_API_KEY=sk_live_...
|
|
43
47
|
npx agentbundle-cli
|
|
44
48
|
```
|
package/bin/agent-bundle.js
CHANGED
|
@@ -4,10 +4,16 @@
|
|
|
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
11
|
|
|
12
|
+
const MODULES = {
|
|
13
|
+
agentcard,
|
|
14
|
+
agentmail,
|
|
15
|
+
agentphone,
|
|
16
|
+
};
|
|
11
17
|
const STEPS = {
|
|
12
18
|
agentcard: agentcard.step,
|
|
13
19
|
agentmail: agentmail.step,
|
|
@@ -15,7 +21,7 @@ const STEPS = {
|
|
|
15
21
|
};
|
|
16
22
|
|
|
17
23
|
function parseArgs(argv) {
|
|
18
|
-
const ctx = { dryRun: false, sandbox: 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() };
|
|
19
25
|
for (let i = 0; i < argv.length; i++) {
|
|
20
26
|
const a = argv[i];
|
|
21
27
|
if (a === '--dry-run') ctx.dryRun = true;
|
|
@@ -25,6 +31,8 @@ function parseArgs(argv) {
|
|
|
25
31
|
else if (a === '--help' || a === '-h') ctx.help = true;
|
|
26
32
|
else if (a === '--only') ctx.only = (argv[++i] || '').toLowerCase();
|
|
27
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();
|
|
28
36
|
}
|
|
29
37
|
return ctx;
|
|
30
38
|
}
|
|
@@ -35,13 +43,14 @@ function help() {
|
|
|
35
43
|
ui.info(ui.c.bold('Usage:') + ' npx agentbundle-cli [options]\n');
|
|
36
44
|
ui.info(ui.c.bold('Options:'));
|
|
37
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)');
|
|
38
47
|
ui.dim('--sandbox walk the whole flow with stub signups + a throwaway config (no money, no real accounts)');
|
|
39
48
|
ui.dim('--dry-run print what it would do, change nothing');
|
|
40
49
|
ui.dim('--yes, -y accept defaults, skip confirmations');
|
|
41
50
|
ui.dim('--no-color disable colored output');
|
|
42
51
|
ui.dim('--help, -h show this help');
|
|
43
52
|
ui.blank();
|
|
44
|
-
ui.info(ui.c.bold('Pre-set
|
|
53
|
+
ui.info(ui.c.bold('Pre-set to skip prompts:') + ' AGENT_BUNDLE_EMAIL, AGENTMAIL_API_KEY, AGENTPHONE_API_KEY');
|
|
45
54
|
ui.info(ui.c.bold('Test safely:') + ' AGENT_BUNDLE_MCP_CONFIG=/tmp/x.json npx agentbundle-cli --sandbox');
|
|
46
55
|
ui.blank();
|
|
47
56
|
}
|
|
@@ -50,6 +59,24 @@ function icon(kind) {
|
|
|
50
59
|
return kind === 'ok' ? ui.c.green('✓') : kind === 'warn' ? ui.c.yellow('!') : ui.c.red('✗');
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
// One email drives all three sign-ups (Agentcard magic link, AgentMail + AgentPhone OTP). 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
|
+
|
|
53
80
|
async function main() {
|
|
54
81
|
const ctx = parseArgs(process.argv.slice(2));
|
|
55
82
|
if (ctx.help) return help();
|
|
@@ -73,6 +100,23 @@ async function main() {
|
|
|
73
100
|
order = [ctx.only];
|
|
74
101
|
}
|
|
75
102
|
|
|
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
|
|
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
|
+
}
|
|
119
|
+
|
|
76
120
|
if (!ctx.yes && !ctx.dryRun) {
|
|
77
121
|
ui.blank();
|
|
78
122
|
const go = await ui.confirm('Ready to set up ' + (ctx.only ? ctx.only : 'all three') + '?', true);
|
package/lib/steps/agentcard.js
CHANGED
|
@@ -21,9 +21,10 @@ function run(cmd, args) {
|
|
|
21
21
|
return !r.error && r.status === 0;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
|
|
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.
|
|
26
|
+
async function completeSignup(ctx) {
|
|
27
|
+
let email = ctx && ctx.email && ac.isValidEmail(ctx.email) ? ctx.email : '';
|
|
27
28
|
for (let tries = 0; tries < 3 && !email; tries++) {
|
|
28
29
|
const entered = await ui.ask('What email should your Agentcard account use?');
|
|
29
30
|
if (!entered) return null; // blank = skip
|
|
@@ -66,7 +67,7 @@ async function step(ctx) {
|
|
|
66
67
|
// 1. Get an authenticated session (JWT).
|
|
67
68
|
let jwt;
|
|
68
69
|
if (ctx.dryRun) {
|
|
69
|
-
ui.step('would
|
|
70
|
+
ui.step('would use the shared email, POST /auth/signup (x-platform: agentbundle), and wait for the magic link');
|
|
70
71
|
ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL} --header "Authorization: Bearer <jwt>"`);
|
|
71
72
|
return { product: 'Agentcard', icon: 'ok', note: 'configured (dry-run)' };
|
|
72
73
|
} else if (ctx.sandbox) {
|
|
@@ -81,7 +82,7 @@ async function step(ctx) {
|
|
|
81
82
|
ui.info('Re-run this in a terminal so it can collect your email and the magic link.');
|
|
82
83
|
return { product: 'Agentcard', icon: 'warn', note: 'needs interactive signup' };
|
|
83
84
|
} else {
|
|
84
|
-
jwt = await completeSignup();
|
|
85
|
+
jwt = await completeSignup(ctx);
|
|
85
86
|
if (!jwt) {
|
|
86
87
|
ui.warn('Skipped Agentcard signup — re-run with --only agentcard when ready.');
|
|
87
88
|
return { product: 'Agentcard', icon: 'warn', note: 'signup not completed' };
|
|
@@ -122,4 +123,10 @@ async function step(ctx) {
|
|
|
122
123
|
return { product: 'Agentcard', icon: 'ok', note };
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
|
|
126
|
+
// Whether a real run would need the shared email for this product (i.e. it isn't already set up).
|
|
127
|
+
// Lets the launcher skip the up-front email prompt when nothing needs signup.
|
|
128
|
+
function needsSetup() {
|
|
129
|
+
return !mcp.exists(MCP_NAME) && !ac.readConfig().jwt;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { step, MCP_NAME, needsSetup };
|
package/lib/steps/agentmail.js
CHANGED
|
@@ -1,12 +1,66 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// [2/3] AgentMail — an email inbox. API-
|
|
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,38 +71,94 @@ async function step(ctx) {
|
|
|
17
71
|
return { product: 'AgentMail', icon: 'ok', note: 'already configured' };
|
|
18
72
|
}
|
|
19
73
|
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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');
|
|
29
84
|
ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL} --header "x-api-key: …"`);
|
|
30
85
|
return { product: 'AgentMail', icon: 'ok', note: 'configured (dry-run)' };
|
|
31
|
-
} else {
|
|
32
|
-
ui.info('AgentMail needs an API key (starts with ' + ui.c.bold('am_') + ').');
|
|
33
|
-
ui.info('Get one (free tier) at ' + ui.c.cyan(CONSOLE) + ' → Settings → API Keys.');
|
|
34
|
-
key = await ui.ask('Paste your AgentMail API key (or leave blank to skip for now):');
|
|
35
|
-
if (!key) {
|
|
36
|
-
ui.warn('No key provided — skipping AgentMail. Re-run later with the key set.');
|
|
37
|
-
return { product: 'AgentMail', icon: 'warn', note: 'needs key (set AGENTMAIL_API_KEY)' };
|
|
38
|
-
}
|
|
39
86
|
}
|
|
40
87
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
if (res.error) {
|
|
47
|
-
ui.fail('Failed to register MCP server: ' + res.error);
|
|
48
|
-
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)');
|
|
49
92
|
}
|
|
50
|
-
|
|
51
|
-
|
|
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();
|
|
52
162
|
}
|
|
53
163
|
|
|
54
|
-
module.exports = { step, MCP_NAME };
|
|
164
|
+
module.exports = { step, MCP_NAME, needsSetup };
|
package/lib/steps/agentphone.js
CHANGED
|
@@ -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,14 +55,16 @@ async function step(ctx) {
|
|
|
53
55
|
return { product: 'AgentPhone', icon: 'ok', note: 'already configured' };
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
// Existing key in env?
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
}
|
|
@@ -71,8 +75,14 @@ async function step(ctx) {
|
|
|
71
75
|
return register('sk_live_sandbox_placeholder_key', ctx, 'configured (sandbox number)');
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
// Real flow.
|
|
75
|
-
|
|
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
|
+
}
|
|
76
86
|
if (!email) {
|
|
77
87
|
ui.warn('No email — skipping AgentPhone. Re-run later.');
|
|
78
88
|
return { product: 'AgentPhone', icon: 'warn', note: 'skipped (no email)' };
|
|
@@ -84,7 +94,7 @@ async function step(ctx) {
|
|
|
84
94
|
if (signup.status === 409) {
|
|
85
95
|
ui.warn('You already have an AgentPhone account for that email.');
|
|
86
96
|
ui.info('Grab a key from ' + ui.c.cyan(SETTINGS) + ' → API Keys.');
|
|
87
|
-
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();
|
|
88
98
|
if (!key) return { product: 'AgentPhone', icon: 'warn', note: 'needs key (set AGENTPHONE_API_KEY)' };
|
|
89
99
|
return register(key, ctx);
|
|
90
100
|
}
|
|
@@ -101,7 +111,7 @@ async function step(ctx) {
|
|
|
101
111
|
}
|
|
102
112
|
|
|
103
113
|
ui.step('Verifying and provisioning your number…');
|
|
104
|
-
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 });
|
|
105
115
|
if (!verify.ok || !verify.data.api_key) {
|
|
106
116
|
ui.fail('Verification failed: ' + (verify.data.error || verify.data.message || `HTTP ${verify.status}`));
|
|
107
117
|
return { product: 'AgentPhone', icon: 'fail', note: 'verification failed' };
|
|
@@ -112,4 +122,9 @@ async function step(ctx) {
|
|
|
112
122
|
return register(verify.data.api_key, ctx, `live number ${verify.data.phone_number}`);
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentbundle-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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",
|