Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 141 additions & 2 deletions packages/atxp/src/commands/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import chalk from 'chalk';
import { createInterface } from 'readline';
import { getConnection } from '../config.js';

const DEFAULT_ACCOUNTS_URL = 'https://accounts.atxp.ai';

function getAccountsAuth(): { baseUrl: string; token: string } {
const connection = getConnection();
if (!connection) {
Expand All @@ -17,11 +20,25 @@ function getAccountsAuth(): { baseUrl: string; token: string } {
return { baseUrl: `${url.protocol}//${url.host}`, token };
}

function getBaseUrl(): string {
const connection = getConnection();
if (connection) {
try {
const url = new URL(connection);
return `${url.protocol}//${url.host}`;
} catch {
// Fall through to default
}
}
return DEFAULT_ACCOUNTS_URL;
}

function showAgentHelp(): void {
console.log(chalk.bold('Agent Commands:'));
console.log();
console.log(' ' + chalk.cyan('npx atxp agent create') + ' ' + 'Create a new agent account');
console.log(' ' + chalk.cyan('npx atxp agent list') + ' ' + 'List your agents');
console.log(' ' + chalk.cyan('npx atxp agent create') + ' ' + 'Create a new agent account (requires login)');
console.log(' ' + chalk.cyan('npx atxp agent list') + ' ' + 'List your agents (requires login)');
console.log(' ' + chalk.cyan('npx atxp agent register') + ' ' + 'Self-register as an agent (no login required)');
console.log();
console.log(chalk.bold('Details:'));
console.log(' Each agent gets:');
Expand All @@ -30,9 +47,15 @@ function showAgentHelp(): void {
console.log(' - 10 IOU tokens to start');
console.log(' - A connection token for SDK/CLI access');
console.log();
console.log(chalk.bold('Register Options:'));
console.log(' ' + chalk.yellow('--server') + ' ' + 'Accounts server URL (default: https://accounts.atxp.ai)');
console.log(' ' + chalk.yellow('--answer') + ' ' + 'Provide the challenge answer non-interactively');
console.log();
console.log(chalk.bold('Examples:'));
console.log(' npx atxp agent create');
console.log(' npx atxp agent list');
console.log(' npx atxp agent register');
console.log(' npx atxp agent register --server http://localhost:8016');
console.log(' CONNECTION_TOKEN=<agent_token> npx atxp email inbox');
}

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

case 'register':
await registerAgent();
break;

default:
console.error(chalk.red(`Unknown agent command: ${subCommand}`));
console.log();
Expand Down Expand Up @@ -105,6 +132,118 @@ async function createAgent(): Promise<void> {
console.log(' ' + chalk.yellow(`CONNECTION_TOKEN=${data.connectionToken} npx atxp email inbox`));
}

function getArgValue(flag: string): string | undefined {
const index = process.argv.findIndex((arg) => arg === flag);
return index !== -1 ? process.argv[index + 1] : undefined;
}

function promptForInput(prompt: string): Promise<string> {
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}

async function registerAgent(): Promise<void> {
const baseUrl = getArgValue('--server') || getBaseUrl();
const presetAnswer = getArgValue('--answer');

// Step 1: Get challenge
console.log(chalk.gray(`Requesting challenge from ${baseUrl}...`));

const challengeRes = await fetch(`${baseUrl}/agents/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});

if (!challengeRes.ok) {
const body = await challengeRes.json().catch(() => ({})) as Record<string, string>;
console.error(chalk.red(`Error: ${body.error_description || body.error || challengeRes.statusText}`));
process.exit(1);
}

const challenge = await challengeRes.json() as {
registration_id: string;
challenge: string;
instructions: string;
expires_at: string;
};

// Decode base64 challenge and instructions
const decodedChallenge = Buffer.from(challenge.challenge, 'base64').toString('utf-8');
const decodedInstructions = Buffer.from(challenge.instructions, 'base64').toString('utf-8');

console.log();
console.log(chalk.bold('Challenge:'));
console.log(' ' + chalk.yellow(decodedChallenge));
console.log();
console.log(chalk.bold('Instructions:'));
console.log(' ' + decodedInstructions);
console.log();
console.log(chalk.gray(`Registration ID: ${challenge.registration_id}`));
console.log(chalk.gray(`Expires at: ${challenge.expires_at}`));
console.log();

// Step 2: Get answer
let answer: string;
if (presetAnswer) {
answer = presetAnswer;
console.log(chalk.gray(`Using provided answer: ${answer}`));
} else {
answer = await promptForInput(chalk.bold('Your answer: '));
if (!answer) {
console.error(chalk.red('No answer provided.'));
process.exit(1);
}
}

// Step 3: Verify and create account
console.log();
console.log(chalk.gray('Verifying answer and creating account...'));

const verifyRes = await fetch(`${baseUrl}/agents/register/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
registration_id: challenge.registration_id,
answer,
}),
});

if (!verifyRes.ok) {
const body = await verifyRes.json().catch(() => ({})) as Record<string, string>;
console.error(chalk.red(`Error: ${body.error_description || body.error || verifyRes.statusText}`));
process.exit(1);
}

const data = await verifyRes.json() as {
agentId: string;
connectionToken: string;
connectionString: string;
email: string;
walletAddress: string;
fundedAmount: string;
};

console.log();
console.log(chalk.green.bold('Agent self-registered successfully!'));
console.log();
console.log(' ' + chalk.bold('Agent ID:') + ' ' + data.agentId);
console.log(' ' + chalk.bold('Email:') + ' ' + chalk.cyan(data.email));
console.log(' ' + chalk.bold('Connection Token:') + ' ' + data.connectionToken);
console.log(' ' + chalk.bold('Wallet:') + ' ' + data.walletAddress);
console.log(' ' + chalk.bold('Funded:') + ' ' + data.fundedAmount + ' IOU');
console.log();
console.log(chalk.bold('Connection String:'));
console.log(' ' + chalk.cyan(data.connectionString));
console.log();
console.log(chalk.bold('Use this to authenticate as the agent:'));
console.log(' ' + chalk.yellow(`npx atxp login --token "${data.connectionString}"`));
}

async function listAgents(): Promise<void> {
const { baseUrl, token } = getAccountsAuth();

Expand Down
5 changes: 3 additions & 2 deletions packages/atxp/src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ export function showHelp(): void {
console.log();

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

console.log(chalk.bold('PAAS Examples:'));
Expand Down
Loading