agentbundle-cli 0.1.0
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 +64 -0
- package/assets/agentbundle.txt +5 -0
- package/bin/agent-bundle.js +125 -0
- package/lib/agentcard-api.js +114 -0
- package/lib/mcp.js +98 -0
- package/lib/steps/agentcard.js +125 -0
- package/lib/steps/agentmail.js +54 -0
- package/lib/steps/agentphone.js +115 -0
- package/lib/ui.js +107 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# agent-bundle
|
|
2
|
+
|
|
3
|
+
One command to give your AI agent the three things it needs to act online — a **card**
|
|
4
|
+
(Agentcard), an **inbox** (AgentMail), and a **phone number** (AgentPhone). `agent-bundle` walks
|
|
5
|
+
you through installing all three, one at a time, and registers each as an MCP server.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx agentbundle
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
A guided, three-step onboarding:
|
|
14
|
+
|
|
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`).
|
|
20
|
+
3. **AgentPhone** — fully automated: requests a verification code by email, you type it in, it
|
|
21
|
+
provisions a real US number, and registers the stdio MCP server (`npx -y agentphone-mcp`). No browser.
|
|
22
|
+
|
|
23
|
+
All MCP servers are registered at **user scope** (available in every project), via `claude mcp add`.
|
|
24
|
+
|
|
25
|
+
It's **idempotent** — anything already configured is skipped, so it's safe to re-run.
|
|
26
|
+
|
|
27
|
+
## Options
|
|
28
|
+
|
|
29
|
+
| Flag | Effect |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `--only <agentcard\|agentmail\|agentphone>` | Run just one product. |
|
|
32
|
+
| `--sandbox` | Walk the whole flow with stub signups + a throwaway config — no real accounts, no charges. |
|
|
33
|
+
| `--dry-run` | Print what it would do, change nothing. |
|
|
34
|
+
| `--yes`, `-y` | Accept defaults, skip confirmations. |
|
|
35
|
+
| `--no-color` | Plain output. |
|
|
36
|
+
| `--help`, `-h` | Help. |
|
|
37
|
+
|
|
38
|
+
## Skip prompts with env vars
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export AGENTMAIL_API_KEY=am_...
|
|
42
|
+
export AGENTPHONE_API_KEY=sk_live_...
|
|
43
|
+
npx agentbundle
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Test it safely
|
|
47
|
+
|
|
48
|
+
This writes the MCP config to a throwaway file and uses stub signups — your real `~/.claude`
|
|
49
|
+
config is never touched and no money is spent:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
AGENT_BUNDLE_MCP_CONFIG=/tmp/bundle-test.json npx . --sandbox
|
|
53
|
+
cat /tmp/bundle-test.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## How config is written
|
|
57
|
+
|
|
58
|
+
By default it uses the `claude` CLI (`claude mcp add --transport http …`). If `claude` isn't on
|
|
59
|
+
your PATH, or you set `AGENT_BUNDLE_MCP_CONFIG`, it writes the MCP entries to that JSON file
|
|
60
|
+
instead. API keys are stored as request headers in the config and never printed to the terminal.
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
Node 18+ (for built-in `fetch`). Zero runtime dependencies.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// agent-bundle — guided onboarding that installs Agentcard, AgentMail, and AgentPhone one by one.
|
|
4
|
+
|
|
5
|
+
const ui = require('../lib/ui');
|
|
6
|
+
const mcp = require('../lib/mcp');
|
|
7
|
+
const agentcard = require('../lib/steps/agentcard');
|
|
8
|
+
const agentmail = require('../lib/steps/agentmail');
|
|
9
|
+
const agentphone = require('../lib/steps/agentphone');
|
|
10
|
+
|
|
11
|
+
const STEPS = {
|
|
12
|
+
agentcard: agentcard.step,
|
|
13
|
+
agentmail: agentmail.step,
|
|
14
|
+
agentphone: agentphone.step,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const ctx = { dryRun: false, sandbox: false, yes: false, only: null, help: false };
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
if (a === '--dry-run') ctx.dryRun = true;
|
|
22
|
+
else if (a === '--sandbox') ctx.sandbox = true;
|
|
23
|
+
else if (a === '--yes' || a === '-y') ctx.yes = true;
|
|
24
|
+
else if (a === '--no-color') {/* handled in ui */}
|
|
25
|
+
else if (a === '--help' || a === '-h') ctx.help = true;
|
|
26
|
+
else if (a === '--only') ctx.only = (argv[++i] || '').toLowerCase();
|
|
27
|
+
else if (a.startsWith('--only=')) ctx.only = a.slice(7).toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
return ctx;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function help() {
|
|
33
|
+
ui.banner();
|
|
34
|
+
ui.info('Guided setup for your agent\'s card, inbox, and phone.\n');
|
|
35
|
+
ui.info(ui.c.bold('Usage:') + ' npx agent-bundle [options]\n');
|
|
36
|
+
ui.info(ui.c.bold('Options:'));
|
|
37
|
+
ui.dim('--only <agentcard|agentmail|agentphone> run just one product');
|
|
38
|
+
ui.dim('--sandbox walk the whole flow with stub signups + a throwaway config (no money, no real accounts)');
|
|
39
|
+
ui.dim('--dry-run print what it would do, change nothing');
|
|
40
|
+
ui.dim('--yes, -y accept defaults, skip confirmations');
|
|
41
|
+
ui.dim('--no-color disable colored output');
|
|
42
|
+
ui.dim('--help, -h show this help');
|
|
43
|
+
ui.blank();
|
|
44
|
+
ui.info(ui.c.bold('Pre-set keys to skip prompts:') + ' AGENTMAIL_API_KEY, AGENTPHONE_API_KEY');
|
|
45
|
+
ui.info(ui.c.bold('Test safely:') + ' AGENT_BUNDLE_MCP_CONFIG=/tmp/x.json npx agent-bundle --sandbox');
|
|
46
|
+
ui.blank();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function icon(kind) {
|
|
50
|
+
return kind === 'ok' ? ui.c.green('✓') : kind === 'warn' ? ui.c.yellow('!') : ui.c.red('✗');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
const ctx = parseArgs(process.argv.slice(2));
|
|
55
|
+
if (ctx.help) return help();
|
|
56
|
+
|
|
57
|
+
ui.banner();
|
|
58
|
+
if (ctx.dryRun) ui.warn('DRY RUN — nothing will be changed.');
|
|
59
|
+
if (ctx.sandbox) ui.warn('SANDBOX — stub signups, throwaway config, no real accounts or charges.');
|
|
60
|
+
|
|
61
|
+
// Preflight
|
|
62
|
+
ui.heading(0, 3, 'Preflight');
|
|
63
|
+
ui.info('Node ' + process.version);
|
|
64
|
+
ui.info('MCP config target: ' + ui.c.bold(mcp.describeBackend()));
|
|
65
|
+
ui.dim('Each product loads as MCP tools after you restart your agent session at the end.');
|
|
66
|
+
|
|
67
|
+
let order = ['agentcard', 'agentmail', 'agentphone'];
|
|
68
|
+
if (ctx.only) {
|
|
69
|
+
if (!STEPS[ctx.only]) {
|
|
70
|
+
ui.fail(`Unknown --only target: ${ctx.only} (use agentcard | agentmail | agentphone)`);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
order = [ctx.only];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!ctx.yes && !ctx.dryRun) {
|
|
77
|
+
ui.blank();
|
|
78
|
+
const go = await ui.confirm('Ready to set up ' + (ctx.only ? ctx.only : 'all three') + '?', true);
|
|
79
|
+
if (!go) {
|
|
80
|
+
ui.info('No problem — run again whenever you\'re ready.');
|
|
81
|
+
ui.closeUI();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Run steps (failure-isolated: one product failing doesn't stop the others)
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const key of order) {
|
|
89
|
+
try {
|
|
90
|
+
results.push(await STEPS[key](ctx));
|
|
91
|
+
} catch (e) {
|
|
92
|
+
ui.fail(`${key} crashed: ${e.message}`);
|
|
93
|
+
results.push({ product: key, icon: 'fail', note: e.message });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Summary
|
|
98
|
+
ui.blank();
|
|
99
|
+
ui.info(ui.c.bold('── Summary ──────────────────────────────────────────'));
|
|
100
|
+
for (const r of results) {
|
|
101
|
+
process.stdout.write(' ' + icon(r.icon) + ' ' + ui.c.bold(r.product.padEnd(11)) + ' ' + ui.c.dim(r.note) + '\n');
|
|
102
|
+
}
|
|
103
|
+
ui.blank();
|
|
104
|
+
|
|
105
|
+
const anyConfigured = results.some((r) => r.icon === 'ok');
|
|
106
|
+
if (anyConfigured && !ctx.dryRun) {
|
|
107
|
+
ui.info(ui.c.bold('Next:'));
|
|
108
|
+
ui.step('Restart your agent session so the new MCP tools load.');
|
|
109
|
+
ui.dim('Then try: list_cards (Agentcard) · list_inboxes (AgentMail) · check usage (AgentPhone).');
|
|
110
|
+
}
|
|
111
|
+
if (results.some((r) => r.icon === 'warn')) {
|
|
112
|
+
ui.warn('Some products need a key — re-run with --only <product> after setting it.');
|
|
113
|
+
}
|
|
114
|
+
ui.blank();
|
|
115
|
+
ui.dim('Security: API keys live in your MCP config, never printed. Don\'t send a key to any other domain.');
|
|
116
|
+
ui.blank();
|
|
117
|
+
|
|
118
|
+
ui.closeUI();
|
|
119
|
+
process.exit(results.some((r) => r.icon === 'fail') ? 1 : 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main().catch((e) => {
|
|
123
|
+
ui.fail('Unexpected error: ' + e.stack);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
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.
|
|
4
|
+
//
|
|
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.
|
|
9
|
+
|
|
10
|
+
const fs = require('node:fs');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const os = require('node:os');
|
|
13
|
+
|
|
14
|
+
const SOURCE = 'agentbundle'; // the x-platform value the backend tracks signups under
|
|
15
|
+
const API_URL = process.env.AGENT_CARDS_API_URL || 'https://api.agentcard.sh';
|
|
16
|
+
const CONFIG_DIR = path.join(os.homedir(), '.agent-cards');
|
|
17
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
18
|
+
|
|
19
|
+
function readConfig() {
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeConfig(data) {
|
|
28
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
const merged = { ...readConfig(), ...data };
|
|
30
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function callerContext() {
|
|
34
|
+
return process.env.CLAUDECODE || process.env.CLAUDE_CODE ? 'agent:claude-code' : 'unidentified';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function headers(extra) {
|
|
38
|
+
return {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
'x-platform': SOURCE,
|
|
41
|
+
'x-caller-context': callerContext(),
|
|
42
|
+
...(extra || {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function postJson(pathname, body, extraHeaders) {
|
|
47
|
+
const ctrl = new AbortController();
|
|
48
|
+
const t = setTimeout(() => ctrl.abort(), 30000);
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(API_URL + pathname, {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: headers(extraHeaders),
|
|
53
|
+
body: JSON.stringify(body || {}),
|
|
54
|
+
signal: ctrl.signal,
|
|
55
|
+
});
|
|
56
|
+
const text = await res.text();
|
|
57
|
+
let data;
|
|
58
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
59
|
+
return { ok: res.ok, status: res.status, data };
|
|
60
|
+
} finally {
|
|
61
|
+
clearTimeout(t);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getJson(pathname, extraHeaders) {
|
|
66
|
+
const ctrl = new AbortController();
|
|
67
|
+
const t = setTimeout(() => ctrl.abort(), 30000);
|
|
68
|
+
try {
|
|
69
|
+
const res = await fetch(API_URL + pathname, { headers: headers(extraHeaders), signal: ctrl.signal });
|
|
70
|
+
const text = await res.text();
|
|
71
|
+
let data;
|
|
72
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
73
|
+
return { ok: res.ok, status: res.status, data };
|
|
74
|
+
} finally {
|
|
75
|
+
clearTimeout(t);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Request a magic link. Returns { sessionId } or throws with a readable message.
|
|
80
|
+
async function requestMagicLink(email) {
|
|
81
|
+
const r = await postJson('/auth/signup', { email });
|
|
82
|
+
if (!r.ok || !r.data.sessionId) {
|
|
83
|
+
throw new Error(r.data.error || r.data.message || `signup failed (HTTP ${r.status})`);
|
|
84
|
+
}
|
|
85
|
+
return { sessionId: r.data.sessionId };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Poll until the user clicks the magic link. Returns { jwt, email, mode }.
|
|
89
|
+
async function waitForVerification(sessionId, { intervalMs = 2500, timeoutMs = 10 * 60 * 1000 } = {}) {
|
|
90
|
+
const deadline = Date.now() + timeoutMs;
|
|
91
|
+
// Date.now is fine at runtime; this module is never run inside the workflow sandbox.
|
|
92
|
+
for (;;) {
|
|
93
|
+
const r = await getJson('/auth/me', { 'X-Magic-Token': sessionId });
|
|
94
|
+
if (r.ok && r.data && r.data.status === 'verified' && r.data.jwt) {
|
|
95
|
+
return { jwt: r.data.jwt, email: r.data.email, mode: r.data.mode };
|
|
96
|
+
}
|
|
97
|
+
if (Date.now() > deadline) throw new Error('timed out waiting for the magic link to be clicked');
|
|
98
|
+
await new Promise((res) => setTimeout(res, intervalMs));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isValidEmail(s) {
|
|
103
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
SOURCE,
|
|
108
|
+
API_URL,
|
|
109
|
+
readConfig,
|
|
110
|
+
writeConfig,
|
|
111
|
+
requestMagicLink,
|
|
112
|
+
waitForVerification,
|
|
113
|
+
isValidEmail,
|
|
114
|
+
};
|
package/lib/mcp.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Idempotent MCP-server registration.
|
|
3
|
+
//
|
|
4
|
+
// Two backends, auto-selected:
|
|
5
|
+
// * "claude": uses the `claude` CLI (`claude mcp get` / `claude mcp add`). Default in real use.
|
|
6
|
+
// * "file": merges into a JSON config file. Used when AGENT_BUNDLE_MCP_CONFIG is set, or when
|
|
7
|
+
// `claude` isn't on PATH. Perfect for isolated/sandbox testing — never touches the
|
|
8
|
+
// user's real ~/.claude config.
|
|
9
|
+
|
|
10
|
+
const { spawnSync } = require('node:child_process');
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
|
|
15
|
+
function hasClaude() {
|
|
16
|
+
const r = spawnSync('claude', ['--version'], { stdio: 'ignore' });
|
|
17
|
+
return !r.error && r.status === 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pickBackend() {
|
|
21
|
+
const file = process.env.AGENT_BUNDLE_MCP_CONFIG;
|
|
22
|
+
if (file) return { backend: 'file', file };
|
|
23
|
+
if (hasClaude()) return { backend: 'claude', file: null };
|
|
24
|
+
return { backend: 'file', file: path.join(os.homedir(), '.agent-bundle-mcp.json') };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readConfig(file) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
30
|
+
} catch {
|
|
31
|
+
return { mcpServers: {} };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function exists(name) {
|
|
36
|
+
const { backend, file } = pickBackend();
|
|
37
|
+
if (backend === 'claude') {
|
|
38
|
+
const r = spawnSync('claude', ['mcp', 'get', name], { stdio: 'ignore' });
|
|
39
|
+
return !r.error && r.status === 0;
|
|
40
|
+
}
|
|
41
|
+
const cfg = readConfig(file);
|
|
42
|
+
return !!(cfg.mcpServers && cfg.mcpServers[name]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// addServer(spec) -> { added, skipped, error? }
|
|
46
|
+
// spec for an HTTP server: { name, transport: 'http', url, headers }
|
|
47
|
+
// spec for a stdio server: { name, transport: 'stdio', command, args, env }
|
|
48
|
+
function addServer(spec, { dryRun = false } = {}) {
|
|
49
|
+
const { name, transport = 'http' } = spec;
|
|
50
|
+
if (exists(name)) return { added: false, skipped: true };
|
|
51
|
+
|
|
52
|
+
const { backend, file } = pickBackend();
|
|
53
|
+
if (dryRun) return { added: false, skipped: false, dryRun: true, backend, file };
|
|
54
|
+
|
|
55
|
+
if (backend === 'claude') {
|
|
56
|
+
// Default to USER scope so the server is available in every project/session, not just the
|
|
57
|
+
// directory this ran in (`claude mcp add` defaults to project-local scope). Override with
|
|
58
|
+
// AGENT_BUNDLE_MCP_SCOPE=local|user|project.
|
|
59
|
+
const scope = process.env.AGENT_BUNDLE_MCP_SCOPE || 'user';
|
|
60
|
+
let args;
|
|
61
|
+
if (transport === 'stdio') {
|
|
62
|
+
// Name MUST come before --env, or the variadic --env flag swallows it.
|
|
63
|
+
args = ['mcp', 'add', name, '--scope', scope];
|
|
64
|
+
for (const [k, v] of Object.entries(spec.env || {})) args.push('--env', `${k}=${v}`);
|
|
65
|
+
args.push('--', spec.command, ...(spec.args || []));
|
|
66
|
+
} else {
|
|
67
|
+
args = ['mcp', 'add', '--scope', scope, '--transport', 'http', name, spec.url];
|
|
68
|
+
for (const [k, v] of Object.entries(spec.headers || {})) args.push('--header', `${k}: ${v}`);
|
|
69
|
+
}
|
|
70
|
+
const r = spawnSync('claude', args, { stdio: 'ignore' });
|
|
71
|
+
if (r.error) return { added: false, skipped: false, error: r.error.message };
|
|
72
|
+
if (r.status !== 0) return { added: false, skipped: false, error: `claude mcp add exited ${r.status}` };
|
|
73
|
+
return { added: true, skipped: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// file backend
|
|
77
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
78
|
+
const cfg = readConfig(file);
|
|
79
|
+
cfg.mcpServers = cfg.mcpServers || {};
|
|
80
|
+
if (transport === 'stdio') {
|
|
81
|
+
const entry = { command: spec.command, args: spec.args || [] };
|
|
82
|
+
if (spec.env && Object.keys(spec.env).length) entry.env = spec.env;
|
|
83
|
+
cfg.mcpServers[name] = entry;
|
|
84
|
+
} else {
|
|
85
|
+
const entry = { type: 'http', url: spec.url };
|
|
86
|
+
if (spec.headers && Object.keys(spec.headers).length) entry.headers = spec.headers;
|
|
87
|
+
cfg.mcpServers[name] = entry;
|
|
88
|
+
}
|
|
89
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
90
|
+
return { added: true, skipped: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function describeBackend() {
|
|
94
|
+
const { backend, file } = pickBackend();
|
|
95
|
+
return backend === 'file' ? `file (${file})` : 'claude CLI';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { exists, addServer, describeBackend, pickBackend };
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const ui = require('../ui');
|
|
7
|
+
const mcp = require('../mcp');
|
|
8
|
+
const ac = require('../agentcard-api');
|
|
9
|
+
|
|
10
|
+
const MCP_NAME = 'agent-cards';
|
|
11
|
+
const MCP_URL = 'https://mcp.agentcard.sh/mcp';
|
|
12
|
+
|
|
13
|
+
function have(cmd) {
|
|
14
|
+
const r = spawnSync(cmd, ['--version'], { stdio: 'ignore' });
|
|
15
|
+
return !r.error && r.status === 0;
|
|
16
|
+
}
|
|
17
|
+
function run(cmd, args) {
|
|
18
|
+
ui.pause();
|
|
19
|
+
const r = spawnSync(cmd, args, { stdio: 'inherit' });
|
|
20
|
+
ui.resume();
|
|
21
|
+
return !r.error && r.status === 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Ask for the email, send the magic link, and wait for the click. Returns the JWT, or null.
|
|
25
|
+
async function completeSignup() {
|
|
26
|
+
let email = '';
|
|
27
|
+
for (let tries = 0; tries < 3 && !email; tries++) {
|
|
28
|
+
const entered = await ui.ask('What email should your Agentcard account use?');
|
|
29
|
+
if (!entered) return null; // blank = skip
|
|
30
|
+
if (ac.isValidEmail(entered)) email = entered;
|
|
31
|
+
else ui.warn('That doesn\'t look like an email — try again.');
|
|
32
|
+
}
|
|
33
|
+
if (!email) return null;
|
|
34
|
+
|
|
35
|
+
let sessionId;
|
|
36
|
+
try {
|
|
37
|
+
ui.step(`Sending a magic link to ${email}…`);
|
|
38
|
+
({ sessionId } = await ac.requestMagicLink(email));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
ui.fail('Could not start signup: ' + e.message);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ui.ok(`Magic link sent to ${ui.c.bold(email)}.`);
|
|
45
|
+
ui.info('Open your inbox and click the link — ' + ui.c.dim('waiting for you…'));
|
|
46
|
+
try {
|
|
47
|
+
const { jwt, email: verifiedEmail, mode } = await ac.waitForVerification(sessionId);
|
|
48
|
+
ac.writeConfig({ email: verifiedEmail || email, jwt, mode });
|
|
49
|
+
ui.ok(`Signed in as ${ui.c.bold(verifiedEmail || email)}` + (mode ? ` (${mode} mode)` : ''));
|
|
50
|
+
return jwt;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
ui.fail('Signup not completed: ' + e.message);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function step(ctx) {
|
|
58
|
+
ui.heading(1, 3, 'Agentcard — a virtual card for your agent');
|
|
59
|
+
ui.dim('Prepaid virtual cards with per-task budgets, controlled through MCP tools.');
|
|
60
|
+
|
|
61
|
+
if (mcp.exists(MCP_NAME)) {
|
|
62
|
+
ui.ok('Agentcard MCP server already configured — skipping.');
|
|
63
|
+
return { product: 'Agentcard', icon: 'ok', note: 'already configured' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 1. Get an authenticated session (JWT).
|
|
67
|
+
let jwt;
|
|
68
|
+
if (ctx.dryRun) {
|
|
69
|
+
ui.step('would ask for your email, POST /auth/signup (x-platform: agentbundle), and wait for the magic link');
|
|
70
|
+
ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL} --header "Authorization: Bearer <jwt>"`);
|
|
71
|
+
return { product: 'Agentcard', icon: 'ok', note: 'configured (dry-run)' };
|
|
72
|
+
} else if (ctx.sandbox) {
|
|
73
|
+
ui.step('[sandbox] simulating email → magic link → verified');
|
|
74
|
+
jwt = 'sandbox_placeholder_jwt';
|
|
75
|
+
} else if (ac.readConfig().jwt) {
|
|
76
|
+
const cfg = ac.readConfig();
|
|
77
|
+
ui.ok(`Already signed in to Agentcard${cfg.email ? ' as ' + ui.c.bold(cfg.email) : ''} — skipping signup.`);
|
|
78
|
+
jwt = cfg.jwt;
|
|
79
|
+
} else if (!process.stdin.isTTY) {
|
|
80
|
+
ui.warn('Not signed in to Agentcard, and no interactive terminal to complete signup.');
|
|
81
|
+
ui.info('Re-run this in a terminal so it can collect your email and the magic link.');
|
|
82
|
+
return { product: 'Agentcard', icon: 'warn', note: 'needs interactive signup' };
|
|
83
|
+
} else {
|
|
84
|
+
jwt = await completeSignup();
|
|
85
|
+
if (!jwt) {
|
|
86
|
+
ui.warn('Skipped Agentcard signup — re-run with --only agentcard when ready.');
|
|
87
|
+
return { product: 'Agentcard', icon: 'warn', note: 'signup not completed' };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Register the MCP server with the JWT (matches the authenticated hosted-HTTP setup).
|
|
92
|
+
ui.step('Registering the Agentcard MCP server…');
|
|
93
|
+
const res = mcp.addServer(
|
|
94
|
+
{ name: MCP_NAME, transport: 'http', url: MCP_URL, headers: { Authorization: `Bearer ${jwt}` } },
|
|
95
|
+
{ dryRun: ctx.dryRun }
|
|
96
|
+
);
|
|
97
|
+
if (res.error) {
|
|
98
|
+
ui.fail('Failed to register MCP server: ' + res.error);
|
|
99
|
+
return { product: 'Agentcard', icon: 'fail', note: res.error };
|
|
100
|
+
}
|
|
101
|
+
ui.ok('Agentcard MCP server registered.');
|
|
102
|
+
|
|
103
|
+
// 3. Production (go live) — needs payment method + identity; opt-in. Uses the CLI when present.
|
|
104
|
+
let note = 'configured (TEST mode)';
|
|
105
|
+
if (ctx.sandbox) {
|
|
106
|
+
ui.dim('TEST mode by default. Going live (real cards) is offered here in a real run.');
|
|
107
|
+
} else {
|
|
108
|
+
const live = ctx.yes ? false : await ui.confirm('Switch to PRODUCTION now (issue real cards)? Needs a payment method + a quick identity check.', false);
|
|
109
|
+
if (live) {
|
|
110
|
+
if (have('agent-cards') && run('agent-cards', ['mode', 'prod'])) {
|
|
111
|
+
ui.info('To fund real cards, run ' + ui.c.bold('agent-cards payment-method') + ' (one-time Stripe page) and finish the identity prompts.');
|
|
112
|
+
note = 'configured (PROD mode — finish payment method to issue real cards)';
|
|
113
|
+
} else {
|
|
114
|
+
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.');
|
|
115
|
+
note = 'configured (TEST mode — install CLI to go live)';
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
ui.info('Staying in ' + ui.c.bold('TEST') + ' mode (sandbox cards). Go live anytime with ' + ui.c.bold('agent-cards mode prod') + '.');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { product: 'Agentcard', icon: 'ok', note };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { step, MCP_NAME };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// [2/3] AgentMail — an email inbox. API-key driven (one console visit only if no key yet).
|
|
3
|
+
|
|
4
|
+
const ui = require('../ui');
|
|
5
|
+
const mcp = require('../mcp');
|
|
6
|
+
|
|
7
|
+
const MCP_NAME = 'agentmail';
|
|
8
|
+
const MCP_URL = 'https://mcp.agentmail.to/mcp';
|
|
9
|
+
const CONSOLE = 'https://console.agentmail.to';
|
|
10
|
+
|
|
11
|
+
async function step(ctx) {
|
|
12
|
+
ui.heading(2, 3, 'AgentMail — an email inbox for your agent');
|
|
13
|
+
ui.dim('Real inboxes the agent owns — for sign-ups, verification emails, and outreach.');
|
|
14
|
+
|
|
15
|
+
if (mcp.exists(MCP_NAME)) {
|
|
16
|
+
ui.ok('AgentMail MCP server already configured — skipping.');
|
|
17
|
+
return { product: 'AgentMail', icon: 'ok', note: 'already configured' };
|
|
18
|
+
}
|
|
19
|
+
|
|
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.sandbox) {
|
|
25
|
+
key = 'am_sandbox_placeholder_key';
|
|
26
|
+
ui.step('[sandbox] using a placeholder AgentMail key.');
|
|
27
|
+
} else if (ctx.dryRun) {
|
|
28
|
+
ui.step('would prompt for an AgentMail API key (am_…)');
|
|
29
|
+
ui.dim(`would: claude mcp add --transport http ${MCP_NAME} ${MCP_URL} --header "x-api-key: …"`);
|
|
30
|
+
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
|
+
}
|
|
40
|
+
|
|
41
|
+
ui.step('Registering the AgentMail MCP server…');
|
|
42
|
+
const res = mcp.addServer(
|
|
43
|
+
{ name: MCP_NAME, url: MCP_URL, headers: { 'x-api-key': key } },
|
|
44
|
+
{ dryRun: ctx.dryRun }
|
|
45
|
+
);
|
|
46
|
+
if (res.error) {
|
|
47
|
+
ui.fail('Failed to register MCP server: ' + res.error);
|
|
48
|
+
return { product: 'AgentMail', icon: 'fail', note: res.error };
|
|
49
|
+
}
|
|
50
|
+
ui.ok('AgentMail MCP server registered.');
|
|
51
|
+
return { product: 'AgentMail', icon: 'ok', note: 'configured' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { step, MCP_NAME };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// [3/3] AgentPhone — a real phone number. Fully API-driven: sign-up -> OTP -> verify. No browser.
|
|
3
|
+
|
|
4
|
+
const ui = require('../ui');
|
|
5
|
+
const mcp = require('../mcp');
|
|
6
|
+
|
|
7
|
+
const MCP_NAME = 'agentphone';
|
|
8
|
+
const API = 'https://api.agentphone.ai';
|
|
9
|
+
const SETTINGS = 'https://agentphone.ai/settings';
|
|
10
|
+
|
|
11
|
+
async function apiPost(path, body) {
|
|
12
|
+
const ctrl = new AbortController();
|
|
13
|
+
const t = setTimeout(() => ctrl.abort(), 30000);
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(API + path, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify(body),
|
|
19
|
+
signal: ctrl.signal,
|
|
20
|
+
});
|
|
21
|
+
const text = await res.text();
|
|
22
|
+
let data;
|
|
23
|
+
try { data = JSON.parse(text); } catch { data = { raw: text }; }
|
|
24
|
+
return { ok: res.ok, status: res.status, data };
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return { ok: false, status: 0, data: { error: e.message } };
|
|
27
|
+
} finally {
|
|
28
|
+
clearTimeout(t);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function register(key, ctx, extraNote) {
|
|
33
|
+
ui.step('Registering the AgentPhone MCP server…');
|
|
34
|
+
// AgentPhone ships a stdio MCP server (npm: agentphone-mcp); the key is passed via env.
|
|
35
|
+
const res = mcp.addServer(
|
|
36
|
+
{ name: MCP_NAME, transport: 'stdio', command: 'npx', args: ['-y', 'agentphone-mcp'], env: { AGENTPHONE_API_KEY: key } },
|
|
37
|
+
{ dryRun: ctx.dryRun }
|
|
38
|
+
);
|
|
39
|
+
if (res.error) {
|
|
40
|
+
ui.fail('Failed to register MCP server: ' + res.error);
|
|
41
|
+
return { product: 'AgentPhone', icon: 'fail', note: res.error };
|
|
42
|
+
}
|
|
43
|
+
ui.ok('AgentPhone MCP server registered.');
|
|
44
|
+
return { product: 'AgentPhone', icon: 'ok', note: extraNote || 'configured' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function step(ctx) {
|
|
48
|
+
ui.heading(3, 3, 'AgentPhone — a real phone number for your agent');
|
|
49
|
+
ui.dim('A live US number for SMS + voice. This step is fully automated — no browser needed.');
|
|
50
|
+
|
|
51
|
+
if (mcp.exists(MCP_NAME)) {
|
|
52
|
+
ui.ok('AgentPhone MCP server already configured — skipping.');
|
|
53
|
+
return { product: 'AgentPhone', icon: 'ok', note: 'already configured' };
|
|
54
|
+
}
|
|
55
|
+
|
|
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);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ctx.dryRun) {
|
|
63
|
+
ui.step('would: POST /v0/agent/sign-up → ask for OTP → POST /v0/agent/verify → get sk_live_ key + number');
|
|
64
|
+
ui.dim(`would: claude mcp add ${MCP_NAME} --env AGENTPHONE_API_KEY=… -- npx -y agentphone-mcp`);
|
|
65
|
+
return { product: 'AgentPhone', icon: 'ok', note: 'configured (dry-run)' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (ctx.sandbox) {
|
|
69
|
+
ui.step('[sandbox] simulating sign-up → OTP → verify…');
|
|
70
|
+
ui.ok('[sandbox] provisioned number +1 (555) 010-0042');
|
|
71
|
+
return register('sk_live_sandbox_placeholder_key', ctx, 'configured (sandbox number)');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Real flow.
|
|
75
|
+
const email = await ui.ask('What email should AgentPhone send your verification code to?');
|
|
76
|
+
if (!email) {
|
|
77
|
+
ui.warn('No email — skipping AgentPhone. Re-run later.');
|
|
78
|
+
return { product: 'AgentPhone', icon: 'warn', note: 'skipped (no email)' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ui.step('Requesting a verification code…');
|
|
82
|
+
const signup = await apiPost('/v0/agent/sign-up', { human_email: email, agent_name: 'my-agent' });
|
|
83
|
+
|
|
84
|
+
if (signup.status === 409) {
|
|
85
|
+
ui.warn('You already have an AgentPhone account for that email.');
|
|
86
|
+
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:');
|
|
88
|
+
if (!key) return { product: 'AgentPhone', icon: 'warn', note: 'needs key (set AGENTPHONE_API_KEY)' };
|
|
89
|
+
return register(key, ctx);
|
|
90
|
+
}
|
|
91
|
+
if (!signup.ok || !signup.data.verification_id) {
|
|
92
|
+
ui.fail('Sign-up failed: ' + (signup.data.error || signup.data.message || `HTTP ${signup.status}`));
|
|
93
|
+
return { product: 'AgentPhone', icon: 'fail', note: 'sign-up failed' };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ui.ok(`Code sent to ${email}. Check your inbox.`);
|
|
97
|
+
const otp = await ui.ask('Enter the 6-digit code:');
|
|
98
|
+
if (!otp) {
|
|
99
|
+
ui.warn('No code entered — skipping AgentPhone.');
|
|
100
|
+
return { product: 'AgentPhone', icon: 'warn', note: 'skipped (no code)' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ui.step('Verifying and provisioning your number…');
|
|
104
|
+
const verify = await apiPost('/v0/agent/verify', { verification_id: signup.data.verification_id, otp_code: otp });
|
|
105
|
+
if (!verify.ok || !verify.data.api_key) {
|
|
106
|
+
ui.fail('Verification failed: ' + (verify.data.error || verify.data.message || `HTTP ${verify.status}`));
|
|
107
|
+
return { product: 'AgentPhone', icon: 'fail', note: 'verification failed' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ui.ok('Verified! Your agent now has a number: ' + ui.c.bold(verify.data.phone_number));
|
|
111
|
+
ui.dim('API key stored in the MCP config (masked: ' + ui.mask(verify.data.api_key) + '). Never share it.');
|
|
112
|
+
return register(verify.data.api_key, ctx, `live number ${verify.data.phone_number}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { step, MCP_NAME };
|
package/lib/ui.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Zero-dependency terminal UI helpers: colors, headings, prompts, masking.
|
|
3
|
+
|
|
4
|
+
const readline = require('node:readline/promises');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { stdin, stdout } = require('node:process');
|
|
8
|
+
|
|
9
|
+
const useColor = stdout.isTTY && !process.env.NO_COLOR && !process.argv.includes('--no-color');
|
|
10
|
+
const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
11
|
+
|
|
12
|
+
const c = {
|
|
13
|
+
bold: wrap('1'),
|
|
14
|
+
dim: wrap('2'),
|
|
15
|
+
red: wrap('31'),
|
|
16
|
+
green: wrap('32'),
|
|
17
|
+
yellow: wrap('33'),
|
|
18
|
+
blue: wrap('34'),
|
|
19
|
+
cyan: wrap('36'),
|
|
20
|
+
gray: wrap('90'),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Header matches the Agentcard support agent: figlet "Small" font, rendered in green.
|
|
24
|
+
function banner() {
|
|
25
|
+
let art;
|
|
26
|
+
try {
|
|
27
|
+
art = fs.readFileSync(path.join(__dirname, '..', 'assets', 'agentbundle.txt'), 'utf8').replace(/\s+$/, '');
|
|
28
|
+
} catch {
|
|
29
|
+
art = 'agentbundle';
|
|
30
|
+
}
|
|
31
|
+
stdout.write('\n');
|
|
32
|
+
for (const lineText of art.split('\n')) stdout.write(c.green(lineText) + '\n');
|
|
33
|
+
stdout.write(
|
|
34
|
+
' ' + c.bold('Agentcard') + c.dim(' · ') + c.bold('AgentMail') + c.dim(' · ') + c.bold('AgentPhone') + '\n'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function heading(n, total, title) {
|
|
39
|
+
stdout.write('\n' + c.bold(c.blue(`[${n}/${total}] ${title}`)) + '\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const info = (m) => stdout.write(' ' + m + '\n');
|
|
43
|
+
const step = (m) => stdout.write(' ' + c.cyan('→') + ' ' + m + '\n');
|
|
44
|
+
const ok = (m) => stdout.write(' ' + c.green('✓') + ' ' + m + '\n');
|
|
45
|
+
const warn = (m) => stdout.write(' ' + c.yellow('!') + ' ' + m + '\n');
|
|
46
|
+
const fail = (m) => stdout.write(' ' + c.red('✗') + ' ' + m + '\n');
|
|
47
|
+
const dim = (m) => stdout.write(' ' + c.dim(m) + '\n');
|
|
48
|
+
const blank = () => stdout.write('\n');
|
|
49
|
+
|
|
50
|
+
// One shared readline interface with a line QUEUE. Reading all stdin lines into a queue means
|
|
51
|
+
// no line is ever dropped — including pre-buffered/piped input, where readline would otherwise
|
|
52
|
+
// emit a 'line' event before the next prompt is registered. Works the same for a live terminal.
|
|
53
|
+
// We don't pass `output`, so the terminal's own echo handles typed input and readline never
|
|
54
|
+
// fights an interactive child process for the TTY. Paused around child spawns (pause/resume).
|
|
55
|
+
let _rl;
|
|
56
|
+
const _queue = [];
|
|
57
|
+
let _waiter = null;
|
|
58
|
+
let _closed = false;
|
|
59
|
+
|
|
60
|
+
function initRl() {
|
|
61
|
+
if (_rl) return;
|
|
62
|
+
_rl = readline.createInterface({ input: stdin });
|
|
63
|
+
_rl.on('line', (line) => {
|
|
64
|
+
if (_waiter) { const w = _waiter; _waiter = null; w(line); }
|
|
65
|
+
else _queue.push(line);
|
|
66
|
+
});
|
|
67
|
+
_rl.on('close', () => { _closed = true; if (_waiter) { const w = _waiter; _waiter = null; w(null); } });
|
|
68
|
+
}
|
|
69
|
+
function nextLine() {
|
|
70
|
+
initRl();
|
|
71
|
+
if (_queue.length) return Promise.resolve(_queue.shift());
|
|
72
|
+
if (_closed) return Promise.resolve(null);
|
|
73
|
+
return new Promise((res) => { _waiter = res; });
|
|
74
|
+
}
|
|
75
|
+
function pause() { if (_rl) _rl.pause(); }
|
|
76
|
+
function resume() { if (_rl) _rl.resume(); }
|
|
77
|
+
function closeUI() { if (_rl) { _rl.close(); _rl = null; } }
|
|
78
|
+
|
|
79
|
+
async function ask(question, fallback = '') {
|
|
80
|
+
stdout.write(' ' + c.cyan('?') + ' ' + question + ' ');
|
|
81
|
+
const line = await nextLine();
|
|
82
|
+
const a = (line == null ? '' : line).trim();
|
|
83
|
+
if (line == null) stdout.write('\n');
|
|
84
|
+
return a || fallback;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function confirm(question, defaultYes = true) {
|
|
88
|
+
const hint = defaultYes ? '(Y/n)' : '(y/N)';
|
|
89
|
+
const a = (await ask(`${question} ${c.dim(hint)}`)).toLowerCase();
|
|
90
|
+
if (!a) return defaultYes;
|
|
91
|
+
return a === 'y' || a === 'yes';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function pressEnter(message = 'Press Enter to continue…') {
|
|
95
|
+
await ask(c.dim(message));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Mask a secret for display: show prefix + last 4, hide the middle.
|
|
99
|
+
function mask(secret) {
|
|
100
|
+
if (!secret) return '(none)';
|
|
101
|
+
const s = String(secret);
|
|
102
|
+
if (s.length <= 10) return s.slice(0, 2) + '…';
|
|
103
|
+
const head = s.includes('_') ? s.slice(0, s.indexOf('_') + 1) : s.slice(0, 3);
|
|
104
|
+
return head + '…' + s.slice(-4);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { c, banner, heading, info, step, ok, warn, fail, dim, blank, ask, confirm, pressEnter, mask, pause, resume, closeUI };
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentbundle-cli",
|
|
3
|
+
"version": "0.1.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.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"agentbundle-cli": "bin/agent-bundle.js",
|
|
7
|
+
"agentbundle": "bin/agent-bundle.js",
|
|
8
|
+
"agent-bundle": "bin/agent-bundle.js"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"lib",
|
|
16
|
+
"assets",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai-agent",
|
|
21
|
+
"agentcard",
|
|
22
|
+
"agentmail",
|
|
23
|
+
"agentphone",
|
|
24
|
+
"mcp",
|
|
25
|
+
"onboarding",
|
|
26
|
+
"cli"
|
|
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
|
+
},
|
|
36
|
+
"license": "MIT"
|
|
37
|
+
}
|