diff --git a/packages/atxp/src/commands/agent.ts b/packages/atxp/src/commands/agent.ts index 4dd8255..b1adcc1 100644 --- a/packages/atxp/src/commands/agent.ts +++ b/packages/atxp/src/commands/agent.ts @@ -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'; @@ -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= npx atxp email inbox'); } @@ -139,100 +135,23 @@ function getArgValue(flag: string): string | undefined { return index !== -1 ? process.argv[index + 1] : undefined; } -function promptForInput(prompt: string): Promise { - 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 { 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; - 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; - console.error(chalk.red(`Error: ${body.error_description || body.error || verifyRes.statusText}`)); + if (!res.ok) { + const body = await res.json().catch(() => ({})) as Record; + 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; @@ -242,7 +161,7 @@ async function registerAgent(): Promise { }; 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)); diff --git a/packages/atxp/src/commands/whoami.ts b/packages/atxp/src/commands/whoami.ts new file mode 100644 index 0000000..d6e6068 --- /dev/null +++ b/packages/atxp/src/commands/whoami.ts @@ -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 { + 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); + } +} diff --git a/packages/atxp/src/help.ts b/packages/atxp/src/help.ts index 4327359..640ac40 100644 --- a/packages/atxp/src/help.ts +++ b/packages/atxp/src/help.ts @@ -26,6 +26,7 @@ export function showHelp(): void { console.log(' ' + chalk.cyan('email') + ' ' + chalk.yellow('') + ' ' + '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('') + ' ' + 'Create and manage agent accounts'); console.log(); @@ -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(); diff --git a/packages/atxp/src/index.ts b/packages/atxp/src/index.ts index 600d88b..48f3002 100644 --- a/packages/atxp/src/index.ts +++ b/packages/atxp/src/index.ts @@ -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; @@ -318,6 +319,10 @@ async function main() { await depositCommand(); break; + case 'whoami': + await whoamiCommand(); + break; + case 'paas': await paasCommand(paasArgs, paasOptions); break;