/** * Agent-Scoped Memory - Support for Claude Code's 3-scope agent memory directories * * Claude Code organizes agent memory into three scopes: * - **project**: Shared across all collaborators (checked into git) * - **local**: Machine-specific, not shared (gitignored) * - **user**: Global per-user, spans all projects * * Each scope stores agent-specific memory in a named subdirectory, * enabling isolated yet transferable knowledge between agents. * * @module @claude-flow/memory/agent-memory-scope */ import * as path from 'node:path'; import { existsSync, readdirSync, statSync } from 'node:fs'; import { AutoMemoryBridge } from './auto-memory-bridge.js'; // ===== Internal Helpers ===== /** * Find the git root directory by walking up from a starting directory. * Synchronous variant for path resolution (no async needed for stat checks). */ function findGitRootSync(dir) { let current = path.resolve(dir); const root = path.parse(current).root; while (current !== root) { if (existsSync(path.join(current, '.git'))) { return current; } current = path.dirname(current); } return null; } /** * List agent subdirectories inside a given directory. * Returns an empty array if the directory does not exist or is unreadable. */ function listAgentsInDir(dir) { if (!existsSync(dir)) return []; try { return readdirSync(dir).filter((name) => { try { return statSync(path.join(dir, name)).isDirectory(); } catch { return false; } }); } catch { return []; } } // ===== Public Functions ===== /** * Resolve the agent memory directory for a given agent name, scope, and working directory. * * Path resolution matches Claude Code binary behavior: * ``` * project: /.claude/agent-memory// * local: /.claude/agent-memory-local// * user: ~/.claude/agent-memory// * ``` * * Agent names are sanitized to prevent path traversal attacks. * * @param agentName - The agent identifier * @param scope - Memory scope: project, local, or user * @param workingDir - Working directory for git root detection (defaults to cwd) * @returns Absolute path to the agent's memory directory */ export function resolveAgentMemoryDir(agentName, scope, workingDir) { // Sanitize agent name to prevent path traversal const safeName = agentName.replace(/[^a-zA-Z0-9_-]/g, '_'); if (scope === 'user') { const home = process.env.HOME || process.env.USERPROFILE || (process.env.HOMEDRIVE && process.env.HOMEPATH ? process.env.HOMEDRIVE + process.env.HOMEPATH : ''); if (!home) { throw new Error('Cannot determine home directory: HOME, USERPROFILE, and HOMEDRIVE+HOMEPATH are all undefined'); } return path.join(home, '.claude', 'agent-memory', safeName); } // For project and local scopes, find git root const effectiveDir = workingDir || process.cwd(); const gitRoot = findGitRootSync(effectiveDir); const baseDir = gitRoot || effectiveDir; if (scope === 'local') { return path.join(baseDir, '.claude', 'agent-memory-local', safeName); } // scope === 'project' return path.join(baseDir, '.claude', 'agent-memory', safeName); } /** * Create an AutoMemoryBridge configured for a specific agent scope. * * This is the primary factory for creating scoped bridges. It resolves * the correct memory directory based on agent name and scope, then * delegates to AutoMemoryBridge for the actual sync logic. * * @param backend - The AgentDB memory backend * @param config - Agent-scoped configuration * @returns A configured AutoMemoryBridge instance * * @example * ```typescript * const bridge = createAgentBridge(backend, { * agentName: 'coder', * scope: 'project', * syncMode: 'on-write', * }); * await bridge.recordInsight({ ... }); * ``` */ export function createAgentBridge(backend, config) { const memoryDir = resolveAgentMemoryDir(config.agentName, config.scope, config.workingDir); return new AutoMemoryBridge(backend, { ...config, memoryDir, }); } /** * Transfer knowledge from a source backend namespace into a target bridge. * * Queries high-confidence entries from the source and records them as * insights in the target bridge. Useful for cross-agent knowledge sharing * or promoting learnings from one scope to another. * * @param sourceBackend - Backend to query entries from * @param targetBridge - Bridge to record insights into * @param options - Transfer options (namespace, filters, limits) * @returns Transfer result with counts of transferred and skipped entries * * @example * ```typescript * const result = await transferKnowledge(sourceBackend, targetBridge, { * sourceNamespace: 'learnings', * minConfidence: 0.9, * maxEntries: 10, * categories: ['architecture', 'security'], * }); * console.log(`Transferred ${result.transferred}, skipped ${result.skipped}`); * ``` */ export async function transferKnowledge(sourceBackend, targetBridge, options) { const { sourceNamespace, minConfidence = 0.8, maxEntries = 20, categories, } = options; let transferred = 0; let skipped = 0; // Query high-confidence entries from source (fetch extra to allow for filtering) const entries = await sourceBackend.query({ type: 'hybrid', namespace: sourceNamespace, tags: ['insight'], limit: maxEntries * 2, }); for (const entry of entries) { if (transferred >= maxEntries) break; const confidence = entry.metadata?.confidence || 0; if (confidence < minConfidence) { skipped++; continue; } // Filter by category if specified const entryCategory = entry.metadata?.category; if (categories && categories.length > 0 && entryCategory && !categories.includes(entryCategory)) { skipped++; continue; } // Record as insight in target bridge const insight = { category: entryCategory || 'project-patterns', summary: entry.metadata?.summary || entry.content.split('\n')[0], detail: entry.content, source: `transfer:${sourceNamespace}`, confidence, agentDbId: entry.id, }; await targetBridge.recordInsight(insight); transferred++; } return { transferred, skipped }; } /** * List all agent scopes and their agents for the current project. * * Scans the three scope directories (project, local, user) and returns * the agent names found in each. Useful for discovery and diagnostics. * * @param workingDir - Working directory for git root detection (defaults to cwd) * @returns Array of scope/agents pairs * * @example * ```typescript * const scopes = listAgentScopes('/workspaces/my-project'); * // [ * // { scope: 'project', agents: ['coder', 'tester'] }, * // { scope: 'local', agents: ['researcher'] }, * // { scope: 'user', agents: ['planner'] }, * // ] * ``` */ export function listAgentScopes(workingDir) { const effectiveDir = workingDir || process.cwd(); const gitRoot = findGitRootSync(effectiveDir); const baseDir = gitRoot || effectiveDir; const home = process.env.HOME || process.env.USERPROFILE || (process.env.HOMEDRIVE && process.env.HOMEPATH ? process.env.HOMEDRIVE + process.env.HOMEPATH : '') || ''; const projectDir = path.join(baseDir, '.claude', 'agent-memory'); const localDir = path.join(baseDir, '.claude', 'agent-memory-local'); const userDir = path.join(home, '.claude', 'agent-memory'); return [ { scope: 'project', agents: listAgentsInDir(projectDir) }, { scope: 'local', agents: listAgentsInDir(localDir) }, { scope: 'user', agents: listAgentsInDir(userDir) }, ]; } //# sourceMappingURL=agent-memory-scope.js.map