From d9a4de9340ec8cdb38da1af4a84e07ad5fc36a06 Mon Sep 17 00:00:00 2001 From: bozicovichsantiago20-oss Date: Sun, 7 Jun 2026 13:27:06 -0300 Subject: [PATCH 1/4] fix: lazy-load optional ai peer --- src/tool.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/tool.ts b/src/tool.ts index ad1a2153..62912401 100644 --- a/src/tool.ts +++ b/src/tool.ts @@ -1,4 +1,4 @@ -import { type JSONSchema7 as AISDKJSONSchema, jsonSchema } from 'ai'; +import type { JSONSchema7 as AISDKJSONSchema } from 'ai'; import type { Tool as AnthropicTool } from '@anthropic-ai/sdk/resources'; import type { McpSdkServerConfigWithInstance } from '@anthropic-ai/claude-agent-sdk'; import type { ChatCompletionFunctionTool } from 'openai/resources/chat/completions'; @@ -248,7 +248,11 @@ export class BaseTool { args: Record, ) => Promise<{ content: Array<{ type: 'text'; text: string }> }>; }> { - const inputSchema = jsonSchema(this.toJsonSchema()); + const ai = await tryImport( + 'ai', + `npm install ai (requires ${peerDependencies.ai})`, + ); + const inputSchema = ai.jsonSchema(this.toJsonSchema()); const execute = this.execute.bind(this); return { From 3551e2ead81e0ef7aea8941feef4b7b6b584ef58 Mon Sep 17 00:00:00 2001 From: bozicovichsantiago20-oss Date: Wed, 10 Jun 2026 06:59:17 -0300 Subject: [PATCH 2/4] test: cover optional ai peer import regression --- src/tool-optional-ai.test.ts | 64 ++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/tool-optional-ai.test.ts diff --git a/src/tool-optional-ai.test.ts b/src/tool-optional-ai.test.ts new file mode 100644 index 00000000..2b1fb3cd --- /dev/null +++ b/src/tool-optional-ai.test.ts @@ -0,0 +1,64 @@ +import { readFile } from 'node:fs/promises'; +import { StackOneError } from './utils/error-stackone'; +import { peerDependencies } from '../package.json'; +import { ParameterLocation, type ExecuteConfig, type ToolParameters } from './types'; + +const createExecuteConfig = (): ExecuteConfig => ({ + kind: 'http', + method: 'GET', + url: 'https://api.example.com/test/{id}', + bodyType: 'json', + params: [ + { + name: 'id', + location: ParameterLocation.PATH, + type: 'string', + }, + ], +}); + +const createParameters = (): ToolParameters => ({ + type: 'object', + properties: { id: { type: 'string', description: 'ID parameter' } }, +}); + +describe('BaseTool optional ai peer handling', () => { + afterEach(() => { + vi.resetModules(); + vi.restoreAllMocks(); + }); + + it('rejects toClaudeAgentSdkTool with StackOneError and install hint when ai is unavailable', async () => { + vi.doMock('./utils/try-import', () => ({ + tryImport: vi.fn(async (moduleName: string, installHint: string) => { + if (moduleName === 'ai') { + throw new StackOneError( + `${moduleName} is not installed. Please install it with: ${installHint}`, + ); + } + + throw new Error(`Unexpected module request: ${moduleName}`); + }), + })); + + const { BaseTool } = await import('./tool'); + const tool = new BaseTool( + 'test_tool', + 'Test tool', + createParameters(), + createExecuteConfig(), + ); + + await expect(tool.toClaudeAgentSdkTool()).rejects.toBeInstanceOf(StackOneError); + await expect(tool.toClaudeAgentSdkTool()).rejects.toThrow( + `ai is not installed. Please install it with: npm install ai (requires ${peerDependencies.ai})`, + ); + }); + + it('keeps the built module free of a top-level ai import', async () => { + const builtToolModule = await readFile(new URL('../dist/src/tool.mjs', import.meta.url), 'utf8'); + + expect(builtToolModule).toContain('tryImport("ai"'); + expect(builtToolModule).not.toMatch(/^\s*import\s+.*['"]ai['"]/m); + }); +}); From 088252d320169fdd2be5ae777d89c1bce981bef0 Mon Sep 17 00:00:00 2001 From: Shashi-Stackone Date: Tue, 7 Jul 2026 11:23:54 +0100 Subject: [PATCH 3/4] style(test): format tool-optional-ai.test.ts with oxfmt --- src/tool-optional-ai.test.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/tool-optional-ai.test.ts b/src/tool-optional-ai.test.ts index 2b1fb3cd..df30ad81 100644 --- a/src/tool-optional-ai.test.ts +++ b/src/tool-optional-ai.test.ts @@ -42,12 +42,7 @@ describe('BaseTool optional ai peer handling', () => { })); const { BaseTool } = await import('./tool'); - const tool = new BaseTool( - 'test_tool', - 'Test tool', - createParameters(), - createExecuteConfig(), - ); + const tool = new BaseTool('test_tool', 'Test tool', createParameters(), createExecuteConfig()); await expect(tool.toClaudeAgentSdkTool()).rejects.toBeInstanceOf(StackOneError); await expect(tool.toClaudeAgentSdkTool()).rejects.toThrow( @@ -56,7 +51,10 @@ describe('BaseTool optional ai peer handling', () => { }); it('keeps the built module free of a top-level ai import', async () => { - const builtToolModule = await readFile(new URL('../dist/src/tool.mjs', import.meta.url), 'utf8'); + const builtToolModule = await readFile( + new URL('../dist/src/tool.mjs', import.meta.url), + 'utf8', + ); expect(builtToolModule).toContain('tryImport("ai"'); expect(builtToolModule).not.toMatch(/^\s*import\s+.*['"]ai['"]/m); From 17315ea166fb4f08b1d0f29b0eb82c01b22fb742 Mon Sep 17 00:00:00 2001 From: Shashi-Stackone Date: Tue, 7 Jul 2026 11:34:08 +0100 Subject: [PATCH 4/4] test: cover CJS output in optional ai peer regression test --- src/tool-optional-ai.test.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/tool-optional-ai.test.ts b/src/tool-optional-ai.test.ts index df30ad81..986d2499 100644 --- a/src/tool-optional-ai.test.ts +++ b/src/tool-optional-ai.test.ts @@ -1,8 +1,13 @@ +import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { StackOneError } from './utils/error-stackone'; import { peerDependencies } from '../package.json'; import { ParameterLocation, type ExecuteConfig, type ToolParameters } from './types'; +const builtToolEsm = new URL('../dist/src/tool.mjs', import.meta.url); +const builtToolCjs = new URL('../dist/src/tool.cjs', import.meta.url); +const distReady = existsSync(builtToolEsm) && existsSync(builtToolCjs); + const createExecuteConfig = (): ExecuteConfig => ({ kind: 'http', method: 'GET', @@ -50,13 +55,17 @@ describe('BaseTool optional ai peer handling', () => { ); }); - it('keeps the built module free of a top-level ai import', async () => { - const builtToolModule = await readFile( - new URL('../dist/src/tool.mjs', import.meta.url), - 'utf8', - ); + it.skipIf(!distReady)('keeps the built ESM module free of a top-level ai import', async () => { + const builtToolModule = await readFile(builtToolEsm, 'utf8'); expect(builtToolModule).toContain('tryImport("ai"'); expect(builtToolModule).not.toMatch(/^\s*import\s+.*['"]ai['"]/m); }); + + it.skipIf(!distReady)('keeps the built CJS module free of a top-level ai require', async () => { + const builtToolModule = await readFile(builtToolCjs, 'utf8'); + + expect(builtToolModule).toContain('tryImport("ai"'); + expect(builtToolModule).not.toMatch(/require\(["']ai["']\)/); + }); });