@obol/cli 0.1.2 → 0.2.1

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/bin/obol.js CHANGED
@@ -4,7 +4,7 @@ import { runSetup } from '../src/setup.js';
4
4
  import { runStatus } from '../src/status.js';
5
5
  import { runConnect } from '../src/connect.js';
6
6
  import { runCreditsBuy } from '../src/credits.js';
7
- import { getBalance, getTiers } from '../src/api.js';
7
+ import { getBalance, getTiers, createReferralCode, getReferralStats, listEndpoints, listConnections, refreshToken } from '../src/api.js';
8
8
  import { loadEnv } from '../src/config.js';
9
9
  import pc from 'picocolors';
10
10
 
@@ -35,6 +35,34 @@ async function main() {
35
35
  }
36
36
  break;
37
37
 
38
+ case 'token': {
39
+ const env = loadEnv();
40
+ if (!env.OBOL_JWT_TOKEN) {
41
+ console.log(pc.red('\n No agent configured yet. Run: npx @obol/cli setup\n'));
42
+ process.exit(1);
43
+ }
44
+ // Try to refresh the token (in case it's expired)
45
+ let token = env.OBOL_JWT_TOKEN;
46
+ const gwUrl = env.OBOL_GATEWAY_URL || 'https://www.obolagents.com';
47
+ try {
48
+ const data = await refreshToken(gwUrl, token);
49
+ if (data.token) {
50
+ token = data.token;
51
+ // Save refreshed token to .env
52
+ const { saveEnv } = await import('../src/config.js');
53
+ saveEnv({ OBOL_GATEWAY_URL: gwUrl, OBOL_AGENT_ID: env.OBOL_AGENT_ID, OBOL_JWT_TOKEN: token });
54
+ console.log(`\n ${pc.green('Token refreshed!')} Expires: ${pc.dim(new Date(data.expires_at * 1000).toLocaleDateString())}`);
55
+ }
56
+ } catch {
57
+ // Refresh failed — show existing token anyway
58
+ }
59
+ console.log(`\n ${pc.bold('Your Agent Token')}`);
60
+ console.log(` ${pc.dim('Agent:')} ${pc.green(env.OBOL_AGENT_ID || '(unknown)')}`);
61
+ console.log(`\n ${pc.dim('Copy this token and paste it into the dashboard at obolagents.com/dashboard')}`);
62
+ console.log(`\n ${pc.cyan(token)}\n`);
63
+ break;
64
+ }
65
+
38
66
  case 'balance': {
39
67
  const env = loadEnv();
40
68
  if (!env.OBOL_GATEWAY_URL || !env.OBOL_JWT_TOKEN) {
@@ -74,20 +102,122 @@ async function main() {
74
102
  break;
75
103
  }
76
104
 
105
+ case 'referral': {
106
+ const env = loadEnv();
107
+ if (!env.OBOL_GATEWAY_URL || !env.OBOL_JWT_TOKEN) {
108
+ console.log(pc.red('No agent configured. Run: obol setup'));
109
+ process.exit(1);
110
+ }
111
+ if (subcommand === 'code') {
112
+ try {
113
+ const data = await createReferralCode(env.OBOL_GATEWAY_URL, env.OBOL_JWT_TOKEN);
114
+ console.log(`\n ${pc.bold('Your Referral Code')}`);
115
+ console.log(` ${pc.green(pc.bold(data.code))}`);
116
+ console.log(`\n Share this code. Both you and the new agent earn free credits!`);
117
+ console.log();
118
+ } catch (err) {
119
+ console.log(pc.red(` ${err.message}`));
120
+ process.exit(1);
121
+ }
122
+ } else if (subcommand === 'stats') {
123
+ try {
124
+ const data = await getReferralStats(env.OBOL_GATEWAY_URL, env.OBOL_JWT_TOKEN);
125
+ console.log(`\n ${pc.bold('Referral Stats')}`);
126
+ console.log(` Code: ${data.referral_code ? pc.green(data.referral_code) : pc.dim('(none yet)')}`);
127
+ console.log(` Eligible: ${data.referral_eligible ? pc.green('Yes') : pc.yellow('No — make a purchase first')}`);
128
+ const pending = data.direct_referrals_pending || 0;
129
+ const active = data.direct_referrals - pending;
130
+ console.log(` Direct refs: ${pc.bold(String(active))} active${pending > 0 ? `, ${pc.yellow(String(pending) + ' pending first payment')}` : ''}`);
131
+ console.log(` 2nd-level refs: ${pc.bold(String(data.second_level_referrals))}`);
132
+ console.log(` Commissions: ${pc.green('$' + data.total_commission_earned_usdc.toFixed(2))}`);
133
+ console.log(` Signup bonuses: ${pc.green('$' + data.total_signup_bonuses_earned_usdc.toFixed(2))}`);
134
+ console.log(` Total earned: ${pc.green(pc.bold('$' + data.total_earned_usdc.toFixed(2)))}`);
135
+ if (data.own_bonus_status === 'pending') {
136
+ console.log(`\n ${pc.yellow('Your signup bonus is pending — make your first API call to unlock it!')}`);
137
+ }
138
+ console.log();
139
+ } catch (err) {
140
+ console.log(pc.red(` ${err.message}`));
141
+ process.exit(1);
142
+ }
143
+ } else {
144
+ console.log(`\n ${pc.bold('obol referral')} commands:\n`);
145
+ console.log(` ${pc.green('code')} Generate or show your referral code`);
146
+ console.log(` ${pc.green('stats')} View referral earnings and stats\n`);
147
+ }
148
+ break;
149
+ }
150
+
151
+ case 'test': {
152
+ const env = loadEnv();
153
+ if (!env.OBOL_GATEWAY_URL || !env.OBOL_JWT_TOKEN) {
154
+ console.log(pc.red('\n No account configured. Run: npx @obol/cli setup\n'));
155
+ process.exit(1);
156
+ }
157
+ console.log(`\n ${pc.bold('Obol Connection Test')}\n`);
158
+
159
+ // Test gateway connectivity + endpoints
160
+ try {
161
+ const epData = await listEndpoints(env.OBOL_GATEWAY_URL, env.OBOL_JWT_TOKEN);
162
+ const eps = epData.endpoints || [];
163
+ const names = eps.map(e => e.provider || e.name).filter(Boolean);
164
+ console.log(` ${pc.green('✓')} Connection Connected to Obol Gateway`);
165
+ console.log(` ${pc.green('✓')} Account ${pc.bold(env.OBOL_AGENT_ID || '(unknown)')}`);
166
+ console.log(` ${pc.green('✓')} Endpoints ${eps.length} available${names.length ? ` (${names.join(', ')})` : ''}`);
167
+ } catch (err) {
168
+ console.log(` ${pc.red('✗')} Connection ${err.message}`);
169
+ process.exit(1);
170
+ }
171
+
172
+ // Check balance
173
+ try {
174
+ const bal = await getBalance(env.OBOL_GATEWAY_URL, env.OBOL_JWT_TOKEN);
175
+ const balStr = '$' + (bal.balance_usdc || 0).toFixed(2);
176
+ const color = bal.balance_usdc > 0 ? pc.green : pc.yellow;
177
+ console.log(` ${color(bal.balance_usdc > 0 ? '✓' : '!')} Balance ${color(balStr)} USDC`);
178
+ if (bal.balance_usdc === 0) {
179
+ console.log(`\n ${pc.yellow('Buy credits to start executing:')} ${pc.green('npx @obol/cli credits buy')}`);
180
+ }
181
+ } catch {
182
+ console.log(` ${pc.dim('-')} Balance ${pc.dim('could not fetch')}`);
183
+ }
184
+
185
+ // Check connections
186
+ try {
187
+ const connData = await listConnections(env.OBOL_GATEWAY_URL, env.OBOL_JWT_TOKEN);
188
+ const conns = connData.connections || connData || [];
189
+ if (Array.isArray(conns) && conns.length > 0) {
190
+ const connNames = conns.map(c => c.provider).filter(Boolean);
191
+ console.log(` ${pc.green('✓')} Connections ${conns.length} active${connNames.length ? ` (${connNames.join(', ')})` : ''}`);
192
+ } else {
193
+ console.log(` ${pc.dim('-')} Connections none yet`);
194
+ }
195
+ } catch {
196
+ console.log(` ${pc.dim('-')} Connections ${pc.dim('could not fetch')}`);
197
+ }
198
+
199
+ console.log();
200
+ break;
201
+ }
202
+
77
203
  case 'help':
78
204
  case '--help':
79
205
  case '-h':
80
206
  case undefined:
81
207
  console.log(`
82
- ${pc.bold(pc.green('obol'))} — CLI for the Obol Agent Execution Gateway
208
+ ${pc.bold(pc.green('obol'))} — Connect your agent to the Obol Execution Gateway
83
209
 
84
210
  ${pc.bold('Commands:')}
85
- ${pc.green('setup')} Set up a new agent (interactive wizard)
211
+ ${pc.green('setup')} Connect your agent to Obol (interactive wizard)
212
+ ${pc.green('test')} Verify your Obol connection is working
86
213
  ${pc.green('status')} Check agent health, balance & connections
214
+ ${pc.green('token')} Show your JWT token (for the dashboard)
87
215
  ${pc.green('connect')} ${pc.dim('<provider>')} Add a service API key (github, slack, etc.)
88
216
  ${pc.green('credits buy')} Purchase credits via Stripe
89
217
  ${pc.green('balance')} Check your credit balance
90
218
  ${pc.green('tiers')} Show credit purchase tiers
219
+ ${pc.green('referral code')} Generate or show your referral code
220
+ ${pc.green('referral stats')} View referral earnings
91
221
  ${pc.green('help')} Show this help message
92
222
 
93
223
  ${pc.bold('Quick start:')}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@obol/cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for the Obol Agent Execution Gateway",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.js CHANGED
@@ -25,7 +25,10 @@ async function request(url, options = {}) {
25
25
  } catch { detail = res.statusText; }
26
26
  throw new ApiError(detail, res.status);
27
27
  }
28
- return res.json();
28
+ const ct = res.headers.get('content-type') || '';
29
+ if (ct.includes('application/json')) return res.json();
30
+ const text = await res.text();
31
+ return text ? { raw: text } : {};
29
32
  }
30
33
 
31
34
  function adminHeaders(adminKey) {
@@ -42,9 +45,21 @@ export async function checkHealth(gatewayUrl) {
42
45
  return request(`${gatewayUrl}/healthz`);
43
46
  }
44
47
 
45
- export async function registerAgent(gatewayUrl, adminKey, agentId, secret, ownerId) {
48
+ export async function signup(gatewayUrl, agentId, services, referralCode) {
49
+ const body = { agent_id: agentId };
50
+ if (services && services.length > 0) body.services = services;
51
+ if (referralCode) body.referral_code = referralCode;
52
+ return request(`${gatewayUrl}/v1/signup`, {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify(body),
56
+ });
57
+ }
58
+
59
+ export async function registerAgent(gatewayUrl, adminKey, agentId, secret, ownerId, referralCode) {
46
60
  const body = { agent_id: agentId, secret };
47
61
  if (ownerId) body.owner_id = ownerId;
62
+ if (referralCode) body.referral_code = referralCode;
48
63
  return request(`${gatewayUrl}/v1/agents`, {
49
64
  method: 'POST',
50
65
  headers: adminHeaders(adminKey),
@@ -104,8 +119,46 @@ export async function createCheckout(gatewayUrl, jwt, amountUsd) {
104
119
  });
105
120
  }
106
121
 
122
+ export async function createWebCheckout(gatewayUrl, agentId, amountUsd) {
123
+ return request(`${gatewayUrl}/v1/credits/web-checkout`, {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: JSON.stringify({ agent_id: agentId, amount_usdc: amountUsd }),
127
+ });
128
+ }
129
+
130
+ export async function createReferralCode(gatewayUrl, jwt) {
131
+ return request(`${gatewayUrl}/v1/referrals/code`, {
132
+ method: 'POST',
133
+ headers: jwtHeaders(jwt),
134
+ });
135
+ }
136
+
137
+ export async function getReferralStats(gatewayUrl, jwt) {
138
+ return request(`${gatewayUrl}/v1/referrals/stats`, { headers: jwtHeaders(jwt) });
139
+ }
140
+
141
+ export async function validateReferralCode(gatewayUrl, code) {
142
+ return request(`${gatewayUrl}/v1/referrals/validate`, {
143
+ method: 'POST',
144
+ headers: { 'Content-Type': 'application/json' },
145
+ body: JSON.stringify({ code }),
146
+ });
147
+ }
148
+
149
+ export async function listEndpoints(gatewayUrl, jwt) {
150
+ return request(`${gatewayUrl}/v1/endpoints`, { headers: jwtHeaders(jwt) });
151
+ }
152
+
107
153
  export async function getAgentInfo(gatewayUrl, adminKey, agentId) {
108
154
  return request(`${gatewayUrl}/v1/agents/${agentId}`, {
109
155
  headers: adminHeaders(adminKey),
110
156
  });
111
157
  }
158
+
159
+ export async function refreshToken(gatewayUrl, jwt) {
160
+ return request(`${gatewayUrl}/v1/token/refresh`, {
161
+ method: 'POST',
162
+ headers: jwtHeaders(jwt),
163
+ });
164
+ }
package/src/config.js CHANGED
@@ -12,7 +12,6 @@ const OBOL_KEYS = [
12
12
  'OBOL_AGENT_ID',
13
13
  'OBOL_JWT_TOKEN',
14
14
  'OBOL_WALLET_ADDRESS',
15
- 'OBOL_ADMIN_API_KEY',
16
15
  ];
17
16
 
18
17
  export function loadEnv(path) {
package/src/connect.js CHANGED
@@ -9,7 +9,8 @@ const providerInfo = {
9
9
  slack: { name: 'Slack', keyHint: 'Bot token (xoxb-...)', prefix: 'xoxb-' },
10
10
  notion: { name: 'Notion', keyHint: 'Internal integration token (ntn_ or secret_)', prefix: '' },
11
11
  discord: { name: 'Discord', keyHint: 'Bot token from Discord Developer Portal', prefix: '' },
12
- twitter: { name: 'Twitter/X', keyHint: 'Bearer token from Twitter Developer Portal', prefix: '' },
12
+ twitter: { name: 'Twitter/X', keyHint: 'OAuth 2.0 user access token (for posting tweets)', prefix: '' },
13
+ agentmail: { name: 'AgentMail', keyHint: 'API key (am_...)', prefix: 'am_' },
13
14
  };
14
15
 
15
16
  export async function runConnect(providerArg) {
package/src/credits.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import * as p from '@clack/prompts';
2
2
  import pc from 'picocolors';
3
- import { getTiers, createCheckout } from './api.js';
3
+ import { getTiers, createWebCheckout } from './api.js';
4
4
  import { loadEnv } from './config.js';
5
5
 
6
6
  export async function runCreditsBuy() {
7
7
  const env = loadEnv();
8
8
 
9
- if (!env.OBOL_GATEWAY_URL || !env.OBOL_JWT_TOKEN) {
9
+ if (!env.OBOL_GATEWAY_URL || !env.OBOL_AGENT_ID) {
10
10
  console.log();
11
11
  p.log.warn('No agent configured yet.');
12
12
  p.log.info(`Run ${pc.green('npx @obol/cli setup')} first.`);
@@ -48,18 +48,23 @@ export async function runCreditsBuy() {
48
48
  });
49
49
  if (p.isCancel(confirmed) || !confirmed) { p.cancel('Cancelled.'); return; }
50
50
 
51
- // Create checkout
51
+ // Create checkout session (returns a Stripe checkout URL)
52
52
  s.start('Creating Stripe checkout...');
53
53
  try {
54
- const result = await createCheckout(url, env.OBOL_JWT_TOKEN, tier.price_usd);
55
- s.stop(pc.green('Checkout created!'));
54
+ const result = await createWebCheckout(url, env.OBOL_AGENT_ID, tier.price_usd);
55
+ s.stop(pc.green('Checkout ready!'));
56
56
 
57
57
  console.log();
58
- p.log.info(`Tier: ${pc.bold(result.tier)} — $${result.amount_usd} → ${pc.green(result.credits_usdc + ' credits')}`);
59
- p.log.info(`Payment Intent: ${pc.dim(result.payment_intent_id)}`);
58
+ p.log.info(`Tier: ${pc.bold(tier.name)} — $${tier.price_usd} → ${pc.green(tier.credits_usdc + ' credits')}`);
60
59
  console.log();
61
- p.log.warn('Stripe Checkout requires a browser to complete payment.');
62
- p.log.info(`Use the Stripe Dashboard or integrate Stripe.js in your app to confirm payment intent: ${pc.cyan(result.payment_intent_id)}`);
60
+ p.log.info(`Open this link to complete payment:`);
61
+ console.log(` ${pc.cyan(result.checkout_url)}`);
62
+ console.log();
63
+
64
+ // Try to open in browser
65
+ const { exec } = await import('node:child_process');
66
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
67
+ exec(`${cmd} "${result.checkout_url}"`);
63
68
  } catch (err) {
64
69
  s.stop(pc.red(`Couldn't create checkout: ${err.message}`));
65
70
  }
package/src/setup.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, writeFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
1
4
  import * as p from '@clack/prompts';
2
5
  import pc from 'picocolors';
3
- import { randomBytes } from 'node:crypto';
4
- import { checkHealth, registerAgent, issueToken, createPolicy } from './api.js';
6
+ import { checkHealth, signup, storeBYOK, validateReferralCode, listEndpoints } from './api.js';
5
7
  import { saveEnv } from './config.js';
6
8
 
7
9
  const cheers = ['Nice!', 'Boom!', "Let's go.", 'Easy.', 'Done.'];
@@ -11,39 +13,30 @@ export async function runSetup() {
11
13
  console.log();
12
14
  p.intro(pc.bgGreen(pc.black(' Welcome to Obol! ')));
13
15
 
14
- p.log.info("Let's get your agent connected. This takes about 60 seconds.");
16
+ p.log.info("Let's connect your agent to Obol. Takes about 60 seconds.");
15
17
 
16
18
  const url = 'https://www.obolagents.com';
17
19
 
18
20
  // Verify connectivity
19
21
  const healthSpin = p.spinner();
20
- healthSpin.start('Saying hello to the gateway...');
22
+ healthSpin.start('Connecting to Obol...');
21
23
  try {
22
24
  await checkHealth(url);
23
- healthSpin.stop(pc.green('Gateway is up and running! ') + cheer());
25
+ healthSpin.stop(pc.green('Connected! ') + cheer());
24
26
  } catch (err) {
25
27
  healthSpin.stop(pc.red('Hmm, couldn\'t reach the gateway.'));
26
- p.log.error(`Make sure it's running at ${pc.bold(url)}`);
28
+ p.log.error(`Make sure you have an internet connection.`);
27
29
  p.log.error(`Details: ${err.message}`);
28
- p.outro(pc.dim('Fix the URL and try again.'));
30
+ p.outro(pc.dim('Try again in a moment.'));
29
31
  process.exit(1);
30
32
  }
31
33
 
32
- // --- Admin Key ---
33
- const adminKey = await p.password({
34
- message: 'Paste your admin API key',
35
- validate: (val) => {
36
- if (!val || val.length < 8) return 'That looks too short. Check your Render dashboard → Environment Variables → OBOL_ADMIN_API_KEY';
37
- },
38
- });
39
- if (p.isCancel(adminKey)) return cancelled();
40
-
41
- // --- Agent Name ---
34
+ // --- Account Name ---
42
35
  const agentId = await p.text({
43
- message: 'Give your agent a name',
36
+ message: 'Pick a name for your Obol account',
44
37
  placeholder: 'my-agent',
45
38
  validate: (val) => {
46
- if (!val) return 'Your agent needs a name!';
39
+ if (!val) return 'Your account needs a name!';
47
40
  if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,62}[a-zA-Z0-9]$/.test(val)) {
48
41
  return 'Letters, numbers, hyphens, and underscores only (3-64 chars)';
49
42
  }
@@ -51,18 +44,31 @@ export async function runSetup() {
51
44
  });
52
45
  if (p.isCancel(agentId)) return cancelled();
53
46
 
54
- // --- Wallet Address ---
55
- const walletAddress = await p.text({
56
- message: `Got a USDC wallet on Base? ${pc.dim('(recommended for automatic micropayments)')}`,
57
- placeholder: '0x... or press Enter to skip',
47
+ // --- Referral Code ---
48
+ let referralCode = await p.text({
49
+ message: `Got a referral code? ${pc.dim('(earns you $5 in free credits — press Enter to skip)')}`,
50
+ placeholder: 'e.g. X7K9M2QP',
58
51
  validate: (val) => {
59
- if (!val) return; // optional
60
- if (!/^0x[0-9a-fA-F]{40}$/.test(val)) {
61
- return 'That doesn\'t look like an EVM address. Should be 0x followed by 40 hex characters.';
52
+ if (!val) return;
53
+ if (!/^[A-Za-z0-9]{6,12}$/.test(val.trim())) {
54
+ return 'Referral codes are 6-12 alphanumeric characters';
62
55
  }
63
56
  },
64
57
  });
65
- if (p.isCancel(walletAddress)) return cancelled();
58
+ if (p.isCancel(referralCode)) return cancelled();
59
+
60
+ // --- Validate Referral Code ---
61
+ if (referralCode && referralCode.trim()) {
62
+ const valSpin = p.spinner();
63
+ valSpin.start('Checking referral code...');
64
+ try {
65
+ const validation = await validateReferralCode(url, referralCode.trim());
66
+ valSpin.stop(pc.green(`Referral code valid! You'll get $${validation.signup_bonus_usdc} in credits after your first API call.`));
67
+ } catch (err) {
68
+ valSpin.stop(pc.yellow('That referral code isn\'t valid — continuing without it.'));
69
+ referralCode = undefined;
70
+ }
71
+ }
66
72
 
67
73
  // --- Services ---
68
74
  const services = await p.multiselect({
@@ -74,94 +80,294 @@ export async function runSetup() {
74
80
  { value: 'notion', label: 'Notion', hint: 'pages, databases' },
75
81
  { value: 'discord', label: 'Discord', hint: 'messages, servers' },
76
82
  { value: 'twitter', label: 'Twitter/X', hint: 'tweets, threads' },
83
+ { value: 'agentmail', label: 'AgentMail', hint: 'email inboxes, send & receive' },
77
84
  ],
78
85
  required: true,
79
86
  });
80
87
  if (p.isCancel(services)) return cancelled();
81
88
 
82
- // --- Confirm ---
83
- const confirmed = await p.confirm({
84
- message: `Register ${pc.bold(pc.green(agentId))} with ${services.length} service${services.length > 1 ? 's' : ''}?`,
89
+ // --- API Keys (optional) ---
90
+ const keyHints = {
91
+ stripe: 'Starts with sk_live_ or sk_test_',
92
+ github: 'Personal access token (ghp_ or github_pat_)',
93
+ slack: 'Bot token (xoxb-...)',
94
+ notion: 'Internal integration token (ntn_ or secret_)',
95
+ discord: 'Bot token from Discord Developer Portal',
96
+ twitter: 'OAuth 2.0 user access token',
97
+ agentmail: 'API key (am_...)',
98
+ };
99
+ const serviceNames = {
100
+ stripe: 'Stripe', github: 'GitHub', slack: 'Slack', notion: 'Notion',
101
+ discord: 'Discord', twitter: 'Twitter/X', agentmail: 'AgentMail',
102
+ };
103
+
104
+ const addKeys = await p.confirm({
105
+ message: 'Want to add your API keys now? (you can also do this later)',
106
+ initialValue: true,
85
107
  });
108
+ if (p.isCancel(addKeys)) return cancelled();
109
+
110
+ const apiKeys = {};
111
+ if (addKeys) {
112
+ for (const svc of services) {
113
+ const key = await p.password({
114
+ message: `${serviceNames[svc] || svc} API key ${pc.dim(`(${keyHints[svc] || 'paste key'})`)}`,
115
+ validate: (val) => {
116
+ if (!val) return; // allow skip
117
+ if (val.length < 10) return 'That looks too short — press Enter to skip';
118
+ },
119
+ });
120
+ if (p.isCancel(key)) return cancelled();
121
+ if (key && key.trim()) apiKeys[svc] = key.trim();
122
+ }
123
+ }
124
+
125
+ // --- Confirm ---
126
+ const keyCount = Object.keys(apiKeys).length;
127
+ const confirmMsg = keyCount
128
+ ? `Create Obol account ${pc.bold(pc.green(agentId))} with ${services.length} service${services.length > 1 ? 's' : ''} and ${keyCount} API key${keyCount > 1 ? 's' : ''}?`
129
+ : `Create Obol account ${pc.bold(pc.green(agentId))} with ${services.length} service${services.length > 1 ? 's' : ''}?`;
130
+ const confirmed = await p.confirm({ message: confirmMsg });
86
131
  if (p.isCancel(confirmed) || !confirmed) return cancelled();
87
132
 
88
133
  // --- Execute! ---
89
134
  console.log();
90
135
  const s = p.spinner();
91
136
 
92
- // 1. Register agent
93
- s.start('Registering your agent...');
94
- const secret = randomBytes(32).toString('base64url');
137
+ // 1. Sign up
138
+ s.start('Creating your account...');
139
+ let jwt;
140
+ let expiresAt;
95
141
  try {
96
- await registerAgent(url, adminKey, agentId, secret);
97
- s.stop(pc.green(`Agent "${agentId}" registered. `) + cheer());
142
+ const result = await signup(url, agentId, services, referralCode?.trim() || undefined);
143
+ jwt = result.token;
144
+ expiresAt = new Date(result.expires_at * 1000);
145
+
146
+ if (result.referral) {
147
+ s.stop(pc.green(`Account "${agentId}" created with referral! $${result.referral.signup_bonus_referee} bonus unlocks after your first API call `) + cheer());
148
+ } else {
149
+ s.stop(pc.green(`Account "${agentId}" created! `) + cheer());
150
+ }
98
151
  } catch (err) {
99
152
  if (err.status === 409) {
100
- s.stop(pc.yellow(`Agent "${agentId}" already existsthat's fine, we'll keep going.`));
153
+ s.stop(pc.red(`That account name was revoked try a different name.`));
101
154
  } else {
102
- s.stop(pc.red(`Couldn't register agent: ${err.message}`));
103
- p.outro(pc.dim('Check your admin key and try again.'));
104
- process.exit(1);
155
+ s.stop(pc.red(`Couldn't create account: ${err.message}`));
105
156
  }
106
- }
107
-
108
- // 2. Issue JWT
109
- s.start('Getting your agent a JWT token...');
110
- let jwt;
111
- try {
112
- const tokenData = await issueToken(url, adminKey, agentId);
113
- jwt = tokenData.token;
114
- const expiresAt = new Date(tokenData.expires_at * 1000);
115
- s.stop(pc.green('JWT issued ') + pc.dim(`(expires ${expiresAt.toLocaleDateString()})`));
116
- } catch (err) {
117
- s.stop(pc.red(`Couldn't issue token: ${err.message}`));
118
- p.outro(pc.dim('Something went wrong. Try again or check the gateway logs.'));
157
+ p.outro(pc.dim('Try again.'));
119
158
  process.exit(1);
120
159
  }
121
160
 
122
- // 3. Create default policy
123
- s.start('Setting up execution policy...');
124
- try {
125
- await createPolicy(url, adminKey, agentId, services);
126
- s.stop(pc.green(`Policy created for ${services.join(', ')}. `) + cheer());
127
- } catch (err) {
128
- // Non-fatal agent can still work without a named policy
129
- s.stop(pc.yellow(`Couldn't create policy (${err.message}) — no worries, defaults apply.`));
161
+ // 2. Store API keys (if any)
162
+ if (Object.keys(apiKeys).length > 0) {
163
+ for (const [svc, key] of Object.entries(apiKeys)) {
164
+ s.start(`Encrypting ${serviceNames[svc] || svc} key...`);
165
+ try {
166
+ await storeBYOK(url, jwt, svc, key);
167
+ s.stop(pc.green(`${serviceNames[svc] || svc} connected! `) + cheer());
168
+ } catch (err) {
169
+ s.stop(pc.yellow(`Couldn't store ${serviceNames[svc] || svc} key: ${err.message}`));
170
+ }
171
+ }
130
172
  }
131
173
 
132
- // 4. Save config
174
+ // 3. Save config
133
175
  s.start('Saving configuration...');
134
176
  const vars = {
135
177
  OBOL_GATEWAY_URL: url,
136
178
  OBOL_AGENT_ID: agentId,
137
179
  OBOL_JWT_TOKEN: jwt,
138
180
  };
139
- if (walletAddress) {
140
- vars.OBOL_WALLET_ADDRESS = walletAddress;
141
- }
142
181
  const envPath = saveEnv(vars);
143
182
  s.stop(pc.green(`Saved to ${envPath}`));
144
183
 
145
- // --- Celebration! ---
184
+ // 4. Detect project type and install SDK
185
+ let lang = detectProject();
186
+ let sdkInstalled = false;
187
+ let integrationFile = null;
188
+
189
+ if (!lang) {
190
+ lang = await p.select({
191
+ message: 'What language is your agent built in?',
192
+ options: [
193
+ { value: 'js', label: 'JavaScript / TypeScript' },
194
+ { value: 'python', label: 'Python' },
195
+ ],
196
+ });
197
+ if (p.isCancel(lang)) return cancelled();
198
+ }
199
+
200
+ const hasPackageJson = existsSync(resolve(process.cwd(), 'package.json'));
201
+
202
+ if (lang === 'js') {
203
+ if (hasPackageJson) {
204
+ s.start('Installing @obol/sdk...');
205
+ try {
206
+ execSync('npm install @obol/sdk', { stdio: 'pipe', timeout: 60000 });
207
+ s.stop(pc.green('SDK installed! ') + cheer());
208
+ sdkInstalled = true;
209
+ } catch {
210
+ s.stop(pc.yellow('Could not auto-install SDK. Run manually: npm install @obol/sdk'));
211
+ }
212
+ } else {
213
+ p.log.info(`Install the SDK when you're ready: ${pc.green('npm install @obol/sdk')}`);
214
+ }
215
+
216
+ // Generate integration file
217
+ const obolFile = resolve(process.cwd(), 'obol.js');
218
+ if (!existsSync(obolFile)) {
219
+ s.start('Generating integration file...');
220
+ writeFileSync(obolFile, jsIntegrationFile(agentId, services));
221
+ s.stop(pc.green('Created obol.js'));
222
+ integrationFile = 'obol.js';
223
+ }
224
+ } else if (lang === 'python') {
225
+ s.start('Installing obol-sdk...');
226
+ try {
227
+ execSync('pip install obol-sdk', { stdio: 'pipe', timeout: 60000 });
228
+ s.stop(pc.green('SDK installed! ') + cheer());
229
+ sdkInstalled = true;
230
+ } catch {
231
+ s.stop(pc.yellow('Could not auto-install SDK. Run manually: pip install obol-sdk'));
232
+ }
233
+
234
+ // Generate integration file
235
+ const obolFile = resolve(process.cwd(), 'obol_setup.py');
236
+ if (!existsSync(obolFile)) {
237
+ s.start('Generating integration file...');
238
+ writeFileSync(obolFile, pyIntegrationFile(agentId, services));
239
+ s.stop(pc.green('Created obol_setup.py'));
240
+ integrationFile = 'obol_setup.py';
241
+ }
242
+ }
243
+
244
+ // 5. Test connectivity
245
+ s.start('Testing connection...');
246
+ try {
247
+ const epData = await listEndpoints(url, jwt);
248
+ const eps = epData.endpoints || [];
249
+ const names = eps.map(e => e.provider || e.name).filter(Boolean);
250
+ s.stop(pc.green(`Connected! ${eps.length} endpoints available`) + (names.length ? ` (${names.join(', ')})` : ''));
251
+ } catch {
252
+ s.stop(pc.yellow('Could not verify connection — run obol test later to check'));
253
+ }
254
+
255
+ // --- Summary ---
146
256
  console.log();
147
- p.log.success(pc.bold(`Your agent "${agentId}" is live on Obol!`));
257
+ p.log.success(pc.bold(`Your agent is connected to Obol!`));
148
258
  console.log();
149
259
 
150
- if (walletAddress) {
151
- p.log.info(`Micropayments enabled — your agent pays a few cents per call from ${pc.cyan(walletAddress.slice(0, 6) + '...' + walletAddress.slice(-4))}.`);
152
- } else {
153
- p.log.info(`No wallet linked. Buy credits with ${pc.green('obol tiers')} or add a wallet later.`);
260
+ if (sdkInstalled) {
261
+ console.log(` ${pc.dim('SDK installed:')} ${pc.green(lang === 'js' ? '@obol/sdk' : 'obol-sdk')}`);
262
+ }
263
+ if (integrationFile) {
264
+ console.log(` ${pc.dim('Integration file:')} ${pc.green('./' + integrationFile)}`);
154
265
  }
266
+ console.log(` ${pc.dim('Config saved:')} ${pc.green(envPath)}`);
267
+ console.log();
155
268
 
269
+ // Prominent $0 balance warning
270
+ p.log.warn(pc.bold(pc.yellow('Your balance is $0.00 — you need credits before your agent can execute.')));
156
271
  console.log();
157
- console.log(` ${pc.dim('Check status:')} ${pc.green('npx @obol/cli status')}`);
158
- console.log(` ${pc.dim('Add a key:')} ${pc.green('npx @obol/cli connect github')}`);
159
- console.log(` ${pc.dim('View balance:')} ${pc.green('npx @obol/cli balance')}`);
272
+
273
+ console.log(` ${pc.bold('Next steps:')}`);
274
+ console.log(` ${pc.dim('1.')} ${pc.bold('Buy credits')} ${pc.dim('(required)')}: ${pc.green('npx @obol/cli credits buy')}`);
275
+ console.log(` ${pc.dim('or visit:')} ${pc.cyan('obolagents.com/dashboard')}`);
276
+ if (lang === 'js' && integrationFile) {
277
+ console.log(` ${pc.dim('2.')} Use in your code:`);
278
+ console.log(` ${pc.cyan("import { obol } from './obol.js';")}`);
279
+ console.log(` ${pc.cyan("const result = await obol.execute(2, 'Create issue ...');")}`);
280
+ } else if (lang === 'python' && integrationFile) {
281
+ console.log(` ${pc.dim('2.')} Use in your code:`);
282
+ console.log(` ${pc.cyan("from obol_setup import obol")}`);
283
+ console.log(` ${pc.cyan("result = obol.execute(2, 'Create issue ...', idempotency_key='...')")}`);
284
+ }
285
+ console.log();
286
+ console.log(` ${pc.dim('Dashboard:')} ${pc.green('npx @obol/cli token')} → paste at ${pc.cyan('obolagents.com/dashboard')}`);
287
+ console.log(` ${pc.dim('Token:')} Refreshes automatically — run ${pc.green('npx @obol/cli test')} anytime to verify.`);
288
+ console.log();
289
+
290
+ p.log.info(`${pc.bold('Earn credits by referring other agents!')} Run ${pc.green('npx @obol/cli referral code')} to get started.`);
160
291
  console.log();
161
292
 
162
293
  p.outro('Go build something awesome. ' + pc.green('⚡'));
163
294
  }
164
295
 
296
+ function detectProject() {
297
+ const cwd = process.cwd();
298
+ if (existsSync(resolve(cwd, 'package.json'))) return 'js';
299
+ if (existsSync(resolve(cwd, 'requirements.txt'))) return 'python';
300
+ if (existsSync(resolve(cwd, 'pyproject.toml'))) return 'python';
301
+ return null;
302
+ }
303
+
304
+ function jsIntegrationFile(agentId, services) {
305
+ const providerMap = { stripe: 1, github: 2, slack: 3, notion: 4, discord: 5, twitter: 6, agentmail: 7 };
306
+ const examples = services
307
+ .map(svc => {
308
+ const id = providerMap[svc];
309
+ const ex = {
310
+ stripe: `await obol.execute(${id}, 'Create invoice for $50 to customer@example.com');`,
311
+ github: `await obol.execute(${id}, 'Create issue titled "Bug report" in owner/repo');`,
312
+ slack: `await obol.execute(${id}, 'Send message "Hello team" to #general');`,
313
+ notion: `await obol.execute(${id}, 'Create page titled "Meeting Notes"');`,
314
+ discord: `await obol.execute(${id}, 'Send message "Hello" to channel 123456');`,
315
+ twitter: `await obol.execute(${id}, 'Post tweet: Just shipped a new feature!');`,
316
+ agentmail: `await obol.execute(${id}, 'Send email to user@example.com subject "Hello"');`,
317
+ };
318
+ return `// ${ex[svc] || `await obol.execute(${id}, 'your intent here');`}`;
319
+ })
320
+ .join('\n');
321
+
322
+ return `import { ObolClient } from '@obol/sdk';
323
+
324
+ // Pre-configured Obol client — reads OBOL_JWT_TOKEN from .env automatically
325
+ export const obol = new ObolClient();
326
+
327
+ // Provider IDs: 1=Stripe, 2=GitHub, 3=Slack, 4=Notion, 5=Discord, 6=Twitter, 7=AgentMail
328
+ //
329
+ // Usage:
330
+ // import { obol } from './obol.js';
331
+ ${examples}
332
+ `;
333
+ }
334
+
335
+ function pyIntegrationFile(agentId, services) {
336
+ const providerMap = { stripe: 1, github: 2, slack: 3, notion: 4, discord: 5, twitter: 6, agentmail: 7 };
337
+ const examples = services
338
+ .map(svc => {
339
+ const id = providerMap[svc];
340
+ const ex = {
341
+ stripe: `# obol.execute(${id}, 'Create invoice for $50 to customer@example.com', idempotency_key='unique-key')`,
342
+ github: `# obol.execute(${id}, 'Create issue titled "Bug report" in owner/repo', idempotency_key='unique-key')`,
343
+ slack: `# obol.execute(${id}, 'Send message "Hello team" to #general', idempotency_key='unique-key')`,
344
+ notion: `# obol.execute(${id}, 'Create page titled "Meeting Notes"', idempotency_key='unique-key')`,
345
+ discord: `# obol.execute(${id}, 'Send message "Hello" to channel 123456', idempotency_key='unique-key')`,
346
+ twitter: `# obol.execute(${id}, 'Post tweet: Just shipped a new feature!', idempotency_key='unique-key')`,
347
+ agentmail: `# obol.execute(${id}, 'Send email to user@example.com subject "Hello"', idempotency_key='unique-key')`,
348
+ };
349
+ return ex[svc] || `# obol.execute(${id}, 'your intent here', idempotency_key='unique-key')`;
350
+ })
351
+ .join('\n');
352
+
353
+ return `import os
354
+ from obol_sdk import ObolClient
355
+
356
+ # Pre-configured Obol client — reads from environment variables
357
+ obol = ObolClient(
358
+ gateway_url=os.getenv("OBOL_GATEWAY_URL", "https://www.obolagents.com"),
359
+ agent_id=os.getenv("OBOL_AGENT_ID", "${agentId}"),
360
+ jwt_token=os.getenv("OBOL_JWT_TOKEN", ""),
361
+ )
362
+
363
+ # Provider IDs: 1=Stripe, 2=GitHub, 3=Slack, 4=Notion, 5=Discord, 6=Twitter, 7=AgentMail
364
+ #
365
+ # Usage:
366
+ # from obol_setup import obol
367
+ ${examples}
368
+ `;
369
+ }
370
+
165
371
  function cancelled() {
166
372
  p.cancel('Setup cancelled. Come back anytime!');
167
373
  process.exit(0);