Skip to content

Commit 390757c

Browse files
authored
Merge pull request #40 from atxp-dev/naveen/agent-register-cli-added
feat: Add agent self-registration CLI command
2 parents 88db529 + 796140e commit 390757c

File tree

2 files changed

+144
-4
lines changed

2 files changed

+144
-4
lines changed

packages/atxp/src/commands/agent.ts

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import chalk from 'chalk';
2+
import { createInterface } from 'readline';
23
import { getConnection } from '../config.js';
34

5+
const DEFAULT_ACCOUNTS_URL = 'https://accounts.atxp.ai';
6+
47
function getAccountsAuth(): { baseUrl: string; token: string } {
58
const connection = getConnection();
69
if (!connection) {
@@ -17,11 +20,25 @@ function getAccountsAuth(): { baseUrl: string; token: string } {
1720
return { baseUrl: `${url.protocol}//${url.host}`, token };
1821
}
1922

23+
function getBaseUrl(): string {
24+
const connection = getConnection();
25+
if (connection) {
26+
try {
27+
const url = new URL(connection);
28+
return `${url.protocol}//${url.host}`;
29+
} catch {
30+
// Fall through to default
31+
}
32+
}
33+
return DEFAULT_ACCOUNTS_URL;
34+
}
35+
2036
function showAgentHelp(): void {
2137
console.log(chalk.bold('Agent Commands:'));
2238
console.log();
23-
console.log(' ' + chalk.cyan('npx atxp agent create') + ' ' + 'Create a new agent account');
24-
console.log(' ' + chalk.cyan('npx atxp agent list') + ' ' + 'List your agents');
39+
console.log(' ' + chalk.cyan('npx atxp agent create') + ' ' + 'Create a new agent account (requires login)');
40+
console.log(' ' + chalk.cyan('npx atxp agent list') + ' ' + 'List your agents (requires login)');
41+
console.log(' ' + chalk.cyan('npx atxp agent register') + ' ' + 'Self-register as an agent (no login required)');
2542
console.log();
2643
console.log(chalk.bold('Details:'));
2744
console.log(' Each agent gets:');
@@ -30,9 +47,15 @@ function showAgentHelp(): void {
3047
console.log(' - 10 IOU tokens to start');
3148
console.log(' - A connection token for SDK/CLI access');
3249
console.log();
50+
console.log(chalk.bold('Register Options:'));
51+
console.log(' ' + chalk.yellow('--server') + ' ' + 'Accounts server URL (default: https://accounts.atxp.ai)');
52+
console.log(' ' + chalk.yellow('--answer') + ' ' + 'Provide the challenge answer non-interactively');
53+
console.log();
3354
console.log(chalk.bold('Examples:'));
3455
console.log(' npx atxp agent create');
3556
console.log(' npx atxp agent list');
57+
console.log(' npx atxp agent register');
58+
console.log(' npx atxp agent register --server http://localhost:8016');
3659
console.log(' CONNECTION_TOKEN=<agent_token> npx atxp email inbox');
3760
}
3861

@@ -51,6 +74,10 @@ export async function agentCommand(subCommand: string): Promise<void> {
5174
await listAgents();
5275
break;
5376

77+
case 'register':
78+
await registerAgent();
79+
break;
80+
5481
default:
5582
console.error(chalk.red(`Unknown agent command: ${subCommand}`));
5683
console.log();
@@ -105,6 +132,118 @@ async function createAgent(): Promise<void> {
105132
console.log(' ' + chalk.yellow(`CONNECTION_TOKEN=${data.connectionToken} npx atxp email inbox`));
106133
}
107134

135+
function getArgValue(flag: string): string | undefined {
136+
const index = process.argv.findIndex((arg) => arg === flag);
137+
return index !== -1 ? process.argv[index + 1] : undefined;
138+
}
139+
140+
function promptForInput(prompt: string): Promise<string> {
141+
const rl = createInterface({ input: process.stdin, output: process.stdout });
142+
return new Promise((resolve) => {
143+
rl.question(prompt, (answer) => {
144+
rl.close();
145+
resolve(answer.trim());
146+
});
147+
});
148+
}
149+
150+
async function registerAgent(): Promise<void> {
151+
const baseUrl = getArgValue('--server') || getBaseUrl();
152+
const presetAnswer = getArgValue('--answer');
153+
154+
// Step 1: Get challenge
155+
console.log(chalk.gray(`Requesting challenge from ${baseUrl}...`));
156+
157+
const challengeRes = await fetch(`${baseUrl}/agents/register`, {
158+
method: 'POST',
159+
headers: { 'Content-Type': 'application/json' },
160+
});
161+
162+
if (!challengeRes.ok) {
163+
const body = await challengeRes.json().catch(() => ({})) as Record<string, string>;
164+
console.error(chalk.red(`Error: ${body.error_description || body.error || challengeRes.statusText}`));
165+
process.exit(1);
166+
}
167+
168+
const challenge = await challengeRes.json() as {
169+
registration_id: string;
170+
challenge: string;
171+
instructions: string;
172+
expires_at: string;
173+
};
174+
175+
// Decode base64 challenge and instructions
176+
const decodedChallenge = Buffer.from(challenge.challenge, 'base64').toString('utf-8');
177+
const decodedInstructions = Buffer.from(challenge.instructions, 'base64').toString('utf-8');
178+
179+
console.log();
180+
console.log(chalk.bold('Challenge:'));
181+
console.log(' ' + chalk.yellow(decodedChallenge));
182+
console.log();
183+
console.log(chalk.bold('Instructions:'));
184+
console.log(' ' + decodedInstructions);
185+
console.log();
186+
console.log(chalk.gray(`Registration ID: ${challenge.registration_id}`));
187+
console.log(chalk.gray(`Expires at: ${challenge.expires_at}`));
188+
console.log();
189+
190+
// Step 2: Get answer
191+
let answer: string;
192+
if (presetAnswer) {
193+
answer = presetAnswer;
194+
console.log(chalk.gray(`Using provided answer: ${answer}`));
195+
} else {
196+
answer = await promptForInput(chalk.bold('Your answer: '));
197+
if (!answer) {
198+
console.error(chalk.red('No answer provided.'));
199+
process.exit(1);
200+
}
201+
}
202+
203+
// Step 3: Verify and create account
204+
console.log();
205+
console.log(chalk.gray('Verifying answer and creating account...'));
206+
207+
const verifyRes = await fetch(`${baseUrl}/agents/register/verify`, {
208+
method: 'POST',
209+
headers: { 'Content-Type': 'application/json' },
210+
body: JSON.stringify({
211+
registration_id: challenge.registration_id,
212+
answer,
213+
}),
214+
});
215+
216+
if (!verifyRes.ok) {
217+
const body = await verifyRes.json().catch(() => ({})) as Record<string, string>;
218+
console.error(chalk.red(`Error: ${body.error_description || body.error || verifyRes.statusText}`));
219+
process.exit(1);
220+
}
221+
222+
const data = await verifyRes.json() as {
223+
agentId: string;
224+
connectionToken: string;
225+
connectionString: string;
226+
email: string;
227+
walletAddress: string;
228+
fundedAmount: string;
229+
};
230+
231+
console.log();
232+
console.log(chalk.green.bold('Agent self-registered successfully!'));
233+
console.log();
234+
console.log(' ' + chalk.bold('Agent ID:') + ' ' + data.agentId);
235+
console.log(' ' + chalk.bold('Email:') + ' ' + chalk.cyan(data.email));
236+
console.log(' ' + chalk.bold('Connection Token:') + ' ' + data.connectionToken);
237+
console.log(' ' + chalk.bold('Wallet:') + ' ' + data.walletAddress);
238+
console.log(' ' + chalk.bold('Funded:') + ' ' + data.fundedAmount + ' IOU');
239+
console.log();
240+
console.log(chalk.bold('Connection String:'));
241+
console.log(' ' + chalk.cyan(data.connectionString));
242+
console.log();
243+
console.log(chalk.bold('Use this to authenticate as the agent:'));
244+
console.log(' ' + chalk.yellow(`npx atxp login --token "${data.connectionString}"`));
245+
}
246+
108247
async function listAgents(): Promise<void> {
109248
const { baseUrl, token } = getAccountsAuth();
110249

packages/atxp/src/help.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ export function showHelp(): void {
8686
console.log();
8787

8888
console.log(chalk.bold('Agent Examples:'));
89-
console.log(' npx atxp agent create # Create a new agent');
90-
console.log(' npx atxp agent list # List your agents');
89+
console.log(' npx atxp agent create # Create a new agent (requires login)');
90+
console.log(' npx atxp agent list # List your agents (requires login)');
91+
console.log(' npx atxp agent register # Self-register as an agent (no login)');
9192
console.log();
9293

9394
console.log(chalk.bold('PAAS Examples:'));

0 commit comments

Comments
 (0)