@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 +95 -0
- package/package.json +16 -0
- package/src/api.js +103 -0
- package/src/config.js +79 -0
- package/src/connect.js +78 -0
- package/src/setup.js +181 -0
- package/src/status.js +72 -0
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
|
+
}
|