summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/cli/src/config/config.test.ts87
-rw-r--r--packages/cli/src/config/config.ts21
-rw-r--r--packages/cli/src/config/settings.test.ts42
-rw-r--r--packages/cli/src/config/settings.ts9
-rw-r--r--packages/cli/src/ui/App.tsx6
-rw-r--r--packages/cli/src/ui/commands/directoryCommand.test.tsx13
-rw-r--r--packages/cli/src/ui/commands/directoryCommand.tsx34
-rw-r--r--packages/cli/src/ui/commands/memoryCommand.test.ts4
-rw-r--r--packages/cli/src/ui/commands/memoryCommand.ts3
-rw-r--r--packages/cli/src/ui/commands/types.ts1
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts3
-rw-r--r--packages/cli/src/utils/resolvePath.ts21
-rw-r--r--packages/core/src/config/config.ts8
-rw-r--r--packages/core/src/utils/memoryDiscovery.test.ts102
-rw-r--r--packages/core/src/utils/memoryDiscovery.ts39
-rw-r--r--packages/core/src/utils/workspaceContext.ts43
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