@stackone/ai
Advanced tools
| /** | ||
| * Authentication and account management patterns. | ||
| * | ||
| * Shows every way to configure API keys and account IDs with the Node SDK. | ||
| * | ||
| * Run with: | ||
| * STACKONE_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/auth-management.ts | ||
| */ | ||
| import process from 'node:process'; | ||
| import { StackOneToolSet } from '@stackone/ai'; | ||
| if (!process.env.STACKONE_API_KEY) { | ||
| console.error('Set STACKONE_API_KEY to run this example.'); | ||
| process.exit(1); | ||
| } | ||
| // --- 1. API Key setup --- | ||
| const apiKeyFromEnv = async (): Promise<void> => { | ||
| console.log('=== 1a. API Key from environment ===\n'); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env automatically | ||
| const toolset = new StackOneToolSet(); | ||
| const tools = await toolset.fetchTools(); | ||
| console.log(` Loaded ${tools.toOpenAI().length} tools using env API key\n`); | ||
| }; | ||
| const apiKeyExplicit = async (): Promise<void> => { | ||
| console.log('=== 1b. Explicit API key ===\n'); | ||
| const toolset = new StackOneToolSet({ apiKey: process.env.STACKONE_API_KEY }); | ||
| const tools = await toolset.fetchTools(); | ||
| console.log(` Loaded ${tools.toOpenAI().length} tools using explicit API key\n`); | ||
| }; | ||
| // --- 2. Account ID from environment --- | ||
| const accountIdFromEnv = async (): Promise<void> => { | ||
| console.log('=== 2. Account ID from environment ===\n'); | ||
| // The Node SDK reads STACKONE_ACCOUNT_ID from env automatically | ||
| const accountId = process.env.STACKONE_ACCOUNT_ID; | ||
| console.log(` STACKONE_ACCOUNT_ID is ${accountId ? 'set' : 'not set'}`); | ||
| const toolset = new StackOneToolSet(); | ||
| const tools = await toolset.fetchTools(); | ||
| console.log(` Loaded ${tools.toOpenAI().length} tools for account from env\n`); | ||
| }; | ||
| // --- 3. Account ID in constructor --- | ||
| const accountIdInConstructor = async (): Promise<void> => { | ||
| console.log('=== 3. Account ID in constructor ===\n'); | ||
| const accountId = process.env.STACKONE_ACCOUNT_ID ?? 'my-account'; | ||
| const toolset = new StackOneToolSet({ accountId }); | ||
| const tools = await toolset.fetchTools(); | ||
| console.log(` Loaded ${tools.toOpenAI().length} tools for configured account\n`); | ||
| }; | ||
| // --- 4. setAccounts() — set accounts globally --- | ||
| const setAccountsGlobally = async (): Promise<void> => { | ||
| console.log('=== 4. setAccounts() — global account list ===\n'); | ||
| const accountId = process.env.STACKONE_ACCOUNT_ID ?? 'my-account'; | ||
| const toolset = new StackOneToolSet(); | ||
| toolset.setAccounts([accountId]); | ||
| console.log(' Called setAccounts() with configured account'); | ||
| // Subsequent fetchTools uses the globally set accounts | ||
| const tools = await toolset.fetchTools(); | ||
| console.log(` Loaded ${tools.toOpenAI().length} tools after setAccounts()\n`); | ||
| }; | ||
| // --- 5. Per-tool account override --- | ||
| const perToolOverride = async (): Promise<void> => { | ||
| console.log('=== 5. Per-tool account override ===\n'); | ||
| const toolset = new StackOneToolSet(); | ||
| const tools = await toolset.fetchTools(); | ||
| // Override account on a single tool (use getStackOneTool for account methods) | ||
| try { | ||
| const tool = tools.getStackOneTool('workday_list_workers'); | ||
| tool.setAccountId('per-tool-account'); | ||
| const current = tool.getAccountId(); | ||
| console.log(` Single tool account: "${current}"\n`); | ||
| } catch { | ||
| console.log(' (workday_list_workers not available — skipping single-tool demo)\n'); | ||
| } | ||
| }; | ||
| // --- Run all sections --- | ||
| await apiKeyFromEnv(); | ||
| await apiKeyExplicit(); | ||
| await accountIdFromEnv(); | ||
| await accountIdInConstructor(); | ||
| await setAccountsGlobally(); | ||
| await perToolOverride(); | ||
| console.log('Done — all auth patterns demonstrated.'); |
+1
-1
| //#region package.json | ||
| var version = "2.7.0"; | ||
| var version = "2.8.0"; | ||
| var peerDependencies = { | ||
@@ -4,0 +4,0 @@ "@anthropic-ai/claude-agent-sdk": "catalog:peer", |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"package.mjs","names":[],"sources":["../package.json"],"sourcesContent":["{\n\t\"name\": \"@stackone/ai\",\n\t\"version\": \"2.7.0\",\n\t\"description\": \"Tools for agents to perform actions on your SaaS\",\n\t\"keywords\": [\n\t\t\"agents\",\n\t\t\"ai\",\n\t\t\"ai sdk\",\n\t\t\"mcp\",\n\t\t\"model context protocol\",\n\t\t\"stackone\",\n\t\t\"tools\"\n\t],\n\t\"homepage\": \"https://github.com/StackOneHQ/stackone-ai-node#readme\",\n\t\"bugs\": \"https://github.com/StackOneHQ/stackone-ai-node/issues\",\n\t\"license\": \"Apache-2.0\",\n\t\"author\": \"StackOne\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/StackOneHQ/stackone-ai-node.git\"\n\t},\n\t\"files\": [\n\t\t\"dist\",\n\t\t\"LICENSE\",\n\t\t\"README.md\",\n\t\t\"src\",\n\t\t\"!examples/*.test.ts\",\n\t\t\"examples/*.ts\",\n\t\t\"!src/**/*.test-d.ts\",\n\t\t\"!src/**/*.test.ts\"\n\t],\n\t\"type\": \"module\",\n\t\"main\": \"./dist/index.mjs\",\n\t\"module\": \"./dist/index.mjs\",\n\t\"types\": \"./dist/index.d.mts\",\n\t\"exports\": {\n\t\t\".\": \"./src/index.ts\",\n\t\t\"./package.json\": \"./package.json\"\n\t},\n\t\"publishConfig\": {\n\t\t\"exports\": {\n\t\t\t\".\": \"./dist/index.mjs\",\n\t\t\t\"./package.json\": \"./package.json\"\n\t\t}\n\t},\n\t\"scripts\": {\n\t\t\"build\": \"tsdown\",\n\t\t\"format\": \"pnpm --no-bail --aggregate-output run '/^format:/'\",\n\t\t\"format:oxfmt\": \"oxfmt --no-error-on-unmatched-pattern .\",\n\t\t\"format:oxlint\": \"oxlint --max-warnings=0 --type-aware --type-check --fix\",\n\t\t\"format:knip\": \"knip --fix --no-exit-code\",\n\t\t\"lint\": \"pnpm --aggregate-output run '/^lint:/'\",\n\t\t\"lint:oxfmt\": \"oxfmt --no-error-on-unmatched-pattern --check .\",\n\t\t\"lint:oxlint\": \"oxlint --max-warnings=0 --type-aware --type-check\",\n\t\t\"lint:knip\": \"knip\",\n\t\t\"prepack\": \"npm pkg delete scripts.preinstall && pnpm run build\",\n\t\t\"test\": \"vitest\",\n\t\t\"coverage\": \"vitest run --coverage\"\n\t},\n\t\"dependencies\": {\n\t\t\"@modelcontextprotocol/sdk\": \"catalog:prod\",\n\t\t\"@orama/orama\": \"catalog:prod\",\n\t\t\"defu\": \"catalog:prod\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@fast-check/vitest\": \"catalog:dev\",\n\t\t\"@hono/mcp\": \"catalog:dev\",\n\t\t\"@types/node\": \"catalog:dev\",\n\t\t\"@vitest/coverage-v8\": \"catalog:dev\",\n\t\t\"ai\": \"catalog:dev\",\n\t\t\"hono\": \"catalog:dev\",\n\t\t\"knip\": \"catalog:dev\",\n\t\t\"msw\": \"catalog:dev\",\n\t\t\"openai\": \"catalog:peer\",\n\t\t\"publint\": \"catalog:dev\",\n\t\t\"tsdown\": \"catalog:dev\",\n\t\t\"type-fest\": \"catalog:dev\",\n\t\t\"unplugin-unused\": \"catalog:dev\",\n\t\t\"vitest\": \"catalog:dev\",\n\t\t\"zod\": \"catalog:dev\"\n\t},\n\t\"peerDependencies\": {\n\t\t\"@anthropic-ai/claude-agent-sdk\": \"catalog:peer\",\n\t\t\"@anthropic-ai/sdk\": \"catalog:peer\",\n\t\t\"ai\": \"catalog:peer\",\n\t\t\"openai\": \"catalog:peer\",\n\t\t\"zod\": \"catalog:peer\"\n\t},\n\t\"peerDependenciesMeta\": {\n\t\t\"@anthropic-ai/claude-agent-sdk\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"@anthropic-ai/sdk\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"ai\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"openai\": {\n\t\t\t\"optional\": true\n\t\t}\n\t},\n\t\"devEngines\": {\n\t\t\"runtime\": [\n\t\t\t{\n\t\t\t\t\"name\": \"node\",\n\t\t\t\t\"version\": \"^24.11.0\",\n\t\t\t\t\"onFail\": \"download\"\n\t\t\t}\n\t\t]\n\t},\n\t\"engines\": {\n\t\t\"node\": \">=20.19.6\"\n\t},\n\t\"packageManager\": \"pnpm@10.26.0\"\n}\n"],"mappings":";cAEY;uBA+ES;CACnB,kCAAkC;CAClC,qBAAqB;CACrB,MAAM;CACN,UAAU;CACV,OAAO;CACP"} | ||
| {"version":3,"file":"package.mjs","names":[],"sources":["../package.json"],"sourcesContent":["{\n\t\"name\": \"@stackone/ai\",\n\t\"version\": \"2.8.0\",\n\t\"description\": \"Tools for agents to perform actions on your SaaS\",\n\t\"keywords\": [\n\t\t\"agents\",\n\t\t\"ai\",\n\t\t\"ai sdk\",\n\t\t\"mcp\",\n\t\t\"model context protocol\",\n\t\t\"stackone\",\n\t\t\"tools\"\n\t],\n\t\"homepage\": \"https://github.com/StackOneHQ/stackone-ai-node#readme\",\n\t\"bugs\": \"https://github.com/StackOneHQ/stackone-ai-node/issues\",\n\t\"license\": \"Apache-2.0\",\n\t\"author\": \"StackOne\",\n\t\"repository\": {\n\t\t\"type\": \"git\",\n\t\t\"url\": \"git+https://github.com/StackOneHQ/stackone-ai-node.git\"\n\t},\n\t\"files\": [\n\t\t\"dist\",\n\t\t\"LICENSE\",\n\t\t\"README.md\",\n\t\t\"src\",\n\t\t\"examples/*.ts\",\n\t\t\"!src/**/*.test-d.ts\",\n\t\t\"!src/**/*.test.ts\"\n\t],\n\t\"type\": \"module\",\n\t\"main\": \"./dist/index.mjs\",\n\t\"module\": \"./dist/index.mjs\",\n\t\"types\": \"./dist/index.d.mts\",\n\t\"exports\": {\n\t\t\".\": \"./src/index.ts\",\n\t\t\"./package.json\": \"./package.json\"\n\t},\n\t\"publishConfig\": {\n\t\t\"exports\": {\n\t\t\t\".\": \"./dist/index.mjs\",\n\t\t\t\"./package.json\": \"./package.json\"\n\t\t}\n\t},\n\t\"scripts\": {\n\t\t\"build\": \"tsdown\",\n\t\t\"format\": \"pnpm --no-bail --aggregate-output run '/^format:/'\",\n\t\t\"format:oxfmt\": \"oxfmt --no-error-on-unmatched-pattern .\",\n\t\t\"format:oxlint\": \"oxlint --max-warnings=0 --type-aware --type-check --fix\",\n\t\t\"format:knip\": \"knip --fix --no-exit-code\",\n\t\t\"lint\": \"pnpm --aggregate-output run '/^lint:/'\",\n\t\t\"lint:oxfmt\": \"oxfmt --no-error-on-unmatched-pattern --check .\",\n\t\t\"lint:oxlint\": \"oxlint --max-warnings=0 --type-aware --type-check\",\n\t\t\"lint:knip\": \"knip\",\n\t\t\"prepack\": \"npm pkg delete scripts.preinstall && pnpm run build\",\n\t\t\"test\": \"vitest\",\n\t\t\"coverage\": \"vitest run --coverage\",\n\t\t\"run:example\": \"tsx --env-file .env\"\n\t},\n\t\"dependencies\": {\n\t\t\"@modelcontextprotocol/sdk\": \"catalog:prod\",\n\t\t\"@orama/orama\": \"catalog:prod\",\n\t\t\"defu\": \"catalog:prod\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@fast-check/vitest\": \"catalog:dev\",\n\t\t\"@hono/mcp\": \"catalog:dev\",\n\t\t\"@types/node\": \"catalog:dev\",\n\t\t\"@vitest/coverage-v8\": \"catalog:dev\",\n\t\t\"ai\": \"catalog:dev\",\n\t\t\"hono\": \"catalog:dev\",\n\t\t\"knip\": \"catalog:dev\",\n\t\t\"msw\": \"catalog:dev\",\n\t\t\"openai\": \"catalog:peer\",\n\t\t\"publint\": \"catalog:dev\",\n\t\t\"tsdown\": \"catalog:dev\",\n\t\t\"type-fest\": \"catalog:dev\",\n\t\t\"unplugin-unused\": \"catalog:dev\",\n\t\t\"vitest\": \"catalog:dev\",\n\t\t\"zod\": \"catalog:dev\"\n\t},\n\t\"peerDependencies\": {\n\t\t\"@anthropic-ai/claude-agent-sdk\": \"catalog:peer\",\n\t\t\"@anthropic-ai/sdk\": \"catalog:peer\",\n\t\t\"ai\": \"catalog:peer\",\n\t\t\"openai\": \"catalog:peer\",\n\t\t\"zod\": \"catalog:peer\"\n\t},\n\t\"peerDependenciesMeta\": {\n\t\t\"@anthropic-ai/claude-agent-sdk\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"@anthropic-ai/sdk\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"ai\": {\n\t\t\t\"optional\": true\n\t\t},\n\t\t\"openai\": {\n\t\t\t\"optional\": true\n\t\t}\n\t},\n\t\"devEngines\": {\n\t\t\"runtime\": [\n\t\t\t{\n\t\t\t\t\"name\": \"node\",\n\t\t\t\t\"version\": \"^24.11.0\",\n\t\t\t\t\"onFail\": \"download\"\n\t\t\t}\n\t\t]\n\t},\n\t\"engines\": {\n\t\t\"node\": \">=20.19.6\"\n\t},\n\t\"packageManager\": \"pnpm@10.26.0\"\n}\n"],"mappings":";cAEY;uBA+ES;CACnB,kCAAkC;CAClC,qBAAqB;CACrB,MAAM;CACN,UAAU;CACV,OAAO;CACP"} |
@@ -13,3 +13,2 @@ /** | ||
| import assert from 'node:assert'; | ||
| import process from 'node:process'; | ||
@@ -25,2 +24,6 @@ import { openai } from '@ai-sdk/openai'; | ||
| } | ||
| if (!process.env.OPENAI_API_KEY) { | ||
| console.log('Skipping: OPENAI_API_KEY is not set'); | ||
| process.exit(0); | ||
| } | ||
@@ -31,19 +34,32 @@ const aiSdkIntegration = async (): Promise<void> => { | ||
| // Fetch all tools for this account via MCP | ||
| const tools = await toolset.fetchTools(); | ||
| // Filter to specific tools to keep token usage manageable | ||
| const tools = await toolset.fetchTools({ | ||
| actions: ['workday_list_workers', 'workday_get_worker', 'workday_get_current_user'], | ||
| }); | ||
| // Convert to AI SDK tools | ||
| const aiSdkTools = await tools.toAISDK(); | ||
| console.log(`Loaded ${Object.keys(aiSdkTools).length} tools for AI SDK`); | ||
| // The AI SDK will automatically call the tool if needed | ||
| const { text } = await generateText({ | ||
| const { text, steps } = await generateText({ | ||
| model: openai('gpt-5.1'), | ||
| tools: aiSdkTools, | ||
| prompt: 'Get all details about employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', | ||
| prompt: 'List the first 5 employees', | ||
| stopWhen: stepCountIs(3), | ||
| }); | ||
| assert(text.includes('Michael'), 'Expected employee name to be included in the response'); | ||
| console.log(`AI response: ${text}`); | ||
| console.log(`Steps taken: ${steps.length}`); | ||
| for (const step of steps) { | ||
| if (step.toolCalls && step.toolCalls.length > 0) { | ||
| for (const toolCall of step.toolCalls) { | ||
| console.log(` Tool call: ${toolCall.toolName}`); | ||
| console.log(` Arguments: ${JSON.stringify((toolCall as Record<string, unknown>).args)}`); | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| await aiSdkIntegration(); |
@@ -5,3 +5,2 @@ /** | ||
| import assert from 'node:assert'; | ||
| import process from 'node:process'; | ||
@@ -16,2 +15,6 @@ import Anthropic from '@anthropic-ai/sdk'; | ||
| } | ||
| if (!process.env.ANTHROPIC_API_KEY) { | ||
| console.log('Skipping: ANTHROPIC_API_KEY is not set'); | ||
| process.exit(0); | ||
| } | ||
@@ -27,2 +30,3 @@ const anthropicIntegration = async (): Promise<void> => { | ||
| const anthropicTools = tools.toAnthropic(); | ||
| console.log(`Loaded ${anthropicTools.length} tools for Anthropic`); | ||
@@ -40,3 +44,3 @@ // Initialize Anthropic client | ||
| role: 'user', | ||
| content: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', | ||
| content: 'List the first 5 employees', | ||
| }, | ||
@@ -47,13 +51,12 @@ ], | ||
| // Verify the response contains tool use | ||
| assert(response.content.length > 0, 'Expected at least one content block in the response'); | ||
| console.log(`Response content blocks: ${response.content.length}`); | ||
| const toolUseBlock = response.content.find((block) => block.type === 'tool_use'); | ||
| assert(toolUseBlock !== undefined, 'Expected a tool_use block in the response'); | ||
| assert(toolUseBlock.type === 'tool_use', 'Expected block to be tool_use type'); | ||
| assert(toolUseBlock.name === 'hris_get_employee', 'Expected tool call to be hris_get_employee'); | ||
| // Verify the input contains the expected fields | ||
| const input = toolUseBlock.input as Record<string, unknown>; | ||
| assert(input.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query'); | ||
| for (const block of response.content) { | ||
| if (block.type === 'tool_use') { | ||
| console.log(` Tool use: ${block.name}`); | ||
| console.log(` Input: ${JSON.stringify(block.input)}`); | ||
| } else if (block.type === 'text') { | ||
| console.log(` Text: ${block.text}`); | ||
| } | ||
| } | ||
| }; | ||
@@ -60,0 +63,0 @@ |
@@ -11,3 +11,2 @@ /** | ||
| import assert from 'node:assert'; | ||
| import process from 'node:process'; | ||
@@ -22,2 +21,6 @@ import { query } from '@anthropic-ai/claude-agent-sdk'; | ||
| } | ||
| if (!process.env.ANTHROPIC_API_KEY) { | ||
| console.log('Skipping: ANTHROPIC_API_KEY is not set'); | ||
| process.exit(0); | ||
| } | ||
@@ -31,2 +34,3 @@ const claudeAgentSdkIntegration = async (): Promise<void> => { | ||
| const mcpServer = await tools.toClaudeAgentSdk(); | ||
| console.log('Claude Agent SDK MCP server created'); | ||
@@ -36,3 +40,3 @@ // Use the Claude Agent SDK query with the StackOne MCP server | ||
| const result = query({ | ||
| prompt: 'Get the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', | ||
| prompt: 'List the first 5 employees', | ||
| options: { | ||
@@ -51,8 +55,11 @@ model: 'claude-sonnet-4-5-20250929', | ||
| // Process the stream and collect results | ||
| let hasToolCall = false; | ||
| console.log('Processing agent stream...'); | ||
| for await (const message of result) { | ||
| if (message.type === 'assistant') { | ||
| for (const block of message.message.content) { | ||
| if (block.type === 'tool_use' && block.name === 'hris_get_employee') { | ||
| hasToolCall = true; | ||
| if (block.type === 'tool_use') { | ||
| console.log(` Tool use: ${block.name}`); | ||
| console.log(` Input: ${JSON.stringify(block.input)}`); | ||
| } else if (block.type === 'text') { | ||
| console.log(` Assistant: ${block.text}`); | ||
| } | ||
@@ -63,5 +70,5 @@ } | ||
| assert(hasToolCall, 'Expected at least one tool call to hris_get_employee'); | ||
| console.log('Agent stream completed'); | ||
| }; | ||
| await claudeAgentSdkIntegration(); |
@@ -5,3 +5,2 @@ /** | ||
| import assert from 'node:assert'; | ||
| import process from 'node:process'; | ||
@@ -16,2 +15,6 @@ import { StackOneToolSet } from '@stackone/ai'; | ||
| } | ||
| if (!process.env.OPENAI_API_KEY) { | ||
| console.log('Skipping: OPENAI_API_KEY is not set'); | ||
| process.exit(0); | ||
| } | ||
@@ -22,5 +25,8 @@ const openaiIntegration = async (): Promise<void> => { | ||
| // Fetch all tools for this account via MCP | ||
| const tools = await toolset.fetchTools(); | ||
| // Filter to specific tools to stay within OpenAI's 128-tool limit | ||
| const tools = await toolset.fetchTools({ | ||
| actions: ['workday_list_workers', 'workday_get_worker', 'workday_get_current_user'], | ||
| }); | ||
| const openAITools = tools.toOpenAI(); | ||
| console.log(`Loaded ${openAITools.length} tools for OpenAI`); | ||
@@ -36,7 +42,7 @@ // Initialize OpenAI client | ||
| role: 'system', | ||
| content: 'You are a helpful assistant that can access BambooHR information.', | ||
| content: 'You are a helpful assistant that can access HR information.', | ||
| }, | ||
| { | ||
| role: 'user', | ||
| content: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', | ||
| content: 'List the first 5 employees', | ||
| }, | ||
@@ -47,19 +53,14 @@ ], | ||
| // Verify the response contains tool calls | ||
| assert(response.choices.length > 0, 'Expected at least one choice in the response'); | ||
| console.log(`Model returned ${response.choices.length} choice(s)`); | ||
| const choice = response.choices[0]; | ||
| assert(choice.message.tool_calls !== undefined, 'Expected tool_calls to be defined'); | ||
| assert(choice.message.tool_calls.length > 0, 'Expected at least one tool call'); | ||
| const toolCalls = choice.message.tool_calls ?? []; | ||
| console.log(`Tool calls made: ${toolCalls.length}`); | ||
| const toolCall = choice.message.tool_calls[0]; | ||
| assert(toolCall.type === 'function', 'Expected tool call to be a function'); | ||
| assert( | ||
| toolCall.function.name === 'bamboohr_get_employee', | ||
| 'Expected tool call to be bamboohr_get_employee', | ||
| ); | ||
| // Parse the arguments to verify they contain the expected fields | ||
| const args = JSON.parse(toolCall.function.arguments); | ||
| assert(args.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query'); | ||
| for (const toolCall of toolCalls) { | ||
| if ('function' in toolCall) { | ||
| console.log(` Tool: ${toolCall.function.name}`); | ||
| console.log(` Arguments: ${toolCall.function.arguments}`); | ||
| } | ||
| } | ||
| }; | ||
@@ -66,0 +67,0 @@ |
@@ -5,3 +5,2 @@ /** | ||
| import assert from 'node:assert'; | ||
| import process from 'node:process'; | ||
@@ -16,2 +15,6 @@ import { StackOneToolSet } from '@stackone/ai'; | ||
| } | ||
| if (!process.env.OPENAI_API_KEY) { | ||
| console.log('Skipping: OPENAI_API_KEY is not set'); | ||
| process.exit(0); | ||
| } | ||
@@ -27,2 +30,3 @@ const openaiResponsesIntegration = async (): Promise<void> => { | ||
| const openAIResponsesTools = tools.toOpenAIResponses(); | ||
| console.log(`Loaded ${openAIResponsesTools.length} tools for OpenAI Responses API`); | ||
@@ -36,9 +40,8 @@ // Initialize OpenAI client | ||
| instructions: 'You are a helpful assistant that can access various tools.', | ||
| input: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', | ||
| input: 'List the first 5 employees', | ||
| tools: openAIResponsesTools, | ||
| }); | ||
| // Verify the response contains expected data | ||
| assert(response.id, 'Expected response to have an ID'); | ||
| assert(response.model, 'Expected response to have a model'); | ||
| console.log(`Response ID: ${response.id}`); | ||
| console.log(`Model: ${response.model}`); | ||
@@ -50,13 +53,8 @@ // Check if the model made any tool calls | ||
| assert(toolCalls.length > 0, 'Expected at least one tool call'); | ||
| console.log(`Tool calls found: ${toolCalls.length}`); | ||
| const toolCall = toolCalls[0]; | ||
| assert( | ||
| toolCall.name === 'bamboohr_get_employee', | ||
| 'Expected tool call to be bamboohr_get_employee', | ||
| ); | ||
| // Parse the arguments to verify they contain the expected fields | ||
| const args = JSON.parse(toolCall.arguments); | ||
| assert(args.id === 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', 'Expected id to match the query'); | ||
| for (const toolCall of toolCalls) { | ||
| console.log(` Tool: ${toolCall.name}`); | ||
| console.log(` Arguments: ${toolCall.arguments}`); | ||
| } | ||
| }; | ||
@@ -63,0 +61,0 @@ |
+145
-18
| /** | ||
| * Search tool patterns: callable wrapper and config overrides. | ||
| * Tool discovery with the StackOne AI SDK. | ||
| * | ||
| * For full agent execution, see agent-tool-search.ts. | ||
| * Covers: direct fetch, semantic search, local search, auto search, | ||
| * and the search-and-execute pattern. | ||
| * | ||
@@ -25,26 +26,152 @@ * Run with: | ||
| // --- Example 1: getSearchTool() callable --- | ||
| console.log('=== getSearchTool() callable ===\n'); | ||
| // --------------------------------------------------------------------------- | ||
| // 1. Direct fetch — no search, just action-pattern filtering | ||
| // --------------------------------------------------------------------------- | ||
| async function directFetch(): Promise<void> { | ||
| console.log('\n=== 1. Direct Fetch (action filters) ===\n'); | ||
| const toolset = new StackOneToolSet({ apiKey, accountId, search: {} }); | ||
| const searchTool = toolset.getSearchTool(); | ||
| const toolset = new StackOneToolSet({ apiKey, accountId }); | ||
| const queries = ['cancel an event', 'list employees', 'send a message']; | ||
| for (const query of queries) { | ||
| const tools = await searchTool.search(query, { topK: 3 }); | ||
| // Glob pattern: fetch only Workday tools | ||
| const tools = await toolset.fetchTools({ actions: ['workday_*'] }); | ||
| const names = tools.toArray().map((t) => t.name); | ||
| console.log(` "${query}" -> ${names.join(', ') || '(none)'}`); | ||
| console.log(` Fetched ${names.length} tools matching "workday_*"`); | ||
| console.log(` First 5: ${names.slice(0, 5).join(', ')}`); | ||
| } | ||
| // --- Example 2: Constructor topK vs per-call override --- | ||
| console.log('\n=== Constructor topK vs per-call override ===\n'); | ||
| // --------------------------------------------------------------------------- | ||
| // 2. Semantic search — remote embedding-based similarity | ||
| // --------------------------------------------------------------------------- | ||
| async function semanticSearch(): Promise<void> { | ||
| console.log('\n=== 2. Semantic Search ===\n'); | ||
| const toolset3 = new StackOneToolSet({ apiKey, accountId, search: { topK: 3 } }); | ||
| const toolset = new StackOneToolSet({ | ||
| apiKey, | ||
| accountId, | ||
| search: { method: 'semantic', topK: 5 }, | ||
| }); | ||
| const query = 'manage employee records'; | ||
| const tools = await toolset.searchTools('manage employees'); | ||
| const names = tools.toArray().map((t) => t.name); | ||
| console.log(` Query: "manage employees" -> ${names.length} results`); | ||
| for (const name of names) { | ||
| console.log(` - ${name}`); | ||
| } | ||
| } | ||
| const tools3 = await toolset3.searchTools(query); | ||
| console.log(`Constructor topK=3: got ${tools3.length} tools`); | ||
| // --------------------------------------------------------------------------- | ||
| // 3. Local search — BM25 + TF-IDF, no API call to semantic endpoint | ||
| // --------------------------------------------------------------------------- | ||
| async function localSearch(): Promise<void> { | ||
| console.log('\n=== 3. Local Search (BM25 + TF-IDF) ===\n'); | ||
| const toolsOverride = await toolset3.searchTools(query, { topK: 10 }); | ||
| console.log(`Per-call topK=10 (overrides constructor 3): got ${toolsOverride.length} tools`); | ||
| const toolset = new StackOneToolSet({ | ||
| apiKey, | ||
| accountId, | ||
| search: { method: 'local', topK: 5 }, | ||
| }); | ||
| const tools = await toolset.searchTools('time off requests', { search: 'local' }); | ||
| const names = tools.toArray().map((t) => t.name); | ||
| console.log(` Query: "time off requests" -> ${names.length} results`); | ||
| for (const name of names) { | ||
| console.log(` - ${name}`); | ||
| } | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // 4. Auto search + getSearchTool() callable | ||
| // Tries semantic first, falls back to local if the API is unavailable. | ||
| // --------------------------------------------------------------------------- | ||
| async function autoSearchWithCallable(): Promise<void> { | ||
| console.log('\n=== 4. Auto Search + getSearchTool() Callable ===\n'); | ||
| const toolset = new StackOneToolSet({ | ||
| apiKey, | ||
| accountId, | ||
| search: { method: 'auto', topK: 5 }, | ||
| }); | ||
| // 4a. Direct searchTools() with auto mode | ||
| const tools = await toolset.searchTools('send a message', { search: 'auto' }); | ||
| console.log(` searchTools("send a message") -> ${tools.length} results`); | ||
| // 4b. getSearchTool() — a callable wrapper for agent loops | ||
| const searchTool = toolset.getSearchTool(); | ||
| const queries = ['cancel an event', 'list employees', 'create a job posting']; | ||
| for (const query of queries) { | ||
| const results = await searchTool.search(query, { topK: 3 }); | ||
| const names = results.toArray().map((t) => t.name); | ||
| console.log(` searchTool.search("${query}") -> ${names.join(', ') || '(none)'}`); | ||
| } | ||
| // 4c. Constructor topK vs per-call override | ||
| console.log('\n -- topK override --'); | ||
| const defaultResults = await toolset.searchTools('manage employee records'); | ||
| console.log(` Constructor topK=5: got ${defaultResults.length} tools`); | ||
| const overrideResults = await toolset.searchTools('manage employee records', { topK: 10 }); | ||
| console.log(` Per-call topK=10: got ${overrideResults.length} tools`); | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // 5. Search & execute mode — getTools() with Vercel AI SDK | ||
| // The LLM receives tool_search + tool_execute and discovers tools on demand. | ||
| // --------------------------------------------------------------------------- | ||
| async function searchAndExecute(): Promise<void> { | ||
| console.log('\n=== 5. Search & Execute (Vercel AI SDK) ===\n'); | ||
| // Dynamic imports — these are optional peer dependencies | ||
| const [{ openai }, { generateText, stepCountIs }] = await Promise.all([ | ||
| import('@ai-sdk/openai'), | ||
| import('ai'), | ||
| ]); | ||
| const toolset = new StackOneToolSet({ | ||
| apiKey, | ||
| accountId, | ||
| search: { method: 'semantic', topK: 3 }, | ||
| timeout: 120_000, // increase for slow providers (default: 60s) | ||
| }); | ||
| // getTools() returns tool_search + tool_execute as a Tools collection | ||
| const tools = toolset.getTools({ accountIds: [accountId as string] }); | ||
| console.log(' Tools provided to model: tool_search, tool_execute'); | ||
| const { text, steps } = await generateText({ | ||
| model: openai('gpt-5.1'), | ||
| tools: await tools.toAISDK(), | ||
| prompt: 'List employees and return a short summary.', | ||
| stopWhen: stepCountIs(5), | ||
| }); | ||
| console.log(` Model completed in ${steps.length} step(s)`); | ||
| console.log(` Response: ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`); | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Main — run each section in order | ||
| // --------------------------------------------------------------------------- | ||
| async function main(): Promise<void> { | ||
| await directFetch(); | ||
| await semanticSearch(); | ||
| await localSearch(); | ||
| await autoSearchWithCallable(); | ||
| // Section 5 requires @ai-sdk/openai and ai packages + an OPENAI_API_KEY. | ||
| // Skip gracefully if not available. | ||
| try { | ||
| await searchAndExecute(); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| if (message.includes('Cannot find module') || message.includes('Cannot find package')) { | ||
| console.log('\n=== 5. Search & Execute (Vercel AI SDK) ===\n'); | ||
| console.log(' Skipped: install @ai-sdk/openai and ai to run this section.'); | ||
| } else { | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
| await main(); |
+3
-3
| { | ||
| "name": "@stackone/ai", | ||
| "version": "2.7.0", | ||
| "version": "2.8.0", | ||
| "description": "Tools for agents to perform actions on your SaaS", | ||
@@ -27,3 +27,2 @@ "keywords": [ | ||
| "src", | ||
| "!examples/*.test.ts", | ||
| "examples/*.ts", | ||
@@ -108,4 +107,5 @@ "!src/**/*.test-d.ts", | ||
| "test": "vitest", | ||
| "coverage": "vitest run --coverage" | ||
| "coverage": "vitest run --coverage", | ||
| "run:example": "tsx --env-file .env" | ||
| } | ||
| } |
+53
-87
@@ -36,8 +36,7 @@ # StackOne AI SDK | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-account-id', | ||
| }); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment | ||
| const toolset = new StackOneToolSet(); | ||
| const tools = await toolset.fetchTools(); | ||
| const employeeTool = tools.getTool('bamboohr_list_employees'); | ||
| const employeeTool = tools.getTool('workday_list_workers'); | ||
| const employees = await employeeTool.execute(); | ||
@@ -63,16 +62,20 @@ ``` | ||
| // Single account - simplest approach | ||
| const toolset = new StackOneToolSet({ accountId: 'your-bamboohr-account' }); | ||
| // Simplest: set STACKONE_ACCOUNT_ID environment variable | ||
| const toolset = new StackOneToolSet(); | ||
| const tools = await toolset.fetchTools(); | ||
| // Explicit single account | ||
| const explicitToolset = new StackOneToolSet({ accountId: 'your-workday-account' }); | ||
| const explicitTools = await explicitToolset.fetchTools(); | ||
| // Multiple accounts - returns tools from both integrations | ||
| const multiAccountToolset = new StackOneToolSet(); | ||
| const allTools = await multiAccountToolset.fetchTools({ | ||
| accountIds: ['bamboohr-account-123', 'workday-account-456'], | ||
| accountIds: ['workday-account-123', 'hibob-account-456'], | ||
| }); | ||
| // Filter to specific integration when using multiple accounts | ||
| const bamboohrOnly = await multiAccountToolset.fetchTools({ | ||
| accountIds: ['bamboohr-account-123', 'workday-account-456'], | ||
| actions: ['bamboohr_*'], // Only BambooHR tools | ||
| const workdayOnly = await multiAccountToolset.fetchTools({ | ||
| accountIds: ['workday-account-123', 'hibob-account-456'], | ||
| actions: ['workday_*'], // Only Workday tools | ||
| }); | ||
@@ -100,5 +103,4 @@ | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-account-id', | ||
| }); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment | ||
| const toolset = new StackOneToolSet(); | ||
@@ -112,3 +114,3 @@ const tools = await toolset.fetchTools(); | ||
| role: 'system', | ||
| content: 'You are a helpful HR assistant using BambooHR.', | ||
| content: 'You are a helpful HR assistant using Workday.', | ||
| }, | ||
@@ -139,5 +141,4 @@ { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-account-id', | ||
| }); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment | ||
| const toolset = new StackOneToolSet(); | ||
@@ -171,5 +172,4 @@ const tools = await toolset.fetchTools(); | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-account-id', | ||
| }); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment | ||
| const toolset = new StackOneToolSet(); | ||
@@ -210,5 +210,4 @@ const tools = await toolset.fetchTools(); | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-account-id', | ||
| }); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment | ||
| const toolset = new StackOneToolSet(); | ||
@@ -229,51 +228,2 @@ const tools = await toolset.fetchTools(); | ||
| <details> | ||
| <summary><strong>With TanStack AI</strong></summary> | ||
| ```bash | ||
| npm install @stackone/ai @tanstack/ai @tanstack/ai-openai zod # or: yarn/pnpm/bun add | ||
| ``` | ||
| ```typescript | ||
| import { chat } from '@tanstack/ai'; | ||
| import { openai } from '@tanstack/ai-openai'; | ||
| import { z } from 'zod'; | ||
| import { StackOneToolSet } from '@stackone/ai'; | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-account-id', | ||
| }); | ||
| const tools = await toolset.fetchTools(); | ||
| const employeeTool = tools.getTool('bamboohr_get_employee'); | ||
| // TanStack AI requires Zod schemas for tool input validation | ||
| const getEmployeeTool = { | ||
| name: employeeTool.name, | ||
| description: employeeTool.description, | ||
| inputSchema: z.object({ | ||
| id: z.string().describe('The employee ID'), | ||
| }), | ||
| execute: async (args: { id: string }) => { | ||
| return employeeTool.execute(args); | ||
| }, | ||
| }; | ||
| const adapter = openai(); | ||
| const stream = chat({ | ||
| adapter, | ||
| model: 'gpt-5.1', | ||
| messages: [{ role: 'user', content: 'Get employee with id: abc123' }], | ||
| tools: [getEmployeeTool], | ||
| }); | ||
| for await (const chunk of stream) { | ||
| // Process streaming chunks | ||
| } | ||
| ``` | ||
| [View full example](examples/tanstack-ai-integration.ts) | ||
| </details> | ||
| <details> | ||
| <summary><strong>With Claude Agent SDK</strong></summary> | ||
@@ -289,5 +239,4 @@ | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-account-id', | ||
| }); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment | ||
| const toolset = new StackOneToolSet(); | ||
@@ -334,3 +283,3 @@ // Fetch tools and convert to Claude Agent SDK format | ||
| // Filter by providers | ||
| const tools = await toolset.fetchTools({ providers: ['hibob', 'bamboohr'] }); | ||
| const tools = await toolset.fetchTools({ providers: ['hibob', 'workday'] }); | ||
@@ -359,4 +308,2 @@ // Filter by actions with exact match | ||
| [View full example](examples/fetch-tools.ts) | ||
| ### Search Tool | ||
@@ -371,4 +318,4 @@ | ||
| // Get a callable search tool | ||
| const toolset = new StackOneToolSet({ accountId: 'your-account-id' }); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment | ||
| const toolset = new StackOneToolSet(); | ||
| const searchTool = toolset.getSearchTool(); | ||
@@ -380,3 +327,3 @@ | ||
| // Execute a discovered tool directly | ||
| const listTool = tools.getTool('bamboohr_list_employees'); | ||
| const listTool = tools.getTool('workday_list_workers'); | ||
| const result = await listTool.execute({ query: { limit: 10 } }); | ||
@@ -387,3 +334,3 @@ ``` | ||
| Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the tool is called `bamboohr_create_employee`. | ||
| Discover tools using natural language instead of exact names. Queries like "onboard new hire" resolve to the right actions even when the tool is called `workday_create_employee`. | ||
@@ -393,3 +340,4 @@ ```typescript | ||
| const toolset = new StackOneToolSet({ accountId: 'your-account-id' }); | ||
| // Reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from environment | ||
| const toolset = new StackOneToolSet(); | ||
@@ -440,3 +388,3 @@ // Search by intent — returns Tools collection ready for any framework | ||
| const tools = await toolset.fetchTools(); | ||
| const employeeTool = tools.getTool('bamboohr_list_employees'); | ||
| const employeeTool = tools.getTool('workday_list_workers'); | ||
@@ -509,3 +457,3 @@ // Use dryRun to see the request details | ||
| account_id: 'acc_123456', | ||
| tool_names: ['bamboohr_list_employees', 'bamboohr_create_time_off'], | ||
| tool_names: ['workday_list_workers', 'workday_create_time_off_request'], | ||
| }); | ||
@@ -523,3 +471,3 @@ ``` | ||
| account_id: 'acc_123456', | ||
| tool_names: ['bamboohr_list_employees', 'bamboohr_create_time_off'], | ||
| tool_names: ['workday_list_workers', 'workday_create_time_off_request'], | ||
| }); | ||
@@ -531,3 +479,3 @@ | ||
| account_id: ['acc_123456', 'acc_789012'], | ||
| tool_names: ['bamboohr_list_employees', 'bamboohr_create_time_off'], | ||
| tool_names: ['workday_list_workers', 'workday_create_time_off_request'], | ||
| }); | ||
@@ -571,2 +519,20 @@ ``` | ||
| ## Examples | ||
| ### Running Examples | ||
| ```bash | ||
| # 1. Set up credentials | ||
| cp .env.example .env | ||
| # Edit .env with your API keys | ||
| # 2. Install dependencies | ||
| pnpm install | ||
| # 3. Run any example | ||
| pnpm run:example examples/openai-integration.ts | ||
| ``` | ||
| See the [examples/](examples/) directory for the full list. | ||
| ## Development Environment | ||
@@ -573,0 +539,0 @@ |
| /** | ||
| * This example demonstrates the search and execute tools pattern (tool_search + tool_execute) | ||
| * for LLM-driven tool discovery and execution. | ||
| * | ||
| * Instead of loading all tools upfront, the LLM autonomously searches for | ||
| * relevant tools and executes them — keeping token usage minimal. | ||
| * | ||
| * @example | ||
| * ```bash | ||
| * # Run with required environment variables: | ||
| * STACKONE_API_KEY=your-key OPENAI_API_KEY=your-key STACKONE_ACCOUNT_ID=your-account npx tsx examples/agent-tool-search.ts | ||
| * ``` | ||
| */ | ||
| import process from 'node:process'; | ||
| import { openai } from '@ai-sdk/openai'; | ||
| import { StackOneToolSet } from '@stackone/ai'; | ||
| import { generateText, stepCountIs } from 'ai'; | ||
| import OpenAI from 'openai'; | ||
| const apiKey = process.env.STACKONE_API_KEY; | ||
| if (!apiKey) { | ||
| console.error('STACKONE_API_KEY environment variable is required'); | ||
| process.exit(1); | ||
| } | ||
| if (!process.env.OPENAI_API_KEY) { | ||
| console.error('OPENAI_API_KEY environment variable is required'); | ||
| process.exit(1); | ||
| } | ||
| const accountId = process.env.STACKONE_ACCOUNT_ID; | ||
| /** | ||
| * Example 1: Search and execute with Vercel AI SDK | ||
| * | ||
| * The LLM receives only tool_search and tool_execute — two small tool definitions | ||
| * regardless of how many tools exist. It searches for what it needs and executes. | ||
| */ | ||
| const toolsWithAISDK = async (): Promise<void> => { | ||
| console.log('Example 1: Search and execute with Vercel AI SDK\n'); | ||
| const toolset = new StackOneToolSet({ | ||
| search: { method: 'semantic', topK: 3 }, | ||
| ...(accountId ? { accountId } : {}), | ||
| }); | ||
| // Get search and execute tools — returns a Tools collection with tool_search + tool_execute | ||
| const accountIds = accountId ? [accountId] : []; | ||
| const tools = toolset.getTools({ accountIds }); | ||
| console.log( | ||
| `Search and execute: ${tools | ||
| .toArray() | ||
| .map((t) => t.name) | ||
| .join(', ')}`, | ||
| ); | ||
| console.log(); | ||
| // Pass to the LLM — it will search for calendly tools, then execute | ||
| const { text, steps } = await generateText({ | ||
| model: openai('gpt-5.4'), | ||
| tools: await tools.toAISDK(), | ||
| prompt: 'List my upcoming Calendly events for the next week.', | ||
| stopWhen: stepCountIs(10), | ||
| }); | ||
| console.log('AI Response:', text); | ||
| console.log('\nSteps taken:'); | ||
| for (const step of steps) { | ||
| for (const call of step.toolCalls ?? []) { | ||
| const args = (call as unknown as Record<string, unknown>).args; | ||
| const argsStr = args ? JSON.stringify(args).slice(0, 100) : '{}'; | ||
| console.log(` - ${call.toolName}(${argsStr})`); | ||
| } | ||
| } | ||
| }; | ||
| /** | ||
| * Example 2: Search and execute with OpenAI Chat Completions | ||
| * | ||
| * Same pattern, different framework. The search and execute tools convert to any format. | ||
| */ | ||
| const toolsWithOpenAI = async (): Promise<void> => { | ||
| console.log('\nExample 2: Search and execute with OpenAI Chat Completions\n'); | ||
| const toolset = new StackOneToolSet({ | ||
| search: { method: 'semantic', topK: 3 }, | ||
| ...(accountId ? { accountId } : {}), | ||
| }); | ||
| const accountIds = accountId ? [accountId] : []; | ||
| const tools = toolset.getTools({ accountIds }); | ||
| const openaiTools = tools.toOpenAI(); | ||
| const client = new OpenAI(); | ||
| const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ | ||
| { | ||
| role: 'system', | ||
| content: | ||
| 'You are a helpful scheduling assistant. Use tool_search to find relevant tools, then tool_execute to run them. Always read the parameter schemas from tool_search results carefully. If a tool needs a user URI, first search for and call a "get current user" tool to obtain it. If a tool execution fails, try different parameters or a different tool.', | ||
| }, | ||
| { | ||
| role: 'user', | ||
| content: 'Check my upcoming Calendly events and list them.', | ||
| }, | ||
| ]; | ||
| // Agent loop — let the LLM drive search and execution | ||
| const maxIterations = 10; | ||
| for (let i = 0; i < maxIterations; i++) { | ||
| const response = await client.chat.completions.create({ | ||
| model: 'gpt-5.4', | ||
| messages, | ||
| tools: openaiTools, | ||
| tool_choice: 'auto', | ||
| }); | ||
| const choice = response.choices[0]; | ||
| if (!choice.message.tool_calls?.length) { | ||
| console.log('Final response:', choice.message.content); | ||
| break; | ||
| } | ||
| // Add assistant message with tool calls | ||
| messages.push(choice.message); | ||
| // Execute each tool call | ||
| for (const toolCall of choice.message.tool_calls) { | ||
| if (toolCall.type !== 'function') { | ||
| continue; | ||
| } | ||
| console.log(`LLM called: ${toolCall.function.name}(${toolCall.function.arguments})`); | ||
| const tool = tools.getTool(toolCall.function.name); | ||
| if (!tool) { | ||
| messages.push({ | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| content: JSON.stringify({ error: `Unknown tool: ${toolCall.function.name}` }), | ||
| }); | ||
| continue; | ||
| } | ||
| const result = await tool.execute(toolCall.function.arguments); | ||
| messages.push({ | ||
| role: 'tool', | ||
| tool_call_id: toolCall.id, | ||
| content: JSON.stringify(result), | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
| // Main execution | ||
| const main = async (): Promise<void> => { | ||
| try { | ||
| await toolsWithAISDK(); | ||
| await toolsWithOpenAI(); | ||
| } catch (error) { | ||
| console.error('Error running examples:', error); | ||
| } | ||
| }; | ||
| await main(); |
| /** | ||
| * E2E test for ai-sdk-integration.ts example | ||
| * | ||
| * Tests the complete flow of using StackOne tools with the AI SDK. | ||
| */ | ||
| import { openai } from '@ai-sdk/openai'; | ||
| import { generateText, stepCountIs } from 'ai'; | ||
| import { TEST_BASE_URL } from '../mocks/constants'; | ||
| import { StackOneToolSet } from '../src'; | ||
| describe('ai-sdk-integration example e2e', () => { | ||
| beforeEach(() => { | ||
| vi.stubEnv('STACKONE_API_KEY', 'test-key'); | ||
| vi.stubEnv('OPENAI_API_KEY', 'test-openai-key'); | ||
| }); | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
| it('should fetch tools, convert to AI SDK format, and generate text with tool calls', async () => { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-bamboohr-account-id', | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| // Fetch all tools for this account via MCP | ||
| const tools = await toolset.fetchTools(); | ||
| expect(tools.length).toBeGreaterThan(0); | ||
| // Convert to AI SDK tools | ||
| const aiSdkTools = await tools.toAISDK(); | ||
| expect(aiSdkTools).toBeDefined(); | ||
| expect(Object.keys(aiSdkTools).length).toBeGreaterThan(0); | ||
| // Verify the tools have the expected structure | ||
| const toolNames = Object.keys(aiSdkTools); | ||
| expect(toolNames).toContain('bamboohr_list_employees'); | ||
| expect(toolNames).toContain('bamboohr_get_employee'); | ||
| // The AI SDK will automatically call the tool if needed | ||
| const { text } = await generateText({ | ||
| model: openai('gpt-5'), | ||
| tools: aiSdkTools, | ||
| prompt: 'Get all details about employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', | ||
| stopWhen: stepCountIs(3), | ||
| }); | ||
| // The mocked OpenAI response includes 'Michael' in the text | ||
| expect(text).toContain('Michael'); | ||
| }); | ||
| }); |
| /** | ||
| * Benchmark: measure SDK search latency with caching. | ||
| * | ||
| * Runs fetchTools, local (BM25+TF-IDF) search, and semantic search N times, | ||
| * reports cold vs warm average latency and the speedup from caching. | ||
| * | ||
| * Prerequisites: | ||
| * - STACKONE_API_KEY environment variable | ||
| * - STACKONE_ACCOUNT_ID environment variable | ||
| * | ||
| * Run with: | ||
| * STACKONE_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/benchmark-search.ts | ||
| * STACKONE_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/benchmark-search.ts --iterations 50 | ||
| */ | ||
| import process from 'node:process'; | ||
| import { StackOneToolSet } from '@stackone/ai'; | ||
| const QUERIES = [ | ||
| 'list events', | ||
| 'cancel a meeting', | ||
| 'send a message', | ||
| 'get current user', | ||
| 'list employees', | ||
| ]; | ||
| function parseArgs(): number { | ||
| const idx = process.argv.indexOf('--iterations'); | ||
| const idxShort = process.argv.indexOf('-n'); | ||
| const pos = idx !== -1 ? idx : idxShort; | ||
| if (pos !== -1 && process.argv[pos + 1]) { | ||
| return Number.parseInt(process.argv[pos + 1], 10); | ||
| } | ||
| return 100; | ||
| } | ||
| async function bench( | ||
| fn: () => Promise<unknown>, | ||
| n: number, | ||
| ): Promise<{ cold: number; warmAvg: number }> { | ||
| const times: number[] = []; | ||
| for (let i = 0; i < n; i++) { | ||
| const start = performance.now(); | ||
| await fn(); | ||
| times.push(performance.now() - start); | ||
| } | ||
| const cold = times[0]; | ||
| const warmTimes = times.slice(1); | ||
| const warmAvg = | ||
| warmTimes.length > 0 ? warmTimes.reduce((a, b) => a + b, 0) / warmTimes.length : cold; | ||
| return { cold, warmAvg }; | ||
| } | ||
| function fmtMs(ms: number): string { | ||
| return `${ms.toFixed(1)}ms`.padStart(10); | ||
| } | ||
| async function main(): Promise<void> { | ||
| const iterations = parseArgs(); | ||
| const apiKey = process.env.STACKONE_API_KEY; | ||
| const accountId = process.env.STACKONE_ACCOUNT_ID; | ||
| if (!apiKey) { | ||
| console.error('Set STACKONE_API_KEY to run this benchmark.'); | ||
| process.exit(1); | ||
| } | ||
| if (!accountId) { | ||
| console.error('Set STACKONE_ACCOUNT_ID to run this benchmark.'); | ||
| process.exit(1); | ||
| } | ||
| console.log( | ||
| `Benchmarking with account ${accountId.slice(0, 8)}..., ${iterations} iterations each\n`, | ||
| ); | ||
| const ts = new StackOneToolSet({ | ||
| apiKey, | ||
| accountId, | ||
| search: { method: 'auto', topK: 5 }, | ||
| }); | ||
| const results: Array<{ name: string; cold: number; warmAvg: number; speedup: number }> = []; | ||
| let queryIdx = 0; | ||
| const nextQuery = (): string => QUERIES[queryIdx++ % QUERIES.length]; | ||
| // --- 1. fetchTools --- | ||
| console.log(`[1/3] fetchTools x${iterations} ...`); | ||
| ts.clearCatalogCache(); | ||
| const fetch = await bench(() => ts.fetchTools(), iterations); | ||
| const fetchSpeedup = fetch.cold / fetch.warmAvg; | ||
| results.push({ name: 'fetchTools', ...fetch, speedup: fetchSpeedup }); | ||
| console.log( | ||
| ` cold=${fmtMs(fetch.cold)} warm_avg=${fmtMs(fetch.warmAvg)} speedup=${fetchSpeedup.toFixed(0)}x`, | ||
| ); | ||
| // --- 2. local search (BM25 + TF-IDF) --- | ||
| console.log(`[2/3] searchTools (local) x${iterations} ...`); | ||
| ts.clearCatalogCache(); | ||
| queryIdx = 0; | ||
| const local = await bench(() => ts.searchTools(nextQuery(), { search: 'local' }), iterations); | ||
| const localSpeedup = local.cold / local.warmAvg; | ||
| results.push({ name: 'search (local/BM25)', ...local, speedup: localSpeedup }); | ||
| console.log( | ||
| ` cold=${fmtMs(local.cold)} warm_avg=${fmtMs(local.warmAvg)} speedup=${localSpeedup.toFixed(0)}x`, | ||
| ); | ||
| // --- 3. semantic search (auto) --- | ||
| console.log(`[3/3] searchTools (semantic/auto) x${iterations} ...`); | ||
| ts.clearCatalogCache(); | ||
| queryIdx = 0; | ||
| const semantic = await bench(() => ts.searchTools(nextQuery(), { search: 'auto' }), iterations); | ||
| const semanticSpeedup = semantic.cold / semantic.warmAvg; | ||
| results.push({ name: 'search (semantic)', ...semantic, speedup: semanticSpeedup }); | ||
| console.log( | ||
| ` cold=${fmtMs(semantic.cold)} warm_avg=${fmtMs(semantic.warmAvg)} speedup=${semanticSpeedup.toFixed(0)}x`, | ||
| ); | ||
| // --- Summary --- | ||
| console.log('\n' + '='.repeat(65)); | ||
| console.log( | ||
| `${'Benchmark'.padEnd(22)} ${'Cold'.padStart(10)} ${'Warm (avg)'.padStart(10)} ${'Speedup'.padStart(10)}`, | ||
| ); | ||
| console.log('-'.repeat(65)); | ||
| for (const r of results) { | ||
| console.log( | ||
| `${r.name.padEnd(22)} ${fmtMs(r.cold)} ${fmtMs(r.warmAvg)} ${`${r.speedup.toFixed(0)}x`.padStart(10)}`, | ||
| ); | ||
| } | ||
| console.log('='.repeat(65)); | ||
| console.log(`\nWarm = average of ${iterations - 1} calls after the first (cold) call.`); | ||
| console.log('Speedup = cold / warm_avg — shows the benefit of caching.\n'); | ||
| } | ||
| void main(); |
| /** | ||
| * E2E test for claude-agent-sdk-integration.ts example | ||
| * | ||
| * Tests the setup of StackOne tools with Claude Agent SDK. | ||
| * | ||
| * Note: The Claude Agent SDK spawns a subprocess to run claude-code, which | ||
| * requires the ANTHROPIC_API_KEY environment variable and a running claude-code | ||
| * installation. This test validates the tool setup and MCP server creation, | ||
| * but does not test the actual query execution. | ||
| */ | ||
| import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'; | ||
| import { z } from 'zod'; | ||
| import { TEST_BASE_URL } from '../mocks/constants'; | ||
| import { StackOneToolSet } from '../src'; | ||
| describe('claude-agent-sdk-integration example e2e', () => { | ||
| beforeEach(() => { | ||
| vi.stubEnv('STACKONE_API_KEY', 'test-key'); | ||
| }); | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
| it('should fetch tools and create Claude Agent SDK tool wrapper', async () => { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-bamboohr-account-id', | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| // Fetch all tools for this account via MCP | ||
| const tools = await toolset.fetchTools(); | ||
| expect(tools.length).toBeGreaterThan(0); | ||
| // Get a specific tool | ||
| const employeeTool = tools.getTool('bamboohr_get_employee'); | ||
| expect(employeeTool).toBeDefined(); | ||
| assert(employeeTool !== undefined); | ||
| // Create Claude Agent SDK tool from StackOne tool | ||
| const getEmployeeTool = tool( | ||
| employeeTool.name, | ||
| employeeTool.description, | ||
| { | ||
| id: z.string().describe('The employee ID'), | ||
| }, | ||
| async (args) => { | ||
| const result = await employeeTool.execute(args); | ||
| return { | ||
| content: [{ type: 'text' as const, text: JSON.stringify(result) }], | ||
| }; | ||
| }, | ||
| ); | ||
| expect(getEmployeeTool.name).toBe('bamboohr_get_employee'); | ||
| expect(getEmployeeTool.description).toContain('employee'); | ||
| expect(getEmployeeTool.inputSchema).toHaveProperty('id'); | ||
| expect(typeof getEmployeeTool.handler).toBe('function'); | ||
| }); | ||
| it('should create MCP server with StackOne tools', async () => { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-bamboohr-account-id', | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| const tools = await toolset.fetchTools(); | ||
| const employeeTool = tools.getTool('bamboohr_get_employee'); | ||
| assert(employeeTool !== undefined); | ||
| // Create Claude Agent SDK tool | ||
| const getEmployeeTool = tool( | ||
| employeeTool.name, | ||
| employeeTool.description, | ||
| { | ||
| id: z.string().describe('The employee ID'), | ||
| }, | ||
| async (args) => { | ||
| const result = await employeeTool.execute(args); | ||
| return { | ||
| content: [{ type: 'text' as const, text: JSON.stringify(result) }], | ||
| }; | ||
| }, | ||
| ); | ||
| // Create an MCP server with the StackOne tool | ||
| const mcpServer = createSdkMcpServer({ | ||
| name: 'stackone-tools', | ||
| version: '1.0.0', | ||
| tools: [getEmployeeTool], | ||
| }); | ||
| // Verify MCP server was created | ||
| expect(mcpServer).toBeDefined(); | ||
| expect(mcpServer.name).toBe('stackone-tools'); | ||
| expect(mcpServer.instance).toBeDefined(); | ||
| }); | ||
| it('should execute tool handler directly', async () => { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-bamboohr-account-id', | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| const tools = await toolset.fetchTools(); | ||
| const employeeTool = tools.getTool('bamboohr_get_employee'); | ||
| assert(employeeTool !== undefined); | ||
| // Create Claude Agent SDK tool | ||
| const getEmployeeTool = tool( | ||
| employeeTool.name, | ||
| employeeTool.description, | ||
| { | ||
| id: z.string().describe('The employee ID'), | ||
| }, | ||
| async (args) => { | ||
| const result = await employeeTool.execute(args); | ||
| return { | ||
| content: [{ type: 'text' as const, text: JSON.stringify(result) }], | ||
| }; | ||
| }, | ||
| ); | ||
| // Execute the tool handler directly | ||
| const result = await getEmployeeTool.handler( | ||
| { id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA' }, | ||
| {} as unknown, | ||
| ); | ||
| expect(result).toBeDefined(); | ||
| expect(result.content).toHaveLength(1); | ||
| expect(result.content[0]?.type).toBe('text'); | ||
| // Parse the result text and verify it contains employee data | ||
| const textContent = result.content[0]; | ||
| assert(textContent?.type === 'text'); | ||
| const data = JSON.parse(textContent.text) as unknown; | ||
| expect(data).toHaveProperty('data'); | ||
| }); | ||
| }); |
| /** | ||
| * Fetch Tools Debug CLI | ||
| * | ||
| * 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/fetch-tools-debug.ts | ||
| * ``` | ||
| */ | ||
| import process from 'node:process'; | ||
| import * as clack from '@clack/prompts'; | ||
| import { StackOneToolSet } from '@stackone/ai'; | ||
| /** | ||
| * Mask a sensitive value, showing only the first few and last few characters | ||
| */ | ||
| function maskValue(value: string, visibleStart = 4, visibleEnd = 4): string { | ||
| if (value.length <= visibleStart + visibleEnd) { | ||
| return '*'.repeat(value.length); | ||
| } | ||
| const start = value.slice(0, visibleStart); | ||
| const end = value.slice(-visibleEnd); | ||
| const masked = '*'.repeat(Math.min(value.length - visibleStart - visibleEnd, 8)); | ||
| return `${start}${masked}${end}`; | ||
| } | ||
| clack.intro('Welcome to StackOne AI Tool Tester'); | ||
| // Get API key | ||
| let apiKey: string; | ||
| const envApiKey = process.env.STACKONE_API_KEY; | ||
| if (envApiKey) { | ||
| const apiKeyChoice = await clack.select({ | ||
| message: 'StackOne API Key:', | ||
| options: [ | ||
| { value: 'env', label: 'Use environment variable', hint: maskValue(envApiKey) }, | ||
| { value: 'input', label: 'Enter manually' }, | ||
| ], | ||
| }); | ||
| if (clack.isCancel(apiKeyChoice)) { | ||
| clack.cancel('Operation cancelled'); | ||
| process.exit(0); | ||
| } | ||
| if (apiKeyChoice === 'env') { | ||
| apiKey = envApiKey; | ||
| } else { | ||
| 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); | ||
| } | ||
| apiKey = apiKeyInput; | ||
| } | ||
| } else { | ||
| 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); | ||
| } | ||
| apiKey = apiKeyInput; | ||
| } | ||
| // Get base URL | ||
| let baseUrl: string; | ||
| const envBaseUrl = process.env.STACKONE_BASE_URL; | ||
| if (envBaseUrl) { | ||
| const baseUrlChoice = await clack.select({ | ||
| message: 'StackOne Base URL:', | ||
| options: [ | ||
| { value: 'env', label: 'Use environment variable', hint: maskValue(envBaseUrl, 8, 8) }, | ||
| { value: 'input', label: 'Enter manually' }, | ||
| ], | ||
| }); | ||
| if (clack.isCancel(baseUrlChoice)) { | ||
| clack.cancel('Operation cancelled'); | ||
| process.exit(0); | ||
| } | ||
| if (baseUrlChoice === 'env') { | ||
| baseUrl = envBaseUrl; | ||
| } else { | ||
| const baseUrlInput = await clack.text({ | ||
| message: 'Enter StackOne Base URL:', | ||
| placeholder: 'https://api.stackone.com', | ||
| defaultValue: 'https://api.stackone.com', | ||
| }); | ||
| if (clack.isCancel(baseUrlInput)) { | ||
| clack.cancel('Operation cancelled'); | ||
| process.exit(0); | ||
| } | ||
| baseUrl = baseUrlInput; | ||
| } | ||
| } else { | ||
| 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); | ||
| } | ||
| baseUrl = baseUrlInput; | ||
| } | ||
| // Get account ID | ||
| let accountId: string; | ||
| const envAccountId = process.env.STACKONE_ACCOUNT_ID; | ||
| if (envAccountId) { | ||
| const accountIdChoice = await clack.select({ | ||
| message: 'StackOne Account ID:', | ||
| options: [ | ||
| { value: 'env', label: 'Use environment variable', hint: maskValue(envAccountId) }, | ||
| { value: 'input', label: 'Enter manually' }, | ||
| ], | ||
| }); | ||
| if (clack.isCancel(accountIdChoice)) { | ||
| clack.cancel('Operation cancelled'); | ||
| process.exit(0); | ||
| } | ||
| if (accountIdChoice === 'env') { | ||
| accountId = envAccountId; | ||
| } else { | ||
| 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); | ||
| } | ||
| accountId = accountIdInput as string; | ||
| } | ||
| } else { | ||
| 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); | ||
| } | ||
| accountId = accountIdInput as string; | ||
| } | ||
| // @ts-expect-error Bun global is not in Node.js types | ||
| if ((typeof globalThis.Bun as any) !== 'undefined') { | ||
| const detailedLog = await clack.confirm({ | ||
| message: 'Enable detailed logging? (recommended for Bun.js users)', | ||
| }); | ||
| if (clack.isCancel(detailedLog)) { | ||
| clack.cancel('Operation cancelled'); | ||
| process.exit(0); | ||
| } | ||
| if (detailedLog) { | ||
| process.env.BUN_CONFIG_VERBOSE_FETCH = 'curl'; | ||
| } | ||
| } | ||
| const spinner = clack.spinner(); | ||
| spinner.start('Initializing 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:'); | ||
| // Display result based on its structure | ||
| if (Array.isArray(result)) { | ||
| // For array results, use console.table for better readability | ||
| if (result.length > 0 && typeof result[0] === 'object') { | ||
| console.table(result); | ||
| } else { | ||
| console.log(result); | ||
| } | ||
| } else if (result && typeof result === 'object') { | ||
| // Check if result has a data array property (common API response pattern) | ||
| const data = (result as Record<string, unknown>).data; | ||
| if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') { | ||
| console.log('\nData:'); | ||
| console.table(data); | ||
| // Show other properties | ||
| const otherProps = Object.fromEntries( | ||
| Object.entries(result as Record<string, unknown>).filter(([key]) => key !== 'data'), | ||
| ); | ||
| if (Object.keys(otherProps).length > 0) { | ||
| console.log('\nMetadata:'); | ||
| console.log(JSON.stringify(otherProps, null, 2)); | ||
| } | ||
| } else { | ||
| console.log(JSON.stringify(result, null, 2)); | ||
| } | ||
| } else { | ||
| console.log(result); | ||
| } | ||
| clack.outro('Done!'); | ||
| } catch (error) { | ||
| spinner.stop('Execution failed'); | ||
| if (error instanceof Error) { | ||
| clack.log.error(`Error: ${error.message}`); | ||
| if (error.cause) { | ||
| clack.log.info(`Cause: ${JSON.stringify(error.cause, null, 2)}`); | ||
| } | ||
| if (error.stack) { | ||
| clack.log.info(`Stack trace:\n${error.stack}`); | ||
| } | ||
| } else { | ||
| clack.log.error(`Error: ${JSON.stringify(error, null, 2)}`); | ||
| } | ||
| clack.outro('Failed'); | ||
| process.exit(1); | ||
| } |
| /** | ||
| * E2E test for fetch-tools.ts example | ||
| * | ||
| * Tests the complete flow of fetching and filtering tools via MCP. | ||
| */ | ||
| import { http, HttpResponse } from 'msw'; | ||
| import { server } from '../mocks/node'; | ||
| import { TEST_BASE_URL } from '../mocks/constants'; | ||
| import { StackOneToolSet } from '../src'; | ||
| describe('fetch-tools example e2e', () => { | ||
| beforeEach(() => { | ||
| vi.stubEnv('STACKONE_API_KEY', 'test-key'); | ||
| }); | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
| it('should fetch tools, filter by various criteria, and execute a tool', async () => { | ||
| // Setup RPC handler for tool execution | ||
| server.use( | ||
| http.post(`${TEST_BASE_URL}/actions/rpc`, async ({ request }) => { | ||
| const body: unknown = await request.json(); | ||
| assert(typeof body === 'object' && body !== null); | ||
| const { action } = body as Record<string, unknown>; | ||
| if (action === 'bamboohr_list_employees') { | ||
| return HttpResponse.json({ | ||
| data: [ | ||
| { id: '1', name: 'Employee 1' }, | ||
| { id: '2', name: 'Employee 2' }, | ||
| { id: '3', name: 'Employee 3' }, | ||
| { id: '4', name: 'Employee 4' }, | ||
| { id: '5', name: 'Employee 5' }, | ||
| ], | ||
| }); | ||
| } | ||
| return HttpResponse.json({ data: {} }); | ||
| }), | ||
| ); | ||
| const toolset = new StackOneToolSet({ | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| // Example 1: Fetch all tools (without account filter) | ||
| const allTools = await toolset.fetchTools(); | ||
| expect(allTools.length).toBeGreaterThan(0); | ||
| // Example 2: Filter by account IDs using setAccounts() | ||
| toolset.setAccounts(['your-bamboohr-account-id']); | ||
| const toolsByAccounts = await toolset.fetchTools(); | ||
| expect(toolsByAccounts.length).toBeGreaterThan(0); | ||
| // Example 3: Filter by account IDs using options | ||
| const toolsByAccountsOption = await toolset.fetchTools({ | ||
| accountIds: ['your-bamboohr-account-id'], | ||
| }); | ||
| expect(toolsByAccountsOption.length).toBeGreaterThan(0); | ||
| // Example 4: Filter by providers | ||
| const toolsByProviders = await toolset.fetchTools({ | ||
| accountIds: ['your-bamboohr-account-id'], | ||
| providers: ['bamboohr'], | ||
| }); | ||
| expect(toolsByProviders.length).toBeGreaterThan(0); | ||
| const providerToolNames = toolsByProviders.toArray().map((t) => t.name); | ||
| expect( | ||
| providerToolNames.every((name) => name.startsWith('bamboohr_') || name.startsWith('tool_')), | ||
| ).toBe(true); | ||
| // Example 5: Filter by actions with exact match | ||
| const toolsByActions = await toolset.fetchTools({ | ||
| accountIds: ['your-bamboohr-account-id'], | ||
| actions: ['bamboohr_list_employees', 'bamboohr_create_employee'], | ||
| }); | ||
| const actionToolNames = toolsByActions.toArray().map((t) => t.name); | ||
| expect(actionToolNames).toContain('bamboohr_list_employees'); | ||
| expect(actionToolNames).toContain('bamboohr_create_employee'); | ||
| // Example 6: Filter by actions with glob pattern | ||
| const toolsByGlobPattern = await toolset.fetchTools({ | ||
| accountIds: ['your-bamboohr-account-id'], | ||
| actions: ['*_list_employees'], | ||
| }); | ||
| const globToolNames = toolsByGlobPattern | ||
| .toArray() | ||
| .filter((t) => !t.name.startsWith('tool_')) | ||
| .map((t) => t.name); | ||
| expect(globToolNames).toContain('bamboohr_list_employees'); | ||
| // Execute a tool | ||
| const tool = toolsByAccounts.getTool('bamboohr_list_employees'); | ||
| expect(tool).toBeDefined(); | ||
| const result = await tool!.execute({ | ||
| query: { limit: 5 }, | ||
| }); | ||
| expect(result.data).toBeDefined(); | ||
| expect(Array.isArray(result.data)).toBe(true); | ||
| }); | ||
| }); |
| /** | ||
| * Example: fetch the latest StackOne tool catalog with filtering options. | ||
| * | ||
| * Set `STACKONE_API_KEY` before running. | ||
| * By default the script exits early in test environments where a real key is | ||
| * not available. | ||
| */ | ||
| import process from 'node:process'; | ||
| import { StackOneToolSet } from '@stackone/ai'; | ||
| const apiKey = process.env.STACKONE_API_KEY; | ||
| if (!apiKey) { | ||
| console.error('STACKONE_API_KEY environment variable is required'); | ||
| process.exit(1); | ||
| } | ||
| const toolset = new StackOneToolSet({}); | ||
| // Example 1: Fetch all tools | ||
| console.log('\n=== Example 1: Fetch all tools ==='); | ||
| const allTools = await toolset.fetchTools(); | ||
| console.log(`Loaded ${allTools.length} tools`); | ||
| // Example 2: Filter by account IDs using setAccounts() | ||
| console.log('\n=== Example 2: Filter by account IDs (using setAccounts) ==='); | ||
| toolset.setAccounts(['account-123', 'account-456']); | ||
| const toolsByAccounts = await toolset.fetchTools(); | ||
| console.log(`Loaded ${toolsByAccounts.length} tools for specified accounts`); | ||
| // Example 3: Filter by account IDs using options | ||
| console.log('\n=== Example 3: Filter by account IDs (using options) ==='); | ||
| const toolsByAccountsOption = await toolset.fetchTools({ | ||
| accountIds: ['your-account-id'], | ||
| }); | ||
| console.log(`Loaded ${toolsByAccountsOption.length} tools for your-account-id`); | ||
| // Example 4: Filter by providers | ||
| console.log('\n=== Example 4: Filter by providers ==='); | ||
| const toolsByProviders = await toolset.fetchTools({ | ||
| providers: ['hibob', 'bamboohr'], | ||
| }); | ||
| console.log(`Loaded ${toolsByProviders.length} tools for HiBob and BambooHR`); | ||
| // Example 5: Filter by actions with exact match | ||
| console.log('\n=== Example 5: Filter by actions (exact match) ==='); | ||
| const toolsByActions = await toolset.fetchTools({ | ||
| actions: ['bamboohr_list_employees', 'bamboohr_create_employee'], | ||
| }); | ||
| console.log(`Loaded ${toolsByActions.length} tools matching exact action names`); | ||
| // Example 6: Filter by actions with glob pattern | ||
| console.log('\n=== Example 6: Filter by actions (glob pattern) ==='); | ||
| const toolsByGlobPattern = await toolset.fetchTools({ | ||
| actions: ['*_list_employees'], | ||
| }); | ||
| console.log(`Loaded ${toolsByGlobPattern.length} tools matching *_list_employees pattern`); | ||
| // Example 7: Combine multiple filters | ||
| console.log('\n=== Example 7: Combine multiple filters ==='); | ||
| const toolsCombined = await toolset.fetchTools({ | ||
| accountIds: ['account-123'], | ||
| providers: ['hibob'], | ||
| actions: ['*_list_*'], | ||
| }); | ||
| console.log( | ||
| `Loaded ${toolsCombined.length} tools for account-123, provider hibob, matching *_list_* pattern`, | ||
| ); | ||
| // Execute a tool | ||
| console.log('\n=== Executing a tool ==='); | ||
| const tool = allTools.getTool('bamboohr_list_employees'); | ||
| if (!tool) { | ||
| throw new Error('Tool bamboohr_list_employees not found in the catalog'); | ||
| } | ||
| const result = await tool.execute({ | ||
| query: { limit: 5 }, | ||
| }); | ||
| console.log('Sample execution result:', JSON.stringify(result, null, 2)); |
| /** | ||
| * E2E test for openai-integration.ts example | ||
| * | ||
| * Tests the complete flow of using StackOne tools with OpenAI Chat Completions API. | ||
| */ | ||
| import OpenAI from 'openai'; | ||
| import { TEST_BASE_URL } from '../mocks/constants'; | ||
| import { StackOneToolSet } from '../src'; | ||
| describe('openai-integration example e2e', () => { | ||
| beforeEach(() => { | ||
| vi.stubEnv('STACKONE_API_KEY', 'test-key'); | ||
| vi.stubEnv('OPENAI_API_KEY', 'test-openai-key'); | ||
| }); | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
| it('should fetch tools, convert to OpenAI format, and create chat completion with tool calls', async () => { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-bamboohr-account-id', | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| // Fetch all tools for this account via MCP | ||
| const tools = await toolset.fetchTools(); | ||
| const openAITools = tools.toOpenAI(); | ||
| // Verify tools are in OpenAI format | ||
| expect(Array.isArray(openAITools)).toBe(true); | ||
| expect(openAITools.length).toBeGreaterThan(0); | ||
| expect(openAITools[0]).toHaveProperty('type', 'function'); | ||
| expect(openAITools[0]).toHaveProperty('function'); | ||
| // Initialize OpenAI client | ||
| const openai = new OpenAI(); | ||
| // Create a chat completion with tool calls | ||
| const response = await openai.chat.completions.create({ | ||
| model: 'gpt-5', | ||
| messages: [ | ||
| { | ||
| role: 'system', | ||
| content: 'You are a helpful assistant that can access BambooHR information.', | ||
| }, | ||
| { | ||
| role: 'user', | ||
| content: | ||
| 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', | ||
| }, | ||
| ], | ||
| tools: openAITools, | ||
| }); | ||
| // Verify the response contains tool calls | ||
| expect(response.choices.length).toBeGreaterThan(0); | ||
| const choice = response.choices[0]; | ||
| expect(choice.message.tool_calls).toBeDefined(); | ||
| expect(choice.message.tool_calls!.length).toBeGreaterThan(0); | ||
| const toolCall = choice.message.tool_calls![0]; | ||
| assert(toolCall.type === 'function'); | ||
| expect(toolCall.function.name).toBe('bamboohr_get_employee'); | ||
| // Parse the arguments to verify they contain the expected fields | ||
| const args: unknown = JSON.parse(toolCall.function.arguments); | ||
| assert(typeof args === 'object' && args !== null && 'id' in args); | ||
| expect(args.id).toBe('c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA'); | ||
| }); | ||
| }); |
| /** | ||
| * E2E test for openai-responses-integration.ts example | ||
| * | ||
| * Tests the complete flow of using StackOne tools with OpenAI Responses API. | ||
| */ | ||
| import OpenAI from 'openai'; | ||
| import { TEST_BASE_URL } from '../mocks/constants'; | ||
| import { StackOneToolSet } from '../src'; | ||
| describe('openai-responses-integration example e2e', () => { | ||
| beforeEach(() => { | ||
| vi.stubEnv('STACKONE_API_KEY', 'test-key'); | ||
| vi.stubEnv('OPENAI_API_KEY', 'test-openai-key'); | ||
| }); | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
| it('should fetch tools, convert to OpenAI Responses format, and create response with tool calls', async () => { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-stackone-account-id', | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| // Fetch tools via MCP with action filter | ||
| const tools = await toolset.fetchTools({ | ||
| actions: ['*_list_*'], | ||
| }); | ||
| const openAIResponsesTools = tools.toOpenAIResponses(); | ||
| // Verify tools are in OpenAI Responses format | ||
| expect(Array.isArray(openAIResponsesTools)).toBe(true); | ||
| expect(openAIResponsesTools.length).toBeGreaterThan(0); | ||
| // Initialize OpenAI client | ||
| const openai = new OpenAI(); | ||
| // Create a response with tool calls using the Responses API | ||
| const response = await openai.responses.create({ | ||
| model: 'gpt-5', | ||
| instructions: 'You are a helpful assistant that can access various tools.', | ||
| input: 'What is the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA phone number?', | ||
| tools: openAIResponsesTools, | ||
| }); | ||
| // Verify the response contains expected data | ||
| expect(response.id).toBeDefined(); | ||
| expect(response.model).toBeDefined(); | ||
| // Check if the model made any tool calls | ||
| const toolCalls = response.output.filter( | ||
| (item): item is OpenAI.Responses.ResponseFunctionToolCall => item.type === 'function_call', | ||
| ); | ||
| expect(toolCalls.length).toBeGreaterThan(0); | ||
| const toolCall = toolCalls[0]; | ||
| expect(toolCall.name).toBe('bamboohr_get_employee'); | ||
| // Parse the arguments to verify they contain the expected fields | ||
| const args = JSON.parse(toolCall.arguments); | ||
| expect(args.id).toBe('c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA'); | ||
| }); | ||
| }); |
| /** | ||
| * E2E test for tanstack-ai-integration.ts example | ||
| * | ||
| * Tests the complete flow of using StackOne tools with TanStack AI. | ||
| * | ||
| * Note: TanStack AI requires Zod schemas for tool input validation. | ||
| * This test validates tool setup and schema conversion, but the actual | ||
| * chat() call requires Zod schemas which are not directly exposed by | ||
| * StackOne tools. | ||
| */ | ||
| import { TEST_BASE_URL } from '../mocks/constants'; | ||
| import { StackOneToolSet } from '../src'; | ||
| describe('tanstack-ai-integration example e2e', () => { | ||
| beforeEach(() => { | ||
| vi.stubEnv('STACKONE_API_KEY', 'test-key'); | ||
| vi.stubEnv('OPENAI_API_KEY', 'test-openai-key'); | ||
| }); | ||
| afterEach(() => { | ||
| vi.unstubAllEnvs(); | ||
| }); | ||
| it('should fetch tools and convert to TanStack AI format', async () => { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-bamboohr-account-id', | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| // Fetch all tools for this account via MCP | ||
| const tools = await toolset.fetchTools(); | ||
| expect(tools.length).toBeGreaterThan(0); | ||
| // Get a specific tool | ||
| const employeeTool = tools.getTool('bamboohr_get_employee'); | ||
| assert(employeeTool, 'Expected bamboohr_get_employee tool to exist'); | ||
| // Create TanStack AI compatible tool wrapper | ||
| // Use toJsonSchema() to get the parameter schema in JSON Schema format | ||
| const getEmployeeTool = { | ||
| name: employeeTool.name, | ||
| description: employeeTool.description, | ||
| inputSchema: employeeTool.toJsonSchema(), | ||
| execute: employeeTool.execute.bind(employeeTool), | ||
| }; | ||
| expect(getEmployeeTool.name).toBe('bamboohr_get_employee'); | ||
| expect(getEmployeeTool.inputSchema.type).toBe('object'); | ||
| }); | ||
| it('should execute tool directly', async () => { | ||
| const toolset = new StackOneToolSet({ | ||
| accountId: 'your-bamboohr-account-id', | ||
| baseUrl: TEST_BASE_URL, | ||
| }); | ||
| const tools = await toolset.fetchTools(); | ||
| const employeeTool = tools.getTool('bamboohr_get_employee'); | ||
| assert(employeeTool !== undefined, 'Expected to find bamboohr_get_employee tool'); | ||
| // Create TanStack AI compatible tool wrapper | ||
| const getEmployeeTool = { | ||
| name: employeeTool.name, | ||
| description: employeeTool.description, | ||
| inputSchema: employeeTool.toJsonSchema(), | ||
| execute: employeeTool.execute.bind(employeeTool), | ||
| }; | ||
| // Execute the tool directly to verify it works | ||
| const result = await getEmployeeTool.execute({ | ||
| id: 'c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', | ||
| }); | ||
| expect(result).toBeDefined(); | ||
| expect(result).toHaveProperty('data'); | ||
| }); | ||
| }); |
| /** | ||
| * This example shows how to use StackOne tools with TanStack AI. | ||
| * | ||
| * TanStack AI requires Zod schemas for tool input validation. | ||
| * This example demonstrates how to wrap StackOne tools for use with TanStack AI | ||
| * by creating Zod schemas that match the tool's JSON Schema. | ||
| */ | ||
| import assert from 'node:assert'; | ||
| import process from 'node:process'; | ||
| import { chat } from '@tanstack/ai'; | ||
| import { openaiText } from '@tanstack/ai-openai'; | ||
| import { z } from 'zod'; | ||
| import { StackOneToolSet } from '@stackone/ai'; | ||
| const apiKey = process.env.STACKONE_API_KEY; | ||
| if (!apiKey) { | ||
| console.error('STACKONE_API_KEY environment variable is required'); | ||
| process.exit(1); | ||
| } | ||
| const tanstackAiIntegration = async (): Promise<void> => { | ||
| // Initialize StackOne — reads STACKONE_API_KEY and STACKONE_ACCOUNT_ID from env | ||
| const toolset = new StackOneToolSet(); | ||
| // Fetch tools from StackOne | ||
| const tools = await toolset.fetchTools(); | ||
| // Get a specific tool and create a TanStack AI compatible tool | ||
| const employeeTool = tools.getTool('bamboohr_get_employee'); | ||
| assert(employeeTool !== undefined, 'Expected to find bamboohr_get_employee tool'); | ||
| // Create a TanStack AI server tool from the StackOne tool | ||
| // TanStack AI requires Zod schemas, so we create one that matches the tool's parameters | ||
| const getEmployeeTool = { | ||
| name: employeeTool.name, | ||
| description: employeeTool.description, | ||
| // TanStack AI requires Zod schema for input validation | ||
| inputSchema: z.object({ | ||
| id: z.string().describe('The employee ID'), | ||
| }), | ||
| execute: async (args: { id: string }) => { | ||
| return employeeTool.execute(args); | ||
| }, | ||
| }; | ||
| // Use TanStack AI chat with the tool | ||
| // The adapter reads OPENAI_API_KEY from the environment automatically | ||
| const adapter = openaiText('gpt-5'); | ||
| const stream = chat({ | ||
| adapter, | ||
| messages: [ | ||
| { | ||
| role: 'user', | ||
| content: 'Get the employee with id: c28xIQaWQ6MzM5MzczMDA2NzMzMzkwNzIwNA', | ||
| }, | ||
| ], | ||
| tools: [getEmployeeTool], | ||
| }); | ||
| // Process the stream using AG-UI protocol events | ||
| let hasToolCall = false; | ||
| for await (const chunk of stream) { | ||
| if (chunk.type === 'TOOL_CALL_START') { | ||
| hasToolCall = true; | ||
| assert( | ||
| chunk.toolName === 'bamboohr_get_employee', | ||
| 'Expected tool call to be bamboohr_get_employee', | ||
| ); | ||
| } | ||
| } | ||
| assert(hasToolCall, 'Expected at least one tool call'); | ||
| }; | ||
| await tanstackAiIntegration(); |
| /** | ||
| * Workday integration: timeout and account scoping for slow providers. | ||
| * | ||
| * Workday can take 10-15s to respond. This example shows how to configure | ||
| * timeout for slow providers. | ||
| * | ||
| * Prerequisites: | ||
| * - STACKONE_API_KEY | ||
| * - STACKONE_ACCOUNT_ID (a Workday-connected account) | ||
| * - OPENAI_API_KEY | ||
| * | ||
| * Run with: | ||
| * STACKONE_API_KEY=xxx OPENAI_API_KEY=xxx STACKONE_ACCOUNT_ID=xxx npx tsx examples/workday-integration.ts | ||
| */ | ||
| import process from 'node:process'; | ||
| import { StackOneToolSet } from '@stackone/ai'; | ||
| import OpenAI from 'openai'; | ||
| const apiKey = process.env.STACKONE_API_KEY ?? ''; | ||
| const accountId = process.env.STACKONE_ACCOUNT_ID ?? ''; | ||
| if (!apiKey) { | ||
| console.error('Set STACKONE_API_KEY to run this example.'); | ||
| process.exit(1); | ||
| } | ||
| if (!accountId) { | ||
| console.error('Set STACKONE_ACCOUNT_ID to run this example.'); | ||
| process.exit(1); | ||
| } | ||
| if (!process.env.OPENAI_API_KEY) { | ||
| console.error('Set OPENAI_API_KEY to run this example.'); | ||
| process.exit(1); | ||
| } | ||
| // Timeout for slow providers (Workday can take 10-15s) | ||
| const toolset = new StackOneToolSet({ | ||
| apiKey, | ||
| accountId, | ||
| search: { method: 'auto', topK: 5 }, | ||
| timeout: 120_000, | ||
| }); | ||
| const client = new OpenAI(); | ||
| async function runAgent( | ||
| messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[], | ||
| tools: OpenAI.Chat.Completions.ChatCompletionTool[], | ||
| maxSteps = 10, | ||
| ): Promise<void> { | ||
| for (let i = 0; i < maxSteps; i++) { | ||
| const response = await client.chat.completions.create({ model: 'gpt-5.4', messages, tools }); | ||
| const choice = response.choices[0]; | ||
| if (!choice.message.tool_calls?.length) { | ||
| console.log(`Answer: ${choice.message.content}`); | ||
| return; | ||
| } | ||
| messages.push(choice.message); | ||
| for (const tc of choice.message.tool_calls) { | ||
| if (tc.type !== 'function') continue; | ||
| console.log(` -> ${tc.function.name}(${tc.function.arguments.slice(0, 80)})`); | ||
| const searchTools = toolset.getTools({ accountIds: [accountId] }); | ||
| const tool = searchTools.getTool(tc.function.name); | ||
| const result = tool ? await tool.execute(tc.function.arguments) : { error: 'Unknown tool' }; | ||
| messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) }); | ||
| } | ||
| } | ||
| } | ||
| // --- Example 1: Search and execute mode --- | ||
| console.log('=== Search and execute mode ===\n'); | ||
| const searchTools = toolset.getTools({ accountIds: [accountId] }).toOpenAI(); | ||
| await runAgent( | ||
| [ | ||
| { role: 'system', content: 'Use tool_search to find tools, then tool_execute to run them.' }, | ||
| { role: 'user', content: 'List the first 5 employees.' }, | ||
| ], | ||
| searchTools, | ||
| ); | ||
| // --- Example 2: Normal mode --- | ||
| console.log('\n=== Normal mode ===\n'); | ||
| const tools = await toolset.fetchTools({ actions: ['workday_*_employee*'] }); | ||
| if (tools.length === 0) { | ||
| console.log('No Workday tools found for this account.'); | ||
| } else { | ||
| await runAgent([{ role: 'user', content: 'List the first 5 employees.' }], tools.toOpenAI()); | ||
| } |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 5 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 5 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
54
-6.9%32
-28.89%551498
-5.66%98
-10.09%6519
-12.74%546
-5.86%