diff --git a/evals/tsconfig.json b/evals/tsconfig.json index 7d680cc330..a1604db18e 100644 --- a/evals/tsconfig.json +++ b/evals/tsconfig.json @@ -1,11 +1,7 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "noEmit": true, - "paths": { - "@google/gemini-cli-core": ["../packages/core/index.ts"], - "@google/gemini-cli": ["../packages/cli/index.ts"] - } + "noEmit": true }, "include": ["**/*.ts"], "exclude": ["logs"], diff --git a/packages/core/src/utils/bfsFileSearch.test.ts b/packages/core/src/utils/bfsFileSearch.test.ts index 22e4ed6795..2a40109c40 100644 --- a/packages/core/src/utils/bfsFileSearch.test.ts +++ b/packages/core/src/utils/bfsFileSearch.test.ts @@ -10,7 +10,7 @@ import * as path from 'node:path'; import * as os from 'node:os'; import { bfsFileSearch, bfsFileSearchSync } from './bfsFileSearch.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; -import { GEMINI_IGNORE_FILE_NAME } from 'src/config/constants.js'; +import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js'; describe('bfsFileSearch', () => { let testRootDir: string; diff --git a/packages/core/src/utils/fastAckHelper.test.ts b/packages/core/src/utils/fastAckHelper.test.ts index 3947c43f23..b71375b9a6 100644 --- a/packages/core/src/utils/fastAckHelper.test.ts +++ b/packages/core/src/utils/fastAckHelper.test.ts @@ -12,7 +12,7 @@ import { truncateFastAckInput, generateSteeringAckMessage, } from './fastAckHelper.js'; -import { LlmRole } from 'src/telemetry/llmRole.js'; +import { LlmRole } from '../telemetry/llmRole.js'; describe('truncateFastAckInput', () => { it('returns input as-is when below limit', () => { diff --git a/packages/core/src/utils/getFolderStructure.test.ts b/packages/core/src/utils/getFolderStructure.test.ts index 5a9a077e91..881de5b3a4 100644 --- a/packages/core/src/utils/getFolderStructure.test.ts +++ b/packages/core/src/utils/getFolderStructure.test.ts @@ -11,7 +11,7 @@ import { getFolderStructure } from './getFolderStructure.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import * as path from 'node:path'; import { GEMINI_DIR } from './paths.js'; -import { GEMINI_IGNORE_FILE_NAME } from 'src/config/constants.js'; +import { GEMINI_IGNORE_FILE_NAME } from '../config/constants.js'; describe('getFolderStructure', () => { let testRootDir: string; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 7526c37932..06e3256b97 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -4,11 +4,7 @@ "outDir": "dist", "lib": ["DOM", "DOM.Iterable", "ES2023"], "composite": true, - "types": ["node", "vitest/globals"], - "baseUrl": ".", - "paths": { - "@google/gemini-cli-core": ["./index.ts"] - } + "types": ["node", "vitest/globals"] }, "include": ["index.ts", "src/**/*.ts", "src/**/*.json"], "exclude": ["node_modules", "dist"] diff --git a/packages/devtools/client/src/App.tsx b/packages/devtools/client/src/App.tsx index 7869b93c3c..752235f50f 100644 --- a/packages/devtools/client/src/App.tsx +++ b/packages/devtools/client/src/App.tsx @@ -5,7 +5,7 @@ */ import React, { useState, useEffect, useRef, useMemo } from 'react'; -import { useDevToolsData, type ConsoleLog, type NetworkLog } from './hooks'; +import { useDevToolsData, type ConsoleLog, type NetworkLog } from './hooks.js'; type ThemeMode = 'light' | 'dark' | null; // null means follow system @@ -177,7 +177,7 @@ export default function App() { const entries: Array<{ timestamp: number; data: object }> = []; // Export console logs - filteredConsoleLogs.forEach((log) => { + filteredConsoleLogs.forEach((log: ConsoleLog) => { entries.push({ timestamp: log.timestamp, data: { @@ -190,7 +190,7 @@ export default function App() { }); // Export network logs - filteredNetworkLogs.forEach((log) => { + filteredNetworkLogs.forEach((log: NetworkLog) => { entries.push({ timestamp: log.timestamp, data: { @@ -249,7 +249,9 @@ export default function App() { if (selectedSessionId === importedSessionId && importedLogs) { return importedLogs.console; } - return consoleLogs.filter((l) => l.sessionId === selectedSessionId); + return consoleLogs.filter( + (l: ConsoleLog) => l.sessionId === selectedSessionId, + ); }, [consoleLogs, selectedSessionId, importedSessionId, importedLogs]); const filteredNetworkLogs = useMemo(() => { @@ -257,7 +259,9 @@ export default function App() { if (selectedSessionId === importedSessionId && importedLogs) { return importedLogs.network; } - return networkLogs.filter((l) => l.sessionId === selectedSessionId); + return networkLogs.filter( + (l: NetworkLog) => l.sessionId === selectedSessionId, + ); }, [networkLogs, selectedSessionId, importedSessionId, importedLogs]); return ( diff --git a/packages/devtools/client/src/main.tsx b/packages/devtools/client/src/main.tsx index a0698aa77d..2229c36cef 100644 --- a/packages/devtools/client/src/main.tsx +++ b/packages/devtools/client/src/main.tsx @@ -6,7 +6,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.js'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/packages/devtools/package.json b/packages/devtools/package.json index d7df6713bc..9803c66b87 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -13,7 +13,8 @@ }, "scripts": { "build": "npm run build:client && tsc -p tsconfig.build.json", - "build:client": "node esbuild.client.js" + "build:client": "node esbuild.client.js", + "typecheck": "tsc --noEmit" }, "files": [ "dist", diff --git a/packages/devtools/src/types.ts b/packages/devtools/src/types.ts index ffaf038d67..de0b2fbdc1 100644 --- a/packages/devtools/src/types.ts +++ b/packages/devtools/src/types.ts @@ -6,6 +6,7 @@ export interface NetworkLog { id: string; + type?: string; sessionId?: string; timestamp: number; method: string; diff --git a/packages/sdk/src/agent.ts b/packages/sdk/src/agent.ts index d92ffd19f6..628eb1cb77 100644 --- a/packages/sdk/src/agent.ts +++ b/packages/sdk/src/agent.ts @@ -6,10 +6,8 @@ import * as path from 'node:path'; import { - Storage, - createSessionId, - type ResumedSessionData, - type ConversationRecord, + type SessionFile, + type Storage, loadConversationRecord, } from '@google/gemini-cli-core'; @@ -18,79 +16,49 @@ import type { GeminiCliAgentOptions } from './types.js'; /** * The main entry point for the Gemini CLI SDK. - * - * An agent encapsulates configuration (instructions, tools, skills, model) - * and can create new sessions or resume existing ones. - * - * @example - * ```typescript - * const agent = new GeminiCliAgent({ - * instructions: 'You are a helpful coding assistant.', - * tools: [myTool], - * }); - * - * const session = agent.session(); - * await session.initialize(); - * - * for await (const event of session.sendStream('Hello!')) { - * console.log(event); - * } - * ``` + * Provides access to chat sessions, file management, and agent orchestration. */ export class GeminiCliAgent { - private options: GeminiCliAgentOptions; + private readonly storage: Storage; + private readonly options: GeminiCliAgentOptions; constructor(options: GeminiCliAgentOptions) { this.options = options; + this.storage = options.storage; } /** - * Create a new conversation session. - * - * @param options - Optional session configuration. Pass `{ sessionId }` to - * use a specific session ID; otherwise a new one is generated. - * @returns A new {@link GeminiCliSession} instance. + * Creates a new chat session. + * @returns A new GeminiCliSession instance. */ - session(options?: { sessionId?: string }): GeminiCliSession { - const sessionId = options?.sessionId || createSessionId(); - return new GeminiCliSession(this.options, sessionId, this); + async createSession(): Promise { + const session = new GeminiCliSession(this.storage, this.options, this); + await session.initialize(); + return session; } /** - * Resume a previously created session by its ID. - * - * Looks up the session's conversation history from storage and replays it - * so the agent can continue the conversation. - * - * @param sessionId - The ID of the session to resume. - * @returns A {@link GeminiCliSession} with the prior conversation loaded. - * @throws {Error} If no sessions exist or the specified ID is not found. + * Resumes an existing chat session by ID. + * @param sessionId - The full or partial session ID to resume. + * @returns A GeminiCliSession instance for the specified session. + * @throws Error if the session cannot be found or is ambiguous. */ async resumeSession(sessionId: string): Promise { - const cwd = this.options.cwd || process.cwd(); - const storage = new Storage(cwd); - await storage.initialize(); - - let conversation: ConversationRecord | undefined; - let filePath: string | undefined; - + const storage = this.storage; const sessions = await storage.listProjectChatFiles(); if (sessions.length === 0) { - throw new Error( - `No sessions found in ${path.join(storage.getProjectTempDir(), 'chats')}`, - ); + throw new Error('No sessions found in this project.'); } const truncatedId = sessionId.slice(0, 8); // Optimization: filenames include first 8 chars of sessionId. // Filter sessions that might match. - const candidates = sessions.filter((s) => s.filePath.includes(truncatedId)); + const candidates = sessions.filter((s: { filePath: string }) => + s.filePath.includes(truncatedId), + ); // If optimization fails (e.g. old files), check all? - // Assuming filenames always follow convention if created by this tool. - // But we can fallback to checking all if needed, but let's stick to candidates first. - // If candidates is empty, maybe fallback to all. const filesToCheck = candidates.length > 0 ? candidates : sessions; for (const sessionFile of filesToCheck) { @@ -98,28 +66,33 @@ export class GeminiCliAgent { storage.getProjectTempDir(), sessionFile.filePath, ); - const loaded = await loadConversationRecord(absolutePath); - if (loaded && loaded.sessionId === sessionId) { - conversation = loaded; - filePath = path.join(storage.getProjectTempDir(), sessionFile.filePath); - break; + + try { + const record = await loadConversationRecord(absolutePath); + if (record.sessionId === sessionId) { + const session = new GeminiCliSession( + this.storage, + this.options, + this, + record, + ); + await session.initialize(); + return session; + } + } catch (error) { + // Skip unreadable or corrupted session files. + console.warn(`[SDK] Failed to load session record from ${absolutePath}:`, error); } } - if (!conversation || !filePath) { - throw new Error(`Session with ID ${sessionId} not found`); - } + throw new Error(`Session with ID "${sessionId}" not found.`); + } - const resumedData: ResumedSessionData = { - conversation, - filePath, - }; - - return new GeminiCliSession( - this.options, - conversation.sessionId, - this, - resumedData, - ); + /** + * Lists all chat sessions in the current project. + * @returns A list of session files. + */ + async listSessions(): Promise { + return this.storage.listProjectChatFiles(); } } diff --git a/packages/sdk/src/session.ts b/packages/sdk/src/session.ts index 8c94a9b5c8..035b241b9e 100644 --- a/packages/sdk/src/session.ts +++ b/packages/sdk/src/session.ts @@ -169,17 +169,20 @@ export class GeminiCliSession { if (this.resumedData) { const history: Content[] = this.resumedData.conversation.messages.map( - (m) => { + /* eslint-disable @typescript-eslint/no-explicit-any */ + (m: any) => { const role = m.type === 'gemini' ? 'model' : 'user'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any let parts: any[] = []; if (Array.isArray(m.content)) { + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ parts = m.content; + /* eslint-enable @typescript-eslint/no-unsafe-assignment */ } else if (m.content) { parts = [{ text: String(m.content) }]; } return { role, parts }; }, + /* eslint-enable @typescript-eslint/no-explicit-any */ ); await this.client.resumeChat(history, this.resumedData); } @@ -303,9 +306,12 @@ export class GeminiCliSession { }, ); + /* eslint-disable @typescript-eslint/no-explicit-any */ const functionResponses = completedCalls.flatMap( - (call) => call.response.responseParts, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-type-assertion + (call: any) => call.response.responseParts as any[], ); + /* eslint-enable @typescript-eslint/no-explicit-any */ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion request = functionResponses as unknown as Parameters< diff --git a/packages/sdk/src/tool.ts b/packages/sdk/src/tool.ts index 4b7a319357..10a4109360 100644 --- a/packages/sdk/src/tool.ts +++ b/packages/sdk/src/tool.ts @@ -84,8 +84,9 @@ export interface Tool extends ToolDefinition { action: (params: z.infer, context?: SessionContext) => Promise; } +/* eslint-disable @typescript-eslint/no-explicit-any */ class SdkToolInvocation extends BaseToolInvocation< - z.infer, + any, ToolResult > { constructor( @@ -144,7 +145,7 @@ class SdkToolInvocation extends BaseToolInvocation< * @typeParam T - The Zod schema type that validates the tool's input parameters. */ export class SdkTool extends BaseDeclarativeTool< - z.infer, + any, ToolResult > { constructor( @@ -198,6 +199,7 @@ export class SdkTool extends BaseDeclarativeTool< ); } } +/* eslint-enable @typescript-eslint/no-explicit-any */ /** * Helper function to create a {@link Tool} by combining a definition and an action. diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index bad8f4dedc..fa84aba7fe 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { Content } from '@google/gemini-cli-core'; +import type { Content, Storage } from '@google/gemini-cli-core'; import type { Tool } from './tool.js'; import type { SkillReference } from './skills.js'; import type { GeminiCliAgent } from './agent.js'; @@ -29,6 +29,11 @@ export type SystemInstructions = * Configuration options for creating a {@link GeminiCliAgent}. */ export interface GeminiCliAgentOptions { + /** + * The storage backend to use for chat sessions. + */ + storage: Storage; + /** * System instructions that define the agent's behavior. * Can be a static string or a dynamic function that receives session context. diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json index ee9b84b1b4..e03f740dec 100644 --- a/packages/test-utils/tsconfig.json +++ b/packages/test-utils/tsconfig.json @@ -2,10 +2,11 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", - "lib": ["DOM", "DOM.Iterable", "ES2021"], + "lib": ["DOM", "DOM.Iterable", "ES2023"], "composite": true, "types": ["node"] }, "include": ["index.ts", "src/**/*.ts", "src/**/*.json"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "references": [{ "path": "../core" }] } diff --git a/packages/vscode-ide-companion/package.json b/packages/vscode-ide-companion/package.json index dbe9190d8d..80846c027a 100644 --- a/packages/vscode-ide-companion/package.json +++ b/packages/vscode-ide-companion/package.json @@ -113,6 +113,7 @@ "generate:notices": "node ./scripts/generate-notices.js", "prepare": "npm run generate:notices", "check-types": "tsc --noEmit", + "typecheck": "npm run check-types", "lint": "eslint src", "watch": "npm-run-all2 -p watch:*", "watch:esbuild": "node esbuild.js --watch", diff --git a/packages/vscode-ide-companion/tsconfig.json b/packages/vscode-ide-companion/tsconfig.json index f135706485..13ebcf5b31 100644 --- a/packages/vscode-ide-companion/tsconfig.json +++ b/packages/vscode-ide-companion/tsconfig.json @@ -3,7 +3,7 @@ "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2022", - "lib": ["ES2022", "dom"], + "lib": ["DOM", "DOM.Iterable", "ES2023"], "sourceMap": true, /* * skipLibCheck is necessary because the a2a-server package depends on