Skip to content

Commit e54cf99

Browse files
committed
feat(example): add interactive CLI demo with @clack/prompts
Add a new example demonstrating how to build an interactive CLI tool for dynamically discovering and executing StackOne tools. Features: - Interactive credential input with environment variable fallback - Dynamic tool discovery via fetchTools() - Interactive tool selection menu with descriptions - Spinner feedback during async operations This provides a user-friendly way to explore and test StackOne tools without hardcoding credentials or tool names.
1 parent ac40adf commit e54cf99

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed

examples/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ Basic example showing how to initialize the toolset and make your first API call
7878
- **API Calls**: Yes
7979
- **Key Features**: Basic tool usage, employee listing
8080

81+
#### [`interactive-cli.ts`](./interactive-cli.ts) - Interactive CLI Demo
82+
83+
Interactive command-line interface for dynamically discovering and executing StackOne tools using [@clack/prompts](https://github.com/bombshell-dev/clack).
84+
85+
- **Account ID**: User-provided or from environment
86+
- **API Calls**: Yes (user selects which tool to execute)
87+
- **Key Features**: Interactive prompts, environment variable fallback, spinner feedback, dynamic tool discovery
88+
8189
#### [`ai-sdk-integration.ts`](./ai-sdk-integration.ts) - AI SDK Integration
8290

8391
Demonstrates integration with Vercel's AI SDK for building AI agents.
@@ -183,6 +191,7 @@ Comprehensive error handling patterns and best practices.
183191
Examples that are stable and recommended for production use:
184192

185193
- `index.ts`
194+
- `interactive-cli.ts`
186195
- `ai-sdk-integration.ts`
187196
- `openai-integration.ts`
188197
- `account-id-usage.ts`

examples/interactive-cli.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* Interactive CLI Demo
3+
*
4+
* This example demonstrates how to build an interactive CLI tool using
5+
* @clack/prompts to dynamically discover and execute StackOne tools.
6+
*
7+
* Features:
8+
* - Interactive credential input with environment variable fallback
9+
* - Dynamic tool discovery and selection
10+
* - Spinner feedback during async operations
11+
*
12+
* Run with:
13+
* ```bash
14+
* npx tsx examples/interactive-cli.ts
15+
* ```
16+
*/
17+
18+
import process from 'node:process';
19+
import * as clack from '@clack/prompts';
20+
import { StackOneToolSet } from '@stackone/ai';
21+
22+
// Enable verbose fetch logging when running with Bun
23+
process.env.BUN_CONFIG_VERBOSE_FETCH = 'curl';
24+
25+
clack.intro('Welcome to StackOne AI Tool Tester');
26+
27+
// Check if environment variables are available
28+
const hasEnvVars = process.env.STACKONE_API_KEY && process.env.STACKONE_ACCOUNT_ID;
29+
30+
let apiKey: string;
31+
let baseUrl: string;
32+
let accountId: string;
33+
34+
if (hasEnvVars) {
35+
const useEnv = await clack.confirm({
36+
message: 'Use environment variables from .env file?',
37+
});
38+
39+
if (clack.isCancel(useEnv)) {
40+
clack.cancel('Operation cancelled');
41+
process.exit(0);
42+
}
43+
44+
if (useEnv) {
45+
apiKey = process.env.STACKONE_API_KEY!;
46+
baseUrl = process.env.STACKONE_BASE_URL || 'https://api.stackone.com';
47+
accountId = process.env.STACKONE_ACCOUNT_ID!;
48+
} else {
49+
const credentials = await promptCredentials();
50+
apiKey = credentials.apiKey;
51+
baseUrl = credentials.baseUrl;
52+
accountId = credentials.accountId;
53+
}
54+
} else {
55+
const credentials = await promptCredentials();
56+
apiKey = credentials.apiKey;
57+
baseUrl = credentials.baseUrl;
58+
accountId = credentials.accountId;
59+
}
60+
61+
async function promptCredentials(): Promise<{
62+
apiKey: string;
63+
baseUrl: string;
64+
accountId: string;
65+
}> {
66+
const apiKeyInput = await clack.text({
67+
message: 'Enter your StackOne API key:',
68+
placeholder: 'v1.us1.xxx...',
69+
validate: (value) => {
70+
if (!value) return 'API key is required';
71+
},
72+
});
73+
74+
if (clack.isCancel(apiKeyInput)) {
75+
clack.cancel('Operation cancelled');
76+
process.exit(0);
77+
}
78+
79+
const baseUrlInput = await clack.text({
80+
message: 'Enter StackOne Base URL (optional):',
81+
placeholder: 'https://api.stackone.com',
82+
defaultValue: 'https://api.stackone.com',
83+
});
84+
85+
if (clack.isCancel(baseUrlInput)) {
86+
clack.cancel('Operation cancelled');
87+
process.exit(0);
88+
}
89+
90+
const accountIdInput = await clack.text({
91+
message: 'Enter your StackOne Account ID:',
92+
placeholder: 'acc_xxx...',
93+
validate: (value) => {
94+
if (!value) return 'Account ID is required';
95+
},
96+
});
97+
98+
if (clack.isCancel(accountIdInput)) {
99+
clack.cancel('Operation cancelled');
100+
process.exit(0);
101+
}
102+
103+
return {
104+
apiKey: apiKeyInput as string,
105+
baseUrl: baseUrlInput as string,
106+
accountId: accountIdInput as string,
107+
};
108+
}
109+
110+
const spinner = clack.spinner();
111+
spinner.start('Initialising StackOne client...');
112+
113+
const toolset = new StackOneToolSet({
114+
apiKey,
115+
baseUrl,
116+
accountId,
117+
});
118+
119+
spinner.message('Fetching available tools...');
120+
const tools = await toolset.fetchTools();
121+
const allTools = tools.toArray();
122+
spinner.stop(`Found ${allTools.length} tools`);
123+
124+
// Select a tool interactively
125+
const selectedToolName = await clack.select({
126+
message: 'Select a tool to execute:',
127+
options: allTools.map((tool) => ({
128+
label: tool.description,
129+
value: tool.name,
130+
hint: tool.name,
131+
})),
132+
});
133+
134+
if (clack.isCancel(selectedToolName)) {
135+
clack.cancel('Operation cancelled');
136+
process.exit(0);
137+
}
138+
139+
const selectedTool = tools.getTool(selectedToolName as string);
140+
if (!selectedTool) {
141+
clack.log.error(`Tool '${selectedToolName}' not found!`);
142+
process.exit(1);
143+
}
144+
145+
spinner.start(`Executing: ${selectedTool.description}`);
146+
try {
147+
const result = await selectedTool.execute({
148+
query: { limit: 5 },
149+
});
150+
spinner.stop('Execution complete');
151+
152+
clack.log.success('Result:');
153+
console.log(JSON.stringify(result, null, 2));
154+
clack.outro('Done!');
155+
} catch (error) {
156+
spinner.stop('Execution failed');
157+
clack.log.error(error instanceof Error ? error.message : String(error));
158+
clack.outro('Failed');
159+
process.exit(1);
160+
}

0 commit comments

Comments
 (0)