diff --git a/eslint.config.js b/eslint.config.js index 7839ae78f67..48af3775f26 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -239,6 +239,18 @@ export default tseslint.config( ], }, }, + { + files: ['packages/sdk/src/**/*.{ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 'error', + { + name: '@google/gemini-cli-sdk', + message: 'Please use relative imports within the @google/gemini-cli-sdk package.', + }, + ], + }, + }, { files: ['packages/*/src/**/*.test.{ts,tsx}'], plugins: { diff --git a/package-lock.json b/package-lock.json index c457eb7d555..6450eced3d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1389,6 +1389,10 @@ "resolved": "packages/core", "link": true }, + "node_modules/@google/gemini-cli-sdk": { + "resolved": "packages/sdk", + "link": true + }, "node_modules/@google/gemini-cli-test-utils": { "resolved": "packages/test-utils", "link": true @@ -17557,6 +17561,23 @@ "uuid": "dist-node/bin/uuid" } }, + "packages/sdk": { + "name": "@google/gemini-cli-sdk", + "version": "0.29.0-nightly.20260203.71f46f116", + "license": "Apache-2.0", + "dependencies": { + "@google/gemini-cli-core": "file:../core", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.1" + }, + "devDependencies": { + "typescript": "^5.3.3", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20" + } + }, "packages/test-utils": { "name": "@google/gemini-cli-test-utils", "version": "0.30.0-nightly.20260210.a2174751d", diff --git a/packages/cli/src/validateNonInterActiveAuth.ts b/packages/cli/src/validateNonInterActiveAuth.ts index 668d0390de8..a9a6bf60352 100644 --- a/packages/cli/src/validateNonInterActiveAuth.ts +++ b/packages/cli/src/validateNonInterActiveAuth.ts @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Config } from '@google/gemini-cli-core'; +import type { Config, AuthType } from '@google/gemini-cli-core'; import { - AuthType, debugLogger, OutputFormat, ExitCodes, + getAuthTypeFromEnv, } from '@google/gemini-cli-core'; import { USER_SETTINGS_PATH } from './config/settings.js'; import { validateAuthMethod } from './config/auth.js'; @@ -17,19 +17,6 @@ import { type LoadedSettings } from './config/settings.js'; import { handleError } from './utils/errors.js'; import { runExitCleanup } from './utils/cleanup.js'; -function getAuthTypeFromEnv(): AuthType | undefined { - if (process.env['GOOGLE_GENAI_USE_GCA'] === 'true') { - return AuthType.LOGIN_WITH_GOOGLE; - } - if (process.env['GOOGLE_GENAI_USE_VERTEXAI'] === 'true') { - return AuthType.USE_VERTEX_AI; - } - if (process.env['GEMINI_API_KEY']) { - return AuthType.USE_GEMINI; - } - return undefined; -} - export async function validateNonInteractiveAuth( configuredAuthType: AuthType | undefined, useExternalAuth: boolean | undefined, diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index c0bb4909a1c..0c9b36634ef 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -56,6 +56,27 @@ export enum AuthType { COMPUTE_ADC = 'compute-default-credentials', } +/** + * Detects the best authentication type based on environment variables. + * + * Checks in order: + * 1. GOOGLE_GENAI_USE_GCA=true -> LOGIN_WITH_GOOGLE + * 2. GOOGLE_GENAI_USE_VERTEXAI=true -> USE_VERTEX_AI + * 3. GEMINI_API_KEY -> USE_GEMINI + */ +export function getAuthTypeFromEnv(): AuthType | undefined { + if (process.env['GOOGLE_GENAI_USE_GCA'] === 'true') { + return AuthType.LOGIN_WITH_GOOGLE; + } + if (process.env['GOOGLE_GENAI_USE_VERTEXAI'] === 'true') { + return AuthType.USE_VERTEX_AI; + } + if (process.env['GEMINI_API_KEY']) { + return AuthType.USE_GEMINI; + } + return undefined; +} + export type ContentGeneratorConfig = { apiKey?: string; vertexai?: boolean; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 448e555df41..c8ba601cbbc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -140,6 +140,7 @@ export * from './prompts/mcp-prompts.js'; export * from './agents/types.js'; export * from './agents/agentLoader.js'; export * from './agents/local-executor.js'; +export * from './agents/agent-scheduler.js'; // Export specific tool logic export * from './tools/read-file.js'; diff --git a/packages/sdk/README.md b/packages/sdk/README.md new file mode 100644 index 00000000000..5500abc5fff --- /dev/null +++ b/packages/sdk/README.md @@ -0,0 +1,36 @@ +# @google/gemini-cli-sdk + +The Gemini CLI SDK provides a programmatic interface to interact with Gemini +models and tools. + +## Installation + +```bash +npm install @google/gemini-cli-sdk +``` + +## Usage + +```typescript +import { GeminiCliAgent } from '@google/gemini-cli-sdk'; + +async function main() { + const agent = new GeminiCliAgent({ + instructions: 'You are a helpful assistant.', + }); + + const controller = new AbortController(); + const signal = controller.signal; + + // Stream responses from the agent + const stream = agent.sendStream('Why is the sky blue?', signal); + + for await (const chunk of stream) { + if (chunk.type === 'content') { + process.stdout.write(chunk.value.text || ''); + } + } +} + +main().catch(console.error); +``` diff --git a/packages/sdk/examples/simple.ts b/packages/sdk/examples/simple.ts new file mode 100644 index 00000000000..6c2773b0c8a --- /dev/null +++ b/packages/sdk/examples/simple.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { GeminiCliAgent, tool, z } from '../src/index.js'; + +async function main() { + const myTool = tool( + { + name: 'add', + description: 'Add two numbers.', + inputSchema: z.object({ + a: z.number().describe('the first number'), + b: z.number().describe('the second number'), + }), + }, + async ({ a, b }) => { + console.log(`Tool 'add' called with a=${a}, b=${b}`); + return { result: a + b }; + }, + ); + + const agent = new GeminiCliAgent({ + instructions: 'Make sure to always talk like a pirate.', + tools: [myTool], + }); + + console.log("Sending prompt: 'add 5 + 6'"); + for await (const chunk of agent.sendStream( + 'add 5 + 6 and tell me a story involving the result', + )) { + console.log(JSON.stringify(chunk, null, 2)); + } +} + +main().catch(console.error); diff --git a/packages/sdk/index.ts b/packages/sdk/index.ts new file mode 100644 index 00000000000..75f51214139 --- /dev/null +++ b/packages/sdk/index.ts @@ -0,0 +1,7 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './src/index.js'; diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 00000000000..19c85ed58ac --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,36 @@ +{ + "name": "@google/gemini-cli-sdk", + "version": "0.29.0-nightly.20260203.71f46f116", + "description": "Gemini CLI SDK", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "git+https://github.com/google-gemini/gemini-cli.git" + }, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "node ../../scripts/build_package.js", + "lint": "eslint . --ext .ts,.tsx", + "format": "prettier --write .", + "test": "vitest run", + "test:ci": "vitest run", + "typecheck": "tsc --noEmit" + }, + "files": [ + "dist" + ], + "dependencies": { + "@google/gemini-cli-core": "file:../core", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.1" + }, + "devDependencies": { + "typescript": "^5.3.3", + "vitest": "^3.1.1" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/sdk/src/agent.ts b/packages/sdk/src/agent.ts new file mode 100644 index 00000000000..b2ac5a18720 --- /dev/null +++ b/packages/sdk/src/agent.ts @@ -0,0 +1,130 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + Config, + type ConfigParameters, + PREVIEW_GEMINI_MODEL_AUTO, + GeminiEventType, + type ToolCallRequestInfo, + type ServerGeminiStreamEvent, + type GeminiClient, + scheduleAgentTools, + getAuthTypeFromEnv, + AuthType, +} from '@google/gemini-cli-core'; + +import { type Tool, SdkTool, type z } from './tool.js'; + +export interface GeminiCliAgentOptions { + instructions: string; + tools?: Array>; + model?: string; + cwd?: string; + debug?: boolean; +} + +export class GeminiCliAgent { + private readonly config: Config; + private readonly tools: Array>; + + constructor(options: GeminiCliAgentOptions) { + const cwd = options.cwd || process.cwd(); + this.tools = options.tools || []; + + const configParams: ConfigParameters = { + sessionId: `sdk-${Date.now()}`, + targetDir: cwd, + cwd, + debugMode: options.debug ?? false, + model: options.model || PREVIEW_GEMINI_MODEL_AUTO, + userMemory: options.instructions, + // Minimal config + enableHooks: false, + mcpEnabled: false, + extensionsEnabled: false, + }; + + this.config = new Config(configParams); + } + + async *sendStream( + prompt: string, + signal?: AbortSignal, + ): AsyncGenerator { + // Lazy initialization of auth and client + if (!this.config.getContentGenerator()) { + const authType = getAuthTypeFromEnv() || AuthType.COMPUTE_ADC; + + await this.config.refreshAuth(authType); + await this.config.initialize(); + + // Register tools now that registry exists + const registry = this.config.getToolRegistry(); + const messageBus = this.config.getMessageBus(); + + for (const toolDef of this.tools) { + const sdkTool = new SdkTool(toolDef, messageBus); + registry.registerTool(sdkTool); + } + } + + const client = this.config.getGeminiClient(); + + let request: Parameters[0] = [ + { text: prompt }, + ]; + const abortSignal = signal ?? new AbortController().signal; + const sessionId = this.config.getSessionId(); + + while (true) { + // sendMessageStream returns AsyncGenerator + const stream = client.sendMessageStream(request, abortSignal, sessionId); + + const toolCallsToSchedule: ToolCallRequestInfo[] = []; + + for await (const event of stream) { + yield event; + if (event.type === GeminiEventType.ToolCallRequest) { + const toolCall = event.value; + let args = toolCall.args; + if (typeof args === 'string') { + args = JSON.parse(args); + } + toolCallsToSchedule.push({ + ...toolCall, + args, + isClientInitiated: false, + prompt_id: sessionId, + }); + } + } + + if (toolCallsToSchedule.length === 0) { + break; + } + + const completedCalls = await scheduleAgentTools( + this.config, + toolCallsToSchedule, + { + schedulerId: sessionId, + toolRegistry: this.config.getToolRegistry(), + signal: abortSignal, + }, + ); + + const functionResponses = completedCalls.flatMap( + (call) => call.response.responseParts, + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + request = functionResponses as unknown as Parameters< + GeminiClient['sendMessageStream'] + >[0]; + } + } +} diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts new file mode 100644 index 00000000000..0ad940d7b2c --- /dev/null +++ b/packages/sdk/src/index.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './agent.js'; +export * from './tool.js'; diff --git a/packages/sdk/src/tool.ts b/packages/sdk/src/tool.ts new file mode 100644 index 00000000000..00cd3802dec --- /dev/null +++ b/packages/sdk/src/tool.ts @@ -0,0 +1,113 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { z } from 'zod'; +import { zodToJsonSchema } from 'zod-to-json-schema'; +import { + BaseDeclarativeTool, + BaseToolInvocation, + type ToolResult, + type ToolInvocation, + Kind, + type MessageBus, +} from '@google/gemini-cli-core'; + +export { z }; + +export interface ToolDefinition { + name: string; + description: string; + inputSchema: T; +} + +export interface Tool extends ToolDefinition { + action: (params: z.infer) => Promise; +} + +class SdkToolInvocation extends BaseToolInvocation< + z.infer, + ToolResult +> { + constructor( + params: z.infer, + messageBus: MessageBus, + private readonly action: (params: z.infer) => Promise, + toolName: string, + ) { + super(params, messageBus, toolName); + } + + getDescription(): string { + return `Executing ${this._toolName}...`; + } + + async execute( + _signal: AbortSignal, + _updateOutput?: (output: string) => void, + ): Promise { + try { + const result = await this.action(this.params); + const output = + typeof result === 'string' ? result : JSON.stringify(result, null, 2); + return { + llmContent: output, + returnDisplay: output, + }; + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + llmContent: `Error: ${errorMessage}`, + returnDisplay: `Error: ${errorMessage}`, + error: { + message: errorMessage, + }, + }; + } + } +} + +export class SdkTool extends BaseDeclarativeTool< + z.infer, + ToolResult +> { + constructor( + private readonly definition: Tool, + messageBus: MessageBus, + ) { + super( + definition.name, + definition.name, + definition.description, + Kind.Other, + zodToJsonSchema(definition.inputSchema), + messageBus, + ); + } + + protected createInvocation( + params: z.infer, + messageBus: MessageBus, + toolName?: string, + ): ToolInvocation, ToolResult> { + return new SdkToolInvocation( + params, + messageBus, + this.definition.action, + toolName || this.name, + ); + } +} + +export function tool( + definition: ToolDefinition, + action: (params: z.infer) => Promise, +): Tool { + return { + ...definition, + action, + }; +} diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100644 index 00000000000..2cd4d6ea73e --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "composite": true, + "lib": ["DOM", "DOM.Iterable", "ES2023"], + "types": ["node", "vitest/globals"] + }, + "include": ["index.ts", "src/**/*.ts", "package.json"], + "exclude": ["node_modules", "dist"], + "references": [{ "path": "../core" }] +} diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts new file mode 100644 index 00000000000..08cdff095c4 --- /dev/null +++ b/packages/sdk/vitest.config.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +});