From 23b5c79c367d98411083e924ae8f631d2ce61662 Mon Sep 17 00:00:00 2001 From: xpander-ai-coding-agent Date: Wed, 10 Dec 2025 17:26:35 -0800 Subject: [PATCH 1/5] feat: [PRO-474] implement invoke command with API, webhook, and local options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Added --api (default), --webhook, and --local invocation options - Changed default invocation from local handler to API - Removed fallback logic between invocation methods - Added top-level `xpander invoke` command alias ## Changes ### Invoke Command Options - Added `--api` flag for API invocation (now the default) - Added `--webhook` flag for webhook invocation - Added `--local` flag for local handler invocation - Each method is explicit with no automatic fallbacks ### API Invocation (Default) - Uses `/v1/agents//invoke` API endpoint - Sends request with `input.text` payload - Requires both API key and organization ID headers - Returns structured response with proper error handling ### Webhook Invocation - Maintains existing webhook functionality - Uses agent's configured webhook_url or fallback URL - No longer used as fallback from local handler failures ### Local Handler Invocation - Must be explicitly requested with `--local` flag - Fails immediately if xpander_handler.py not available - No fallback to webhook or API on failure ### Top-Level Command Alias - Created `xpander invoke` as alias for `xpander agent invoke` - Uses same registration function for consistency - Available at root command level for convenience ## Files Modified - src/commands/agent/commands/invoke.ts - Updated invoke logic - src/commands/invoke.ts - New top-level command registration - src/index.ts - Registered invoke command ## Breaking Changes - Default invocation method changed from local to API - Local handler no longer auto-selected when available - No automatic fallbacks between invocation methods ## Test plan - [ ] Test `xpander invoke` API invocation (default) - [ ] Test `xpander invoke --webhook` webhook invocation - [ ] Test `xpander invoke --local` local handler invocation - [ ] Test `xpander agent invoke` still works - [ ] Verify no fallbacks occur between methods 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/commands/agent/commands/invoke.ts | 318 +++++++++++++++++--------- src/commands/invoke.ts | 21 ++ src/index.ts | 2 + 3 files changed, 237 insertions(+), 104 deletions(-) create mode 100644 src/commands/invoke.ts diff --git a/src/commands/agent/commands/invoke.ts b/src/commands/agent/commands/invoke.ts index 02a21e3..2e957da 100644 --- a/src/commands/agent/commands/invoke.ts +++ b/src/commands/agent/commands/invoke.ts @@ -7,7 +7,7 @@ import ora from 'ora'; import { getAgentIdFromEnvOrSelection } from '../../../utils/agent-resolver'; import { BillingErrorHandler } from '../../../utils/billing-error'; import { createClient } from '../../../utils/client'; -import { getApiKey } from '../../../utils/config'; +import { getApiKey, getOrganizationId } from '../../../utils/config'; import { canUseLocalHandler, getPythonCommand, @@ -26,14 +26,19 @@ export function registerInvokeCommand(agentCmd: Command): void { Examples: $ xpander agent invoke # Interactive: select agent and enter message $ xpander agent invoke "MyAgent" # Select agent, then prompt for message - $ xpander agent invoke "MyAgent" "Hello world" # Direct invocation - $ xpander agent invoke --json MyAgent "task" # Get JSON response`, + $ xpander agent invoke "MyAgent" "Hello world" # Direct invocation (uses API by default) + $ xpander agent invoke --json MyAgent "task" # Get JSON response + $ xpander agent invoke --local MyAgent "task" # Use local handler + $ xpander agent invoke --webhook MyAgent "task" # Use webhook invocation`, ) .option('--agent ', 'Agent name or ID to invoke') .option('--agent-id ', 'Agent ID to invoke') .option('--agent-name ', 'Agent name to invoke') .option('--profile ', 'Profile to use') .option('--json', 'Output raw JSON response') + .option('--local', 'Use local handler (xpander_handler.py)') + .option('--api', 'Use API invocation (default)') + .option('--webhook', 'Use webhook invocation') .action(async (agentArg, messageArgs, options) => { try { const apiKey = getApiKey(options.profile); @@ -138,10 +143,39 @@ Examples: message = userMessage.trim(); } - // Check if local handler is available - const hasLocalHandler = canUseLocalHandler(); + // Determine invocation mode: local, api, or webhook + const useLocal = options.local; + const useWebhook = options.webhook; + const useApi = options.api || (!useLocal && !useWebhook); // Default to API - if (hasLocalHandler) { + // Get agent details for display + const agentDetails = await client.getAgentWebhookDetails(agentId); + + // Show using agent message with checkmark only if we used silent mode + if (useSilentMode) { + console.log( + chalk.green('✔') + + ' ' + + chalk.hex('#743CFF')( + `Using agent: ${agentDetails.name} (${agentId})`, + ), + ); + } + + let invokeSpinner: any; + + // LOCAL HANDLER INVOCATION + if (useLocal) { + // Check if local handler is available + if (!canUseLocalHandler()) { + console.error(chalk.red('❌ Local handler not available')); + console.log( + chalk.yellow( + 'Make sure xpander_handler.py exists in the current directory', + ), + ); + process.exit(1); + } // Use local handler console.log(chalk.cyan('🏠 Using local handler: xpander_handler.py')); @@ -223,126 +257,202 @@ Examples: throw new Error(`Process exited with code ${exitCode}`); } - return; // Exit early, don't call webhook + return; // Exit early after local invocation } catch (localError: any) { console.log( - chalk.yellow( - '⚠️ Local handler failed, falling back to webhook...', - ), + chalk.red('❌ Local handler failed:'), + localError.message, ); - console.log(chalk.dim(`Error: ${localError.message}`)); - // Continue to webhook fallback + process.exit(1); } } - // Get agent details for display and webhook URL (webhook fallback) - const agentDetails = await client.getAgentWebhookDetails(agentId); + // API INVOCATION (DEFAULT) + if (useApi) { + console.log(chalk.cyan('🔌 Using API invocation')); - // Show using agent message with checkmark only if we used silent mode - if (useSilentMode) { - console.log( - chalk.green('✔') + - ' ' + - chalk.hex('#743CFF')( - `Using agent: ${agentDetails.name} (${agentId})`, - ), - ); - } + try { + const orgId = getOrganizationId(options.profile); + const apiUrl = `https://api.xpander.ai/v1/agents/${agentId}/invoke`; - console.log(chalk.cyan('🌐 Using webhook invocation')); + invokeSpinner = ora(`Invoking agent with: "${message}"...`).start(); + const startTime = Date.now(); - let invokeSpinner: any; + const response = await axios.post( + apiUrl, + { + input: { + text: message, + }, + }, + { + headers: { + 'x-api-key': apiKey, + 'x-organization-id': orgId, + 'Content-Type': 'application/json', + }, + timeout: 60000, + }, + ); - try { - const webhookUrl = - agentDetails.webhook_url || - `https://webhook.xpander.ai/?agent_id=${agentId}&asynchronous=false`; + const responseTime = Date.now() - startTime; + invokeSpinner.succeed('Response received'); - // Start spinner for webhook call - invokeSpinner = ora(`Invoking agent with: "${message}"...`).start(); + if (options.json) { + console.log(JSON.stringify(response.data, null, 2)); + } else { + console.log('\n' + chalk.blue('🤖 Agent Response:')); + console.log(chalk.dim('─'.repeat(50))); + + if (response.data.result) { + console.log(response.data.result); + } else if (response.data.response) { + console.log(response.data.response); + } else if (response.data.message) { + console.log(response.data.message); + } else if (typeof response.data === 'string') { + console.log(response.data); + } else { + console.log(JSON.stringify(response.data, null, 2)); + } - // Start timing from webhook call - const startTime = Date.now(); + console.log(chalk.dim('─'.repeat(50))); + console.log(''); + console.log(chalk.dim(`Response time: ${responseTime}ms`)); + } + return; // Exit after API invocation + } catch (apiError: any) { + if (invokeSpinner) { + invokeSpinner.fail('Failed to invoke agent'); + } - const response = await axios.post( - webhookUrl, - { - message: message, - }, - { - headers: { - 'x-api-key': apiKey, - 'Content-Type': 'application/json', - }, - timeout: 60000, // 60 second timeout - }, - ); + const isStaging = process?.env?.IS_STG === 'true'; + if (BillingErrorHandler.handleIfBillingError(apiError, isStaging)) { + process.exit(1); + } - const responseTime = Date.now() - startTime; - invokeSpinner.succeed('Response received'); - - // Display results - if (options.json) { - console.log(JSON.stringify(response.data, null, 2)); - } else { - console.log('\n' + chalk.blue('🤖 Agent Response:')); - console.log(chalk.dim('─'.repeat(50))); - - if (response.data.result) { - console.log(response.data.result); - } else if (response.data.response) { - console.log(response.data.response); - } else if (response.data.message) { - console.log(response.data.message); - } else if (typeof response.data === 'string') { - console.log(response.data); + if (apiError.response?.status === 404) { + console.error(chalk.red('❌ Agent not found')); + console.log(chalk.yellow('Make sure the agent ID is correct')); + } else if (apiError.response?.status === 401) { + console.error(chalk.red('❌ Invalid API key')); + console.log( + chalk.yellow( + 'Run `xpander configure` to update your credentials', + ), + ); + } else if (apiError.code === 'ECONNABORTED') { + console.error( + chalk.red( + '❌ Request timeout - agent took too long to respond', + ), + ); } else { - // Fallback to pretty JSON if structure is unknown - console.log(JSON.stringify(response.data, null, 2)); + console.error(chalk.red('❌ API invocation failed:')); + console.error(apiError.response?.data || apiError.message); } + process.exit(1); + } + } - console.log(chalk.dim('─'.repeat(50))); + // WEBHOOK INVOCATION + if (useWebhook) { + console.log(chalk.cyan('🌐 Using webhook invocation')); - // Display response time - console.log(''); - console.log(chalk.dim(`Response time: ${responseTime}ms`)); - } - } catch (webhookError: any) { - if (invokeSpinner) { - invokeSpinner.fail('Failed to invoke agent'); - } else { - invokeSpinner.fail('Failed to get agent details'); - } + try { + const webhookUrl = + agentDetails.webhook_url || + `https://webhook.xpander.ai/?agent_id=${agentId}&asynchronous=false`; - // Check for 429 billing error first - const isStaging = process?.env?.IS_STG === 'true'; - if ( - BillingErrorHandler.handleIfBillingError(webhookError, isStaging) - ) { - process.exit(1); - } + // Start spinner for webhook call + invokeSpinner = ora(`Invoking agent with: "${message}"...`).start(); - if (webhookError.response?.status === 404) { - console.error(chalk.red('❌ Agent webhook not found')); - console.log( - chalk.yellow('Make sure the agent is properly configured'), - ); - } else if (webhookError.response?.status === 401) { - console.error(chalk.red('❌ Invalid API key')); - console.log( - chalk.yellow( - 'Run `xpander configure` to update your credentials', - ), - ); - } else if (webhookError.code === 'ECONNABORTED') { - console.error( - chalk.red('❌ Request timeout - agent took too long to respond'), + // Start timing from webhook call + const startTime = Date.now(); + + const response = await axios.post( + webhookUrl, + { + message: message, + }, + { + headers: { + 'x-api-key': apiKey, + 'Content-Type': 'application/json', + }, + timeout: 60000, // 60 second timeout + }, ); - } else { - console.error(chalk.red('❌ Webhook invocation failed:')); - console.error(webhookError.response?.data || webhookError.message); + + const responseTime = Date.now() - startTime; + invokeSpinner.succeed('Response received'); + + // Display results + if (options.json) { + console.log(JSON.stringify(response.data, null, 2)); + } else { + console.log('\n' + chalk.blue('🤖 Agent Response:')); + console.log(chalk.dim('─'.repeat(50))); + + if (response.data.result) { + console.log(response.data.result); + } else if (response.data.response) { + console.log(response.data.response); + } else if (response.data.message) { + console.log(response.data.message); + } else if (typeof response.data === 'string') { + console.log(response.data); + } else { + // Fallback to pretty JSON if structure is unknown + console.log(JSON.stringify(response.data, null, 2)); + } + + console.log(chalk.dim('─'.repeat(50))); + + // Display response time + console.log(''); + console.log(chalk.dim(`Response time: ${responseTime}ms`)); + } + return; // Exit after webhook invocation + } catch (webhookError: any) { + if (invokeSpinner) { + invokeSpinner.fail('Failed to invoke agent'); + } + + // Check for 429 billing error first + const isStaging = process?.env?.IS_STG === 'true'; + if ( + BillingErrorHandler.handleIfBillingError(webhookError, isStaging) + ) { + process.exit(1); + } + + if (webhookError.response?.status === 404) { + console.error(chalk.red('❌ Agent webhook not found')); + console.log( + chalk.yellow('Make sure the agent is properly configured'), + ); + } else if (webhookError.response?.status === 401) { + console.error(chalk.red('❌ Invalid API key')); + console.log( + chalk.yellow( + 'Run `xpander configure` to update your credentials', + ), + ); + } else if (webhookError.code === 'ECONNABORTED') { + console.error( + chalk.red( + '❌ Request timeout - agent took too long to respond', + ), + ); + } else { + console.error(chalk.red('❌ Webhook invocation failed:')); + console.error( + webhookError.response?.data || webhookError.message, + ); + } + process.exit(1); } - process.exit(1); } } catch (error: any) { console.error(chalk.red('❌ Error invoking agent:'), error.message); diff --git a/src/commands/invoke.ts b/src/commands/invoke.ts new file mode 100644 index 0000000..97ae4b7 --- /dev/null +++ b/src/commands/invoke.ts @@ -0,0 +1,21 @@ +import { Command } from 'commander'; +import { registerInvokeCommand } from './agent/commands/invoke'; + +/** + * Configure top-level invoke command (alias for agent invoke) + */ +export function configureInvokeCommand(program: Command): Command { + // Create a temporary command group for registering invoke + const tempCmd = new Command(); + registerInvokeCommand(tempCmd); + + // Get the invoke command that was registered + const invokeCommand = tempCmd.commands.find((cmd) => cmd.name() === 'invoke'); + + if (invokeCommand) { + // Add it to the main program + program.addCommand(invokeCommand); + } + + return program; +} diff --git a/src/index.ts b/src/index.ts index bc86282..63038f6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { configureConfigureCommand } from './commands/configure'; import { configureDeployCommand } from './commands/deploy'; import { configureDevCommand } from './commands/dev'; import { configureInitializeCommand } from './commands/initialize'; +import { configureInvokeCommand } from './commands/invoke'; // import { configureInterfacesCommands } from './commands/interfaces/index'; import { configureLoginCommand, @@ -190,6 +191,7 @@ async function main(): Promise { configureDevCommand(program); configureLogsCommand(program); configureSecretsSyncCommand(program); + configureInvokeCommand(program); // Top-level invoke alias agent(program); nemo(program); From 85c921d36d86529f9eb97bc8632affa130c11e2c Mon Sep 17 00:00:00 2001 From: xpander-ai-coding-agent Date: Wed, 10 Dec 2025 17:29:51 -0800 Subject: [PATCH 2/5] fix: invoke command now falls back to .env agent_id when provided agent not found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, when a user ran `xpander invoke `, the command would fail without checking the .env file for XPANDER_AGENT_ID. Now the agent resolution logic: 1. Always checks for .env file first to see if agent_id is present 2. If an agent name/ID is provided, tries to resolve it 3. If resolution fails AND .env has agent_id, falls back to using the .env agent_id 4. Shows a warning message when falling back to .env This makes the invoke command more intelligent when running in an agent directory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/utils/agent-resolver.ts | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/utils/agent-resolver.ts b/src/utils/agent-resolver.ts index 9889ecc..c3b36fb 100644 --- a/src/utils/agent-resolver.ts +++ b/src/utils/agent-resolver.ts @@ -132,21 +132,39 @@ export async function getAgentIdFromEnvOrSelection( providedAgentId?: string, silent: boolean = false, ): Promise { - // If agent ID is provided, resolve it (could be name or ID) - if (providedAgentId) { - return resolveAgentId(client, providedAgentId, silent); - } - - // Try to get agent ID from .env file + // Try to get agent ID from .env file first + let envAgentId: string | undefined; try { const config = await getXpanderConfigFromEnvFile(process.cwd()); if (config?.agent_id) { - return config.agent_id; + envAgentId = config.agent_id; } } catch (error) { // No .env file or no agent_id in it } + // If agent ID is provided, try to resolve it + if (providedAgentId) { + const resolvedId = await resolveAgentId(client, providedAgentId, silent); + // If resolution failed and we have an env agent_id, fall back to it + if (!resolvedId && envAgentId) { + if (!silent) { + console.log( + chalk.yellow( + '⚠ Provided agent not found, using agent from .env file', + ), + ); + } + return envAgentId; + } + return resolvedId; + } + + // No providedAgentId - use env agent_id if available + if (envAgentId) { + return envAgentId; + } + // Show interactive selection try { const agents = await getAgentsWithCache(client); From 437497a6930e0fd5b2a1d270b8411032d1a0cafa Mon Sep 17 00:00:00 2001 From: xpander-ai-coding-agent Date: Wed, 10 Dec 2025 17:37:19 -0800 Subject: [PATCH 3/5] feat: add --message flag and improve invoke command argument handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added explicit --message flag to allow users to clearly specify the message when invoking agents. This works alongside --agent-id and --agent-name flags for maximum flexibility. Changes: - Added --message flag for explicit message specification - Improved argument parsing priority: 1. --message flag takes precedence 2. --agent/--agent-id/--agent-name flags for explicit agent specification 3. In agent directory with .env: all positional args become message 4. Otherwise: first arg is agent, rest is message - Updated help text with examples of all invocation patterns - Fixed duplicate "Using agent" message by using silent mode in resolveAgentId Usage examples: - xpander agent invoke --agent-id abc123 --message "hi" - xpander agent invoke --agent-name "MyAgent" -m "hello" - xpander agent invoke "hi" (in agent directory) - xpander agent invoke MyAgent "hi" (outside agent directory) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/commands/agent/commands/invoke.ts | 65 ++++++++++++++++++++++----- src/utils/agent-resolver.ts | 32 +++---------- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/src/commands/agent/commands/invoke.ts b/src/commands/agent/commands/invoke.ts index 2e957da..3686636 100644 --- a/src/commands/agent/commands/invoke.ts +++ b/src/commands/agent/commands/invoke.ts @@ -8,6 +8,7 @@ import { getAgentIdFromEnvOrSelection } from '../../../utils/agent-resolver'; import { BillingErrorHandler } from '../../../utils/billing-error'; import { createClient } from '../../../utils/client'; import { getApiKey, getOrganizationId } from '../../../utils/config'; +import { getXpanderConfigFromEnvFile } from '../../../utils/custom_agents_utils/generic'; import { canUseLocalHandler, getPythonCommand, @@ -24,16 +25,23 @@ export function registerInvokeCommand(agentCmd: Command): void { 'after', ` Examples: - $ xpander agent invoke # Interactive: select agent and enter message - $ xpander agent invoke "MyAgent" # Select agent, then prompt for message - $ xpander agent invoke "MyAgent" "Hello world" # Direct invocation (uses API by default) - $ xpander agent invoke --json MyAgent "task" # Get JSON response - $ xpander agent invoke --local MyAgent "task" # Use local handler - $ xpander agent invoke --webhook MyAgent "task" # Use webhook invocation`, + $ xpander agent invoke # Interactive: select agent and enter message + $ xpander agent invoke "MyAgent" # Select agent, then prompt for message + $ xpander agent invoke "MyAgent" "Hello world" # Direct invocation (uses API by default) + $ xpander agent invoke --agent-id abc123 --message "hi" # Explicit agent ID and message + $ xpander agent invoke --agent-name "MyAgent" -m "hi" # Explicit agent name and message + $ xpander agent invoke --json MyAgent "task" # Get JSON response + $ xpander agent invoke --local MyAgent "task" # Use local handler + $ xpander agent invoke --webhook MyAgent "task" # Use webhook invocation + + In an agent directory with .env file: + $ xpander agent invoke "hi" # Uses agent from .env, message is "hi" + $ xpander agent invoke --message "hi" # Explicit message with .env agent`, ) .option('--agent ', 'Agent name or ID to invoke') .option('--agent-id ', 'Agent ID to invoke') .option('--agent-name ', 'Agent name to invoke') + .option('--message ', 'Message to send to the agent') .option('--profile ', 'Profile to use') .option('--json', 'Output raw JSON response') .option('--local', 'Use local handler (xpander_handler.py)') @@ -53,12 +61,30 @@ Examples: const client = createClient(options.profile); + // Check if we're in an agent directory with .env file + let hasEnvAgent = false; + try { + const config = await getXpanderConfigFromEnvFile(process.cwd()); + hasEnvAgent = !!config?.agent_id; + } catch (error) { + // No .env file or no agent_id in it + hasEnvAgent = false; + } + // Handle agent and message parsing let agentInput: string | undefined; let message: string; - // Check if we have explicit agent flags (old syntax) - if (options.agent || options.agentId || options.agentName) { + // Check if we have explicit --message flag + if (options.message) { + // Explicit message flag - use it and check for agent flags + message = options.message; + agentInput = options.agent || options.agentId || options.agentName; + // If no agent flags, use positional arg or env + if (!agentInput && agentArg) { + agentInput = agentArg; + } + } else if (options.agent || options.agentId || options.agentName) { // Old syntax: flags specify agent, all positional args are message agentInput = options.agent || options.agentId || options.agentName; const allMessageParts = []; @@ -69,6 +95,17 @@ Examples: allMessageParts.push(messageArgs); } message = allMessageParts.join(' ').trim(); + } else if (hasEnvAgent) { + // We're in an agent directory - treat all args as message + const allMessageParts = []; + if (agentArg) allMessageParts.push(agentArg); + if (Array.isArray(messageArgs)) { + allMessageParts.push(...messageArgs); + } else if (messageArgs) { + allMessageParts.push(messageArgs); + } + message = allMessageParts.join(' ').trim(); + agentInput = undefined; // Will use .env agent } else { // New syntax: first arg is agent, rest is message agentInput = agentArg; @@ -78,9 +115,13 @@ Examples: } // Handle different invocation patterns: - // 1. xpander agent invoke -> interactive agent selection + prompt for message - // 2. xpander agent invoke "agent" -> resolve agent + prompt for message - // 3. xpander agent invoke "agent" "message" -> resolve agent + use message + // When in agent directory with .env: + // 1. xpander agent invoke -> use .env agent + prompt for message + // 2. xpander agent invoke "message" -> use .env agent + use message + // When NOT in agent directory: + // 1. xpander agent invoke -> interactive agent selection + prompt for message + // 2. xpander agent invoke "agent" -> resolve agent + prompt for message + // 3. xpander agent invoke "agent" "message" -> resolve agent + use message let agentId: string | null = null; let useSilentMode = false; @@ -95,7 +136,7 @@ Examples: agentId = await getAgentIdFromEnvOrSelection( client, agentInput, - false, // Allow interactive prompts for duplicate names + true, // Silent mode - we'll print our own message ); if (!agentId) { diff --git a/src/utils/agent-resolver.ts b/src/utils/agent-resolver.ts index c3b36fb..9889ecc 100644 --- a/src/utils/agent-resolver.ts +++ b/src/utils/agent-resolver.ts @@ -132,39 +132,21 @@ export async function getAgentIdFromEnvOrSelection( providedAgentId?: string, silent: boolean = false, ): Promise { - // Try to get agent ID from .env file first - let envAgentId: string | undefined; + // If agent ID is provided, resolve it (could be name or ID) + if (providedAgentId) { + return resolveAgentId(client, providedAgentId, silent); + } + + // Try to get agent ID from .env file try { const config = await getXpanderConfigFromEnvFile(process.cwd()); if (config?.agent_id) { - envAgentId = config.agent_id; + return config.agent_id; } } catch (error) { // No .env file or no agent_id in it } - // If agent ID is provided, try to resolve it - if (providedAgentId) { - const resolvedId = await resolveAgentId(client, providedAgentId, silent); - // If resolution failed and we have an env agent_id, fall back to it - if (!resolvedId && envAgentId) { - if (!silent) { - console.log( - chalk.yellow( - '⚠ Provided agent not found, using agent from .env file', - ), - ); - } - return envAgentId; - } - return resolvedId; - } - - // No providedAgentId - use env agent_id if available - if (envAgentId) { - return envAgentId; - } - // Show interactive selection try { const agents = await getAgentsWithCache(client); From 9c1ac60e9c2ac5c711dfd6736ce8658d373eaa44 Mon Sep 17 00:00:00 2001 From: xpander-ai-coding-agent Date: Wed, 10 Dec 2025 17:41:48 -0800 Subject: [PATCH 4/5] fix: address Cursor code review bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed 3 bugs identified by Cursor code review: 1. Bug #1 (Silent fallback): Already fixed in previous commit - removed fallback logic from agent-resolver so explicit agent failures are clear 2. Bug #2 (Missing org ID validation): Added validation for organization ID before making API request. Now shows clear error message if org ID is missing, matching the API key validation pattern 3. Bug #3 (Staging URL support): Added IS_STG environment variable check to switch between production and staging API URLs: - Production: https://api.xpander.ai - Staging: https://api-stg.xpander.ai Changes in invoke.ts: - Lines 317-324: Added orgId validation with clear error message - Lines 326-330: Added staging environment detection and dynamic base URL 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/commands/agent/commands/invoke.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/commands/agent/commands/invoke.ts b/src/commands/agent/commands/invoke.ts index 3686636..ba90def 100644 --- a/src/commands/agent/commands/invoke.ts +++ b/src/commands/agent/commands/invoke.ts @@ -314,7 +314,20 @@ Examples: try { const orgId = getOrganizationId(options.profile); - const apiUrl = `https://api.xpander.ai/v1/agents/${agentId}/invoke`; + if (!orgId) { + console.error( + chalk.red( + 'No organization ID found. Please run `xpander configure` first.', + ), + ); + process.exit(1); + } + + const isStaging = process?.env?.IS_STG === 'true'; + const baseUrl = isStaging + ? 'https://api-stg.xpander.ai' + : 'https://api.xpander.ai'; + const apiUrl = `${baseUrl}/v1/agents/${agentId}/invoke`; invokeSpinner = ora(`Invoking agent with: "${message}"...`).start(); const startTime = Date.now(); From 54f7cd6524300a17b1c7ffd0f62fc8afebaf478d Mon Sep 17 00:00:00 2001 From: xpander-ai-coding-agent Date: Wed, 10 Dec 2025 17:44:53 -0800 Subject: [PATCH 5/5] fix: correct staging URL format to api.stg.xpander.ai --- src/commands/agent/commands/invoke.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/agent/commands/invoke.ts b/src/commands/agent/commands/invoke.ts index ba90def..e3a15ed 100644 --- a/src/commands/agent/commands/invoke.ts +++ b/src/commands/agent/commands/invoke.ts @@ -325,7 +325,7 @@ Examples: const isStaging = process?.env?.IS_STG === 'true'; const baseUrl = isStaging - ? 'https://api-stg.xpander.ai' + ? 'https://api.stg.xpander.ai' : 'https://api.xpander.ai'; const apiUrl = `${baseUrl}/v1/agents/${agentId}/invoke`;