diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/cli/src/config/config.test.ts | 87 | ||||
| -rw-r--r-- | packages/cli/src/config/config.ts | 21 | ||||
| -rw-r--r-- | packages/cli/src/config/settings.test.ts | 42 | ||||
| -rw-r--r-- | packages/cli/src/config/settings.ts | 9 | ||||
| -rw-r--r-- | packages/cli/src/ui/App.tsx | 6 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/directoryCommand.test.tsx | 13 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/directoryCommand.tsx | 34 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/memoryCommand.test.ts | 4 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/memoryCommand.ts | 3 | ||||
| -rw-r--r-- | packages/cli/src/ui/commands/types.ts | 1 | ||||
| -rw-r--r-- | packages/cli/src/ui/hooks/slashCommandProcessor.ts | 3 | ||||
| -rw-r--r-- | packages/cli/src/utils/resolvePath.ts | 21 | ||||
| -rw-r--r-- | packages/core/src/config/config.ts | 8 | ||||
| -rw-r--r-- | packages/core/src/utils/memoryDiscovery.test.ts | 102 | ||||
| -rw-r--r-- | packages/core/src/utils/memoryDiscovery.ts | 39 | ||||
| -rw-r--r-- | packages/core/src/utils/workspaceContext.ts | 43 |
16 files changed, 370 insertions, 66 deletions
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 431b1375..f5d0ddf8 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -6,6 +6,8 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; import { loadCliConfig, parseArguments } from './config.js'; import { Settings } from './settings.js'; import { Extension } from './extension.js'; @@ -44,7 +46,7 @@ vi.mock('@google/gemini-cli-core', async () => { }, loadEnvironment: vi.fn(), loadServerHierarchicalMemory: vi.fn( - (cwd, debug, fileService, extensionPaths, _maxDirs) => + (cwd, dirs, debug, fileService, extensionPaths, _maxDirs) => Promise.resolve({ memoryContent: extensionPaths?.join(',') || '', fileCount: extensionPaths?.length || 0, @@ -487,6 +489,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { await loadCliConfig(settings, extensions, 'session-id', argv); expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( expect.any(String), + [], false, expect.any(Object), [ @@ -1015,3 +1018,85 @@ describe('loadCliConfig ideModeFeature', () => { expect(config.getIdeModeFeature()).toBe(false); }); }); + +vi.mock('fs', async () => { + const actualFs = await vi.importActual<typeof fs>('fs'); + const MOCK_CWD1 = process.cwd(); + const MOCK_CWD2 = path.resolve(path.sep, 'home', 'user', 'project'); + + const mockPaths = new Set([ + MOCK_CWD1, + MOCK_CWD2, + path.resolve(path.sep, 'cli', 'path1'), + path.resolve(path.sep, 'settings', 'path1'), + path.join(os.homedir(), 'settings', 'path2'), + path.join(MOCK_CWD2, 'cli', 'path2'), + path.join(MOCK_CWD2, 'settings', 'path3'), + ]); + + return { + ...actualFs, + existsSync: vi.fn((p) => mockPaths.has(p.toString())), + statSync: vi.fn((p) => { + if (mockPaths.has(p.toString())) { + return { isDirectory: () => true }; + } + // Fallback for other paths if needed, though the test should be specific. + return actualFs.statSync(p); + }), + realpathSync: vi.fn((p) => p), + }; +}); + +describe('loadCliConfig with includeDirectories', () => { + const originalArgv = process.argv; + const originalEnv = { ...process.env }; + + beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(os.homedir).mockReturnValue('/mock/home/user'); + process.env.GEMINI_API_KEY = 'test-api-key'; + vi.spyOn(process, 'cwd').mockReturnValue( + path.resolve(path.sep, 'home', 'user', 'project'), + ); + }); + + afterEach(() => { + process.argv = originalArgv; + process.env = originalEnv; + vi.restoreAllMocks(); + }); + + it('should combine and resolve paths from settings and CLI arguments', async () => { + const mockCwd = path.resolve(path.sep, 'home', 'user', 'project'); + process.argv = [ + 'node', + 'script.js', + '--include-directories', + `${path.resolve(path.sep, 'cli', 'path1')},${path.join(mockCwd, 'cli', 'path2')}`, + ]; + const argv = await parseArguments(); + const settings: Settings = { + includeDirectories: [ + path.resolve(path.sep, 'settings', 'path1'), + path.join(os.homedir(), 'settings', 'path2'), + path.join(mockCwd, 'settings', 'path3'), + ], + }; + const config = await loadCliConfig(settings, [], 'test-session', argv); + const expected = [ + mockCwd, + path.resolve(path.sep, 'cli', 'path1'), + path.join(mockCwd, 'cli', 'path2'), + path.resolve(path.sep, 'settings', 'path1'), + path.join(os.homedir(), 'settings', 'path2'), + path.join(mockCwd, 'settings', 'path3'), + ]; + expect(config.getWorkspaceContext().getDirectories()).toEqual( + expect.arrayContaining(expected), + ); + expect(config.getWorkspaceContext().getDirectories()).toHaveLength( + expected.length, + ); + }); +}); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 0395ac0f..d3d37c6a 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -29,6 +29,7 @@ import { Settings } from './settings.js'; import { Extension, annotateActiveExtensions } from './extension.js'; import { getCliVersion } from '../utils/version.js'; import { loadSandboxConfig } from './sandboxConfig.js'; +import { resolvePath } from '../utils/resolvePath.js'; // Simple console logger for now - replace with actual logger if available const logger = { @@ -65,6 +66,7 @@ export interface CliArgs { ideModeFeature: boolean | undefined; proxy: string | undefined; includeDirectories: string[] | undefined; + loadMemoryFromIncludeDirectories: boolean | undefined; } export async function parseArguments(): Promise<CliArgs> { @@ -212,6 +214,12 @@ export async function parseArguments(): Promise<CliArgs> { // Handle comma-separated values dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())), }) + .option('load-memory-from-include-directories', { + type: 'boolean', + description: + 'If true, when refreshing memory, GEMINI.md files should be loaded from all directories that are added. If false, GEMINI.md files should only be loaded from the primary working directory.', + default: false, + }) .version(await getCliVersion()) // This will enable the --version flag based on package.json .alias('v', 'version') .help() @@ -239,6 +247,7 @@ export async function parseArguments(): Promise<CliArgs> { // TODO: Consider if App.tsx should get memory via a server call or if Config should refresh itself. export async function loadHierarchicalGeminiMemory( currentWorkingDirectory: string, + includeDirectoriesToReadGemini: readonly string[] = [], debugMode: boolean, fileService: FileDiscoveryService, settings: Settings, @@ -264,6 +273,7 @@ export async function loadHierarchicalGeminiMemory( // Directly call the server function with the corrected path. return loadServerHierarchicalMemory( effectiveCwd, + includeDirectoriesToReadGemini, debugMode, fileService, extensionContextFilePaths, @@ -325,9 +335,14 @@ export async function loadCliConfig( ...settings.fileFiltering, }; + const includeDirectories = (settings.includeDirectories || []) + .map(resolvePath) + .concat((argv.includeDirectories || []).map(resolvePath)); + // Call the (now wrapper) loadHierarchicalGeminiMemory which calls the server's version const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( process.cwd(), + settings.loadMemoryFromIncludeDirectories ? includeDirectories : [], debugMode, fileService, settings, @@ -393,7 +408,11 @@ export async function loadCliConfig( embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, sandbox: sandboxConfig, targetDir: process.cwd(), - includeDirectories: argv.includeDirectories, + includeDirectories, + loadMemoryFromIncludeDirectories: + argv.loadMemoryFromIncludeDirectories || + settings.loadMemoryFromIncludeDirectories || + false, debugMode, question: argv.promptInteractive || argv.prompt || '', fullContext: argv.allFiles || argv.all_files || false, diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index 4099e778..d0266720 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -112,6 +112,7 @@ describe('Settings Loading and Merging', () => { expect(settings.merged).toEqual({ customThemes: {}, mcpServers: {}, + includeDirectories: [], }); expect(settings.errors.length).toBe(0); }); @@ -145,6 +146,7 @@ describe('Settings Loading and Merging', () => { ...systemSettingsContent, customThemes: {}, mcpServers: {}, + includeDirectories: [], }); }); @@ -178,6 +180,7 @@ describe('Settings Loading and Merging', () => { ...userSettingsContent, customThemes: {}, mcpServers: {}, + includeDirectories: [], }); }); @@ -209,6 +212,7 @@ describe('Settings Loading and Merging', () => { ...workspaceSettingsContent, customThemes: {}, mcpServers: {}, + includeDirectories: [], }); }); @@ -246,6 +250,7 @@ describe('Settings Loading and Merging', () => { contextFileName: 'WORKSPACE_CONTEXT.md', customThemes: {}, mcpServers: {}, + includeDirectories: [], }); }); @@ -295,6 +300,7 @@ describe('Settings Loading and Merging', () => { allowMCPServers: ['server1', 'server2'], customThemes: {}, mcpServers: {}, + includeDirectories: [], }); }); @@ -616,6 +622,40 @@ describe('Settings Loading and Merging', () => { expect(settings.merged.mcpServers).toEqual({}); }); + it('should merge includeDirectories from all scopes', () => { + (mockFsExistsSync as Mock).mockReturnValue(true); + const systemSettingsContent = { + includeDirectories: ['/system/dir'], + }; + const userSettingsContent = { + includeDirectories: ['/user/dir1', '/user/dir2'], + }; + const workspaceSettingsContent = { + includeDirectories: ['/workspace/dir'], + }; + + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === getSystemSettingsPath()) + return JSON.stringify(systemSettingsContent); + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === MOCK_WORKSPACE_SETTINGS_PATH) + return JSON.stringify(workspaceSettingsContent); + return '{}'; + }, + ); + + const settings = loadSettings(MOCK_WORKSPACE_DIR); + + expect(settings.merged.includeDirectories).toEqual([ + '/system/dir', + '/user/dir1', + '/user/dir2', + '/workspace/dir', + ]); + }); + it('should handle JSON parsing errors gracefully', () => { (mockFsExistsSync as Mock).mockReturnValue(true); // Both files "exist" const invalidJsonContent = 'invalid json'; @@ -654,6 +694,7 @@ describe('Settings Loading and Merging', () => { expect(settings.merged).toEqual({ customThemes: {}, mcpServers: {}, + includeDirectories: [], }); // Check that error objects are populated in settings.errors @@ -1090,6 +1131,7 @@ describe('Settings Loading and Merging', () => { ...systemSettingsContent, customThemes: {}, mcpServers: {}, + includeDirectories: [], }); }); }); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 20a7b14a..722af628 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -126,6 +126,10 @@ export interface Settings { // Environment variables to exclude from project .env files excludedProjectEnvVars?: string[]; dnsResolutionOrder?: DnsResolutionOrder; + + includeDirectories?: string[]; + + loadMemoryFromIncludeDirectories?: boolean; } export interface SettingsError { @@ -181,6 +185,11 @@ export class LoadedSettings { ...(workspace.mcpServers || {}), ...(system.mcpServers || {}), }, + includeDirectories: [ + ...(system.includeDirectories || []), + ...(user.includeDirectories || []), + ...(workspace.includeDirectories || []), + ], }; } diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 3b695111..f07a5386 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -276,6 +276,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { try { const { memoryContent, fileCount } = await loadHierarchicalGeminiMemory( process.cwd(), + settings.merged.loadMemoryFromIncludeDirectories + ? config.getWorkspaceContext().getDirectories() + : [], config.getDebugMode(), config.getFileService(), settings.merged, @@ -480,6 +483,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { openPrivacyNotice, toggleVimEnabled, setIsProcessing, + setGeminiMdFileCount, ); const { @@ -599,7 +603,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { if (config) { setGeminiMdFileCount(config.getGeminiMdFileCount()); } - }, [config]); + }, [config, config.getGeminiMdFileCount]); const logger = useLogger(); const [userMessages, setUserMessages] = useState<string[]>([]); diff --git a/packages/cli/src/ui/commands/directoryCommand.test.tsx b/packages/cli/src/ui/commands/directoryCommand.test.tsx index 081083d3..fee8ae40 100644 --- a/packages/cli/src/ui/commands/directoryCommand.test.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.test.tsx @@ -40,11 +40,24 @@ describe('directoryCommand', () => { getGeminiClient: vi.fn().mockReturnValue({ addDirectoryContext: vi.fn(), }), + getWorkingDir: () => '/test/dir', + shouldLoadMemoryFromIncludeDirectories: () => false, + getDebugMode: () => false, + getFileService: () => ({}), + getExtensionContextFilePaths: () => [], + getFileFilteringOptions: () => ({ ignore: [], include: [] }), + setUserMemory: vi.fn(), + setGeminiMdFileCount: vi.fn(), } as unknown as Config; mockContext = { services: { config: mockConfig, + settings: { + merged: { + memoryDiscoveryMaxDirs: 1000, + }, + }, }, ui: { addItem: vi.fn(), diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index 18f7e78f..6c667f44 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -8,6 +8,7 @@ import { SlashCommand, CommandContext, CommandKind } from './types.js'; import { MessageType } from '../types.js'; import * as os from 'os'; import * as path from 'path'; +import { loadServerHierarchicalMemory } from '@google/gemini-cli-core'; export function expandHomeDir(p: string): string { if (!p) { @@ -16,7 +17,7 @@ export function expandHomeDir(p: string): string { let expandedPath = p; if (p.toLowerCase().startsWith('%userprofile%')) { expandedPath = os.homedir() + p.substring('%userprofile%'.length); - } else if (p.startsWith('~')) { + } else if (p === '~' || p.startsWith('~/')) { expandedPath = os.homedir() + p.substring(1); } return path.normalize(expandedPath); @@ -90,6 +91,37 @@ export const directoryCommand: SlashCommand = { } } + try { + if (config.shouldLoadMemoryFromIncludeDirectories()) { + const { memoryContent, fileCount } = + await loadServerHierarchicalMemory( + config.getWorkingDir(), + [ + ...config.getWorkspaceContext().getDirectories(), + ...pathsToAdd, + ], + config.getDebugMode(), + config.getFileService(), + config.getExtensionContextFilePaths(), + context.services.settings.merged.memoryImportFormat || 'tree', // Use setting or default to 'tree' + config.getFileFilteringOptions(), + context.services.settings.merged.memoryDiscoveryMaxDirs, + ); + config.setUserMemory(memoryContent); + config.setGeminiMdFileCount(fileCount); + context.ui.setGeminiMdFileCount(fileCount); + } + addItem( + { + type: MessageType.INFO, + text: `Successfully added GEMINI.md files from the following directories if there are:\n- ${added.join('\n- ')}`, + }, + Date.now(), + ); + } catch (error) { + errors.push(`Error refreshing memory: ${(error as Error).message}`); + } + if (added.length > 0) { const gemini = config.getGeminiClient(); if (gemini) { diff --git a/packages/cli/src/ui/commands/memoryCommand.test.ts b/packages/cli/src/ui/commands/memoryCommand.test.ts index 74614fa7..670ca796 100644 --- a/packages/cli/src/ui/commands/memoryCommand.test.ts +++ b/packages/cli/src/ui/commands/memoryCommand.test.ts @@ -161,6 +161,10 @@ describe('memoryCommand', () => { getDebugMode: () => false, getFileService: () => ({}) as FileDiscoveryService, getExtensionContextFilePaths: () => [], + shouldLoadMemoryFromIncludeDirectories: () => false, + getWorkspaceContext: () => ({ + getDirectories: () => [], + }), getFileFilteringOptions: () => ({ ignore: [], include: [], diff --git a/packages/cli/src/ui/commands/memoryCommand.ts b/packages/cli/src/ui/commands/memoryCommand.ts index 370bb1fb..b046e7f8 100644 --- a/packages/cli/src/ui/commands/memoryCommand.ts +++ b/packages/cli/src/ui/commands/memoryCommand.ts @@ -89,6 +89,9 @@ export const memoryCommand: SlashCommand = { const { memoryContent, fileCount } = await loadServerHierarchicalMemory( config.getWorkingDir(), + config.shouldLoadMemoryFromIncludeDirectories() + ? config.getWorkspaceContext().getDirectories() + : [], config.getDebugMode(), config.getFileService(), config.getExtensionContextFilePaths(), diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts index 2de221f0..09d79e9d 100644 --- a/packages/cli/src/ui/commands/types.ts +++ b/packages/cli/src/ui/commands/types.ts @@ -59,6 +59,7 @@ export interface CommandContext { /** Toggles a special display mode. */ toggleCorgiMode: () => void; toggleVimEnabled: () => Promise<boolean>; + setGeminiMdFileCount: (count: number) => void; }; // Session-specific data session: { diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 6d9f4643..cfe4b385 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -51,6 +51,7 @@ export const useSlashCommandProcessor = ( openPrivacyNotice: () => void, toggleVimEnabled: () => Promise<boolean>, setIsProcessing: (isProcessing: boolean) => void, + setGeminiMdFileCount: (count: number) => void, ) => { const session = useSessionStats(); const [commands, setCommands] = useState<readonly SlashCommand[]>([]); @@ -163,6 +164,7 @@ export const useSlashCommandProcessor = ( setPendingItem: setPendingCompressionItem, toggleCorgiMode, toggleVimEnabled, + setGeminiMdFileCount, }, session: { stats: session.stats, @@ -185,6 +187,7 @@ export const useSlashCommandProcessor = ( toggleCorgiMode, toggleVimEnabled, sessionShellAllowlist, + setGeminiMdFileCount, ], ); diff --git a/packages/cli/src/utils/resolvePath.ts b/packages/cli/src/utils/resolvePath.ts new file mode 100644 index 00000000..b26ed8fc --- /dev/null +++ b/packages/cli/src/utils/resolvePath.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as os from 'os'; +import * as path from 'path'; + +export function resolvePath(p: string): string { + if (!p) { + return ''; + } + let expandedPath = p; + if (p.toLowerCase().startsWith('%userprofile%')) { + expandedPath = os.homedir() + p.substring('%userprofile%'.length); + } else if (p === '~' || p.startsWith('~/')) { + expandedPath = os.homedir() + p.substring(1); + } + return path.normalize(expandedPath); +} diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 3f5c11a0..22996f3e 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -188,6 +188,7 @@ export interface ConfigParameters { ideModeFeature?: boolean; ideMode?: boolean; ideClient: IdeClient; + loadMemoryFromIncludeDirectories?: boolean; } export class Config { @@ -247,6 +248,7 @@ export class Config { | Record<string, SummarizeToolOutputSettings> | undefined; private readonly experimentalAcp: boolean = false; + private readonly loadMemoryFromIncludeDirectories: boolean = false; constructor(params: ConfigParameters) { this.sessionId = params.sessionId; @@ -304,6 +306,8 @@ export class Config { this.ideModeFeature = params.ideModeFeature ?? false; this.ideMode = params.ideMode ?? false; this.ideClient = params.ideClient; + this.loadMemoryFromIncludeDirectories = + params.loadMemoryFromIncludeDirectories ?? false; if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); @@ -366,6 +370,10 @@ export class Config { return this.sessionId; } + shouldLoadMemoryFromIncludeDirectories(): boolean { + return this.loadMemoryFromIncludeDirectories; + } + getContentGeneratorConfig(): ContentGeneratorConfig { return this.contentGeneratorConfig; } diff --git a/packages/core/src/utils/memoryDiscovery.test.ts b/packages/core/src/utils/memoryDiscovery.test.ts index 8c7a294d..6c229dbb 100644 --- a/packages/core/src/utils/memoryDiscovery.test.ts +++ b/packages/core/src/utils/memoryDiscovery.test.ts @@ -67,6 +67,7 @@ describe('loadServerHierarchicalMemory', () => { it('should return empty memory and count if no context files are found', async () => { const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); @@ -85,14 +86,13 @@ describe('loadServerHierarchicalMemory', () => { const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} --- -default context content ---- End of Context from: ${path.relative(cwd, defaultContextFile)} ---`, + memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---\ndefault context content\n--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---`, fileCount: 1, }); }); @@ -108,14 +108,13 @@ default context content const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, customContextFile)} --- -custom context content ---- End of Context from: ${path.relative(cwd, customContextFile)} ---`, + memoryContent: `--- Context from: ${path.relative(cwd, customContextFile)} ---\ncustom context content\n--- End of Context from: ${path.relative(cwd, customContextFile)} ---`, fileCount: 1, }); }); @@ -135,18 +134,13 @@ custom context content const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, projectContextFile)} --- -project context content ---- End of Context from: ${path.relative(cwd, projectContextFile)} --- - ---- Context from: ${path.relative(cwd, cwdContextFile)} --- -cwd context content ---- End of Context from: ${path.relative(cwd, cwdContextFile)} ---`, + memoryContent: `--- Context from: ${path.relative(cwd, projectContextFile)} ---\nproject context content\n--- End of Context from: ${path.relative(cwd, projectContextFile)} ---\n\n--- Context from: ${path.relative(cwd, cwdContextFile)} ---\ncwd context content\n--- End of Context from: ${path.relative(cwd, cwdContextFile)} ---`, fileCount: 2, }); }); @@ -163,18 +157,13 @@ cwd context content const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); expect(result).toEqual({ - memoryContent: `--- Context from: ${customFilename} --- -CWD custom memory ---- End of Context from: ${customFilename} --- - ---- Context from: ${path.join('subdir', customFilename)} --- -Subdir custom memory ---- End of Context from: ${path.join('subdir', customFilename)} ---`, + memoryContent: `--- Context from: ${customFilename} ---\nCWD custom memory\n--- End of Context from: ${customFilename} ---\n\n--- Context from: ${path.join('subdir', customFilename)} ---\nSubdir custom memory\n--- End of Context from: ${path.join('subdir', customFilename)} ---`, fileCount: 2, }); }); @@ -191,18 +180,13 @@ Subdir custom memory const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, projectRootGeminiFile)} --- -Project root memory ---- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} --- - ---- Context from: ${path.relative(cwd, srcGeminiFile)} --- -Src directory memory ---- End of Context from: ${path.relative(cwd, srcGeminiFile)} ---`, + memoryContent: `--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\nProject root memory\n--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, srcGeminiFile)} ---\nSrc directory memory\n--- End of Context from: ${path.relative(cwd, srcGeminiFile)} ---`, fileCount: 2, }); }); @@ -219,18 +203,13 @@ Src directory memory const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); expect(result).toEqual({ - memoryContent: `--- Context from: ${DEFAULT_CONTEXT_FILENAME} --- -CWD memory ---- End of Context from: ${DEFAULT_CONTEXT_FILENAME} --- - ---- Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} --- -Subdir memory ---- End of Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---`, + memoryContent: `--- Context from: ${DEFAULT_CONTEXT_FILENAME} ---\nCWD memory\n--- End of Context from: ${DEFAULT_CONTEXT_FILENAME} ---\n\n--- Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---\nSubdir memory\n--- End of Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---`, fileCount: 2, }); }); @@ -259,30 +238,13 @@ Subdir memory const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} --- -default context content ---- End of Context from: ${path.relative(cwd, defaultContextFile)} --- - ---- Context from: ${path.relative(cwd, rootGeminiFile)} --- -Project parent memory ---- End of Context from: ${path.relative(cwd, rootGeminiFile)} --- - ---- Context from: ${path.relative(cwd, projectRootGeminiFile)} --- -Project root memory ---- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} --- - ---- Context from: ${path.relative(cwd, cwdGeminiFile)} --- -CWD memory ---- End of Context from: ${path.relative(cwd, cwdGeminiFile)} --- - ---- Context from: ${path.relative(cwd, subDirGeminiFile)} --- -Subdir memory ---- End of Context from: ${path.relative(cwd, subDirGeminiFile)} ---`, + memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---\ndefault context content\n--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---\n\n--- Context from: ${path.relative(cwd, rootGeminiFile)} ---\nProject parent memory\n--- End of Context from: ${path.relative(cwd, rootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\nProject root memory\n--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, cwdGeminiFile)} ---\nCWD memory\n--- End of Context from: ${path.relative(cwd, cwdGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, subDirGeminiFile)} ---\nSubdir memory\n--- End of Context from: ${path.relative(cwd, subDirGeminiFile)} ---`, fileCount: 5, }); }); @@ -302,6 +264,7 @@ Subdir memory const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), [], @@ -314,9 +277,7 @@ Subdir memory ); expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, regularSubDirGeminiFile)} --- -My code memory ---- End of Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---`, + memoryContent: `--- Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---\nMy code memory\n--- End of Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---`, fileCount: 1, }); }); @@ -333,6 +294,7 @@ My code memory // Pass the custom limit directly to the function await loadServerHierarchicalMemory( cwd, + [], true, new FileDiscoveryService(projectRoot), [], @@ -353,6 +315,7 @@ My code memory const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), ); @@ -371,15 +334,36 @@ My code memory const result = await loadServerHierarchicalMemory( cwd, + [], false, new FileDiscoveryService(projectRoot), [extensionFilePath], ); expect(result).toEqual({ - memoryContent: `--- Context from: ${path.relative(cwd, extensionFilePath)} --- -Extension memory content ---- End of Context from: ${path.relative(cwd, extensionFilePath)} ---`, + memoryContent: `--- Context from: ${path.relative(cwd, extensionFilePath)} ---\nExtension memory content\n--- End of Context from: ${path.relative(cwd, extensionFilePath)} ---`, + fileCount: 1, + }); + }); + + it('should load memory from included directories', async () => { + const includedDir = await createEmptyDir( + path.join(testRootDir, 'included'), + ); + const includedFile = await createTestFile( + path.join(includedDir, DEFAULT_CONTEXT_FILENAME), + 'included directory memory', + ); + + const result = await loadServerHierarchicalMemory( + cwd, + [includedDir], + false, + new FileDiscoveryService(projectRoot), + ); + + expect(result).toEqual({ + memoryContent: `--- Context from: ${path.relative(cwd, includedFile)} ---\nincluded directory memory\n--- End of Context from: ${path.relative(cwd, includedFile)} ---`, fileCount: 1, }); }); diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index 323b13c5..f53d27a9 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -83,6 +83,36 @@ async function findProjectRoot(startDir: string): Promise<string | null> { async function getGeminiMdFilePathsInternal( currentWorkingDirectory: string, + includeDirectoriesToReadGemini: readonly string[], + userHomePath: string, + debugMode: boolean, + fileService: FileDiscoveryService, + extensionContextFilePaths: string[] = [], + fileFilteringOptions: FileFilteringOptions, + maxDirs: number, +): Promise<string[]> { + const dirs = new Set<string>([ + ...includeDirectoriesToReadGemini, + currentWorkingDirectory, + ]); + const paths = []; + for (const dir of dirs) { + const pathsByDir = await getGeminiMdFilePathsInternalForEachDir( + dir, + userHomePath, + debugMode, + fileService, + extensionContextFilePaths, + fileFilteringOptions, + maxDirs, + ); + paths.push(...pathsByDir); + } + return Array.from(new Set<string>(paths)); +} + +async function getGeminiMdFilePathsInternalForEachDir( + dir: string, userHomePath: string, debugMode: boolean, fileService: FileDiscoveryService, @@ -115,8 +145,8 @@ async function getGeminiMdFilePathsInternal( // FIX: Only perform the workspace search (upward and downward scans) // if a valid currentWorkingDirectory is provided. - if (currentWorkingDirectory) { - const resolvedCwd = path.resolve(currentWorkingDirectory); + if (dir) { + const resolvedCwd = path.resolve(dir); if (debugMode) logger.debug( `Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`, @@ -257,6 +287,7 @@ function concatenateInstructions( */ export async function loadServerHierarchicalMemory( currentWorkingDirectory: string, + includeDirectoriesToReadGemini: readonly string[], debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], @@ -274,6 +305,7 @@ export async function loadServerHierarchicalMemory( const userHomePath = homedir(); const filePaths = await getGeminiMdFilePathsInternal( currentWorkingDirectory, + includeDirectoriesToReadGemini, userHomePath, debugMode, fileService, @@ -282,7 +314,8 @@ export async function loadServerHierarchicalMemory( maxDirs, ); if (filePaths.length === 0) { - if (debugMode) logger.debug('No GEMINI.md files found in hierarchy.'); + if (debugMode) + logger.debug('No GEMINI.md files found in hierarchy of the workspace.'); return { memoryContent: '', fileCount: 0 }; } const contentsWithPaths = await readGeminiMdFiles( diff --git a/packages/core/src/utils/workspaceContext.ts b/packages/core/src/utils/workspaceContext.ts index 16d1b4c9..efbc8a4c 100644 --- a/packages/core/src/utils/workspaceContext.ts +++ b/packages/core/src/utils/workspaceContext.ts @@ -15,6 +15,8 @@ import * as path from 'path'; export class WorkspaceContext { private directories: Set<string>; + private initialDirectories: Set<string>; + /** * Creates a new WorkspaceContext with the given initial directory and optional additional directories. * @param initialDirectory The initial working directory (usually cwd) @@ -22,11 +24,14 @@ export class WorkspaceContext { */ constructor(initialDirectory: string, additionalDirectories: string[] = []) { this.directories = new Set<string>(); + this.initialDirectories = new Set<string>(); this.addDirectoryInternal(initialDirectory); + this.addInitialDirectoryInternal(initialDirectory); for (const dir of additionalDirectories) { this.addDirectoryInternal(dir); + this.addInitialDirectoryInternal(dir); } } @@ -69,6 +74,33 @@ export class WorkspaceContext { this.directories.add(realPath); } + private addInitialDirectoryInternal( + directory: string, + basePath: string = process.cwd(), + ): void { + const absolutePath = path.isAbsolute(directory) + ? directory + : path.resolve(basePath, directory); + + if (!fs.existsSync(absolutePath)) { + throw new Error(`Directory does not exist: ${absolutePath}`); + } + + const stats = fs.statSync(absolutePath); + if (!stats.isDirectory()) { + throw new Error(`Path is not a directory: ${absolutePath}`); + } + + let realPath: string; + try { + realPath = fs.realpathSync(absolutePath); + } catch (_error) { + throw new Error(`Failed to resolve path: ${absolutePath}`); + } + + this.initialDirectories.add(realPath); + } + /** * Gets a copy of all workspace directories. * @returns Array of absolute directory paths @@ -77,6 +109,17 @@ export class WorkspaceContext { return Array.from(this.directories); } + getInitialDirectories(): readonly string[] { + return Array.from(this.initialDirectories); + } + + setDirectories(directories: readonly string[]): void { + this.directories.clear(); + for (const dir of directories) { + this.addDirectoryInternal(dir); + } + } + /** * Checks if a given path is within any of the workspace directories. * @param pathToCheck The path to validate |
