@obol/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/bin/obol.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runSetup } from '../src/setup.js';
4
+ import { runStatus } from '../src/status.js';
5
+ import { runConnect } from '../src/connect.js';
6
+ import { getBalance, getTiers } from '../src/api.js';
7
+ import { loadEnv } from '../src/config.js';
8
+ import pc from 'picocolors';
9
+
10
+ const command = process.argv[2];
11
+
12
+ async function main() {
13
+ switch (command) {
14
+ case 'setup':
15
+ await runSetup();
16
+ break;
17
+
18
+ case 'status':
19
+ await runStatus();
20
+ break;
21
+
22
+ case 'connect':
23
+ await runConnect(process.argv[3]);
24
+ break;
25
+
26
+ case 'balance': {
27
+ const env = loadEnv();
28
+ if (!env.OBOL_GATEWAY_URL || !env.OBOL_JWT_TOKEN) {
29
+ console.log(pc.red('No agent configured. Run: obol setup'));
30
+ process.exit(1);
31
+ }
32
+ try {
33
+ const data = await getBalance(env.OBOL_GATEWAY_URL, env.OBOL_JWT_TOKEN);
34
+ console.log(`\n ${pc.bold('Credit Balance')}`);
35
+ console.log(` ${pc.green('$' + data.balance_usdc.toFixed(2))} USDC available`);
36
+ if (data.reserved_usdc > 0) {
37
+ console.log(` ${pc.yellow('$' + data.reserved_usdc.toFixed(2))} reserved in active sessions`);
38
+ }
39
+ console.log();
40
+ } catch (err) {
41
+ console.log(pc.red(` Couldn't fetch balance: ${err.message}`));
42
+ process.exit(1);
43
+ }
44
+ break;
45
+ }
46
+
47
+ case 'tiers': {
48
+ const env = loadEnv();
49
+ const url = env.OBOL_GATEWAY_URL || 'https://www.obolagents.com';
50
+ try {
51
+ const data = await getTiers(url);
52
+ console.log(`\n ${pc.bold('Credit Tiers')}\n`);
53
+ for (const t of data.tiers) {
54
+ const bonus = t.bonus_pct > 0 ? pc.green(` +${t.bonus_pct}% bonus`) : '';
55
+ console.log(` ${pc.bold(t.name.padEnd(10))} $${t.price_usd} → ${pc.green(t.credits_usdc + ' credits')}${bonus}`);
56
+ }
57
+ console.log();
58
+ } catch (err) {
59
+ console.log(pc.red(` Couldn't fetch tiers: ${err.message}`));
60
+ process.exit(1);
61
+ }
62
+ break;
63
+ }
64
+
65
+ case 'help':
66
+ case '--help':
67
+ case '-h':
68
+ case undefined:
69
+ console.log(`
70
+ ${pc.bold(pc.green('obol'))} — CLI for the Obol Agent Execution Gateway
71
+
72
+ ${pc.bold('Commands:')}
73
+ ${pc.green('setup')} Set up a new agent (interactive wizard)
74
+ ${pc.green('status')} Check agent health, balance & connections
75
+ ${pc.green('connect')} ${pc.dim('<provider>')} Add a service API key (github, slack, etc.)
76
+ ${pc.green('balance')} Check your credit balance
77
+ ${pc.green('tiers')} Show credit purchase tiers
78
+ ${pc.green('help')} Show this help message
79
+
80
+ ${pc.bold('Quick start:')}
81
+ ${pc.dim('$')} npx @obol/cli setup
82
+ `);
83
+ break;
84
+
85
+ default:
86
+ console.log(pc.red(` Unknown command: ${command}`));
87
+ console.log(` Run ${pc.green('obol help')} to see available commands.`);
88
+ process.exit(1);
89
+ }
90
+ }
91
+
92
+ main().catch(err => {
93
+ console.error(pc.red(`\n Something went wrong: ${err.message}`));
94
+ process.exit(1);
95
+ });
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@obol/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI for the Obol Agent Execution Gateway",
5
+ "type": "module",
6
+ "bin": {
7
+ "obol": "./bin/obol.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "dependencies": {
13
+ "@clack/prompts": "^0.9.1",
14
+ "picocolors": "^1.1.1"
15
+ }
16
+ }
package/src/api.js ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Obol Gateway API client — uses built-in fetch (Node 18+).
3
+ */
4
+
5
+ class ApiError extends Error {
6
+ constructor(message, status) {
7
+ super(message);
8
+ this.status = status;
9
+ }
10
+ }
11
+
12
+ async function request(url, options = {}) {
13
+ let res;
14
+ try {
15
+ res = await fetch(url, { ...options, signal: AbortSignal.timeout(15000) });
16
+ } catch (err) {
17
+ if (err.name === 'TimeoutError') throw new ApiError('Request timed out', 0);
18
+ throw new ApiError(`Couldn't reach the gateway: ${err.message}`, 0);
19
+ }
20
+ if (!res.ok) {
21
+ let detail = '';
22
+ try {
23
+ const body = await res.json();
24
+ detail = body.detail || JSON.stringify(body);
25
+ } catch { detail = res.statusText; }
26
+ throw new ApiError(detail, res.status);
27
+ }
28
+ return res.json();
29
+ }
30
+
31
+ function adminHeaders(adminKey) {
32
+ return { 'Content-Type': 'application/json', 'x-obol-admin-key': adminKey };
33
+ }
34
+
35
+ function jwtHeaders(jwt) {
36
+ return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${jwt}` };
37
+ }
38
+
39
+ // --- Public API ---
40
+
41
+ export async function checkHealth(gatewayUrl) {
42
+ return request(`${gatewayUrl}/healthz`);
43
+ }
44
+
45
+ export async function registerAgent(gatewayUrl, adminKey, agentId, secret, ownerId) {
46
+ const body = { agent_id: agentId, secret };
47
+ if (ownerId) body.owner_id = ownerId;
48
+ return request(`${gatewayUrl}/v1/agents`, {
49
+ method: 'POST',
50
+ headers: adminHeaders(adminKey),
51
+ body: JSON.stringify(body),
52
+ });
53
+ }
54
+
55
+ export async function issueToken(gatewayUrl, adminKey, agentId, policyId) {
56
+ const body = {};
57
+ if (policyId) body.policy_id = policyId;
58
+ return request(`${gatewayUrl}/v1/agents/${agentId}/token`, {
59
+ method: 'POST',
60
+ headers: adminHeaders(adminKey),
61
+ body: JSON.stringify(body),
62
+ });
63
+ }
64
+
65
+ export async function createPolicy(gatewayUrl, adminKey, agentId, protocols) {
66
+ return request(`${gatewayUrl}/v1/agents/${agentId}/policies`, {
67
+ method: 'POST',
68
+ headers: adminHeaders(adminKey),
69
+ body: JSON.stringify({
70
+ name: 'default',
71
+ allowed_protocols: protocols,
72
+ is_default: true,
73
+ }),
74
+ });
75
+ }
76
+
77
+ export async function storeBYOK(gatewayUrl, jwt, provider, apiKey, resourceScopes) {
78
+ const body = { provider, api_key: apiKey };
79
+ if (resourceScopes) body.resource_scopes = resourceScopes;
80
+ return request(`${gatewayUrl}/v1/connections/byok`, {
81
+ method: 'POST',
82
+ headers: jwtHeaders(jwt),
83
+ body: JSON.stringify(body),
84
+ });
85
+ }
86
+
87
+ export async function listConnections(gatewayUrl, jwt) {
88
+ return request(`${gatewayUrl}/v1/connections`, { headers: jwtHeaders(jwt) });
89
+ }
90
+
91
+ export async function getBalance(gatewayUrl, jwt) {
92
+ return request(`${gatewayUrl}/v1/credits/balance`, { headers: jwtHeaders(jwt) });
93
+ }
94
+
95
+ export async function getTiers(gatewayUrl) {
96
+ return request(`${gatewayUrl}/v1/credits/tiers`);
97
+ }
98
+
99
+ export async function getAgentInfo(gatewayUrl, adminKey, agentId) {
100
+ return request(`${gatewayUrl}/v1/agents/${agentId}`, {
101
+ headers: adminHeaders(adminKey),
102
+ });
103
+ }
package/src/config.js ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Read/write .env config file for Obol CLI.
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+
8
+ const ENV_PATH = resolve(process.cwd(), '.env');
9
+
10
+ const OBOL_KEYS = [
11
+ 'OBOL_GATEWAY_URL',
12
+ 'OBOL_AGENT_ID',
13
+ 'OBOL_JWT_TOKEN',
14
+ 'OBOL_WALLET_ADDRESS',
15
+ 'OBOL_ADMIN_API_KEY',
16
+ ];
17
+
18
+ export function loadEnv(path) {
19
+ const file = path || ENV_PATH;
20
+ const env = {};
21
+ if (!existsSync(file)) return env;
22
+ const lines = readFileSync(file, 'utf-8').split('\n');
23
+ for (const line of lines) {
24
+ const trimmed = line.trim();
25
+ if (!trimmed || trimmed.startsWith('#')) continue;
26
+ const eqIdx = trimmed.indexOf('=');
27
+ if (eqIdx === -1) continue;
28
+ const key = trimmed.slice(0, eqIdx).trim();
29
+ let val = trimmed.slice(eqIdx + 1).trim();
30
+ // Strip quotes
31
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
32
+ val = val.slice(1, -1);
33
+ }
34
+ env[key] = val;
35
+ }
36
+ return env;
37
+ }
38
+
39
+ export function saveEnv(vars, path) {
40
+ const file = path || ENV_PATH;
41
+ let existing = '';
42
+ if (existsSync(file)) {
43
+ existing = readFileSync(file, 'utf-8');
44
+ }
45
+
46
+ // Parse existing lines, update OBOL_ keys, preserve everything else
47
+ const lines = existing ? existing.split('\n') : [];
48
+ const updated = new Set();
49
+ const result = [];
50
+
51
+ for (const line of lines) {
52
+ const trimmed = line.trim();
53
+ const eqIdx = trimmed.indexOf('=');
54
+ if (eqIdx !== -1) {
55
+ const key = trimmed.slice(0, eqIdx).trim();
56
+ if (key in vars) {
57
+ result.push(`${key}=${vars[key]}`);
58
+ updated.add(key);
59
+ continue;
60
+ }
61
+ }
62
+ result.push(line);
63
+ }
64
+
65
+ // Add new keys that weren't in the file
66
+ const newKeys = Object.keys(vars).filter(k => !updated.has(k));
67
+ if (newKeys.length > 0) {
68
+ if (result.length > 0 && result[result.length - 1].trim() !== '') {
69
+ result.push('');
70
+ }
71
+ result.push('# Obol Agent Configuration');
72
+ for (const key of newKeys) {
73
+ result.push(`${key}=${vars[key]}`);
74
+ }
75
+ }
76
+
77
+ writeFileSync(file, result.join('\n') + '\n');
78
+ return file;
79
+ }
package/src/connect.js ADDED
@@ -0,0 +1,78 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { storeBYOK, listConnections } from './api.js';
4
+ import { loadEnv } from './config.js';
5
+
6
+ const providerInfo = {
7
+ stripe: { name: 'Stripe', keyHint: 'Starts with sk_live_ or sk_test_', prefix: 'sk_' },
8
+ github: { name: 'GitHub', keyHint: 'Personal access token (ghp_ or github_pat_)', prefix: '' },
9
+ slack: { name: 'Slack', keyHint: 'Bot token (xoxb-...)', prefix: 'xoxb-' },
10
+ notion: { name: 'Notion', keyHint: 'Internal integration token (ntn_ or secret_)', prefix: '' },
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: '' },
13
+ };
14
+
15
+ export async function runConnect(providerArg) {
16
+ const env = loadEnv();
17
+
18
+ if (!env.OBOL_GATEWAY_URL || !env.OBOL_JWT_TOKEN) {
19
+ console.log();
20
+ p.log.warn('No agent configured yet.');
21
+ p.log.info(`Run ${pc.green('npx @obol/cli setup')} first.`);
22
+ console.log();
23
+ return;
24
+ }
25
+
26
+ console.log();
27
+ p.intro(pc.bgGreen(pc.black(' Connect a Service ')));
28
+
29
+ // Pick provider
30
+ let provider = providerArg?.toLowerCase();
31
+ if (!provider || !providerInfo[provider]) {
32
+ provider = await p.select({
33
+ message: 'Which service do you want to connect?',
34
+ options: Object.entries(providerInfo).map(([key, info]) => ({
35
+ value: key,
36
+ label: info.name,
37
+ })),
38
+ });
39
+ if (p.isCancel(provider)) { p.cancel('Cancelled.'); return; }
40
+ }
41
+
42
+ const info = providerInfo[provider];
43
+
44
+ // Get API key
45
+ const apiKey = await p.password({
46
+ message: `Paste your ${info.name} API key`,
47
+ validate: (val) => {
48
+ if (!val || val.length < 10) return `That looks too short. ${info.keyHint}`;
49
+ },
50
+ });
51
+ if (p.isCancel(apiKey)) { p.cancel('Cancelled.'); return; }
52
+
53
+ // Store it
54
+ const s = p.spinner();
55
+ s.start(`Encrypting and storing your ${info.name} key...`);
56
+
57
+ try {
58
+ const result = await storeBYOK(env.OBOL_GATEWAY_URL, env.OBOL_JWT_TOKEN, provider, apiKey);
59
+ s.stop(pc.green(`${info.name} connected!`) + pc.dim(` (connection: ${result.connection_id?.slice(0, 8)}...)`));
60
+ } catch (err) {
61
+ s.stop(pc.red(`Couldn't connect ${info.name}: ${err.message}`));
62
+ p.outro(pc.dim('Check your API key and try again.'));
63
+ process.exit(1);
64
+ }
65
+
66
+ // Show summary
67
+ s.start('Checking your connections...');
68
+ try {
69
+ const data = await listConnections(env.OBOL_GATEWAY_URL, env.OBOL_JWT_TOKEN);
70
+ const conns = data.connections || data || [];
71
+ const names = conns.map(c => pc.green(c.provider)).join(', ');
72
+ s.stop(`${pc.bold('Connected services:')} ${names}`);
73
+ } catch {
74
+ s.stop();
75
+ }
76
+
77
+ p.outro(`${info.name} is ready to go! Your agent can now use it via Obol.`);
78
+ }
package/src/setup.js ADDED
@@ -0,0 +1,181 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { randomBytes } from 'node:crypto';
4
+ import { checkHealth, registerAgent, issueToken, createPolicy } from './api.js';
5
+ import { saveEnv } from './config.js';
6
+
7
+ const cheers = ['Nice!', 'Boom!', "Let's go.", 'Easy.', 'Done.'];
8
+ const cheer = () => cheers[Math.floor(Math.random() * cheers.length)];
9
+
10
+ export async function runSetup() {
11
+ console.log();
12
+ p.intro(pc.bgGreen(pc.black(' Welcome to Obol! ')));
13
+
14
+ p.log.info("Let's get your agent connected. This takes about 60 seconds.");
15
+
16
+ // --- Gateway URL ---
17
+ const gatewayUrl = await p.text({
18
+ message: "Where's your Obol gateway?",
19
+ placeholder: 'https://www.obolagents.com',
20
+ defaultValue: 'https://www.obolagents.com',
21
+ validate: (val) => {
22
+ const v = val || 'https://www.obolagents.com';
23
+ if (!v.startsWith('http://') && !v.startsWith('https://')) {
24
+ return 'Should start with http:// or https://';
25
+ }
26
+ },
27
+ });
28
+ if (p.isCancel(gatewayUrl)) return cancelled();
29
+ const url = (gatewayUrl || 'https://www.obolagents.com').replace(/\/+$/, '');
30
+
31
+ // Verify connectivity
32
+ const healthSpin = p.spinner();
33
+ healthSpin.start('Saying hello to the gateway...');
34
+ try {
35
+ await checkHealth(url);
36
+ healthSpin.stop(pc.green('Gateway is up and running! ') + cheer());
37
+ } catch (err) {
38
+ healthSpin.stop(pc.red('Hmm, couldn\'t reach the gateway.'));
39
+ p.log.error(`Make sure it's running at ${pc.bold(url)}`);
40
+ p.log.error(`Details: ${err.message}`);
41
+ p.outro(pc.dim('Fix the URL and try again.'));
42
+ process.exit(1);
43
+ }
44
+
45
+ // --- Admin Key ---
46
+ const adminKey = await p.password({
47
+ message: 'Paste your admin API key',
48
+ validate: (val) => {
49
+ if (!val || val.length < 8) return 'That looks too short. Check your Render dashboard → Environment Variables → OBOL_ADMIN_API_KEY';
50
+ },
51
+ });
52
+ if (p.isCancel(adminKey)) return cancelled();
53
+
54
+ // --- Agent Name ---
55
+ const agentId = await p.text({
56
+ message: 'Give your agent a name',
57
+ placeholder: 'my-agent',
58
+ validate: (val) => {
59
+ if (!val) return 'Your agent needs a name!';
60
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{1,62}[a-zA-Z0-9]$/.test(val)) {
61
+ return 'Letters, numbers, hyphens, and underscores only (3-64 chars)';
62
+ }
63
+ },
64
+ });
65
+ if (p.isCancel(agentId)) return cancelled();
66
+
67
+ // --- Wallet Address ---
68
+ const walletAddress = await p.text({
69
+ message: `Got a USDC wallet on Base? ${pc.dim('(recommended for automatic micropayments)')}`,
70
+ placeholder: '0x... or press Enter to skip',
71
+ validate: (val) => {
72
+ if (!val) return; // optional
73
+ if (!/^0x[0-9a-fA-F]{40}$/.test(val)) {
74
+ return 'That doesn\'t look like an EVM address. Should be 0x followed by 40 hex characters.';
75
+ }
76
+ },
77
+ });
78
+ if (p.isCancel(walletAddress)) return cancelled();
79
+
80
+ // --- Services ---
81
+ const services = await p.multiselect({
82
+ message: 'Which services does your agent need?',
83
+ options: [
84
+ { value: 'stripe', label: 'Stripe', hint: 'invoices, payments, subscriptions' },
85
+ { value: 'github', label: 'GitHub', hint: 'issues, PRs, repos' },
86
+ { value: 'slack', label: 'Slack', hint: 'messages, channels' },
87
+ { value: 'notion', label: 'Notion', hint: 'pages, databases' },
88
+ { value: 'discord', label: 'Discord', hint: 'messages, servers' },
89
+ { value: 'twitter', label: 'Twitter/X', hint: 'tweets, threads' },
90
+ ],
91
+ required: true,
92
+ });
93
+ if (p.isCancel(services)) return cancelled();
94
+
95
+ // --- Confirm ---
96
+ const confirmed = await p.confirm({
97
+ message: `Register ${pc.bold(pc.green(agentId))} with ${services.length} service${services.length > 1 ? 's' : ''}?`,
98
+ });
99
+ if (p.isCancel(confirmed) || !confirmed) return cancelled();
100
+
101
+ // --- Execute! ---
102
+ console.log();
103
+ const s = p.spinner();
104
+
105
+ // 1. Register agent
106
+ s.start('Registering your agent...');
107
+ const secret = randomBytes(32).toString('base64url');
108
+ try {
109
+ await registerAgent(url, adminKey, agentId, secret);
110
+ s.stop(pc.green(`Agent "${agentId}" registered. `) + cheer());
111
+ } catch (err) {
112
+ if (err.status === 409) {
113
+ s.stop(pc.yellow(`Agent "${agentId}" already exists — that's fine, we'll keep going.`));
114
+ } else {
115
+ s.stop(pc.red(`Couldn't register agent: ${err.message}`));
116
+ p.outro(pc.dim('Check your admin key and try again.'));
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ // 2. Issue JWT
122
+ s.start('Getting your agent a JWT token...');
123
+ let jwt;
124
+ try {
125
+ const tokenData = await issueToken(url, adminKey, agentId);
126
+ jwt = tokenData.token;
127
+ const expiresAt = new Date(tokenData.expires_at * 1000);
128
+ s.stop(pc.green('JWT issued ') + pc.dim(`(expires ${expiresAt.toLocaleDateString()})`));
129
+ } catch (err) {
130
+ s.stop(pc.red(`Couldn't issue token: ${err.message}`));
131
+ p.outro(pc.dim('Something went wrong. Try again or check the gateway logs.'));
132
+ process.exit(1);
133
+ }
134
+
135
+ // 3. Create default policy
136
+ s.start('Setting up execution policy...');
137
+ try {
138
+ await createPolicy(url, adminKey, agentId, services);
139
+ s.stop(pc.green(`Policy created for ${services.join(', ')}. `) + cheer());
140
+ } catch (err) {
141
+ // Non-fatal — agent can still work without a named policy
142
+ s.stop(pc.yellow(`Couldn't create policy (${err.message}) — no worries, defaults apply.`));
143
+ }
144
+
145
+ // 4. Save config
146
+ s.start('Saving configuration...');
147
+ const vars = {
148
+ OBOL_GATEWAY_URL: url,
149
+ OBOL_AGENT_ID: agentId,
150
+ OBOL_JWT_TOKEN: jwt,
151
+ };
152
+ if (walletAddress) {
153
+ vars.OBOL_WALLET_ADDRESS = walletAddress;
154
+ }
155
+ const envPath = saveEnv(vars);
156
+ s.stop(pc.green(`Saved to ${envPath}`));
157
+
158
+ // --- Celebration! ---
159
+ console.log();
160
+ p.log.success(pc.bold(`Your agent "${agentId}" is live on Obol!`));
161
+ console.log();
162
+
163
+ if (walletAddress) {
164
+ p.log.info(`Micropayments enabled — your agent pays a few cents per call from ${pc.cyan(walletAddress.slice(0, 6) + '...' + walletAddress.slice(-4))}.`);
165
+ } else {
166
+ p.log.info(`No wallet linked. Buy credits with ${pc.green('obol tiers')} or add a wallet later.`);
167
+ }
168
+
169
+ console.log();
170
+ console.log(` ${pc.dim('Check status:')} ${pc.green('npx @obol/cli status')}`);
171
+ console.log(` ${pc.dim('Add a key:')} ${pc.green('npx @obol/cli connect github')}`);
172
+ console.log(` ${pc.dim('View balance:')} ${pc.green('npx @obol/cli balance')}`);
173
+ console.log();
174
+
175
+ p.outro('Go build something awesome. ' + pc.green('⚡'));
176
+ }
177
+
178
+ function cancelled() {
179
+ p.cancel('Setup cancelled. Come back anytime!');
180
+ process.exit(0);
181
+ }
package/src/status.js ADDED
@@ -0,0 +1,72 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { checkHealth, getBalance, listConnections } from './api.js';
4
+ import { loadEnv } from './config.js';
5
+
6
+ export async function runStatus() {
7
+ const env = loadEnv();
8
+
9
+ if (!env.OBOL_GATEWAY_URL || !env.OBOL_AGENT_ID) {
10
+ console.log();
11
+ p.log.warn('No agent configured yet.');
12
+ p.log.info(`Run ${pc.green('npx @obol/cli setup')} to get started.`);
13
+ console.log();
14
+ return;
15
+ }
16
+
17
+ console.log();
18
+ p.intro(pc.bgGreen(pc.black(` ${env.OBOL_AGENT_ID} `)));
19
+
20
+ const url = env.OBOL_GATEWAY_URL;
21
+
22
+ // Gateway health
23
+ const s = p.spinner();
24
+ s.start('Checking gateway...');
25
+ try {
26
+ await checkHealth(url);
27
+ s.stop(pc.green('Gateway is healthy'));
28
+ } catch (err) {
29
+ s.stop(pc.red(`Gateway unreachable: ${err.message}`));
30
+ }
31
+
32
+ // Balance
33
+ if (env.OBOL_JWT_TOKEN) {
34
+ s.start('Fetching balance...');
35
+ try {
36
+ const bal = await getBalance(url, env.OBOL_JWT_TOKEN);
37
+ const balStr = `$${bal.balance_usdc.toFixed(2)} USDC`;
38
+ const reserved = bal.reserved_usdc > 0 ? pc.dim(` ($${bal.reserved_usdc.toFixed(2)} reserved)`) : '';
39
+ s.stop(`${pc.bold('Balance:')} ${pc.green(balStr)}${reserved}`);
40
+ } catch (err) {
41
+ s.stop(pc.yellow(`Couldn't fetch balance: ${err.message}`));
42
+ }
43
+
44
+ // Connections
45
+ s.start('Checking connected services...');
46
+ try {
47
+ const data = await listConnections(url, env.OBOL_JWT_TOKEN);
48
+ const conns = data.connections || data || [];
49
+ if (conns.length === 0) {
50
+ s.stop(pc.dim('No services connected yet'));
51
+ p.log.info(`Add one with ${pc.green('npx @obol/cli connect github')}`);
52
+ } else {
53
+ const names = conns.map(c => pc.green(c.provider)).join(', ');
54
+ s.stop(`${pc.bold('Services:')} ${names} (${conns.length} connected)`);
55
+ }
56
+ } catch (err) {
57
+ s.stop(pc.dim('Couldn\'t fetch connections'));
58
+ }
59
+ }
60
+
61
+ // Wallet
62
+ if (env.OBOL_WALLET_ADDRESS) {
63
+ const addr = env.OBOL_WALLET_ADDRESS;
64
+ p.log.info(`${pc.bold('Wallet:')} ${pc.cyan(addr.slice(0, 6) + '...' + addr.slice(-4))} ${pc.dim('(USDC micropayments)')}`);
65
+ } else {
66
+ p.log.info(`${pc.bold('Wallet:')} ${pc.dim('Not configured')} — add one during setup for automatic micropayments`);
67
+ }
68
+
69
+ p.log.info(`${pc.bold('Gateway:')} ${pc.dim(url)}`);
70
+
71
+ p.outro(pc.dim('Looking good!'));
72
+ }