diff --git a/README.md b/README.md index 3a855c5..8da9192 100644 --- a/README.md +++ b/README.md @@ -38,18 +38,54 @@ For local development, use the `file://` protocol: } ``` +### Environment Variables + +You can customize save locations via environment variables: + +| Variable | Description | Default | +|---|---|---| +| `OPENCODE_AUTOSAVE_DIR` | Local save directory (relative to project root, or absolute path). Set to `false` to disable local saves. | `./conversations` | +| `OPENCODE_AUTOSAVE_GLOBAL_DIR` | Global (secondary) save base directory. The project name is appended as a subdirectory automatically. Set to `false` to disable global saves. | `~/.conversations` | + +**Examples:** + +Save only to a global `~/Notes/conversations/` directory (no local project copies): + +```bash +OPENCODE_AUTOSAVE_DIR=false \ +OPENCODE_AUTOSAVE_GLOBAL_DIR=~/Notes/conversations \ +opencode +``` + +Save locally to a custom directory: + +```bash +OPENCODE_AUTOSAVE_DIR=.opencode/history \ +opencode +``` + +Disable all saving: + +```bash +OPENCODE_AUTOSAVE_DIR=false \ +OPENCODE_AUTOSAVE_GLOBAL_DIR=false \ +opencode +``` + +You can set these permanently in your shell profile (e.g. `~/.zshrc`, `~/.bashrc`). + ## Usage Once installed, the plugin automatically: -1. Creates a `./conversations/` directory in your project +1. Creates a `./conversations/` directory in your project (unless disabled via `OPENCODE_AUTOSAVE_DIR=false`) 2. Creates a new markdown file when you start a conversation 3. Saves all messages when the session becomes idle (2 second debounce) 4. Saves images to `./conversations/images/` directory 5. Includes child session (subagent) content inline in the parent file -6. **Mirrors all saves to `~/.conversations/{project_name}/`** for global backup +6. **Mirrors all saves to `~/.conversations/{project_name}/`** for global backup (configurable via `OPENCODE_AUTOSAVE_GLOBAL_DIR`) -No configuration needed - just install and start chatting! +No configuration needed - just install and start chatting! Customize save locations with environment variables if desired. ## File Naming @@ -134,9 +170,9 @@ The implementation looks good... ## Directory Structure -Conversations are saved to two locations: +Conversations are saved to two locations (both configurable): -**Primary (project-local):** +**Primary (project-local) — `OPENCODE_AUTOSAVE_DIR`:** ``` your-project/ ├── conversations/ @@ -148,7 +184,7 @@ your-project/ └── ... ``` -**Global backup (`~/.conversations/{project_name}/`):** +**Global backup — `OPENCODE_AUTOSAVE_GLOBAL_DIR`:** ``` ~/.conversations/ └── your-project/ @@ -159,7 +195,7 @@ your-project/ └── 20250129-14-22-30-fix-bug.md ``` -Both locations contain identical content. The global backup allows you to access all your conversations across different projects from a single location. +Both locations contain identical content. The global backup allows you to access all your conversations across different projects from a single location. Either location can be disabled independently. ## Troubleshooting diff --git a/src/file-manager.ts b/src/file-manager.ts index 4e22388..42a1d88 100644 --- a/src/file-manager.ts +++ b/src/file-manager.ts @@ -148,7 +148,11 @@ export async function saveImageFromBase64( } } -export function getGlobalSaveDirectory(projectDir: string): string | null { +export function getGlobalSaveDirectory(projectDir: string, config: PluginConfig): string | null { + if (!config.globalEnabled) { + return null; + } + try { const home = homedir(); if (!home) { @@ -158,7 +162,9 @@ export function getGlobalSaveDirectory(projectDir: string): string | null { const projectName = basename(projectDir); const sanitizedProjectName = sanitizeTopic(projectName, 50); - return join(home, '.conversations', sanitizedProjectName); + // Use configured global directory, or fall back to ~/.conversations + const baseDir = config.globalSaveDirectory || join(home, '.conversations'); + return join(baseDir, sanitizedProjectName); } catch { return null; } diff --git a/src/index.ts b/src/index.ts index 6ed1408..9c3a703 100644 --- a/src/index.ts +++ b/src/index.ts @@ -103,14 +103,22 @@ const plugin: Plugin = async (input) => { const { client, directory } = input; const typedClient = client as unknown as OpencodeClient; - const saveDir = await ensureDirectory(directory, DEFAULT_CONFIG); + let saveDir: string | null = null; + if (DEFAULT_CONFIG.localEnabled) { + saveDir = await ensureDirectory(directory, DEFAULT_CONFIG); + } let globalSaveDir: string | null = null; - const globalPath = getGlobalSaveDirectory(directory); + const globalPath = getGlobalSaveDirectory(directory, DEFAULT_CONFIG); if (globalPath) { globalSaveDir = await ensureGlobalDirectory(globalPath); } + // If both save locations are disabled, nothing to do + if (!saveDir && !globalSaveDir) { + return {}; + } + const hooks: Hooks = { event: async ({ event }: { event: Event }) => { try { @@ -132,7 +140,7 @@ async function handleEvent( event: Event, client: OpencodeClient, directory: string, - saveDir: string, + saveDir: string | null, globalSaveDir: string | null ): Promise { switch (event.type) { @@ -181,7 +189,7 @@ function handleSessionIdle( event: SessionIdleEvent, client: OpencodeClient, directory: string, - saveDir: string, + saveDir: string | null, globalSaveDir: string | null ): void { const { sessionID } = event.properties; @@ -205,7 +213,7 @@ async function handleSessionDeleted( event: SessionDeletedEvent, client: OpencodeClient, directory: string, - saveDir: string, + saveDir: string | null, globalSaveDir: string | null ): Promise { const { info } = event.properties; @@ -225,7 +233,7 @@ async function saveSessionToFile( sessionID: string, client: OpencodeClient, directory: string, - saveDir: string, + saveDir: string | null, globalSaveDir: string | null ): Promise { try { @@ -272,7 +280,10 @@ async function saveSessionToFile( if (!session.filePath) { const filename = generateFilename(title || 'untitled', session.createdAt, DEFAULT_CONFIG); - session.filePath = join(saveDir, filename); + // Use local save dir if available, otherwise use global dir for file path reference + const baseDir = saveDir || globalSaveDir; + if (!baseDir) return; + session.filePath = join(baseDir, filename); } const children = getChildSessions(sessionID); @@ -302,7 +313,10 @@ async function saveSessionToFile( messages, childData ); - await writeSessionFile(session.filePath, content); + + if (saveDir) { + await writeSessionFile(session.filePath, content); + } if (globalSaveDir) { await writeToSecondaryLocation(session.filePath, globalSaveDir, content); diff --git a/src/types.ts b/src/types.ts index 46a79cd..79d4a50 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,12 +46,30 @@ export interface FormattedMessage { /** * Plugin configuration options. - * V1 uses hardcoded defaults; future versions may expose these. + * Configurable via environment variables: + * + * OPENCODE_AUTOSAVE_DIR — Local save directory (relative to project root, + * or absolute path). Set to "false" to disable. + * Default: "./conversations" + * + * OPENCODE_AUTOSAVE_GLOBAL_DIR — Global (secondary) save directory. Accepts an + * absolute path. The project name is appended + * automatically as a subdirectory. Set to "false" + * to disable. Default: "~/.conversations" */ export interface PluginConfig { - /** Directory to save conversations (relative to project root) */ + /** Directory to save conversations (relative to project root, or absolute) */ saveDirectory: string; + /** Whether local (primary) saving is enabled */ + localEnabled: boolean; + + /** Global save base directory (project name is appended as subdirectory) */ + globalSaveDirectory: string; + + /** Whether global (secondary) saving is enabled */ + globalEnabled: boolean; + /** Maximum characters for topic portion of filename */ maxTopicLength: number; @@ -59,15 +77,36 @@ export interface PluginConfig { debounceMs: number; } +const ENV_LOCAL_DIR = 'OPENCODE_AUTOSAVE_DIR'; +const ENV_GLOBAL_DIR = 'OPENCODE_AUTOSAVE_GLOBAL_DIR'; + +function isDisabled(value: string | undefined): boolean { + if (!value) return false; + const lower = value.trim().toLowerCase(); + return lower === 'false' || lower === 'off' || lower === '0' || lower === 'no'; +} + +/** + * Build configuration from environment variables, falling back to defaults. + */ +export function resolveConfig(): PluginConfig { + const envLocal = process.env[ENV_LOCAL_DIR]; + const envGlobal = process.env[ENV_GLOBAL_DIR]; + + return { + saveDirectory: (!envLocal || isDisabled(envLocal)) ? './conversations' : envLocal.trim(), + localEnabled: !isDisabled(envLocal), + globalSaveDirectory: (!envGlobal || isDisabled(envGlobal)) ? '' : envGlobal.trim(), + globalEnabled: !isDisabled(envGlobal), + maxTopicLength: 30, + debounceMs: 2000, + }; +} + /** - * Default configuration values for V1. - * These are hardcoded and not user-configurable in this version. + * Default configuration values (no environment overrides). */ -export const DEFAULT_CONFIG: PluginConfig = { - saveDirectory: './conversations', - maxTopicLength: 30, - debounceMs: 2000, -}; +export const DEFAULT_CONFIG: PluginConfig = resolveConfig(); /** * Child session data for inline embedding.