223 lines
7.9 KiB
JavaScript
223 lines
7.9 KiB
JavaScript
/**
|
|
* 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: <gitRoot>/.claude/agent-memory/<agentName>/
|
|
* local: <gitRoot>/.claude/agent-memory-local/<agentName>/
|
|
* user: ~/.claude/agent-memory/<agentName>/
|
|
* ```
|
|
*
|
|
* 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
|