diff --git a/package.json b/package.json index b931d48..9b6bf8b 100644 --- a/package.json +++ b/package.json @@ -64,11 +64,13 @@ "peerDependencies": { "@langchain/core": ">=1.1.31" }, - "devDependencies": { + "optionalDependencies": { "@langchain/anthropic": "^1.0.0", - "@langchain/core": "^1.1.31", "@langchain/google-genai": "^2.1.24", - "@langchain/openai": "^1.2.12", + "@langchain/openai": "^1.2.12" + }, + "devDependencies": { + "@langchain/core": "^1.1.31", "@types/jest": "^29.5.14", "@types/lodash": "^4.17.16", "@types/merge-images": "^1.2.4", diff --git a/src/cli/index.ts b/src/cli/index.ts index 94cf1b5..2a65d16 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -23,6 +23,37 @@ import { } from "@/types"; import { BrowserAgentError } from "@/agent/error"; +/** + * Dynamically load a provider SDK and surface a clear, actionable error if + * it's missing. The provider packages are declared as + * `optionalDependencies`, so they normally ship with the CLI — but a user + * who ran `npm install --omit=optional` (or whose install was interrupted) + * may not have them. In that case, tell them exactly which package to + * install instead of printing a raw `MODULE_NOT_FOUND` stack. + */ +async function loadProvider( + packageName: string, + providerLabel: string +): Promise { + try { + return (await import(packageName)) as T; + } catch (err) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const code = (err as any)?.code; + if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") { + console.error( + chalk.red( + `${providerLabel} provider is not installed.\n` + + `Install it and retry:\n\n` + + ` npm install -g ${packageName}\n` + ) + ); + process.exit(1); + } + throw err; + } +} + /** * Select an LLM based on environment variables. Providers are checked in * priority order: OpenAI, Google, Anthropic. Per-provider model is @@ -31,7 +62,9 @@ import { BrowserAgentError } from "@/agent/error"; */ async function createDefaultLlm(): Promise { if (process.env.OPENAI_API_KEY) { - const { ChatOpenAI } = await import("@langchain/openai"); + const { ChatOpenAI } = await loadProvider< + typeof import("@langchain/openai") + >("@langchain/openai", "OpenAI"); return new ChatOpenAI({ apiKey: process.env.OPENAI_API_KEY, model: process.env.OPENAI_MODEL ?? "gpt-4.1-mini", @@ -39,7 +72,9 @@ async function createDefaultLlm(): Promise { }) as unknown as BaseChatModel; } if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) { - const { ChatGoogleGenerativeAI } = await import("@langchain/google-genai"); + const { ChatGoogleGenerativeAI } = await loadProvider< + typeof import("@langchain/google-genai") + >("@langchain/google-genai", "Google Gemini"); return new ChatGoogleGenerativeAI({ apiKey: process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY, model: process.env.GEMINI_MODEL ?? "gemini-2.5-flash", @@ -47,7 +82,9 @@ async function createDefaultLlm(): Promise { }) as unknown as BaseChatModel; } if (process.env.ANTHROPIC_API_KEY) { - const { ChatAnthropic } = await import("@langchain/anthropic"); + const { ChatAnthropic } = await loadProvider< + typeof import("@langchain/anthropic") + >("@langchain/anthropic", "Anthropic"); return new ChatAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY, model: