Download packages/core/src/config/storage.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/config/storage.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/config/storage.ts
-
curl -L -o storage.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/config/storage.ts
14 kB
| /** | |
| * @license | |
| * Copyright 2025 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import * as path from 'node:path'; | |
| import * as os from 'node:os'; | |
| import * as crypto from 'node:crypto'; | |
| import * as fs from 'node:fs'; | |
| import { | |
| GEMINI_DIR, | |
| homedir, | |
| GOOGLE_ACCOUNTS_FILENAME, | |
| isSubpath, | |
| resolveToRealPath, | |
| normalizePath, | |
| } from '../utils/paths.js'; | |
| import { ProjectRegistry } from './projectRegistry.js'; | |
| import { StorageMigration } from './storageMigration.js'; | |
| export const OAUTH_FILE = 'oauth_creds.json'; | |
| export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json'; | |
| const TMP_DIR_NAME = 'tmp'; | |
| const BIN_DIR_NAME = 'bin'; | |
| const AGENTS_DIR_NAME = '.agents'; | |
| export const AUTO_SAVED_POLICY_FILENAME = 'auto-saved.toml'; | |
| export class Storage { | |
| private readonly targetDir: string; | |
| private sessionId: string | undefined; | |
| private projectIdentifier: string | undefined; | |
| private initPromise: Promise<void> | undefined; | |
| private customPlansDir: string | undefined; | |
| constructor(targetDir: string, sessionId?: string) { | |
| this.targetDir = targetDir; | |
| this.sessionId = sessionId; | |
| } | |
| setCustomPlansDir(dir: string | undefined): void { | |
| this.customPlansDir = dir; | |
| } | |
| setSessionId(sessionId: string | undefined): void { | |
| this.sessionId = sessionId; | |
| } | |
| isInitialized(): boolean { | |
| return !!this.projectIdentifier; | |
| } | |
| static getGlobalGeminiDir(): string { | |
| const homeDir = homedir(); | |
| if (!homeDir) { | |
| return path.join(os.tmpdir(), GEMINI_DIR); | |
| } | |
| return path.join(homeDir, GEMINI_DIR); | |
| } | |
| static getGlobalAgentsDir(): string { | |
| const homeDir = homedir(); | |
| if (!homeDir) { | |
| return ''; | |
| } | |
| return path.join(homeDir, AGENTS_DIR_NAME); | |
| } | |
| static getMcpOAuthTokensPath(): string { | |
| return path.join(Storage.getGlobalRuntimeDir(), 'mcp-oauth-tokens.json'); | |
| } | |
| static getA2AOAuthTokensPath(): string { | |
| return path.join(Storage.getGlobalRuntimeDir(), 'a2a-oauth-tokens.json'); | |
| } | |
| static getGlobalSettingsPath(): string { | |
| return path.join(Storage.getGlobalGeminiDir(), 'settings.json'); | |
| } | |
| /** | |
| * Returns whether the CLI is currently running in sandbox mode. | |
| */ | |
| static isSandbox(): boolean { | |
| return !!process.env['SANDBOX']; | |
| } | |
| /** | |
| * Returns the directory for global runtime state (temp files, chat history, etc.). | |
| */ | |
| static getGlobalRuntimeDir(): string { | |
| // Under macOS Seatbelt (sandbox-exec), writing to the user's home .gemini | |
| // directory is blocked by the seatbelt profile. Route runtime state to a | |
| // dedicated subdirectory within the permitted persistent cache directory | |
| // to ensure history and session state persist across CLI invocations. | |
| if (process.env['SANDBOX'] === 'sandbox-exec') { | |
| const homeDir = homedir(); | |
| if (homeDir) { | |
| return path.join(homeDir, '.cache', GEMINI_DIR); | |
| } | |
| } | |
| // When running in a sandbox, the container launcher mounts an ephemeral | |
| // directory at the global gemini directory location (/home/node/.gemini). | |
| // For non-sandbox mode, runtime state and global config share the same path. | |
| return Storage.getGlobalGeminiDir(); | |
| } | |
| /** | |
| * Asynchronously ensures the global runtime directory exists. | |
| */ | |
| static async ensureGlobalRuntimeDirExists(): Promise<string> { | |
| const runtimeDir = Storage.getGlobalRuntimeDir(); | |
| try { | |
| await fs.promises.mkdir(runtimeDir, { recursive: true }); | |
| } catch { | |
| // Silently ignore directory creation failures (e.g., read-only filesystems or permission denials) | |
| } | |
| return runtimeDir; | |
| } | |
| static getInstallationIdPath(): string { | |
| return path.join(Storage.getGlobalRuntimeDir(), 'installation_id'); | |
| } | |
| static getGoogleAccountsPath(): string { | |
| return path.join(Storage.getGlobalRuntimeDir(), GOOGLE_ACCOUNTS_FILENAME); | |
| } | |
| static getTrustedFoldersPath(): string { | |
| if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) { | |
| return process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']; | |
| } | |
| return path.join(Storage.getGlobalRuntimeDir(), TRUSTED_FOLDERS_FILENAME); | |
| } | |
| static getUserCommandsDir(): string { | |
| return path.join(Storage.getGlobalGeminiDir(), 'commands'); | |
| } | |
| static getUserSkillsDir(): string { | |
| return path.join(Storage.getGlobalGeminiDir(), 'skills'); | |
| } | |
| static getUserAgentSkillsDir(): string { | |
| return path.join(Storage.getGlobalAgentsDir(), 'skills'); | |
| } | |
| static getUserPoliciesDir(): string { | |
| return path.join(Storage.getGlobalGeminiDir(), 'policies'); | |
| } | |
| static getUserKeybindingsPath(): string { | |
| return path.join(Storage.getGlobalGeminiDir(), 'keybindings.json'); | |
| } | |
| static getUserAgentsDir(): string { | |
| return path.join(Storage.getGlobalGeminiDir(), 'agents'); | |
| } | |
| static getAcknowledgedAgentsPath(): string { | |
| return path.join( | |
| Storage.getGlobalGeminiDir(), | |
| 'acknowledgments', | |
| 'agents.json', | |
| ); | |
| } | |
| static getPolicyIntegrityStoragePath(): string { | |
| return path.join(Storage.getGlobalRuntimeDir(), 'policy_integrity.json'); | |
| } | |
| private static getSystemConfigDir(): string { | |
| if (os.platform() === 'darwin') { | |
| return '/Library/Application Support/GeminiCli'; | |
| } else if (os.platform() === 'win32') { | |
| return 'C:\\ProgramData\\gemini-cli'; | |
| } else { | |
| return '/etc/gemini-cli'; | |
| } | |
| } | |
| static getSystemSettingsPath(): string { | |
| if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) { | |
| return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']; | |
| } | |
| return path.join(Storage.getSystemConfigDir(), 'settings.json'); | |
| } | |
| static getSystemPoliciesDir(): string { | |
| return path.join(Storage.getSystemConfigDir(), 'policies'); | |
| } | |
| static getGlobalTempDir(): string { | |
| return path.join(Storage.getGlobalRuntimeDir(), TMP_DIR_NAME); | |
| } | |
| static getGlobalBinDir(): string { | |
| return path.join(Storage.getGlobalTempDir(), BIN_DIR_NAME); | |
| } | |
| getGeminiDir(): string { | |
| return path.join(this.targetDir, GEMINI_DIR); | |
| } | |
| /** | |
| * Checks if the current workspace storage location is the same as the global/user storage location. | |
| * This handles symlinks and platform-specific path normalization. | |
| */ | |
| isWorkspaceHomeDir(): boolean { | |
| const home = homedir(); | |
| if (!home || !this.targetDir) { | |
| return false; | |
| } | |
| try { | |
| return ( | |
| normalizePath(resolveToRealPath(this.targetDir)) === | |
| normalizePath(resolveToRealPath(home)) | |
| ); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| getAgentsDir(): string { | |
| return path.join(this.targetDir, AGENTS_DIR_NAME); | |
| } | |
| getProjectTempDir(): string { | |
| const identifier = this.getProjectIdentifier(); | |
| const tempDir = Storage.getGlobalTempDir(); | |
| return path.join(tempDir, identifier); | |
| } | |
| getWorkspacePoliciesDir(): string { | |
| return path.join(this.getGeminiDir(), 'policies'); | |
| } | |
| getWorkspaceAutoSavedPolicyPath(): string { | |
| return path.join( | |
| this.getWorkspacePoliciesDir(), | |
| AUTO_SAVED_POLICY_FILENAME, | |
| ); | |
| } | |
| getAutoSavedPolicyPath(): string { | |
| return path.join(Storage.getUserPoliciesDir(), AUTO_SAVED_POLICY_FILENAME); | |
| } | |
| ensureProjectTempDirExists(): void { | |
| fs.mkdirSync(this.getProjectTempDir(), { recursive: true }); | |
| } | |
| static getOAuthCredsPath(): string { | |
| return path.join(Storage.getGlobalGeminiDir(), OAUTH_FILE); | |
| } | |
| getProjectRoot(): string { | |
| return this.targetDir; | |
| } | |
| private getFilePathHash(filePath: string): string { | |
| return crypto.createHash('sha256').update(filePath).digest('hex'); | |
| } | |
| private getProjectIdentifier(): string { | |
| if (!this.projectIdentifier) { | |
| throw new Error('Storage must be initialized before use'); | |
| } | |
| return this.projectIdentifier; | |
| } | |
| /** | |
| * Initializes storage by setting up the project registry and performing migrations. | |
| */ | |
| async initialize(): Promise<void> { | |
| if (this.initPromise) { | |
| return this.initPromise; | |
| } | |
| this.initPromise = (async () => { | |
| if (this.projectIdentifier) { | |
| return; | |
| } | |
| await Storage.ensureGlobalRuntimeDirExists(); | |
| const registryPath = path.join( | |
| Storage.getGlobalRuntimeDir(), | |
| 'projects.json', | |
| ); | |
| const registry = new ProjectRegistry(registryPath, [ | |
| Storage.getGlobalTempDir(), | |
| path.join(Storage.getGlobalRuntimeDir(), 'history'), | |
| ]); | |
| await registry.initialize(); | |
| this.projectIdentifier = await registry.getShortId(this.getProjectRoot()); | |
| await this.performMigration(); | |
| })(); | |
| return this.initPromise; | |
| } | |
| /** | |
| * Performs migration of legacy hash-based directories to the new slug-based format. | |
| * This is called internally by initialize(). | |
| */ | |
| private async performMigration(): Promise<void> { | |
| const shortId = this.getProjectIdentifier(); | |
| const oldHash = this.getFilePathHash(this.getProjectRoot()); | |
| // Migrate Temp Dir | |
| const newTempDir = path.join(Storage.getGlobalTempDir(), shortId); | |
| const oldTempDir = path.join(Storage.getGlobalTempDir(), oldHash); | |
| await StorageMigration.migrateDirectory(oldTempDir, newTempDir); | |
| // Migrate History Dir | |
| const historyDir = path.join(Storage.getGlobalRuntimeDir(), 'history'); | |
| const newHistoryDir = path.join(historyDir, shortId); | |
| const oldHistoryDir = path.join(historyDir, oldHash); | |
| await StorageMigration.migrateDirectory(oldHistoryDir, newHistoryDir); | |
| } | |
| getHistoryDir(): string { | |
| const identifier = this.getProjectIdentifier(); | |
| const historyDir = path.join(Storage.getGlobalRuntimeDir(), 'history'); | |
| return path.join(historyDir, identifier); | |
| } | |
| getProjectMemoryDir(): string { | |
| return this.getProjectMemoryTempDir(); | |
| } | |
| getProjectMemoryTempDir(): string { | |
| return path.join(this.getProjectTempDir(), 'memory'); | |
| } | |
| getProjectSkillsMemoryDir(): string { | |
| return path.join(this.getProjectMemoryTempDir(), 'skills'); | |
| } | |
| getWorkspaceSettingsPath(): string { | |
| return path.join(this.getGeminiDir(), 'settings.json'); | |
| } | |
| getProjectCommandsDir(): string { | |
| return path.join(this.getGeminiDir(), 'commands'); | |
| } | |
| getProjectSkillsDir(): string { | |
| return path.join(this.getGeminiDir(), 'skills'); | |
| } | |
| getProjectAgentSkillsDir(): string { | |
| return path.join(this.getAgentsDir(), 'skills'); | |
| } | |
| getProjectAgentsDir(): string { | |
| return path.join(this.getGeminiDir(), 'agents'); | |
| } | |
| getProjectTempCheckpointsDir(): string { | |
| return path.join(this.getProjectTempDir(), 'checkpoints'); | |
| } | |
| getProjectTempLogsDir(): string { | |
| return path.join(this.getProjectTempDir(), 'logs'); | |
| } | |
| getProjectTempPlansDir(): string { | |
| if (this.sessionId) { | |
| return path.join(this.getProjectTempDir(), this.sessionId, 'plans'); | |
| } | |
| return path.join(this.getProjectTempDir(), 'plans'); | |
| } | |
| getProjectTempTrackerDir(): string { | |
| if (this.sessionId) { | |
| return path.join(this.getProjectTempDir(), this.sessionId, 'tracker'); | |
| } | |
| return path.join(this.getProjectTempDir(), 'tracker'); | |
| } | |
| getPlansDir(): string { | |
| if (this.customPlansDir) { | |
| const resolvedPath = path.resolve( | |
| this.getProjectRoot(), | |
| this.customPlansDir, | |
| ); | |
| const realProjectRoot = resolveToRealPath(this.getProjectRoot()); | |
| const realResolvedPath = resolveToRealPath(resolvedPath); | |
| if (!isSubpath(realProjectRoot, realResolvedPath)) { | |
| throw new Error( | |
| `Custom plans directory '${this.customPlansDir}' resolves to '${realResolvedPath}', which is outside the project root '${realProjectRoot}'.`, | |
| ); | |
| } | |
| return resolvedPath; | |
| } | |
| return this.getProjectTempPlansDir(); | |
| } | |
| getProjectTempTasksDir(): string { | |
| if (this.sessionId) { | |
| return path.join(this.getProjectTempDir(), this.sessionId, 'tasks'); | |
| } | |
| return path.join(this.getProjectTempDir(), 'tasks'); | |
| } | |
| async listProjectChatFiles(): Promise< | |
| Array<{ filePath: string; lastUpdated: string }> | |
| > { | |
| const chatsDir = path.join(this.getProjectTempDir(), 'chats'); | |
| try { | |
| const files = await fs.promises.readdir(chatsDir); | |
| const jsonFiles = files.filter( | |
| (f) => f.endsWith('.json') || f.endsWith('.jsonl'), | |
| ); | |
| const sessions = await Promise.all( | |
| jsonFiles.map(async (file) => { | |
| const absolutePath = path.join(chatsDir, file); | |
| const stats = await fs.promises.stat(absolutePath); | |
| return { | |
| filePath: path.join('chats', file), | |
| lastUpdated: stats.mtime.toISOString(), | |
| mtimeMs: stats.mtimeMs, | |
| }; | |
| }), | |
| ); | |
| return sessions | |
| .sort((a, b) => b.mtimeMs - a.mtimeMs) | |
| .map(({ filePath, lastUpdated }) => ({ filePath, lastUpdated })); | |
| } catch (e) { | |
| // If directory doesn't exist, return empty | |
| if ( | |
| e instanceof Error && | |
| 'code' in e && | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| (e as NodeJS.ErrnoException).code === 'ENOENT' | |
| ) { | |
| return []; | |
| } | |
| throw e; | |
| } | |
| } | |
| async loadProjectTempFile<T>(filePath: string): Promise<T | null> { | |
| const absolutePath = path.join(this.getProjectTempDir(), filePath); | |
| try { | |
| const content = await fs.promises.readFile(absolutePath, 'utf8'); | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| return JSON.parse(content) as T; | |
| } catch (e) { | |
| // If file doesn't exist, return null | |
| if ( | |
| e instanceof Error && | |
| 'code' in e && | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion | |
| (e as NodeJS.ErrnoException).code === 'ENOENT' | |
| ) { | |
| return null; | |
| } | |
| throw e; | |
| } | |
| } | |
| getExtensionsDir(): string { | |
| return path.join(this.getGeminiDir(), 'extensions'); | |
| } | |
| getExtensionsConfigPath(): string { | |
| return path.join(this.getExtensionsDir(), 'gemini-extension.json'); | |
| } | |
| getHistoryFilePath(): string { | |
| return path.join(this.getProjectTempDir(), 'shell_history'); | |
| } | |
| } | |