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
97 changes: 8 additions & 89 deletions packages/atxp/src/commands/agent.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import chalk from 'chalk';
import { createInterface } from 'readline';
import { getConnection } from '../config.js';

const DEFAULT_ACCOUNTS_URL = 'https://accounts.atxp.ai';
Expand Down Expand Up @@ -48,16 +47,13 @@ function showAgentHelp(): void {
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(' ' + chalk.yellow('--registration-id') + ' ' + 'Resume a previous challenge (skip fetching a new one)');
console.log(' ' + chalk.yellow('--server') + ' ' + 'Accounts server URL (default: https://accounts.atxp.ai)');
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(' npx atxp agent register --registration-id reg_xxx --answer "535.00"');
console.log(' CONNECTION_TOKEN=<agent_token> npx atxp email inbox');
}

Expand Down Expand Up @@ -139,100 +135,23 @@ function getArgValue(flag: string): string | undefined {
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');
const presetRegistrationId = getArgValue('--registration-id');

let registrationId: string;

if (presetRegistrationId) {
// Resume a previous challenge — skip fetching a new one
registrationId = presetRegistrationId;
console.log(chalk.gray(`Resuming registration ${registrationId}...`));
} else {
// Step 1: Get challenge
console.log(chalk.gray(`Requesting challenge from ${baseUrl}...`));
console.log(chalk.gray(`Registering agent at ${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;
};

registrationId = challenge.registration_id;

// 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: ${registrationId}`));
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`, {
const res = await fetch(`${baseUrl}/agents/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
registration_id: registrationId,
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}`));
if (!res.ok) {
const body = await res.json().catch(() => ({})) as Record<string, string>;
console.error(chalk.red(`Error: ${body.error_description || body.error || res.statusText}`));
process.exit(1);
}

const data = await verifyRes.json() as {
const data = await res.json() as {
agentId: string;
connectionToken: string;
connectionString: string;
Expand All @@ -242,7 +161,7 @@ async function registerAgent(): Promise<void> {
};

console.log();
console.log(chalk.green.bold('Agent self-registered successfully!'));
console.log(chalk.green.bold('Agent registered successfully!'));
console.log();
console.log(' ' + chalk.bold('Agent ID:') + ' ' + data.agentId);
console.log(' ' + chalk.bold('Email:') + ' ' + chalk.cyan(data.email));
Expand Down
99 changes: 99 additions & 0 deletions packages/atxp/src/commands/whoami.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import chalk from 'chalk';
import { getConnection } from '../config.js';

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

function getConnectionToken(connectionString: string): string | null {
try {
const url = new URL(connectionString);
return url.searchParams.get('connection_token');
} catch {
return null;
}
}

function getBaseUrl(connectionString: string): string {
try {
const url = new URL(connectionString);
return `${url.protocol}//${url.host}`;
} catch {
return DEFAULT_ACCOUNTS_URL;
}
}

export async function whoamiCommand(): Promise<void> {
const connection = getConnection();

if (!connection) {
console.error(chalk.red('Not logged in.'));
console.error(`Run: ${chalk.cyan('npx atxp login')}`);
process.exit(1);
}

const token = getConnectionToken(connection);
if (!token) {
console.error(chalk.red('Error: Could not extract connection token.'));
console.error('Your ATXP_CONNECTION may be malformed. Try logging in again:');
console.error(chalk.cyan(' npx atxp login --force'));
process.exit(1);
}

const baseUrl = getBaseUrl(connection);

try {
const credentials = Buffer.from(`${token}:`).toString('base64');
const response = await fetch(`${baseUrl}/me`, {
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json',
},
});

if (!response.ok) {
if (response.status === 401) {
console.error(chalk.red('Error: Invalid or expired connection token.'));
console.error(`Try logging in again: ${chalk.cyan('npx atxp login --force')}`);
} else {
console.error(chalk.red(`Error: ${response.status} ${response.statusText}`));
}
process.exit(1);
}

const data = await response.json() as {
accountId: string;
accountType?: string;
email?: string;
displayName?: string;
sources?: Array<{ chain: string; address: string }>;
team?: { id: string; name: string; role: string };
};

// Find the primary wallet address from sources
const wallet = data.sources?.[0];

console.log();
console.log(' ' + chalk.bold('Account ID:') + ' ' + data.accountId);
console.log(' ' + chalk.bold('Account Type:') + ' ' + (data.accountType || 'human'));
if (data.email) {
console.log(' ' + chalk.bold('Email:') + ' ' + chalk.cyan(data.email));
}
if (data.displayName) {
console.log(' ' + chalk.bold('Display Name:') + ' ' + data.displayName);
}
console.log(' ' + chalk.bold('Connection Token:') + ' ' + token);
if (wallet) {
console.log(' ' + chalk.bold('Wallet:') + ' ' + wallet.address + chalk.gray(` (${wallet.chain})`));
}
if (data.team) {
console.log(' ' + chalk.bold('Team:') + ' ' + data.team.name + chalk.gray(` (${data.team.role})`));
}
console.log();
console.log(chalk.bold('Connection String:'));
console.log(' ' + chalk.cyan(connection));
console.log();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(chalk.red(`Error fetching account info: ${errorMessage}`));
process.exit(1);
}
}
2 changes: 2 additions & 0 deletions packages/atxp/src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function showHelp(): void {
console.log(' ' + chalk.cyan('email') + ' ' + chalk.yellow('<command>') + ' ' + 'Send and receive emails');
console.log(' ' + chalk.cyan('balance') + ' ' + 'Check your ATXP account balance');
console.log(' ' + chalk.cyan('fund') + ' ' + 'Show how to fund your account');
console.log(' ' + chalk.cyan('whoami') + ' ' + 'Show your account info (ID, email, wallet)');
console.log(' ' + chalk.cyan('agent') + ' ' + chalk.yellow('<command>') + ' ' + 'Create and manage agent accounts');
console.log();

Expand Down Expand Up @@ -88,6 +89,7 @@ export function showHelp(): void {
console.log(' npx atxp email release-username # Release your username');
console.log(' npx atxp balance # Check account balance');
console.log(' npx atxp fund # Show how to fund your account');
console.log(' npx atxp whoami # Show account info');
console.log(' npx atxp dev demo # Run the demo');
console.log(' npx atxp dev create my-app # Create a new project');
console.log();
Expand Down
5 changes: 5 additions & 0 deletions packages/atxp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { balanceCommand } from './commands/balance.js';
import { depositCommand } from './commands/deposit.js';
import { paasCommand } from './commands/paas/index.js';
import { agentCommand } from './commands/agent.js';
import { whoamiCommand } from './commands/whoami.js';

interface DemoOptions {
port: number;
Expand Down Expand Up @@ -318,6 +319,10 @@ async function main() {
await depositCommand();
break;

case 'whoami':
await whoamiCommand();
break;

case 'paas':
await paasCommand(paasArgs, paasOptions);
break;
Expand Down
Loading