diff --git a/examples/README.md b/examples/README.md index 40f33bc..0869cd0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -78,6 +78,14 @@ Basic example showing how to initialize the toolset and make your first API call - **API Calls**: Yes - **Key Features**: Basic tool usage, employee listing +#### [`interactive-cli.ts`](./interactive-cli.ts) - Interactive CLI Demo + +Interactive command-line interface for dynamically discovering and executing StackOne tools using [@clack/prompts](https://github.com/bombshell-dev/clack). + +- **Account ID**: User-provided or from environment +- **API Calls**: Yes (user selects which tool to execute) +- **Key Features**: Interactive prompts, environment variable fallback, spinner feedback, dynamic tool discovery + #### [`ai-sdk-integration.ts`](./ai-sdk-integration.ts) - AI SDK Integration Demonstrates integration with Vercel's AI SDK for building AI agents. @@ -183,6 +191,7 @@ Comprehensive error handling patterns and best practices. Examples that are stable and recommended for production use: - `index.ts` +- `interactive-cli.ts` - `ai-sdk-integration.ts` - `openai-integration.ts` - `account-id-usage.ts` diff --git a/examples/interactive-cli.ts b/examples/interactive-cli.ts new file mode 100644 index 0000000..cf36482 --- /dev/null +++ b/examples/interactive-cli.ts @@ -0,0 +1,160 @@ +/** + * Interactive CLI Demo + * + * This example demonstrates how to build an interactive CLI tool using + * @clack/prompts to dynamically discover and execute StackOne tools. + * + * Features: + * - Interactive credential input with environment variable fallback + * - Dynamic tool discovery and selection + * - Spinner feedback during async operations + * + * Run with: + * ```bash + * npx tsx examples/interactive-cli.ts + * ``` + */ + +import process from 'node:process'; +import * as clack from '@clack/prompts'; +import { StackOneToolSet } from '@stackone/ai'; + +// Enable verbose fetch logging when running with Bun +process.env.BUN_CONFIG_VERBOSE_FETCH = 'curl'; + +clack.intro('Welcome to StackOne AI Tool Tester'); + +// Check if environment variables are available +const hasEnvVars = process.env.STACKONE_API_KEY && process.env.STACKONE_ACCOUNT_ID; + +let apiKey: string; +let baseUrl: string; +let accountId: string; + +if (hasEnvVars) { + const useEnv = await clack.confirm({ + message: 'Use environment variables from .env file?', + }); + + if (clack.isCancel(useEnv)) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + if (useEnv) { + apiKey = process.env.STACKONE_API_KEY!; + baseUrl = process.env.STACKONE_BASE_URL || 'https://api.stackone.com'; + accountId = process.env.STACKONE_ACCOUNT_ID!; + } else { + const credentials = await promptCredentials(); + apiKey = credentials.apiKey; + baseUrl = credentials.baseUrl; + accountId = credentials.accountId; + } +} else { + const credentials = await promptCredentials(); + apiKey = credentials.apiKey; + baseUrl = credentials.baseUrl; + accountId = credentials.accountId; +} + +async function promptCredentials(): Promise<{ + apiKey: string; + baseUrl: string; + accountId: string; +}> { + const apiKeyInput = await clack.text({ + message: 'Enter your StackOne API key:', + placeholder: 'v1.us1.xxx...', + validate: (value) => { + if (!value) return 'API key is required'; + }, + }); + + if (clack.isCancel(apiKeyInput)) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + const baseUrlInput = await clack.text({ + message: 'Enter StackOne Base URL (optional):', + placeholder: 'https://api.stackone.com', + defaultValue: 'https://api.stackone.com', + }); + + if (clack.isCancel(baseUrlInput)) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + const accountIdInput = await clack.text({ + message: 'Enter your StackOne Account ID:', + placeholder: 'acc_xxx...', + validate: (value) => { + if (!value) return 'Account ID is required'; + }, + }); + + if (clack.isCancel(accountIdInput)) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + return { + apiKey: apiKeyInput as string, + baseUrl: baseUrlInput as string, + accountId: accountIdInput as string, + }; +} + +const spinner = clack.spinner(); +spinner.start('Initialising StackOne client...'); + +const toolset = new StackOneToolSet({ + apiKey, + baseUrl, + accountId, +}); + +spinner.message('Fetching available tools...'); +const tools = await toolset.fetchTools(); +const allTools = tools.toArray(); +spinner.stop(`Found ${allTools.length} tools`); + +// Select a tool interactively +const selectedToolName = await clack.select({ + message: 'Select a tool to execute:', + options: allTools.map((tool) => ({ + label: tool.description, + value: tool.name, + hint: tool.name, + })), +}); + +if (clack.isCancel(selectedToolName)) { + clack.cancel('Operation cancelled'); + process.exit(0); +} + +const selectedTool = tools.getTool(selectedToolName as string); +if (!selectedTool) { + clack.log.error(`Tool '${selectedToolName}' not found!`); + process.exit(1); +} + +spinner.start(`Executing: ${selectedTool.description}`); +try { + const result = await selectedTool.execute({ + query: { limit: 5 }, + }); + spinner.stop('Execution complete'); + + clack.log.success('Result:'); + console.log(JSON.stringify(result, null, 2)); + clack.outro('Done!'); +} catch (error) { + spinner.stop('Execution failed'); + clack.log.error(error instanceof Error ? error.message : String(error)); + clack.outro('Failed'); + process.exit(1); +} diff --git a/examples/package.json b/examples/package.json index fa089df..17919d4 100644 --- a/examples/package.json +++ b/examples/package.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@ai-sdk/openai": "catalog:dev", + "@clack/prompts": "catalog:dev", "@types/node": "catalog:dev", "ai": "catalog:peer", "openai": "catalog:peer", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13f8310..8efdca9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,9 +6,15 @@ settings: catalogs: dev: + '@ai-sdk/openai': + specifier: ^2.0.80 + version: 2.0.80 '@ai-sdk/provider-utils': specifier: ^3.0.18 version: 3.0.18 + '@clack/prompts': + specifier: ^0.11.0 + version: 0.11.0 '@hono/mcp': specifier: ^0.1.4 version: 0.1.5 @@ -176,6 +182,9 @@ importers: '@ai-sdk/openai': specifier: catalog:dev version: 2.0.80(zod@4.1.13) + '@clack/prompts': + specifier: catalog:dev + version: 0.11.0 '@types/node': specifier: catalog:dev version: 22.19.1 @@ -270,6 +279,18 @@ packages: } engines: { node: '>=18' } + '@clack/core@0.5.0': + resolution: + { + integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==, + } + + '@clack/prompts@0.11.0': + resolution: + { + integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==, + } + '@emnapi/core@1.7.1': resolution: { @@ -3259,6 +3280,12 @@ packages: } engines: { node: '>=14' } + sisteransi@1.0.5: + resolution: + { + integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==, + } + smol-toml@1.5.2: resolution: { @@ -3751,6 +3778,17 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@clack/core@0.5.0': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.11.0': + dependencies: + '@clack/core': 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -5273,6 +5311,8 @@ snapshots: signal-exit@4.1.0: {} + sisteransi@1.0.5: {} + smol-toml@1.5.2: {} source-map-js@1.2.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b621a0a..bfa5265 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,6 +7,7 @@ catalogMode: strict catalogs: dev: '@ai-sdk/openai': ^2.0.80 + '@clack/prompts': ^0.11.0 '@ai-sdk/provider-utils': ^3.0.18 '@hono/mcp': ^0.1.4 '@types/json-schema': ^7.0.15